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