Can set Matrix access token
This commit is contained in:
parent
78700d0be5
commit
bfbc2a690b
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1616,7 +1616,9 @@ dependencies = [
|
|||||||
"rust-embed",
|
"rust-embed",
|
||||||
"rust-s3",
|
"rust-s3",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"thiserror 2.0.11",
|
"thiserror 2.0.11",
|
||||||
|
"urlencoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -10,7 +10,8 @@ clap = { version = "4.5.26", features = ["derive", "env"] }
|
|||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
anyhow = "1.0.95"
|
anyhow = "1.0.95"
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
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-web = "4"
|
||||||
actix-session = { version = "0.10.1", features = ["redis-session"] }
|
actix-session = { version = "0.10.1", features = ["redis-session"] }
|
||||||
light-openid = "1.0.2"
|
light-openid = "1.0.2"
|
||||||
@ -19,3 +20,4 @@ rand = "0.9.0-beta.3"
|
|||||||
rust-embed = "8.5.0"
|
rust-embed = "8.5.0"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
askama = "0.12.1"
|
askama = "0.12.1"
|
||||||
|
urlencoding = "2.1.3"
|
@ -75,11 +75,11 @@ pub struct AppConfig {
|
|||||||
s3_endpoint: String,
|
s3_endpoint: String,
|
||||||
|
|
||||||
/// S3 access key
|
/// S3 access key
|
||||||
#[arg(long, env, default_value = "topsecret")]
|
#[arg(long, env, default_value = "minioadmin")]
|
||||||
s3_access_key: String,
|
s3_access_key: String,
|
||||||
|
|
||||||
/// S3 secret key
|
/// S3 secret key
|
||||||
#[arg(long, env, default_value = "topsecret")]
|
#[arg(long, env, default_value = "minioadmin")]
|
||||||
s3_secret_key: String,
|
s3_secret_key: String,
|
||||||
|
|
||||||
/// S3 skip auto create bucket if not existing
|
/// S3 skip auto create bucket if not existing
|
||||||
|
@ -4,11 +4,16 @@ use actix_web::cookie::Key;
|
|||||||
use actix_web::{web, App, HttpServer};
|
use actix_web::{web, App, HttpServer};
|
||||||
use matrix_gateway::app_config::AppConfig;
|
use matrix_gateway::app_config::AppConfig;
|
||||||
use matrix_gateway::server::web_ui;
|
use matrix_gateway::server::web_ui;
|
||||||
|
use matrix_gateway::user::UserConfig;
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
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
|
// FIXME : not scalable
|
||||||
let secret_key = Key::generate();
|
let secret_key = Key::generate();
|
||||||
|
|
||||||
@ -33,6 +38,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
// Web configuration routes
|
// Web configuration routes
|
||||||
.route("/assets/{tail:.*}", web::get().to(web_ui::static_file))
|
.route("/assets/{tail:.*}", web::get().to(web_ui::static_file))
|
||||||
.route("/", web::get().to(web_ui::home))
|
.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("/oidc_cb", web::get().to(web_ui::oidc_cb))
|
||||||
.route("/sign_out", web::get().to(web_ui::sign_out))
|
.route("/sign_out", web::get().to(web_ui::sign_out))
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ pub enum HttpFailure {
|
|||||||
SessionError(#[from] actix_session::SessionGetError),
|
SessionError(#[from] actix_session::SessionGetError),
|
||||||
#[error("an unspecified open id error occurred: {0}")]
|
#[error("an unspecified open id error occurred: {0}")]
|
||||||
OpenID(Box<dyn Error>),
|
OpenID(Box<dyn Error>),
|
||||||
|
#[error("an error occurred while fetching user configuration: {0}")]
|
||||||
|
FetchUserConfig(anyhow::Error),
|
||||||
#[error("an unspecified internal error occurred: {0}")]
|
#[error("an unspecified internal error occurred: {0}")]
|
||||||
InternalError(#[from] anyhow::Error),
|
InternalError(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::app_config::AppConfig;
|
use crate::app_config::AppConfig;
|
||||||
use crate::constants::{STATE_KEY, USER_SESSION_KEY};
|
use crate::constants::{STATE_KEY, USER_SESSION_KEY};
|
||||||
use crate::server::{HttpFailure, HttpResult};
|
use crate::server::{HttpFailure, HttpResult};
|
||||||
use crate::user::{User, UserID};
|
use crate::user::{User, UserConfig, UserID};
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
use actix_session::Session;
|
use actix_session::Session;
|
||||||
use actix_web::{web, HttpResponse};
|
use actix_web::{web, HttpResponse};
|
||||||
@ -32,10 +32,21 @@ pub async fn static_file(path: web::Path<String>) -> HttpResult {
|
|||||||
struct HomeTemplate {
|
struct HomeTemplate {
|
||||||
name: String,
|
name: String,
|
||||||
matrix_token: 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
|
/// 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
|
// Get user information, requesting authentication if information is missing
|
||||||
let Some(user): Option<User> = session.get(USER_SESSION_KEY)? else {
|
let Some(user): Option<User> = session.get(USER_SESSION_KEY)? else {
|
||||||
// Generate auth state
|
// Generate auth state
|
||||||
@ -54,12 +65,37 @@ pub async fn home(session: Session) -> HttpResult {
|
|||||||
.finish());
|
.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()
|
Ok(HttpResponse::Ok()
|
||||||
.insert_header(("content-type", "text/html"))
|
.insert_header(("content-type", "text/html"))
|
||||||
.body(
|
.body(
|
||||||
HomeTemplate {
|
HomeTemplate {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
matrix_token: "TODO".to_string(),
|
matrix_token: config.obfuscated_matrix_token(),
|
||||||
|
success_message,
|
||||||
|
error_message,
|
||||||
}
|
}
|
||||||
.render()
|
.render()
|
||||||
.unwrap(),
|
.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)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
pub struct UserID(pub String);
|
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)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: UserID,
|
pub id: UserID,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub email: 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 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 {
|
pub fn rand_str(len: usize) -> String {
|
||||||
Alphanumeric.sample_string(&mut rand::rng(), len)
|
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">
|
<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 -->
|
<!-- Matrix authentication token -->
|
||||||
<div class="card border-light mb-3">
|
<div class="card border-light mb-3">
|
||||||
<div class="card-header">Matrix authentication token</div>
|
<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>
|
<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>
|
<div>
|
||||||
<label for="accessTokenInput" class="form-label mt-4">New Matrix access token</label>
|
<label for="accessTokenInput" class="form-label mt-4">New Matrix access token</label>
|
||||||
<input type="text" class="form-control" id="accessTokenInput" aria-describedby="tokenHelp"
|
<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
|
<small id="tokenHelp" class="form-text text-muted">Changing this value will reset all active
|
||||||
connections
|
connections
|
||||||
to Matrix GW.</small>
|
to Matrix GW.</small>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user