From 96a3d840886bd03b628e657ab262ab73a98ee550 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 27 Apr 2025 16:38:47 +0200 Subject: [PATCH] frontend: Add generator for Apple configuration profiles Closes #58 --- README.md | 1 + .../apple_configuration/template.xml | 115 ++++++++++++++++++ .../frontend/public/templates/pages/user.html | 4 +- crates/frontend/src/lib.rs | 73 ++++++++++- docs/index.md | 9 +- 5 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 crates/frontend/public/templates/apple_configuration/template.xml diff --git a/README.md b/README.md index 567df58..151ba5d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ a CalDAV/CardDAV server - adequately fast (I'd love to say blazingly fastâ„¢ :fire: but I don't have any benchmarks) - deleted calendars are recoverable - Nextcloud login flow (In DAVx5 you can login through the Nextcloud flow and automatically generate an app token) +- Apple configuration profiles (skip copy-pasting passwords and instead generate the configuration in the frontend) - OpenID Connect support (with option to disable password login) ## Getting Started diff --git a/crates/frontend/public/templates/apple_configuration/template.xml b/crates/frontend/public/templates/apple_configuration/template.xml new file mode 100644 index 0000000..68fc243 --- /dev/null +++ b/crates/frontend/public/templates/apple_configuration/template.xml @@ -0,0 +1,115 @@ + + + + + PayloadContent + + + CalDAVAccountDescription + {{ account_description }} + + CalDAVHostName + {{ hostname }} + + CalDAVPrincipalURL + {{ caldav_principal_url }} + + CalDAVUseSSL + + + CalDAVUsername + {{ user }} + + CalDAVPassword + {{ token }} + + PayloadDescription + {{ user }} RustiCal CalDAV profile + + PayloadDisplayName + {{ user }} RustiCal CalDAV profile + + PayloadIdentifier + com.github.lennart-k.rustical.cal.{{ user }} + + PayloadOrganization + RustiCal + + PayloadType + com.apple.caldav.account + + PayloadUUID + {{ caldav_profile_uuid }} + + PayloadVersion + 1 + + + + CardDAVAccountDescription + {{ account_description }} + + CardDAVHostName + {{ hostname }} + + CardDAVPrincipalURL + {{ carddav_principal_url }} + + CardDAVUseSSL + + + CardDAVUsername + {{ user }} + + CardDAVPassword + {{ token }} + + PayloadDescription + {{ user }} RustiCal CardDAV profile + + PayloadDisplayName + {{ user }} RustiCal CardDAV profile + + PayloadIdentifier + com.github.lennart-k.rustical.card.{{ user }} + + PayloadOrganization + RustiCal + + PayloadType + com.apple.carddav.account + + PayloadUUID + {{ carddav_profile_uuid }} + + PayloadVersion + 1 + + + + + PayloadDescription + Set up your RustiCal CalDAV/CardDAV account for {{ user }}@{{ hostname }} with app token {{ token_name }} + + PayloadDisplayName + RustiCal CalDAV/CardDAV + + PayloadIdentifier + com.github.lennart-k.rustical.{{ user }} + + PayloadOrganization + {{ hostname }} + + PayloadRemovalDisallowed + + + PayloadType + Configuration + + PayloadUUID + {{ plist_uuid }} + + PayloadVersion + 1 + + diff --git a/crates/frontend/public/templates/pages/user.html b/crates/frontend/public/templates/pages/user.html index 38f0344..1416d53 100644 --- a/crates/frontend/public/templates/pages/user.html +++ b/crates/frontend/public/templates/pages/user.html @@ -29,7 +29,6 @@ {% if let Some(created_at) = app_token.created_at %} {{ chrono_humanize::HumanTime::from(created_at.to_owned()) }} - {% endif %} @@ -49,6 +48,9 @@ + {% if is_apple %} + + {% endif %} diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index 9d4ca68..c4d94aa 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -7,7 +7,7 @@ use actix_web::{ HttpRequest, HttpResponse, Responder, cookie::{Key, SameSite}, dev::ServiceResponse, - http::{Method, StatusCode}, + http::{Method, StatusCode, header}, middleware::{ErrorHandlerResponse, ErrorHandlers}, web::{self, Data, Form, Path, Redirect}, }; @@ -28,6 +28,7 @@ use rustical_store::{ }; use serde::Deserialize; use std::sync::Arc; +use uuid::Uuid; mod assets; mod config; @@ -56,6 +57,7 @@ struct UserPage { pub deleted_calendars: Vec, pub addressbooks: Vec, pub deleted_addressbooks: Vec, + pub is_apple: bool, } async fn route_user_named( @@ -66,7 +68,6 @@ async fn route_user_named impl Responder { - // TODO: Check for authorization let user_id = path.into_inner(); if user_id != user.id { return actix_web::HttpResponse::Unauthorized().body("Unauthorized"); @@ -92,6 +93,13 @@ async fn route_user_named, req: HttpRequest) -> impl Responder { web::Redirect::to(redirect_url.to_string()).permanent() } +#[derive(Template)] +#[template(path = "apple_configuration/template.xml")] +pub struct AppleConfig { + token_name: String, + account_description: String, + hostname: String, + caldav_principal_url: String, + carddav_principal_url: String, + user: String, + token: String, + caldav_profile_uuid: Uuid, + carddav_profile_uuid: Uuid, + plist_uuid: Uuid, +} + #[derive(Debug, Clone, Deserialize)] pub(crate) struct PostAppTokenForm { name: String, + #[serde(default)] + apple: bool, } async fn route_post_app_token( user: User, auth_provider: Data, path: Path, - Form(PostAppTokenForm { name }): Form, -) -> Result { + Form(PostAppTokenForm { apple, name }): Form, + req: HttpRequest, +) -> Result { assert!(!name.is_empty()); assert_eq!(path.into_inner(), user.id); let token = generate_app_token(); auth_provider - .add_app_token(&user.id, name, token.clone()) + .add_app_token(&user.id, name.to_owned(), token.clone()) .await?; - Ok(token) + if apple { + let hostname = req.full_url().host_str().unwrap().to_owned(); + let profile = AppleConfig { + token_name: name, + account_description: format!("{}@{}", &user.id, &hostname), + hostname, + caldav_principal_url: req + .url_for("caldav_principal", [&user.id]) + .unwrap() + .to_string(), + carddav_principal_url: req + .url_for("carddav_principal", [&user.id]) + .unwrap() + .to_string(), + user: user.id.to_owned(), + token, + caldav_profile_uuid: Uuid::new_v4(), + carddav_profile_uuid: Uuid::new_v4(), + plist_uuid: Uuid::new_v4(), + } + .render() + .unwrap(); + Ok(HttpResponse::Ok() + .insert_header(header::ContentDisposition::attachment(format!( + "rustical-{}.mobileconfig", + user.id + ))) + .insert_header(( + header::CONTENT_TYPE, + "application/x-apple-aspen-config; charset=utf-8", + )) + .body(profile)) + } else { + Ok(HttpResponse::Ok().body(token)) + } } async fn route_delete_app_token( diff --git a/docs/index.md b/docs/index.md index bc33b35..ff65cbd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,10 +3,10 @@ a CalDAV/CardDAV server !!! warning - RustiCal is **not production-ready!** - I'm just starting to use it myself so I cannot guarantee that everything will be working smoothly just yet. - I hope there won't be any manual migrations anymore but if you want to be an early adopter some SQL knowledge might be useful just in case. - If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :) +RustiCal is **not production-ready!** +I'm just starting to use it myself so I cannot guarantee that everything will be working smoothly just yet. +I hope there won't be any manual migrations anymore but if you want to be an early adopter some SQL knowledge might be useful just in case. +If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :) ## Features @@ -16,4 +16,5 @@ a CalDAV/CardDAV server - adequately fast (I'd love to say blazingly fastâ„¢ :fire: but I don't have any benchmarks) - deleted calendars are recoverable - Nextcloud login flow (In DAVx5 you can login through the Nextcloud flow and automatically generate an app token) +- Apple configuration profiles (skip copy-pasting passwords and instead generate the configuration in the frontend) - [OpenID Connect](setup/oidc.md) support (with option to disable password login)