diff --git a/geneit_backend/Cargo.lock b/geneit_backend/Cargo.lock index a28ecb5..3cdfa97 100644 --- a/geneit_backend/Cargo.lock +++ b/geneit_backend/Cargo.lock @@ -771,6 +771,7 @@ dependencies = [ "clap", "diesel", "env_logger", + "futures-util", "lazy_static", "lettre", "log", diff --git a/geneit_backend/Cargo.toml b/geneit_backend/Cargo.toml index 3bfb90d..0a39195 100644 --- a/geneit_backend/Cargo.toml +++ b/geneit_backend/Cargo.toml @@ -12,6 +12,7 @@ clap = { version = "4.3.0", features = ["derive", "env"] } lazy_static = "1.4.0" anyhow = "1.0.71" actix-web = "4.3.1" +futures-util = "0.3.28" diesel = { version = "2.0.4", features = ["postgres"] } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" diff --git a/geneit_backend/src/controllers/mod.rs b/geneit_backend/src/controllers/mod.rs index 1ef5a31..a5cc874 100644 --- a/geneit_backend/src/controllers/mod.rs +++ b/geneit_backend/src/controllers/mod.rs @@ -6,6 +6,7 @@ use std::fmt::{Debug, Display, Formatter}; pub mod auth_controller; pub mod config_controller; +pub mod user_controller; /// Custom error to ease controller writing #[derive(Debug)] diff --git a/geneit_backend/src/controllers/user_controller.rs b/geneit_backend/src/controllers/user_controller.rs new file mode 100644 index 0000000..e3620f0 --- /dev/null +++ b/geneit_backend/src/controllers/user_controller.rs @@ -0,0 +1,15 @@ +//! # User controller +//! +//! The actions of the user on his account when he is authenticated. + +use crate::controllers::HttpResult; +use crate::services::login_token_service::LoginToken; +use crate::services::users_service; +use actix_web::HttpResponse; + +/// Get account information +pub async fn auth_info(token: LoginToken) -> HttpResult { + let user = users_service::get_by_id(token.user_id).await?; + + Ok(HttpResponse::Ok().json(user)) +} diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 0b628ca..7b2151d 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -2,7 +2,7 @@ use actix_remote_ip::RemoteIPConfig; use actix_web::middleware::Logger; use actix_web::{web, App, HttpServer}; use geneit_backend::app_config::AppConfig; -use geneit_backend::controllers::{auth_controller, config_controller}; +use geneit_backend::controllers::{auth_controller, config_controller, user_controller}; #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -43,6 +43,8 @@ async fn main() -> std::io::Result<()> { "/auth/password_login", web::post().to(auth_controller::password_login), ) + // User controller + .route("/user/info", web::get().to(user_controller::auth_info)) }) .bind(AppConfig::get().listen_address.as_str())? .run() diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index 2c94f5c..27866a0 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -5,14 +5,17 @@ use diesel::prelude::*; #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] pub struct UserID(pub i32); -#[derive(Queryable, Debug)] +#[derive(Queryable, Debug, serde::Serialize)] pub struct User { pub id: i32, pub name: String, pub email: String, + #[serde(skip_serializing)] pub password: Option, + #[serde(skip_serializing)] pub reset_password_token: Option, pub time_create: i64, + #[serde(skip_serializing)] pub time_gen_reset_token: i64, pub time_activate: i64, pub active: bool, diff --git a/geneit_backend/src/services/login_token_service.rs b/geneit_backend/src/services/login_token_service.rs index 6f732a1..f34bd32 100644 --- a/geneit_backend/src/services/login_token_service.rs +++ b/geneit_backend/src/services/login_token_service.rs @@ -4,18 +4,20 @@ use crate::connections::redis_connection; use crate::models::{User, UserID}; use crate::utils::string_utils; use crate::utils::time_utils::time; +use actix_web::dev::Payload; +use actix_web::{FromRequest, HttpRequest}; use std::time::Duration; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -struct LoginToken { +pub struct LoginToken { expire: u64, hb: u64, - user_id: UserID, + pub user_id: UserID, } impl LoginToken { pub fn new(user: &User) -> (String, Self) { - let key = format!("tok-{}-{}", user.id().0, string_utils::rand_str(40)); + let key = format!("{}-{}", user.id().0, string_utils::rand_str(40)); ( key, @@ -27,8 +29,8 @@ impl LoginToken { ) } - pub fn is_valid(&self) -> bool { - self.expire > time() && self.hb + 3600 > time() + pub fn is_expired(&self) -> bool { + self.expire < time() || self.hb + 3600 < time() } pub fn refresh_hb(&self) -> Option { @@ -41,7 +43,7 @@ impl LoginToken { } } - pub fn lifetime(&self) -> Duration { + pub fn redis_lifetime(&self) -> Duration { Duration::from_secs(if self.expire <= time() { 0 } else { @@ -50,9 +52,73 @@ impl LoginToken { } } +/// Get redis key for a given login token +fn redis_key(tok: &str) -> String { + format!("tok-{tok}") +} + /// Generate a new login token pub async fn gen_new_token(user: &User) -> anyhow::Result { let (key, token) = LoginToken::new(user); - redis_connection::set_value(&key, &token, token.lifetime()).await?; + redis_connection::set_value(&redis_key(&key), &token, token.redis_lifetime()).await?; Ok(key) } + +/// Get a user information from its token +async fn get_token(key: &str) -> anyhow::Result> { + let token = match redis_connection::get_value::(&redis_key(key)).await? { + None => { + log::error!("Could not find token for key '{}' (key absent)", key); + return Ok(None); + } + Some(t) => t, + }; + + if token.is_expired() { + log::error!("Could not find token for key '{}' (token expired)", key); + return Ok(None); + } + + // Check if heartbeat must be updated + if let Some(renew) = token.refresh_hb() { + redis_connection::set_value(&redis_key(key), &renew, renew.redis_lifetime()).await?; + } + + Ok(Some(token)) +} + +impl FromRequest for LoginToken { + type Error = actix_web::Error; + type Future = futures_util::future::LocalBoxFuture<'static, Result>; + + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { + let req = req.clone(); + Box::pin(async move { + let token = match req.headers().get("X-Auth-Token") { + Some(v) => v.to_str().unwrap_or(""), + None => { + return Err(actix_web::error::ErrorBadRequest( + "Missing auth token header!", + )); + } + }; + + let token = match get_token(token).await { + Err(e) => { + log::error!("Failed to load auth token! {}", e); + return Err(actix_web::error::ErrorInternalServerError( + "Failed to check auth token!", + )); + } + Ok(None) => { + return Err(actix_web::error::ErrorPreconditionFailed( + "Invalid auth token!", + )); + } + Ok(Some(t)) => t, + }; + + Ok(token) + }) + } +}