mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
Checkpoint: Migration to axum
This commit is contained in:
@@ -7,14 +7,15 @@ repository.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
tower.workspace = true
|
||||
http.workspace = true
|
||||
axum.workspace = true
|
||||
askama.workspace = true
|
||||
async-trait.workspace = true
|
||||
askama_web.workspace = true
|
||||
actix-session.workspace = true
|
||||
async-trait.workspace = true
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
actix-web.workspace = true
|
||||
rustical_store.workspace = true
|
||||
rust-embed.workspace = true
|
||||
futures-core.workspace = true
|
||||
@@ -27,3 +28,5 @@ uuid.workspace = true
|
||||
url.workspace = true
|
||||
tracing.workspace = true
|
||||
rustical_oidc.workspace = true
|
||||
axum-extra.workspace= true
|
||||
headers.workspace = true
|
||||
|
||||
@@ -1,97 +1,72 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use actix_web::{
|
||||
body::BoxBody,
|
||||
dev::{
|
||||
HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest, ServiceResponse,
|
||||
},
|
||||
http::{header, Method},
|
||||
HttpResponse,
|
||||
use axum::{
|
||||
RequestExt,
|
||||
body::Body,
|
||||
extract::{Path, Request},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use futures_core::future::BoxFuture;
|
||||
use headers::{ContentType, ETag, HeaderMapExt};
|
||||
use http::{Method, StatusCode};
|
||||
use rust_embed::RustEmbed;
|
||||
use std::{convert::Infallible, marker::PhantomData, str::FromStr};
|
||||
use tower::Service;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[derive(Clone, RustEmbed)]
|
||||
#[folder = "public/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EmbedService<E>
|
||||
where
|
||||
E: 'static + RustEmbed,
|
||||
{
|
||||
_embed: PhantomData<E>,
|
||||
prefix: String,
|
||||
}
|
||||
|
||||
impl<E> EmbedService<E>
|
||||
where
|
||||
E: 'static + RustEmbed,
|
||||
{
|
||||
pub fn new(prefix: String) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
prefix,
|
||||
_embed: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> HttpServiceFactory for EmbedService<E>
|
||||
impl<E> Service<Request> for EmbedService<E>
|
||||
where
|
||||
E: 'static + RustEmbed,
|
||||
{
|
||||
fn register(self, config: &mut actix_web::dev::AppService) {
|
||||
let resource_def = if config.is_root() {
|
||||
ResourceDef::root_prefix(&self.prefix)
|
||||
} else {
|
||||
ResourceDef::prefix(&self.prefix)
|
||||
};
|
||||
config.register_service(resource_def, None, self, None);
|
||||
type Response = Response;
|
||||
type Error = Infallible;
|
||||
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
#[inline]
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), Self::Error>> {
|
||||
Ok(()).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> ServiceFactory<ServiceRequest> for EmbedService<E>
|
||||
where
|
||||
E: 'static + RustEmbed,
|
||||
{
|
||||
type Response = ServiceResponse;
|
||||
type Error = actix_web::Error;
|
||||
type Config = ();
|
||||
type Service = EmbedService<E>;
|
||||
type InitError = ();
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
let prefix = self.prefix.clone();
|
||||
Box::pin(async move {
|
||||
Ok(Self {
|
||||
prefix,
|
||||
_embed: PhantomData,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Service<ServiceRequest> for EmbedService<E>
|
||||
where
|
||||
E: 'static + RustEmbed,
|
||||
{
|
||||
type Response = ServiceResponse<BoxBody>;
|
||||
type Error = actix_web::Error;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
actix_web::dev::always_ready!();
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
#[inline]
|
||||
fn call(&mut self, mut req: Request) -> Self::Future {
|
||||
Box::pin(async move {
|
||||
if req.method() != Method::GET && req.method() != Method::HEAD {
|
||||
return Ok(req.into_response(HttpResponse::MethodNotAllowed()));
|
||||
return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response());
|
||||
}
|
||||
let path = req.match_info().unprocessed().trim_start_matches('/');
|
||||
let path: String = if let Ok(Path(path)) = req.extract_parts().await.unwrap() {
|
||||
path
|
||||
} else {
|
||||
return Ok(StatusCode::NOT_FOUND.into_response());
|
||||
};
|
||||
|
||||
match E::get(path) {
|
||||
match E::get(&path) {
|
||||
Some(file) => {
|
||||
let data = file.data;
|
||||
let hash = hex::encode(file.metadata.sha256_hash());
|
||||
let etag = format!("\"{hash}\"");
|
||||
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
||||
|
||||
let body = if req.method() == Method::HEAD {
|
||||
@@ -99,14 +74,13 @@ where
|
||||
} else {
|
||||
data
|
||||
};
|
||||
Ok(req.into_response(
|
||||
HttpResponse::Ok()
|
||||
.content_type(mime)
|
||||
.insert_header((header::ETAG, hash))
|
||||
.body(body),
|
||||
))
|
||||
let mut res = Response::builder().status(StatusCode::OK);
|
||||
let hdrs = res.headers_mut().unwrap();
|
||||
hdrs.typed_insert(ContentType::from(mime));
|
||||
hdrs.typed_insert(ETag::from_str(&etag).unwrap());
|
||||
Ok(res.body(Body::from(body)).unwrap())
|
||||
}
|
||||
None => Ok(req.into_response(HttpResponse::NotFound())),
|
||||
None => Ok(StatusCode::NOT_FOUND.into_response()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,30 +1,23 @@
|
||||
use actix_session::{
|
||||
SessionMiddleware,
|
||||
config::CookieContentSecurity,
|
||||
storage::{CookieSessionStore, SessionStore},
|
||||
};
|
||||
use actix_web::{
|
||||
HttpRequest, HttpResponse, Responder,
|
||||
cookie::{Key, SameSite},
|
||||
dev::ServiceResponse,
|
||||
http::{Method, StatusCode, header},
|
||||
middleware::{ErrorHandlerResponse, ErrorHandlers},
|
||||
web::{self, Data, Form, Path, Redirect},
|
||||
};
|
||||
use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use assets::{Assets, EmbedService};
|
||||
// use askama_web::WebTemplate;
|
||||
// use assets::{Assets, EmbedService};
|
||||
use async_trait::async_trait;
|
||||
use axum::{Extension, Router, response::IntoResponse, routing::get};
|
||||
use http::Uri;
|
||||
use rand::{Rng, distributions::Alphanumeric};
|
||||
use routes::{
|
||||
addressbook::{route_addressbook, route_addressbook_restore},
|
||||
calendar::{route_calendar, route_calendar_restore},
|
||||
login::{route_get_login, route_post_login, route_post_logout},
|
||||
};
|
||||
use rustical_oidc::{OidcConfig, OidcServiceConfig, UserStore, configure_oidc};
|
||||
use rustical_oidc::OidcConfig;
|
||||
// use routes::{
|
||||
// addressbook::{route_addressbook, route_addressbook_restore},
|
||||
// calendar::{route_calendar, route_calendar_restore},
|
||||
// login::{route_get_login, route_post_login, route_post_logout},
|
||||
// };
|
||||
// use rustical_oidc::{OidcConfig, OidcServiceConfig, UserStore, configure_oidc};
|
||||
use rustical_store::{
|
||||
Addressbook, AddressbookStore, Calendar, CalendarStore,
|
||||
auth::{AuthenticationMiddleware, AuthenticationProvider, User, user::AppToken},
|
||||
auth::{
|
||||
AuthenticationMiddleware, AuthenticationProvider, User, middleware::AuthenticationLayer,
|
||||
user::AppToken,
|
||||
},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
@@ -40,6 +33,11 @@ pub const ROUTE_USER_NAMED: &str = "frontend_user_named";
|
||||
|
||||
pub use config::{FrontendConfig, generate_frontend_secret};
|
||||
|
||||
use crate::{
|
||||
assets::{Assets, EmbedService},
|
||||
routes::login::{route_get_login, route_post_login},
|
||||
};
|
||||
|
||||
pub fn generate_app_token() -> String {
|
||||
rand::thread_rng()
|
||||
.sample_iter(Alphanumeric)
|
||||
@@ -48,313 +46,312 @@ pub fn generate_app_token() -> String {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
#[template(path = "pages/user.html")]
|
||||
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,
|
||||
}
|
||||
// #[derive(Template, WebTemplate)]
|
||||
// #[template(path = "pages/user.html")]
|
||||
// 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,
|
||||
// }
|
||||
//
|
||||
// async fn route_user_named<CS: CalendarStore, AS: AddressbookStore, AP: AuthenticationProvider>(
|
||||
// path: Path<String>,
|
||||
// cal_store: Data<CS>,
|
||||
// addr_store: Data<AS>,
|
||||
// auth_provider: Data<AP>,
|
||||
// user: User,
|
||||
// req: HttpRequest,
|
||||
// ) -> impl Responder {
|
||||
// let user_id = path.into_inner();
|
||||
// if user_id != user.id {
|
||||
// return actix_web::HttpResponse::Unauthorized().body("Unauthorized");
|
||||
// }
|
||||
//
|
||||
// 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 = 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,
|
||||
// deleted_calendars,
|
||||
// addressbooks,
|
||||
// deleted_addressbooks,
|
||||
// user,
|
||||
// is_apple,
|
||||
// }
|
||||
// .respond_to(&req)
|
||||
// }
|
||||
//
|
||||
// async fn route_get_home(user: User, req: HttpRequest) -> Redirect {
|
||||
// Redirect::to(
|
||||
// req.url_for(ROUTE_USER_NAMED, &[user.id])
|
||||
// .unwrap()
|
||||
// .to_string(),
|
||||
// )
|
||||
// .see_other()
|
||||
// }
|
||||
//
|
||||
// async fn route_root(user: Option<User>, req: HttpRequest) -> impl Responder {
|
||||
// let redirect_url = match user {
|
||||
// Some(_) => req.url_for_static(ROUTE_NAME_HOME).unwrap(),
|
||||
// None => req
|
||||
// .resource_map()
|
||||
// .url_for::<[_; 0], String>(&req, "frontend_login", [])
|
||||
// .unwrap(),
|
||||
// };
|
||||
// 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 { 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.to_owned(), token.clone())
|
||||
// .await?;
|
||||
// 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>(
|
||||
// user: User,
|
||||
// auth_provider: Data<AP>,
|
||||
// path: Path<(String, String)>,
|
||||
// ) -> Result<Redirect, rustical_store::Error> {
|
||||
// let (path_user, token_id) = path.into_inner();
|
||||
// assert_eq!(path_user, user.id);
|
||||
// auth_provider.remove_app_token(&user.id, &token_id).await?;
|
||||
// Ok(Redirect::to("/frontend/user").see_other())
|
||||
// }
|
||||
//
|
||||
// pub(crate) fn unauthorized_handler<B>(
|
||||
// res: ServiceResponse<B>,
|
||||
// ) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
||||
// let (req, _) = res.into_parts();
|
||||
// let redirect_uri = req.uri().to_string();
|
||||
// let mut login_url = req.url_for_static("frontend_login").unwrap();
|
||||
// login_url
|
||||
// .query_pairs_mut()
|
||||
// .append_pair("redirect_uri", &redirect_uri);
|
||||
// let login_url = login_url.to_string();
|
||||
//
|
||||
// let response = HttpResponse::Unauthorized().body(format!(
|
||||
// r#"<!Doctype html>
|
||||
// <html>
|
||||
// <head>
|
||||
// <meta http-equiv="refresh" content="1; url={login_url}" />
|
||||
// </head>
|
||||
// <body>
|
||||
// Unauthorized, redirecting to <a href="{login_url}">login page</a>
|
||||
// </body>
|
||||
// <html>
|
||||
// "#
|
||||
// ));
|
||||
//
|
||||
// let res = ServiceResponse::new(req, response)
|
||||
// .map_into_boxed_body()
|
||||
// .map_into_right_body();
|
||||
//
|
||||
// Ok(ErrorHandlerResponse::Response(res))
|
||||
// }
|
||||
//
|
||||
// pub fn session_middleware(frontend_secret: [u8; 64]) -> SessionMiddleware<impl SessionStore> {
|
||||
// SessionMiddleware::builder(CookieSessionStore::default(), Key::from(&frontend_secret))
|
||||
// .cookie_secure(true)
|
||||
// .cookie_same_site(SameSite::Strict)
|
||||
// .cookie_content_security(CookieContentSecurity::Private)
|
||||
// .build()
|
||||
// }
|
||||
|
||||
async fn route_user_named<CS: CalendarStore, AS: AddressbookStore, AP: AuthenticationProvider>(
|
||||
path: Path<String>,
|
||||
cal_store: Data<CS>,
|
||||
addr_store: Data<AS>,
|
||||
auth_provider: Data<AP>,
|
||||
user: User,
|
||||
req: HttpRequest,
|
||||
) -> impl Responder {
|
||||
let user_id = path.into_inner();
|
||||
if user_id != user.id {
|
||||
return actix_web::HttpResponse::Unauthorized().body("Unauthorized");
|
||||
}
|
||||
|
||||
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 = 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,
|
||||
deleted_calendars,
|
||||
addressbooks,
|
||||
deleted_addressbooks,
|
||||
user,
|
||||
is_apple,
|
||||
}
|
||||
.respond_to(&req)
|
||||
}
|
||||
|
||||
async fn route_get_home(user: User, req: HttpRequest) -> Redirect {
|
||||
Redirect::to(
|
||||
req.url_for(ROUTE_USER_NAMED, &[user.id])
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)
|
||||
.see_other()
|
||||
}
|
||||
|
||||
async fn route_root(user: Option<User>, req: HttpRequest) -> impl Responder {
|
||||
let redirect_url = match user {
|
||||
Some(_) => req.url_for_static(ROUTE_NAME_HOME).unwrap(),
|
||||
None => req
|
||||
.resource_map()
|
||||
.url_for::<[_; 0], String>(&req, "frontend_login", [])
|
||||
.unwrap(),
|
||||
};
|
||||
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 { 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.to_owned(), token.clone())
|
||||
.await?;
|
||||
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>(
|
||||
user: User,
|
||||
auth_provider: Data<AP>,
|
||||
path: Path<(String, String)>,
|
||||
) -> Result<Redirect, rustical_store::Error> {
|
||||
let (path_user, token_id) = path.into_inner();
|
||||
assert_eq!(path_user, user.id);
|
||||
auth_provider.remove_app_token(&user.id, &token_id).await?;
|
||||
Ok(Redirect::to("/frontend/user").see_other())
|
||||
}
|
||||
|
||||
pub(crate) fn unauthorized_handler<B>(
|
||||
res: ServiceResponse<B>,
|
||||
) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
||||
let (req, _) = res.into_parts();
|
||||
let redirect_uri = req.uri().to_string();
|
||||
let mut login_url = req.url_for_static("frontend_login").unwrap();
|
||||
login_url
|
||||
.query_pairs_mut()
|
||||
.append_pair("redirect_uri", &redirect_uri);
|
||||
let login_url = login_url.to_string();
|
||||
|
||||
let response = HttpResponse::Unauthorized().body(format!(
|
||||
r#"<!Doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="1; url={login_url}" />
|
||||
</head>
|
||||
<body>
|
||||
Unauthorized, redirecting to <a href="{login_url}">login page</a>
|
||||
</body>
|
||||
<html>
|
||||
"#
|
||||
));
|
||||
|
||||
let res = ServiceResponse::new(req, response)
|
||||
.map_into_boxed_body()
|
||||
.map_into_right_body();
|
||||
|
||||
Ok(ErrorHandlerResponse::Response(res))
|
||||
}
|
||||
|
||||
pub fn session_middleware(frontend_secret: [u8; 64]) -> SessionMiddleware<impl SessionStore> {
|
||||
SessionMiddleware::builder(CookieSessionStore::default(), Key::from(&frontend_secret))
|
||||
.cookie_secure(true)
|
||||
.cookie_same_site(SameSite::Strict)
|
||||
.cookie_content_security(CookieContentSecurity::Private)
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn configure_frontend<AP: AuthenticationProvider, CS: CalendarStore, AS: AddressbookStore>(
|
||||
cfg: &mut web::ServiceConfig,
|
||||
pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: AddressbookStore>(
|
||||
auth_provider: Arc<AP>,
|
||||
cal_store: Arc<CS>,
|
||||
addr_store: Arc<AS>,
|
||||
frontend_config: FrontendConfig,
|
||||
oidc_config: Option<OidcConfig>,
|
||||
) {
|
||||
let mut scope = web::scope("")
|
||||
.wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler))
|
||||
.wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
||||
.wrap(session_middleware(frontend_config.secret_key))
|
||||
.app_data(Data::from(auth_provider.clone()))
|
||||
.app_data(Data::from(cal_store.clone()))
|
||||
.app_data(Data::from(addr_store.clone()))
|
||||
.app_data(Data::new(frontend_config.clone()))
|
||||
.app_data(Data::new(oidc_config.clone()))
|
||||
.service(EmbedService::<Assets>::new("/assets".to_owned()))
|
||||
.service(web::resource("").route(web::method(Method::GET).to(route_root)))
|
||||
.service(
|
||||
web::resource("/user")
|
||||
.get(route_get_home)
|
||||
.name(ROUTE_NAME_HOME),
|
||||
)
|
||||
.service(
|
||||
web::resource("/user/{user}")
|
||||
.get(route_user_named::<CS, AS, AP>)
|
||||
.name(ROUTE_USER_NAMED),
|
||||
)
|
||||
// App token management
|
||||
.service(web::resource("/user/{user}/app_token").post(route_post_app_token::<AP>))
|
||||
.service(
|
||||
// POST because HTML5 forms don't support DELETE method
|
||||
web::resource("/user/{user}/app_token/{id}/delete").post(route_delete_app_token::<AP>),
|
||||
)
|
||||
// Calendar
|
||||
.service(web::resource("/user/{user}/calendar/{calendar}").get(route_calendar::<CS>))
|
||||
.service(
|
||||
web::resource("/user/{user}/calendar/{calendar}/restore")
|
||||
.post(route_calendar_restore::<CS>),
|
||||
)
|
||||
// Addressbook
|
||||
.service(
|
||||
web::resource("/user/{user}/addressbook/{addressbook}").get(route_addressbook::<AS>),
|
||||
)
|
||||
.service(
|
||||
web::resource("/user/{user}/addressbook/{addressbook}/restore")
|
||||
.post(route_addressbook_restore::<AS>),
|
||||
)
|
||||
// Login
|
||||
.service(
|
||||
web::resource("/login")
|
||||
.name("frontend_login")
|
||||
.get(route_get_login)
|
||||
.post(route_post_login::<AP>),
|
||||
)
|
||||
.service(
|
||||
web::resource("/logout")
|
||||
.name("frontend_logout")
|
||||
.post(route_post_logout),
|
||||
);
|
||||
) -> Router {
|
||||
let mut router = Router::new().layer(AuthenticationLayer::new(auth_provider.clone()));
|
||||
router = router
|
||||
.route("/login", get(route_get_login).post(route_post_login::<AP>))
|
||||
.route_service("/assets/{*file}", EmbedService::<Assets>::new())
|
||||
.layer(Extension(auth_provider.clone()))
|
||||
.layer(Extension(cal_store.clone()))
|
||||
.layer(Extension(addr_store.clone()))
|
||||
.layer(Extension(frontend_config.clone()))
|
||||
.layer(Extension(oidc_config.clone()));
|
||||
// .wrap(session_middleware(frontend_config.secret_key))
|
||||
// .service(web::resource("").route(web::method(Method::GET).to(route_root)))
|
||||
// .service(
|
||||
// web::resource("/user")
|
||||
// .get(route_get_home)
|
||||
// .name(ROUTE_NAME_HOME),
|
||||
// )
|
||||
// .service(
|
||||
// web::resource("/user/{user}")
|
||||
// .get(route_user_named::<CS, AS, AP>)
|
||||
// .name(ROUTE_USER_NAMED),
|
||||
// )
|
||||
// // App token management
|
||||
// .service(web::resource("/user/{user}/app_token").post(route_post_app_token::<AP>))
|
||||
// .service(
|
||||
// // POST because HTML5 forms don't support DELETE method
|
||||
// web::resource("/user/{user}/app_token/{id}/delete").post(route_delete_app_token::<AP>),
|
||||
// )
|
||||
// // Calendar
|
||||
// .service(web::resource("/user/{user}/calendar/{calendar}").get(route_calendar::<CS>))
|
||||
// .service(
|
||||
// web::resource("/user/{user}/calendar/{calendar}/restore")
|
||||
// .post(route_calendar_restore::<CS>),
|
||||
// )
|
||||
// // Addressbook
|
||||
// .service(
|
||||
// web::resource("/user/{user}/addressbook/{addressbook}").get(route_addressbook::<AS>),
|
||||
// )
|
||||
// .service(
|
||||
// web::resource("/user/{user}/addressbook/{addressbook}/restore")
|
||||
// .post(route_addressbook_restore::<AS>),
|
||||
// )
|
||||
// // Login
|
||||
// .service(
|
||||
// web::resource("/login")
|
||||
// .name("frontend_login")
|
||||
// .get(route_get_login)
|
||||
// .post(route_post_login::<AP>),
|
||||
// )
|
||||
// .service(
|
||||
// web::resource("/logout")
|
||||
// .name("frontend_logout")
|
||||
// .post(route_post_logout),
|
||||
// );
|
||||
|
||||
if let Some(oidc_config) = oidc_config {
|
||||
scope = scope.service(web::scope("/login/oidc").configure(|cfg| {
|
||||
configure_oidc(
|
||||
cfg,
|
||||
oidc_config,
|
||||
OidcServiceConfig {
|
||||
default_redirect_route_name: ROUTE_NAME_HOME,
|
||||
session_key_user_id: "user",
|
||||
},
|
||||
Arc::new(OidcUserStore(auth_provider.clone())),
|
||||
)
|
||||
}));
|
||||
}
|
||||
// if let Some(oidc_config) = oidc_config {
|
||||
// scope = scope.service(web::scope("/login/oidc").configure(|cfg| {
|
||||
// configure_oidc(
|
||||
// cfg,
|
||||
// oidc_config,
|
||||
// OidcServiceConfig {
|
||||
// default_redirect_route_name: ROUTE_NAME_HOME,
|
||||
// session_key_user_id: "user",
|
||||
// },
|
||||
// Arc::new(OidcUserStore(auth_provider.clone())),
|
||||
// )
|
||||
// }));
|
||||
// }
|
||||
|
||||
cfg.service(scope);
|
||||
}
|
||||
|
||||
struct OidcUserStore<AP: AuthenticationProvider>(Arc<AP>);
|
||||
|
||||
#[async_trait]
|
||||
impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> {
|
||||
type Error = rustical_store::Error;
|
||||
|
||||
async fn user_exists(&self, id: &str) -> Result<bool, Self::Error> {
|
||||
Ok(self.0.get_principal(id).await?.is_some())
|
||||
}
|
||||
|
||||
async fn insert_user(&self, id: &str) -> Result<(), Self::Error> {
|
||||
self.0
|
||||
.insert_principal(
|
||||
User {
|
||||
id: id.to_owned(),
|
||||
displayname: None,
|
||||
principal_type: Default::default(),
|
||||
password: None,
|
||||
memberships: vec![],
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
router
|
||||
}
|
||||
//
|
||||
// struct OidcUserStore<AP: AuthenticationProvider>(Arc<AP>);
|
||||
//
|
||||
// #[async_trait]
|
||||
// impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> {
|
||||
// type Error = rustical_store::Error;
|
||||
//
|
||||
// async fn user_exists(&self, id: &str) -> Result<bool, Self::Error> {
|
||||
// Ok(self.0.get_principal(id).await?.is_some())
|
||||
// }
|
||||
//
|
||||
// async fn insert_user(&self, id: &str) -> Result<(), Self::Error> {
|
||||
// self.0
|
||||
// .insert_principal(
|
||||
// User {
|
||||
// id: id.to_owned(),
|
||||
// displayname: None,
|
||||
// principal_type: Default::default(),
|
||||
// password: None,
|
||||
// memberships: vec![],
|
||||
// },
|
||||
// false,
|
||||
// )
|
||||
// .await
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
use actix_web::{
|
||||
http::StatusCode,
|
||||
middleware::ErrorHandlers,
|
||||
web::{self, Data, ServiceConfig},
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use routes::{get_nextcloud_flow, post_nextcloud_flow, post_nextcloud_login, post_nextcloud_poll};
|
||||
// use routes::{get_nextcloud_flow, post_nextcloud_flow, post_nextcloud_login, post_nextcloud_poll};
|
||||
use rustical_store::auth::{AuthenticationMiddleware, AuthenticationProvider};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
mod routes;
|
||||
// mod routes;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct NextcloudFlow {
|
||||
@@ -47,32 +42,32 @@ pub struct NextcloudFlows {
|
||||
flows: RwLock<HashMap<String, NextcloudFlow>>,
|
||||
}
|
||||
|
||||
use crate::{session_middleware, unauthorized_handler};
|
||||
|
||||
pub fn configure_nextcloud_login<AP: AuthenticationProvider>(
|
||||
cfg: &mut ServiceConfig,
|
||||
nextcloud_flows_state: Arc<NextcloudFlows>,
|
||||
auth_provider: Arc<AP>,
|
||||
frontend_secret: [u8; 64],
|
||||
) {
|
||||
cfg.service(
|
||||
web::scope("/index.php/login/v2")
|
||||
.wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler))
|
||||
.wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
||||
.wrap(session_middleware(frontend_secret))
|
||||
.app_data(Data::from(nextcloud_flows_state))
|
||||
.app_data(Data::from(auth_provider.clone()))
|
||||
.service(web::resource("").post(post_nextcloud_login))
|
||||
.service(
|
||||
web::resource("/poll/{flow}")
|
||||
.name("nc_login_poll")
|
||||
.post(post_nextcloud_poll::<AP>),
|
||||
)
|
||||
.service(
|
||||
web::resource("/flow/{flow}")
|
||||
.name("nc_login_flow")
|
||||
.get(get_nextcloud_flow)
|
||||
.post(post_nextcloud_flow),
|
||||
),
|
||||
);
|
||||
}
|
||||
// use crate::{session_middleware, unauthorized_handler};
|
||||
//
|
||||
// pub fn configure_nextcloud_login<AP: AuthenticationProvider>(
|
||||
// cfg: &mut ServiceConfig,
|
||||
// nextcloud_flows_state: Arc<NextcloudFlows>,
|
||||
// auth_provider: Arc<AP>,
|
||||
// frontend_secret: [u8; 64],
|
||||
// ) {
|
||||
// cfg.service(
|
||||
// web::scope("/index.php/login/v2")
|
||||
// .wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler))
|
||||
// .wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
||||
// .wrap(session_middleware(frontend_secret))
|
||||
// .app_data(Data::from(nextcloud_flows_state))
|
||||
// .app_data(Data::from(auth_provider.clone()))
|
||||
// .service(web::resource("").post(post_nextcloud_login))
|
||||
// .service(
|
||||
// web::resource("/poll/{flow}")
|
||||
// .name("nc_login_poll")
|
||||
// .post(post_nextcloud_poll::<AP>),
|
||||
// )
|
||||
// .service(
|
||||
// web::resource("/flow/{flow}")
|
||||
// .name("nc_login_flow")
|
||||
// .get(get_nextcloud_flow)
|
||||
// .post(post_nextcloud_flow),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{FrontendConfig, OidcConfig};
|
||||
use actix_session::Session;
|
||||
use actix_web::{
|
||||
HttpRequest, HttpResponse, Responder,
|
||||
error::{ErrorNotFound, ErrorUnauthorized},
|
||||
web::{Data, Form, Query, Redirect},
|
||||
};
|
||||
use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use rustical_oidc::ROUTE_NAME_OIDC_LOGIN;
|
||||
use axum::{
|
||||
Extension, Form,
|
||||
extract::{OriginalUri, Query},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use http::StatusCode;
|
||||
use rustical_store::auth::AuthenticationProvider;
|
||||
use serde::Deserialize;
|
||||
use tracing::instrument;
|
||||
@@ -30,29 +31,28 @@ pub struct GetLoginQuery {
|
||||
redirect_uri: Option<String>,
|
||||
}
|
||||
|
||||
#[instrument(skip(req, config, oidc_config))]
|
||||
#[instrument(skip(config, oidc_config))]
|
||||
pub async fn route_get_login(
|
||||
Query(GetLoginQuery { redirect_uri }): Query<GetLoginQuery>,
|
||||
req: HttpRequest,
|
||||
config: Data<FrontendConfig>,
|
||||
oidc_config: Data<Option<OidcConfig>>,
|
||||
) -> HttpResponse {
|
||||
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(),
|
||||
});
|
||||
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(),
|
||||
// });
|
||||
LoginPage {
|
||||
redirect_uri,
|
||||
allow_password_login: config.allow_password_login,
|
||||
oidc_data,
|
||||
oidc_data: None,
|
||||
}
|
||||
.respond_to(&req)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -62,43 +62,38 @@ pub struct PostLoginForm {
|
||||
redirect_uri: Option<String>,
|
||||
}
|
||||
|
||||
#[instrument(skip(req, password, auth_provider, session, config))]
|
||||
// #[instrument(skip(password, auth_provider, config))]
|
||||
pub async fn route_post_login<AP: AuthenticationProvider>(
|
||||
req: HttpRequest,
|
||||
Extension(auth_provider): Extension<Arc<AP>>,
|
||||
Extension(config): Extension<FrontendConfig>,
|
||||
OriginalUri(orig_uri): OriginalUri,
|
||||
Form(PostLoginForm {
|
||||
username,
|
||||
password,
|
||||
redirect_uri,
|
||||
}): Form<PostLoginForm>,
|
||||
session: Session,
|
||||
auth_provider: Data<AP>,
|
||||
config: Data<FrontendConfig>,
|
||||
) -> HttpResponse {
|
||||
) -> Response {
|
||||
if !config.allow_password_login {
|
||||
return ErrorNotFound("Password authentication disabled").error_response();
|
||||
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 = req
|
||||
.full_url()
|
||||
.join(&redirect_uri)
|
||||
.ok()
|
||||
.and_then(|uri| req.full_url().make_relative(&uri))
|
||||
.unwrap_or(default_redirect);
|
||||
// let redirect_uri = orig_uri
|
||||
// .join(&redirect_uri)
|
||||
// .ok()
|
||||
// .and_then(|uri| orig_uri.make_relative(&uri))
|
||||
// .unwrap_or(default_redirect);
|
||||
|
||||
if let Ok(Some(user)) = auth_provider.validate_password(&username, &password).await {
|
||||
session.insert("user", user.id).unwrap();
|
||||
Redirect::to(redirect_uri)
|
||||
.see_other()
|
||||
.respond_to(&req)
|
||||
.map_into_boxed_body()
|
||||
// session.insert("user", user.id).unwrap();
|
||||
Redirect::to(&redirect_uri).into_response()
|
||||
} else {
|
||||
ErrorUnauthorized("Unauthorized").error_response()
|
||||
StatusCode::UNAUTHORIZED.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn route_post_logout(req: HttpRequest, session: Session) -> Redirect {
|
||||
session.remove("user");
|
||||
Redirect::to(req.url_for_static("frontend_login").unwrap().to_string()).see_other()
|
||||
pub async fn route_post_logout() -> Redirect {
|
||||
// session.remove("user");
|
||||
Redirect::to("/")
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod addressbook;
|
||||
pub mod calendar;
|
||||
// pub mod addressbook;
|
||||
// pub mod calendar;
|
||||
pub mod login;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user