Update password expiration management

This commit is contained in:
Savanni D'Gerinel 2025-01-20 19:44:04 -05:00
parent 06bb0811e0
commit ef0e9f16b8
6 changed files with 53 additions and 41 deletions

View File

@ -156,6 +156,13 @@ pub fn fatal<A, E: Error, FE: FatalError>(err: FE) -> ResultExt<A, E, FE> {
ResultExt::Fatal(err) ResultExt::Fatal(err)
} }
pub fn result_as_fatal<A, E: Error, FE: FatalError>(result: Result<A, FE>) -> ResultExt<A, E, FE> {
match result {
Ok(a) => ResultExt::Ok(a),
Err(err) => ResultExt::Fatal(err),
}
}
/// Return early from the current function if the value is a fatal error. /// Return early from the current function if the value is a fatal error.
#[macro_export] #[macro_export]
macro_rules! return_fatal { macro_rules! return_fatal {

View File

@ -3,7 +3,7 @@ version: '3'
tasks: tasks:
build: build:
cmds: cmds:
- cargo build - cargo watch -x build
test: test:
cmds: cmds:

View File

@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc};
use async_std::sync::RwLock; use async_std::sync::RwLock;
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Utc};
use mime::Mime; use mime::Mime;
use result_extended::{error, fatal, ok, return_error, ResultExt}; use result_extended::{error, fatal, ok, result_as_fatal, return_error, ResultExt};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
use typeshare::typeshare; use typeshare::typeshare;
@ -12,11 +12,8 @@ use uuid::Uuid;
use crate::{ use crate::{
asset_db::{self, AssetId, Assets}, asset_db::{self, AssetId, Assets},
database::{CharacterId, Database, GameId, SessionId, UserId}, database::{CharacterId, Database, GameId, SessionId, UserId},
types::AccountState,
types::{AppError, FatalError, GameOverview, Message, Rgb, Tabletop, User, UserOverview}, types::{AppError, FatalError, GameOverview, Message, Rgb, Tabletop, User, UserOverview},
types::{
AccountState, AppError, FatalError, GameOverview, Message, Rgb, Tabletop, User,
UserOverview,
},
}; };
const DEFAULT_BACKGROUND_COLOR: Rgb = Rgb { const DEFAULT_BACKGROUND_COLOR: Rgb = Rgb {
@ -28,7 +25,7 @@ const DEFAULT_BACKGROUND_COLOR: Rgb = Rgb {
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
#[typeshare] #[typeshare]
pub struct Status { pub struct Status {
pub admin_enabled: bool, pub ok: bool,
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
@ -36,7 +33,8 @@ pub struct Status {
#[typeshare] #[typeshare]
pub enum AuthResponse { pub enum AuthResponse {
Success(SessionId), Success(SessionId),
Expired, PasswordReset(SessionId),
Locked,
} }
#[derive(Debug)] #[derive(Debug)]
@ -73,6 +71,7 @@ impl Core {
} }
pub async fn status(&self) -> ResultExt<Status, AppError, FatalError> { pub async fn status(&self) -> ResultExt<Status, AppError, FatalError> {
/*
let state = self.0.write().await; let state = self.0.write().await;
let admin_user = return_error!(match state.db.user(&UserId::from("admin")).await { let admin_user = return_error!(match state.db.user(&UserId::from("admin")).await {
Ok(Some(admin_user)) => ok(admin_user), Ok(Some(admin_user)) => ok(admin_user),
@ -85,8 +84,10 @@ impl Core {
}); });
ok(Status { ok(Status {
admin_enabled: !admin_user.password.is_empty(), ok: !admin_user.password.is_empty(),
}) })
*/
ok(Status { ok: true })
} }
pub async fn register_client(&self) -> String { pub async fn register_client(&self) -> String {
@ -351,26 +352,26 @@ impl Core {
) -> ResultExt<AuthResponse, AppError, FatalError> { ) -> ResultExt<AuthResponse, AppError, FatalError> {
let now = Utc::now(); let now = Utc::now();
let state = self.0.read().await; let state = self.0.read().await;
match state.db.user_by_username(username).await { let user_info = return_error!(match state.db.user_by_username(username).await {
Ok(Some(row)) if row.password == password => match row.state { Ok(Some(row)) if row.password == password => ok(row),
AccountState::Normal => match state.db.create_session(&row.id).await {
Ok(session_id) => ok(AuthResponse::Success(session_id)),
Err(err) => fatal(err),
},
AccountState::PasswordReset(exp) => {
if exp < now {
ok(AuthResponse::Expired)
} else {
match state.db.create_session(&row.id).await {
Ok(session_id) => ok(AuthResponse::Success(session_id)),
Err(err) => fatal(err),
}
}
}
AccountState::Locked => error(AppError::AuthFailed),
},
Ok(_) => error(AppError::AuthFailed), Ok(_) => error(AppError::AuthFailed),
Err(err) => fatal(err), Err(err) => fatal(err),
});
match user_info.state {
AccountState::Normal => result_as_fatal(state.db.create_session(&user_info.id).await)
.map(|session_id| AuthResponse::Success(session_id)),
AccountState::PasswordReset(exp) => {
if exp < now {
error(AppError::AuthFailed)
} else {
result_as_fatal(state.db.create_session(&user_info.id).await)
.map(|session_id| AuthResponse::PasswordReset(session_id))
}
}
AccountState::Locked => ok(AuthResponse::Locked),
} }
} }
@ -517,7 +518,8 @@ mod test {
Err(err) => panic!("{}", err), Err(err) => panic!("{}", err),
} }
} }
ResultExt::Ok(AuthResponse::Expired) => panic!("user has expired"), ResultExt::Ok(AuthResponse::PasswordReset(_)) => panic!("user is in password reset state"),
ResultExt::Ok(AuthResponse::Locked) => panic!("user has been locked"),
ResultExt::Err(err) => panic!("{}", err), ResultExt::Err(err) => panic!("{}", err),
ResultExt::Fatal(err) => panic!("{}", err), ResultExt::Fatal(err) => panic!("{}", err),
} }

View File

@ -45,7 +45,7 @@ where
pub async fn healthcheck(core: Core) -> Vec<u8> { pub async fn healthcheck(core: Core) -> Vec<u8> {
match core.status().await { match core.status().await {
ResultExt::Ok(s) => serde_json::to_vec(&HealthCheck { ResultExt::Ok(s) => serde_json::to_vec(&HealthCheck {
ok: s.admin_enabled, ok: s.ok,
}) })
.unwrap(), .unwrap(),
ResultExt::Err(_) => serde_json::to_vec(&HealthCheck { ok: false }).unwrap(), ResultExt::Err(_) => serde_json::to_vec(&HealthCheck { ok: false }).unwrap(),

View File

@ -40,7 +40,11 @@ pub fn routes(core: Core) -> Router {
"/api/v1/auth", "/api/v1/auth",
post({ post({
let core = core.clone(); let core = core.clone();
move |req: Json<AuthRequest>| wrap_handler(|| check_password(core, req)) move |req: Json<AuthRequest>| wrap_handler(|| async {
let password_result = check_password(core, req).await;
println!("check_auth result: {:?}", password_result);
password_result
})
}) })
.layer( .layer(
CorsLayer::new() CorsLayer::new()
@ -117,7 +121,7 @@ pub fn routes(core: Core) -> Router {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::path::PathBuf; use std::{path::PathBuf, time::Duration};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum_test::TestServer; use axum_test::TestServer;
@ -135,9 +139,10 @@ mod test {
}; };
async fn initialize_test_server() -> (Core, TestServer) { async fn initialize_test_server() -> (Core, TestServer) {
let password_exp = Utc::now() + Duration::from_secs(5);
let memory_db: Option<PathBuf> = None; let memory_db: Option<PathBuf> = None;
let conn = DbConn::new(memory_db); let conn = DbConn::new(memory_db);
let admin_id = conn.create_user("admin", "aoeu", true, AccountState::PasswordReset(Utc::now())).await.unwrap(); let _admin_id = conn.create_user("admin", "aoeu", true, AccountState::PasswordReset(password_exp)).await.unwrap();
let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn); let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
let app = routes(core.clone()); let app = routes(core.clone());
let server = TestServer::new(app).unwrap(); let server = TestServer::new(app).unwrap();
@ -223,8 +228,9 @@ mod test {
let session_id = response.json::<Option<AuthResponse>>().unwrap(); let session_id = response.json::<Option<AuthResponse>>().unwrap();
let session_id = match session_id { let session_id = match session_id {
AuthResponse::Success(session_id) => session_id, AuthResponse::PasswordReset(session_id) => session_id,
AuthResponse::Expired => panic!("admin user is already expired"), AuthResponse::Success(_) => panic!("admin user password has already been set"),
AuthResponse::Locked => panic!("admin user is already expired"),
}; };
let response = server let response = server
@ -244,7 +250,7 @@ mod test {
.await; .await;
response.assert_status_ok(); response.assert_status_ok();
let session = response.json::<Option<AuthResponse>>().unwrap(); let session = response.json::<Option<AuthResponse>>().unwrap();
assert_matches!(session, AuthResponse::Expired); assert_matches!(session, AuthResponse::PasswordReset(_));
} }
#[ignore] #[ignore]
@ -291,8 +297,7 @@ mod test {
}) })
.await; .await;
response.assert_status_ok(); response.assert_status_ok();
let session_id: Option<SessionId> = response.json(); assert_matches!(response.json(), Some(AuthResponse::PasswordReset(_)));
assert!(session_id.is_some());
} }
#[tokio::test] #[tokio::test]
@ -310,8 +315,7 @@ mod test {
}) })
.await; .await;
response.assert_status_ok(); response.assert_status_ok();
let session_id: Option<SessionId> = response.json(); let session_id = assert_matches!(response.json(), Some(AuthResponse::PasswordReset(session_id)) => session_id);
let session_id = session_id.unwrap();
println!("it_returns_user_profile: {}", session_id); println!("it_returns_user_profile: {}", session_id);
let response = server let response = server
@ -321,7 +325,6 @@ mod test {
response.assert_status_ok(); response.assert_status_ok();
let profile: Option<UserOverview> = response.json(); let profile: Option<UserOverview> = response.json();
let profile = profile.unwrap(); let profile = profile.unwrap();
assert_eq!(profile.id, UserId::from("admin"));
assert_eq!(profile.name, "admin"); assert_eq!(profile.name, "admin");
} }

View File

@ -76,6 +76,7 @@ pub struct Rgb {
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type", content = "content")]
#[typeshare] #[typeshare]
pub enum AccountState { pub enum AccountState {
Normal, Normal,
@ -91,7 +92,6 @@ impl FromSql for AccountState {
Ok(AccountState::Normal) Ok(AccountState::Normal)
} else if text.starts_with("PasswordReset") { } else if text.starts_with("PasswordReset") {
let exp_str = text.strip_prefix("PasswordReset ").unwrap(); let exp_str = text.strip_prefix("PasswordReset ").unwrap();
println!("{}", exp_str);
let exp = NaiveDateTime::parse_from_str(exp_str, "%Y-%m-%d %H:%M:%S") let exp = NaiveDateTime::parse_from_str(exp_str, "%Y-%m-%d %H:%M:%S")
.unwrap() .unwrap()
.and_utc(); .and_utc();