mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 13:32:16 +00:00
@@ -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)
|
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
|
||||||
- deleted calendars are recoverable
|
- deleted calendars are recoverable
|
||||||
- Nextcloud login flow (In DAVx5 you can login through the Nextcloud flow and automatically generate an app token)
|
- 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)
|
- OpenID Connect support (with option to disable password login)
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -29,7 +29,6 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if let Some(created_at) = app_token.created_at %}
|
{% if let Some(created_at) = app_token.created_at %}
|
||||||
{{ chrono_humanize::HumanTime::from(created_at.to_owned()) }}
|
{{ chrono_humanize::HumanTime::from(created_at.to_owned()) }}
|
||||||
<!-- {{ created_at.to_rfc3339() }} -->
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -49,6 +48,9 @@
|
|||||||
<td></td>
|
<td></td>
|
||||||
<td>
|
<td>
|
||||||
<button type="submit" form="form_generate_app_token">Generate</button>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use actix_web::{
|
|||||||
HttpRequest, HttpResponse, Responder,
|
HttpRequest, HttpResponse, Responder,
|
||||||
cookie::{Key, SameSite},
|
cookie::{Key, SameSite},
|
||||||
dev::ServiceResponse,
|
dev::ServiceResponse,
|
||||||
http::{Method, StatusCode},
|
http::{Method, StatusCode, header},
|
||||||
middleware::{ErrorHandlerResponse, ErrorHandlers},
|
middleware::{ErrorHandlerResponse, ErrorHandlers},
|
||||||
web::{self, Data, Form, Path, Redirect},
|
web::{self, Data, Form, Path, Redirect},
|
||||||
};
|
};
|
||||||
@@ -28,6 +28,7 @@ use rustical_store::{
|
|||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
mod assets;
|
mod assets;
|
||||||
mod config;
|
mod config;
|
||||||
@@ -56,6 +57,7 @@ struct UserPage {
|
|||||||
pub deleted_calendars: Vec<Calendar>,
|
pub deleted_calendars: Vec<Calendar>,
|
||||||
pub addressbooks: Vec<Addressbook>,
|
pub addressbooks: Vec<Addressbook>,
|
||||||
pub deleted_addressbooks: Vec<Addressbook>,
|
pub deleted_addressbooks: Vec<Addressbook>,
|
||||||
|
pub is_apple: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn route_user_named<CS: CalendarStore, AS: AddressbookStore, AP: AuthenticationProvider>(
|
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,
|
user: User,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
// TODO: Check for authorization
|
|
||||||
let user_id = path.into_inner();
|
let user_id = path.into_inner();
|
||||||
if user_id != user.id {
|
if user_id != user.id {
|
||||||
return actix_web::HttpResponse::Unauthorized().body("Unauthorized");
|
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());
|
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 {
|
UserPage {
|
||||||
app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
|
app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
|
||||||
calendars,
|
calendars,
|
||||||
@@ -99,6 +107,7 @@ async fn route_user_named<CS: CalendarStore, AS: AddressbookStore, AP: Authentic
|
|||||||
addressbooks,
|
addressbooks,
|
||||||
deleted_addressbooks,
|
deleted_addressbooks,
|
||||||
user,
|
user,
|
||||||
|
is_apple,
|
||||||
}
|
}
|
||||||
.respond_to(&req)
|
.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()
|
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)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub(crate) struct PostAppTokenForm {
|
pub(crate) struct PostAppTokenForm {
|
||||||
name: String,
|
name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
apple: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn route_post_app_token<AP: AuthenticationProvider>(
|
async fn route_post_app_token<AP: AuthenticationProvider>(
|
||||||
user: User,
|
user: User,
|
||||||
auth_provider: Data<AP>,
|
auth_provider: Data<AP>,
|
||||||
path: Path<String>,
|
path: Path<String>,
|
||||||
Form(PostAppTokenForm { name }): Form<PostAppTokenForm>,
|
Form(PostAppTokenForm { apple, name }): Form<PostAppTokenForm>,
|
||||||
) -> Result<impl Responder, rustical_store::Error> {
|
req: HttpRequest,
|
||||||
|
) -> Result<HttpResponse, rustical_store::Error> {
|
||||||
assert!(!name.is_empty());
|
assert!(!name.is_empty());
|
||||||
assert_eq!(path.into_inner(), user.id);
|
assert_eq!(path.into_inner(), user.id);
|
||||||
let token = generate_app_token();
|
let token = generate_app_token();
|
||||||
auth_provider
|
auth_provider
|
||||||
.add_app_token(&user.id, name, token.clone())
|
.add_app_token(&user.id, name.to_owned(), token.clone())
|
||||||
.await?;
|
.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>(
|
async fn route_delete_app_token<AP: AuthenticationProvider>(
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
a CalDAV/CardDAV server
|
a CalDAV/CardDAV server
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
RustiCal is **not production-ready!**
|
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'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.
|
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. :)
|
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
|
## 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)
|
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
|
||||||
- deleted calendars are recoverable
|
- deleted calendars are recoverable
|
||||||
- Nextcloud login flow (In DAVx5 you can login through the Nextcloud flow and automatically generate an app token)
|
- 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)
|
- [OpenID Connect](setup/oidc.md) support (with option to disable password login)
|
||||||
|
|||||||
Reference in New Issue
Block a user