frontend: Add generator for Apple configuration profiles

Closes #58
This commit is contained in:
Lennart
2025-04-27 16:38:47 +02:00
parent 2e940bed2f
commit 96a3d84088
5 changed files with 191 additions and 11 deletions

View File

@@ -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

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>CalDAVAccountDescription</key>
<string>{{ account_description }}</string>
<key>CalDAVHostName</key>
<string>{{ hostname }}</string>
<key>CalDAVPrincipalURL</key>
<string>{{ caldav_principal_url }}</string>
<key>CalDAVUseSSL</key>
<true/>
<key>CalDAVUsername</key>
<string>{{ user }}</string>
<key>CalDAVPassword</key>
<string>{{ token }}</string>
<key>PayloadDescription</key>
<string>{{ user }} RustiCal CalDAV profile</string>
<key>PayloadDisplayName</key>
<string>{{ user }} RustiCal CalDAV profile</string>
<key>PayloadIdentifier</key>
<string>com.github.lennart-k.rustical.cal.{{ user }}</string>
<key>PayloadOrganization</key>
<string>RustiCal</string>
<key>PayloadType</key>
<string>com.apple.caldav.account</string>
<key>PayloadUUID</key>
<string>{{ caldav_profile_uuid }}</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
<dict>
<key>CardDAVAccountDescription</key>
<string>{{ account_description }}</string>
<key>CardDAVHostName</key>
<string>{{ hostname }}</string>
<key>CardDAVPrincipalURL</key>
<string>{{ carddav_principal_url }}</string>
<key>CardDAVUseSSL</key>
<true/>
<key>CardDAVUsername</key>
<string>{{ user }}</string>
<key>CardDAVPassword</key>
<string>{{ token }}</string>
<key>PayloadDescription</key>
<string>{{ user }} RustiCal CardDAV profile</string>
<key>PayloadDisplayName</key>
<string>{{ user }} RustiCal CardDAV profile</string>
<key>PayloadIdentifier</key>
<string>com.github.lennart-k.rustical.card.{{ user }}</string>
<key>PayloadOrganization</key>
<string>RustiCal</string>
<key>PayloadType</key>
<string>com.apple.carddav.account</string>
<key>PayloadUUID</key>
<string>{{ carddav_profile_uuid }}</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</array>
<key>PayloadDescription</key>
<string>Set up your RustiCal CalDAV/CardDAV account for {{ user }}@{{ hostname }} with app token {{ token_name }}</string>
<key>PayloadDisplayName</key>
<string>RustiCal CalDAV/CardDAV</string>
<key>PayloadIdentifier</key>
<string>com.github.lennart-k.rustical.{{ user }}</string>
<key>PayloadOrganization</key>
<string>{{ hostname }}</string>
<key>PayloadRemovalDisallowed</key>
<false/>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>{{ plist_uuid }}</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>

View File

@@ -29,7 +29,6 @@
<td>
{% if let Some(created_at) = app_token.created_at %}
{{ chrono_humanize::HumanTime::from(created_at.to_owned()) }}
<!-- {{ created_at.to_rfc3339() }} -->
{% endif %}
</td>
<td>
@@ -49,6 +48,9 @@
<td></td>
<td>
<button type="submit" form="form_generate_app_token">Generate</button>
{% if is_apple %}
<button type="submit" form="form_generate_app_token" name="apple" value="true">Apple Configuration Profile (contains token)</button>
{% endif %}
</td>
</tr>
</table>

View File

@@ -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<Calendar>,
pub addressbooks: Vec<Addressbook>,
pub deleted_addressbooks: Vec<Addressbook>,
pub is_apple: bool,
}
async fn route_user_named<CS: CalendarStore, AS: AddressbookStore, AP: AuthenticationProvider>(
@@ -66,7 +68,6 @@ async fn route_user_named<CS: CalendarStore, AS: AddressbookStore, AP: Authentic
user: User,
req: HttpRequest,
) -> 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<CS: CalendarStore, AS: AddressbookStore, AP: Authentic
deleted_addressbooks.extend(addr_store.get_deleted_addressbooks(group).await.unwrap());
}
let is_apple = req
.headers()
.get(header::USER_AGENT)
.and_then(|user_agent| user_agent.to_str().ok())
.map(|ua| ua.contains("Apple") || ua.contains("Mac OS"))
.unwrap_or_default();
UserPage {
app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
calendars,
@@ -99,6 +107,7 @@ async fn route_user_named<CS: CalendarStore, AS: AddressbookStore, AP: Authentic
addressbooks,
deleted_addressbooks,
user,
is_apple,
}
.respond_to(&req)
}
@@ -123,24 +132,76 @@ async fn route_root(user: Option<User>, 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<AP: AuthenticationProvider>(
user: User,
auth_provider: Data<AP>,
path: Path<String>,
Form(PostAppTokenForm { name }): Form<PostAppTokenForm>,
) -> Result<impl Responder, rustical_store::Error> {
Form(PostAppTokenForm { apple, name }): Form<PostAppTokenForm>,
req: HttpRequest,
) -> Result<HttpResponse, rustical_store::Error> {
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<AP: AuthenticationProvider>(

View File

@@ -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)