use crate::app_config::AppConfig; use crate::constants::{STATE_KEY, USER_SESSION_KEY}; use crate::server::{HttpFailure, HttpResult}; use crate::user::{APIClient, APIClientID, User, UserConfig, UserID}; use crate::utils; use actix_session::Session; use actix_web::{web, HttpResponse}; use askama::Template; use ipnet::IpNet; use light_openid::primitives::OpenIDConfig; use std::str::FromStr; /// Static assets #[derive(rust_embed::Embed)] #[folder = "assets/"] struct Assets; /// Serve static file pub async fn static_file(path: web::Path<String>) -> HttpResult { match Assets::get(path.as_ref()) { Some(content) => Ok(HttpResponse::Ok() .content_type( mime_guess::from_path(path.as_str()) .first_or_octet_stream() .as_ref(), ) .body(content.data.into_owned())), None => Ok(HttpResponse::NotFound().body("404 Not Found")), } } #[derive(askama::Template)] #[template(path = "index.html")] struct HomeTemplate { name: String, user_id: UserID, matrix_token: String, clients: Vec<APIClient>, success_message: Option<String>, error_message: Option<String>, } /// HTTP form request #[derive(serde::Deserialize)] pub struct FormRequest { /// Update matrix token new_matrix_token: Option<String>, /// Create a new client new_client_desc: Option<String>, /// Restrict new client to a given network ip_network: Option<String>, /// Grant read only access to client readonly_client: Option<String>, /// Delete a specified client id delete_client_id: Option<APIClientID>, } /// Main route pub async fn home(session: Session, form_req: Option<web::Form<FormRequest>>) -> HttpResult { // Get user information, requesting authentication if information is missing let Some(user): Option<User> = session.get(USER_SESSION_KEY)? else { // Generate auth state let state = utils::rand_str(50); session.insert(STATE_KEY, &state)?; let oidc = AppConfig::get().openid_provider(); let config = OpenIDConfig::load_from_url(oidc.configuration_url) .await .map_err(HttpFailure::OpenID)?; let auth_url = config.gen_authorization_url(oidc.client_id, &state, &oidc.redirect_url); return Ok(HttpResponse::Found() .append_header(("location", auth_url)) .finish()); }; let mut success_message = None; let mut error_message = None; // Retrieve user configuration let mut config = UserConfig::load(&user.id, true) .await .map_err(HttpFailure::FetchUserConfig)?; if let Some(form_req) = form_req { // Update matrix token, if requested if let Some(t) = form_req.0.new_matrix_token { if t.len() < 3 { error_message = Some("Specified Matrix token is too short!".to_string()); } else { // TODO : invalidate all existing connections config.matrix_token = t; config.save().await?; success_message = Some("Matrix token was successfully updated!".to_string()); } } // Create a new client, if requested if let Some(new_token_desc) = form_req.0.new_client_desc { let ip_net = match form_req.0.ip_network.as_deref() { None | Some("") => None, Some(e) => match IpNet::from_str(e) { Ok(n) => Some(n), Err(e) => { log::error!("Failed to parse IP network provided by user: {e}"); error_message = Some(format!("Failed to parse restricted IP network: {e}")); None } }, }; if error_message.is_none() { let mut token = APIClient::generate(new_token_desc, ip_net); token.readonly_client = form_req.0.readonly_client.is_some(); success_message = Some(format!("The secret of your new token is '{}'. Be sure to write it somewhere as you will not be able to recover it later!", token.secret)); config.clients.push(token); config.save().await?; } } // Delete a client if let Some(delete_client_id) = form_req.0.delete_client_id { config.clients.retain(|c| c.id != delete_client_id); config.save().await?; success_message = Some("The client was successfully deleted!".to_string()); // TODO : close connections with given id } } // Render page Ok(HttpResponse::Ok() .insert_header(("content-type", "text/html")) .body( HomeTemplate { name: user.name, user_id: user.id, matrix_token: config.obfuscated_matrix_token(), clients: config.clients, success_message, error_message, } .render() .unwrap(), )) } #[derive(serde::Deserialize)] pub struct AuthCallbackQuery { code: String, state: String, } /// Authenticate user callback pub async fn oidc_cb(session: Session, query: web::Query<AuthCallbackQuery>) -> HttpResult { if session.get(STATE_KEY)? != Some(query.state.to_string()) { return Ok(HttpResponse::BadRequest() .append_header(("content-type", "text/html")) .body("State mismatch! <a href='/'>Try again</a>")); } let oidc = AppConfig::get().openid_provider(); let config = OpenIDConfig::load_from_url(oidc.configuration_url) .await .map_err(HttpFailure::OpenID)?; let (token, _) = match config .request_token( oidc.client_id, oidc.client_secret, &query.code, &oidc.redirect_url, ) .await { Ok(t) => t, Err(e) => { log::error!("Failed to request user token! {e}"); return Ok(HttpResponse::BadRequest() .append_header(("content-type", "text/html")) .body("Authentication failed! <a href='/'>Try again</a>")); } }; let (user, _) = config .request_user_info(&token) .await .map_err(HttpFailure::OpenID)?; let user = User { id: UserID(user.sub), name: user.name.unwrap_or("no_name".to_string()), email: user.email.unwrap_or("no@mail.com".to_string()), }; log::info!("Successful authentication as {:?}", user); session.insert(USER_SESSION_KEY, user)?; Ok(HttpResponse::Found() .insert_header(("location", "/")) .finish()) } /// De-authenticate user pub async fn sign_out(session: Session) -> HttpResult { session.remove(USER_SESSION_KEY); Ok(HttpResponse::Found() .insert_header(("location", "/")) .finish()) }