Can set Matrix access token
This commit is contained in:
		
							
								
								
									
										2
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -1616,7 +1616,9 @@ dependencies = [ | ||||
|  "rust-embed", | ||||
|  "rust-s3", | ||||
|  "serde", | ||||
|  "serde_json", | ||||
|  "thiserror 2.0.11", | ||||
|  "urlencoding", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
|   | ||||
| @@ -10,7 +10,8 @@ clap = { version = "4.5.26", features = ["derive", "env"] } | ||||
| lazy_static = "1.5.0" | ||||
| anyhow = "1.0.95" | ||||
| serde = { version = "1.0.217", features = ["derive"] } | ||||
| rust-s3 = "0.36.0-beta.2" | ||||
| serde_json = "1.0.137" | ||||
| rust-s3 = { version = "0.36.0-beta.2", features = ["tokio"] } | ||||
| actix-web = "4" | ||||
| actix-session = { version = "0.10.1", features = ["redis-session"] } | ||||
| light-openid = "1.0.2" | ||||
| @@ -19,3 +20,4 @@ rand = "0.9.0-beta.3" | ||||
| rust-embed = "8.5.0" | ||||
| mime_guess = "2.0.5" | ||||
| askama = "0.12.1" | ||||
| urlencoding = "2.1.3" | ||||
| @@ -75,11 +75,11 @@ pub struct AppConfig { | ||||
|     s3_endpoint: String, | ||||
|  | ||||
|     /// S3 access key | ||||
|     #[arg(long, env, default_value = "topsecret")] | ||||
|     #[arg(long, env, default_value = "minioadmin")] | ||||
|     s3_access_key: String, | ||||
|  | ||||
|     /// S3 secret key | ||||
|     #[arg(long, env, default_value = "topsecret")] | ||||
|     #[arg(long, env, default_value = "minioadmin")] | ||||
|     s3_secret_key: String, | ||||
|  | ||||
|     /// S3 skip auto create bucket if not existing | ||||
|   | ||||
| @@ -4,11 +4,16 @@ use actix_web::cookie::Key; | ||||
| use actix_web::{web, App, HttpServer}; | ||||
| use matrix_gateway::app_config::AppConfig; | ||||
| use matrix_gateway::server::web_ui; | ||||
| use matrix_gateway::user::UserConfig; | ||||
|  | ||||
| #[actix_web::main] | ||||
| async fn main() -> std::io::Result<()> { | ||||
|     env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); | ||||
|  | ||||
|     UserConfig::create_bucket_if_required() | ||||
|         .await | ||||
|         .expect("Failed to create bucket!"); | ||||
|  | ||||
|     // FIXME : not scalable | ||||
|     let secret_key = Key::generate(); | ||||
|  | ||||
| @@ -33,6 +38,7 @@ async fn main() -> std::io::Result<()> { | ||||
|             // Web configuration routes | ||||
|             .route("/assets/{tail:.*}", web::get().to(web_ui::static_file)) | ||||
|             .route("/", web::get().to(web_ui::home)) | ||||
|             .route("/", web::post().to(web_ui::home)) | ||||
|             .route("/oidc_cb", web::get().to(web_ui::oidc_cb)) | ||||
|             .route("/sign_out", web::get().to(web_ui::sign_out)) | ||||
|  | ||||
|   | ||||
| @@ -16,6 +16,8 @@ pub enum HttpFailure { | ||||
|     SessionError(#[from] actix_session::SessionGetError), | ||||
|     #[error("an unspecified open id error occurred: {0}")] | ||||
|     OpenID(Box<dyn Error>), | ||||
|     #[error("an error occurred while fetching user configuration: {0}")] | ||||
|     FetchUserConfig(anyhow::Error), | ||||
|     #[error("an unspecified internal error occurred: {0}")] | ||||
|     InternalError(#[from] anyhow::Error), | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| use crate::app_config::AppConfig; | ||||
| use crate::constants::{STATE_KEY, USER_SESSION_KEY}; | ||||
| use crate::server::{HttpFailure, HttpResult}; | ||||
| use crate::user::{User, UserID}; | ||||
| use crate::user::{User, UserConfig, UserID}; | ||||
| use crate::utils; | ||||
| use actix_session::Session; | ||||
| use actix_web::{web, HttpResponse}; | ||||
| @@ -32,10 +32,21 @@ pub async fn static_file(path: web::Path<String>) -> HttpResult { | ||||
| struct HomeTemplate { | ||||
|     name: String, | ||||
|     matrix_token: String, | ||||
|     success_message: Option<String>, | ||||
|     error_message: Option<String>, | ||||
| } | ||||
|  | ||||
| /// Update matrix token request | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct UpdateMatrixToken { | ||||
|     new_matrix_token: Option<String>, | ||||
| } | ||||
|  | ||||
| /// Main route | ||||
| pub async fn home(session: Session) -> HttpResult { | ||||
| pub async fn home( | ||||
|     session: Session, | ||||
|     update_matrix_token: Option<web::Form<UpdateMatrixToken>>, | ||||
| ) -> HttpResult { | ||||
|     // Get user information, requesting authentication if information is missing | ||||
|     let Some(user): Option<User> = session.get(USER_SESSION_KEY)? else { | ||||
|         // Generate auth state | ||||
| @@ -54,12 +65,37 @@ pub async fn home(session: Session) -> HttpResult { | ||||
|             .finish()); | ||||
|     }; | ||||
|  | ||||
|     let mut success_message = None; | ||||
|     let mut error_message = None; | ||||
|  | ||||
|     // Retrieve user configuration | ||||
|     let mut config = UserConfig::load(&user.id) | ||||
|         .await | ||||
|         .map_err(HttpFailure::FetchUserConfig)?; | ||||
|  | ||||
|     // Update matrix token, if requested | ||||
|     if let Some(update_matrix_token) = update_matrix_token { | ||||
|         if let Some(t) = update_matrix_token.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()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Render page | ||||
|     Ok(HttpResponse::Ok() | ||||
|         .insert_header(("content-type", "text/html")) | ||||
|         .body( | ||||
|             HomeTemplate { | ||||
|                 name: user.name, | ||||
|                 matrix_token: "TODO".to_string(), | ||||
|                 matrix_token: config.obfuscated_matrix_token(), | ||||
|                 success_message, | ||||
|                 error_message, | ||||
|             } | ||||
|             .render() | ||||
|             .unwrap(), | ||||
|   | ||||
							
								
								
									
										126
									
								
								src/user.rs
									
									
									
									
									
								
							
							
						
						
									
										126
									
								
								src/user.rs
									
									
									
									
									
								
							| @@ -1,9 +1,135 @@ | ||||
| use crate::app_config::AppConfig; | ||||
| use crate::utils::curr_time; | ||||
| use s3::error::S3Error; | ||||
| use s3::request::ResponseData; | ||||
| use s3::{Bucket, BucketConfiguration}; | ||||
| use thiserror::Error; | ||||
|  | ||||
| #[derive(Error, Debug)] | ||||
| pub enum UserError { | ||||
|     #[error("failed to fetch user configuration: {0}")] | ||||
|     FetchUserConfig(S3Error), | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct UserID(pub String); | ||||
|  | ||||
| impl UserID { | ||||
|     fn conf_path_in_bucket(&self) -> String { | ||||
|         format!("confs/{}.json", urlencoding::encode(&self.0)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct User { | ||||
|     pub id: UserID, | ||||
|     pub name: String, | ||||
|     pub email: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| pub struct UserConfig { | ||||
|     /// Target user ID | ||||
|     pub user_id: UserID, | ||||
|  | ||||
|     /// Configuration creation time | ||||
|     pub created: u64, | ||||
|  | ||||
|     /// Configuration last update time | ||||
|     pub updated: u64, | ||||
|  | ||||
|     /// Current user matrix token | ||||
|     pub matrix_token: String, | ||||
| } | ||||
|  | ||||
| impl UserConfig { | ||||
|     /// Create S3 bucket if required | ||||
|     pub async fn create_bucket_if_required() -> anyhow::Result<()> { | ||||
|         if AppConfig::get().s3_skip_auto_create_bucket { | ||||
|             log::debug!("Skipping bucket existence check"); | ||||
|             return Ok(()); | ||||
|         } | ||||
|  | ||||
|         let bucket = AppConfig::get().s3_bucket()?; | ||||
|  | ||||
|         match bucket.location().await { | ||||
|             Ok(_) => { | ||||
|                 log::debug!("The bucket already exists."); | ||||
|                 return Ok(()); | ||||
|             } | ||||
|             Err(S3Error::HttpFailWithBody(404, s)) if s.contains("<Code>NoSuchKey</Code>") => { | ||||
|                 log::warn!("Failed to fetch bucket location, but it seems that bucket exists."); | ||||
|                 return Ok(()); | ||||
|             } | ||||
|             Err(S3Error::HttpFailWithBody(404, s)) if s.contains("<Code>NoSuchBucket</Code>") => { | ||||
|                 log::warn!("The bucket does not seem to exists, trying to create it!") | ||||
|             } | ||||
|             Err(e) => { | ||||
|                 log::error!("Got unexpected error when querying bucket info: {}", e); | ||||
|                 return Err(e.into()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Bucket::create_with_path_style( | ||||
|             &bucket.name, | ||||
|             bucket.region, | ||||
|             AppConfig::get().s3_credentials()?, | ||||
|             BucketConfiguration::private(), | ||||
|         ) | ||||
|         .await?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Get current user configuration | ||||
|     pub async fn load(user_id: &UserID) -> anyhow::Result<Self> { | ||||
|         let res: Result<ResponseData, S3Error> = AppConfig::get() | ||||
|             .s3_bucket()? | ||||
|             .get_object(user_id.conf_path_in_bucket()) | ||||
|             .await; | ||||
|  | ||||
|         match res { | ||||
|             Ok(res) => Ok(serde_json::from_slice(res.as_slice())?), | ||||
|             Err(S3Error::HttpFailWithBody(404, _)) => { | ||||
|                 log::warn!("User configuration does not exists, generating a new one..."); | ||||
|                 Ok(Self { | ||||
|                     user_id: user_id.clone(), | ||||
|                     created: curr_time()?, | ||||
|                     updated: curr_time()?, | ||||
|                     matrix_token: "".to_string(), | ||||
|                 }) | ||||
|             } | ||||
|             Err(e) => Err(UserError::FetchUserConfig(e).into()), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Set user configuration | ||||
|     pub async fn save(&mut self) -> anyhow::Result<()> { | ||||
|         log::info!("Saving new configuration for user {:?}", self.user_id); | ||||
|  | ||||
|         self.updated = curr_time()?; | ||||
|  | ||||
|         // Save updated configuration | ||||
|         AppConfig::get() | ||||
|             .s3_bucket()? | ||||
|             .put_object( | ||||
|                 self.user_id.conf_path_in_bucket(), | ||||
|                 &serde_json::to_vec(self)?, | ||||
|             ) | ||||
|             .await?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Get current user matrix token, in an obfuscated form | ||||
|     pub fn obfuscated_matrix_token(&self) -> String { | ||||
|         self.matrix_token | ||||
|             .chars() | ||||
|             .enumerate() | ||||
|             .map(|(num, c)| match num { | ||||
|                 0 | 1 => c, | ||||
|                 _ => 'X', | ||||
|             }) | ||||
|             .collect() | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										10
									
								
								src/utils.rs
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								src/utils.rs
									
									
									
									
									
								
							| @@ -1,6 +1,14 @@ | ||||
| use rand::distr::{Alphanumeric, SampleString}; | ||||
| use std::time::{SystemTime, UNIX_EPOCH}; | ||||
|  | ||||
| // Generate a random string of a given size | ||||
| /// Generate a random string of a given size | ||||
| pub fn rand_str(len: usize) -> String { | ||||
|     Alphanumeric.sample_string(&mut rand::rng(), len) | ||||
| } | ||||
|  | ||||
| /// Get current time | ||||
| pub fn curr_time() -> anyhow::Result<u64> { | ||||
|     Ok(SystemTime::now() | ||||
|         .duration_since(UNIX_EPOCH) | ||||
|         .map(|t| t.as_secs())?) | ||||
| } | ||||
|   | ||||
| @@ -32,6 +32,20 @@ | ||||
|  | ||||
|  | ||||
| <div class="body-content"> | ||||
|     <!-- Success message --> | ||||
|     {% if let Some(msg) = success_message %} | ||||
|     <div class="alert alert-success"> | ||||
|         {{ msg }} | ||||
|     </div> | ||||
|     {% endif %} | ||||
|  | ||||
|     <!-- Error message --> | ||||
|     {% if let Some(msg) = error_message %} | ||||
|     <div class="alert alert-danger"> | ||||
|         {{ msg }} | ||||
|     </div> | ||||
|     {% endif %} | ||||
|  | ||||
|     <!-- Matrix authentication token --> | ||||
|     <div class="card border-light mb-3"> | ||||
|         <div class="card-header">Matrix authentication token</div> | ||||
| @@ -50,11 +64,11 @@ | ||||
|  | ||||
|             <p>Tip: you can rename the session to easily identify it among all your other sessions!</p> | ||||
|  | ||||
|             <form action="/set_matrix_access_token"> | ||||
|             <form action="/" method="post"> | ||||
|                 <div> | ||||
|                     <label for="accessTokenInput" class="form-label mt-4">New Matrix access token</label> | ||||
|                     <input type="text" class="form-control" id="accessTokenInput" aria-describedby="tokenHelp" | ||||
|                            placeholder="{{ matrix_token }}" required minlength="2"/> | ||||
|                            placeholder="{{ matrix_token }}" required minlength="2" name="new_matrix_token"/> | ||||
|                     <small id="tokenHelp" class="form-text text-muted">Changing this value will reset all active | ||||
|                         connections | ||||
|                         to Matrix GW.</small> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user