Implement almost all previous features

This commit is contained in:
Lennart
2025-06-08 19:38:33 +02:00
parent 95889e3df1
commit 00eb43f048
41 changed files with 906 additions and 916 deletions

View File

@@ -1,10 +1,15 @@
use actix_web::{
HttpRequest, HttpResponse, Responder,
http::{StatusCode, header},
web::{self, Data, Path},
};
use std::sync::Arc;
use askama::Template;
use askama_web::WebTemplate;
use axum::{
Extension,
extract::Path,
response::{IntoResponse, Redirect, Response},
};
use axum_extra::TypedHeader;
use headers::Referer;
use http::StatusCode;
use rustical_store::{Addressbook, AddressbookStore, auth::User};
#[derive(Template, WebTemplate)]
@@ -14,37 +19,31 @@ struct AddressbookPage {
}
pub async fn route_addressbook<AS: AddressbookStore>(
path: Path<(String, String)>,
store: Data<AS>,
Path((owner, addrbook_id)): Path<(String, String)>,
Extension(store): Extension<Arc<AS>>,
user: User,
req: HttpRequest,
) -> Result<impl Responder, rustical_store::Error> {
let (owner, addrbook_id) = path.into_inner();
) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) {
return Ok(HttpResponse::Unauthorized().body("Unauthorized"));
return Ok(StatusCode::UNAUTHORIZED.into_response());
}
Ok(AddressbookPage {
addressbook: store.get_addressbook(&owner, &addrbook_id, true).await?,
}
.respond_to(&req))
.into_response())
}
pub async fn route_addressbook_restore<AS: AddressbookStore>(
path: Path<(String, String)>,
req: HttpRequest,
store: Data<AS>,
Path((owner, addressbook_id)): Path<(String, String)>,
Extension(store): Extension<Arc<AS>>,
user: User,
) -> Result<impl Responder, rustical_store::Error> {
let (owner, addressbook_id) = path.into_inner();
referer: Option<TypedHeader<Referer>>,
) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) {
return Ok(HttpResponse::Unauthorized().body("Unauthorized"));
return Ok(StatusCode::UNAUTHORIZED.into_response());
}
store.restore_addressbook(&owner, &addressbook_id).await?;
Ok(match req.headers().get(header::REFERER) {
Some(referer) => web::Redirect::to(referer.to_str().unwrap().to_owned())
.using_status_code(StatusCode::FOUND)
.respond_to(&req)
.map_into_boxed_body(),
None => HttpResponse::Ok().body("Restored"),
Ok(match referer {
Some(referer) => Redirect::to(&referer.to_string()).into_response(),
None => (StatusCode::CREATED, "Restored").into_response(),
})
}

View File

@@ -0,0 +1,104 @@
use std::{str::FromStr, sync::Arc};
use askama::Template;
use axum::{
Extension, Form,
body::Body,
extract::Path,
response::{IntoResponse, Redirect, Response},
};
use axum_extra::extract::Host;
use headers::{ContentType, HeaderMapExt};
use http::{HeaderValue, StatusCode, header};
use rand::{Rng, distributions::Alphanumeric};
use rustical_store::auth::{AuthenticationProvider, User};
use serde::Deserialize;
use uuid::Uuid;
pub fn generate_app_token() -> String {
rand::thread_rng()
.sample_iter(Alphanumeric)
.map(char::from)
.take(64)
.collect()
}
#[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,
}
pub async fn route_post_app_token<AP: AuthenticationProvider>(
user: User,
Extension(auth_provider): Extension<Arc<AP>>,
Path(user_id): Path<String>,
Host(hostname): Host,
Form(PostAppTokenForm { apple, name }): Form<PostAppTokenForm>,
) -> Result<Response, rustical_store::Error> {
assert!(!name.is_empty());
assert_eq!(user_id, user.id);
let token = generate_app_token();
auth_provider
.add_app_token(&user.id, name.to_owned(), token.clone())
.await?;
if apple {
let profile = AppleConfig {
token_name: name,
account_description: format!("{}@{}", &user.id, &hostname),
hostname: hostname.clone(),
caldav_principal_url: format!("https://{hostname}/caldav/principal/{user_id}"),
carddav_principal_url: format!("https://{hostname}/carddav/principal/{user_id}"),
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();
let mut res = Response::builder().status(StatusCode::OK);
let hdrs = res.headers_mut().unwrap();
hdrs.typed_insert(
ContentType::from_str("application/x-apple-aspen-config; charset=utf-8").unwrap(),
);
let filename = format!("rustical-{}.mobileconfig", user_id);
hdrs.insert(
header::CONTENT_DISPOSITION,
HeaderValue::from_str(&format!(
"attachement; filename*=UTF-8''{} filename={}",
filename, filename
))
.unwrap(),
);
Ok(res.body(Body::new(profile)).unwrap())
} else {
Ok((StatusCode::OK, token).into_response())
}
}
pub async fn route_delete_app_token<AP: AuthenticationProvider>(
user: User,
Extension(auth_provider): Extension<Arc<AP>>,
Path((user_id, token_id)): Path<(String, String)>,
) -> Result<Redirect, rustical_store::Error> {
assert_eq!(user_id, user.id);
auth_provider.remove_app_token(&user.id, &token_id).await?;
Ok(Redirect::to("/frontend/user"))
}

View File

@@ -1,10 +1,15 @@
use actix_web::{
HttpRequest, HttpResponse, Responder,
http::{StatusCode, header},
web::{self, Data, Path},
};
use std::sync::Arc;
use askama::Template;
use askama_web::WebTemplate;
use axum::{
Extension,
extract::Path,
response::{IntoResponse, Redirect, Response},
};
use axum_extra::TypedHeader;
use headers::Referer;
use http::StatusCode;
use rustical_store::{Calendar, CalendarStore, auth::User};
#[derive(Template, WebTemplate)]
@@ -14,37 +19,31 @@ struct CalendarPage {
}
pub async fn route_calendar<C: CalendarStore>(
path: Path<(String, String)>,
store: Data<C>,
Path((owner, cal_id)): Path<(String, String)>,
Extension(store): Extension<Arc<C>>,
user: User,
req: HttpRequest,
) -> Result<HttpResponse, rustical_store::Error> {
let (owner, cal_id) = path.into_inner();
) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) {
return Ok(HttpResponse::Unauthorized().body("Unauthorized"));
return Ok(StatusCode::UNAUTHORIZED.into_response());
}
Ok(CalendarPage {
calendar: store.get_calendar(&owner, &cal_id).await?,
}
.respond_to(&req))
.into_response())
}
pub async fn route_calendar_restore<CS: CalendarStore>(
path: Path<(String, String)>,
req: HttpRequest,
store: Data<CS>,
Path((owner, cal_id)): Path<(String, String)>,
Extension(store): Extension<Arc<CS>>,
user: User,
) -> Result<HttpResponse, rustical_store::Error> {
let (owner, cal_id) = path.into_inner();
referer: Option<TypedHeader<Referer>>,
) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) {
return Ok(HttpResponse::Unauthorized().body("Unauthorized"));
return Ok(StatusCode::UNAUTHORIZED.into_response());
}
store.restore_calendar(&owner, &cal_id).await?;
Ok(match req.headers().get(header::REFERER) {
Some(referer) => web::Redirect::to(referer.to_str().unwrap().to_owned())
.using_status_code(StatusCode::FOUND)
.respond_to(&req)
.map_into_boxed_body(),
None => HttpResponse::Created().body("Restored"),
Ok(match referer {
Some(referer) => Redirect::to(&referer.to_string()).into_response(),
None => (StatusCode::CREATED, "Restored").into_response(),
})
}

View File

@@ -5,13 +5,16 @@ use askama::Template;
use askama_web::WebTemplate;
use axum::{
Extension, Form,
extract::{OriginalUri, Query},
extract::Query,
response::{IntoResponse, Redirect, Response},
};
use axum_extra::extract::Host;
use http::StatusCode;
use rustical_store::auth::AuthenticationProvider;
use serde::Deserialize;
use tower_sessions::Session;
use tracing::instrument;
use url::Url;
#[derive(Template, WebTemplate)]
#[template(path = "pages/login.html")]
@@ -37,20 +40,17 @@ pub async fn route_get_login(
Extension(config): Extension<FrontendConfig>,
Extension(oidc_config): Extension<Option<OidcConfig>>,
) -> Response {
// let oidc_data = oidc_config
// .as_ref()
// .as_ref()
// .map(|oidc_config| OidcProviderData {
// name: &oidc_config.name,
// redirect_url: req
// .url_for_static(ROUTE_NAME_OIDC_LOGIN)
// .unwrap()
// .to_string(),
// });
let oidc_data = oidc_config
.as_ref()
.as_ref()
.map(|oidc_config| OidcProviderData {
name: &oidc_config.name,
redirect_url: "/frontend/login/oidc".to_owned(),
});
LoginPage {
redirect_uri,
allow_password_login: config.allow_password_login,
oidc_data: None,
oidc_data,
}
.into_response()
}
@@ -66,7 +66,8 @@ pub struct PostLoginForm {
pub async fn route_post_login<AP: AuthenticationProvider>(
Extension(auth_provider): Extension<Arc<AP>>,
Extension(config): Extension<FrontendConfig>,
OriginalUri(orig_uri): OriginalUri,
session: Session,
Host(host): Host,
Form(PostLoginForm {
username,
password,
@@ -76,24 +77,32 @@ pub async fn route_post_login<AP: AuthenticationProvider>(
if !config.allow_password_login {
return StatusCode::METHOD_NOT_ALLOWED.into_response();
}
// Ensure that redirect_uri never goes cross-origin
let default_redirect = "/frontend/user".to_string();
let redirect_uri = redirect_uri.unwrap_or(default_redirect.clone());
// let redirect_uri = orig_uri
// .join(&redirect_uri)
// .ok()
// .and_then(|uri| orig_uri.make_relative(&uri))
// .unwrap_or(default_redirect);
// Ensure that redirect_uri never goes cross-origin
let base_url: Url = format!("https://{host}").parse().unwrap();
let redirect_uri = if let Some(redirect_uri) = redirect_uri {
if let Ok(redirect_url) = base_url.join(&redirect_uri) {
if redirect_url.origin() == base_url.origin() {
redirect_url.path().to_owned()
} else {
default_redirect
}
} else {
default_redirect
}
} else {
default_redirect
};
if let Ok(Some(user)) = auth_provider.validate_password(&username, &password).await {
// session.insert("user", user.id).unwrap();
session.insert("user", user.id).await.unwrap();
Redirect::to(&redirect_uri).into_response()
} else {
StatusCode::UNAUTHORIZED.into_response()
}
}
pub async fn route_post_logout() -> Redirect {
// session.remove("user");
pub async fn route_post_logout(session: Session) -> Redirect {
session.remove_value("user").await.unwrap();
Redirect::to("/")
}

View File

@@ -1,3 +1,5 @@
// pub mod addressbook;
// pub mod calendar;
pub mod addressbook;
pub mod app_token;
pub mod calendar;
pub mod login;
pub mod user;

View File

@@ -0,0 +1,89 @@
use std::sync::Arc;
use askama::Template;
use askama_web::WebTemplate;
use axum::{
Extension,
extract::Path,
response::{IntoResponse, Redirect},
};
use axum_extra::TypedHeader;
use headers::UserAgent;
use http::StatusCode;
use rustical_store::{
Addressbook, AddressbookStore, Calendar, CalendarStore,
auth::{AuthenticationProvider, User, user::AppToken},
};
#[derive(Template, WebTemplate)]
#[template(path = "pages/user.html")]
pub struct UserPage {
pub user: User,
pub app_tokens: Vec<AppToken>,
pub calendars: Vec<Calendar>,
pub deleted_calendars: Vec<Calendar>,
pub addressbooks: Vec<Addressbook>,
pub deleted_addressbooks: Vec<Addressbook>,
pub is_apple: bool,
}
pub async fn route_user_named<
CS: CalendarStore,
AS: AddressbookStore,
AP: AuthenticationProvider,
>(
Path(user_id): Path<String>,
Extension(cal_store): Extension<Arc<CS>>,
Extension(addr_store): Extension<Arc<AS>>,
Extension(auth_provider): Extension<Arc<AP>>,
TypedHeader(user_agent): TypedHeader<UserAgent>,
user: User,
) -> impl IntoResponse {
if user_id != user.id {
return StatusCode::UNAUTHORIZED.into_response();
}
let mut calendars = vec![];
for group in user.memberships() {
calendars.extend(cal_store.get_calendars(group).await.unwrap());
}
let mut deleted_calendars = vec![];
for group in user.memberships() {
deleted_calendars.extend(cal_store.get_deleted_calendars(group).await.unwrap());
}
let mut addressbooks = vec![];
for group in user.memberships() {
addressbooks.extend(addr_store.get_addressbooks(group).await.unwrap());
}
let mut deleted_addressbooks = vec![];
for group in user.memberships() {
deleted_addressbooks.extend(addr_store.get_deleted_addressbooks(group).await.unwrap());
}
let is_apple = user_agent.as_str().contains("Apple") || user_agent.as_str().contains("Mac OS");
UserPage {
app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
calendars,
deleted_calendars,
addressbooks,
deleted_addressbooks,
user,
is_apple,
}
.into_response()
}
pub async fn route_get_home(user: User) -> Redirect {
Redirect::to(&format!("/frontend/user/{}", user.id))
}
pub async fn route_root(user: Option<User>) -> Redirect {
match user {
Some(user) => route_get_home(user).await,
None => Redirect::to("/frontend/login"),
}
}