use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, Json}; use mongodb::bson::oid::ObjectId; use serde::{Deserialize, Serialize}; use validator::Validate; use crate::{ auth::jwt::Claims, config::AppState, middleware::ClientIp, models::audit_log::AuditEventType, models::user::User, }; #[derive(Debug, Serialize)] pub struct UserProfileResponse { pub id: String, pub email: String, pub username: String, pub created_at: i64, pub last_active: i64, pub email_verified: bool, } impl TryFrom for UserProfileResponse { type Error = anyhow::Error; fn try_from(user: User) -> Result { Ok(Self { id: user.id.map(|id| id.to_string()).unwrap_or_default(), email: user.email, username: user.username, created_at: user.created_at.timestamp_millis(), last_active: user.last_active.timestamp_millis(), email_verified: user.email_verified, }) } } #[derive(Debug, Deserialize, Validate)] pub struct UpdateProfileRequest { #[validate(length(min = 1))] pub username: Option, } pub async fn get_profile( State(state): State, Extension(claims): Extension, ) -> impl IntoResponse { let user_id = match ObjectId::parse_str(&claims.sub) { Ok(oid) => oid, Err(_) => { return ( StatusCode::UNAUTHORIZED, Json(serde_json::json!({ "error": "invalid token" })), ) .into_response() } }; match state.db.find_user_by_id(&user_id).await { Ok(Some(user)) => { let response: UserProfileResponse = user.try_into().unwrap(); (StatusCode::OK, Json(response)).into_response() } Ok(None) => ( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "user not found" })), ) .into_response(), Err(e) => { tracing::error!("Failed to get user profile: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to get profile" })), ) .into_response() } } } pub async fn update_profile( State(state): State, Extension(claims): Extension, Json(req): Json, ) -> impl IntoResponse { if let Err(errors) = req.validate() { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "validation failed", "details": errors.to_string() })), ) .into_response(); } let user_id = match ObjectId::parse_str(&claims.sub) { Ok(oid) => oid, Err(_) => { return ( StatusCode::UNAUTHORIZED, Json(serde_json::json!({ "error": "invalid token" })), ) .into_response() } }; let mut user = match state.db.find_user_by_id(&user_id).await { Ok(Some(u)) => u, Ok(None) => { return ( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "user not found" })), ) .into_response() } Err(e) => { tracing::error!("Failed to get user: {}", e); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "database error" })), ) .into_response(); } }; if let Some(username) = req.username { user.username = username; } match state.db.update_user(&user).await { Ok(_) => { let response: UserProfileResponse = user.try_into().unwrap(); (StatusCode::OK, Json(response)).into_response() } Err(e) => { tracing::error!("Failed to update user: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to update profile" })), ) .into_response() } } } pub async fn delete_account( State(state): State, Extension(claims): Extension, ) -> impl IntoResponse { let user_id = match ObjectId::parse_str(&claims.sub) { Ok(oid) => oid, Err(_) => { return ( StatusCode::UNAUTHORIZED, Json(serde_json::json!({ "error": "invalid token" })), ) .into_response() } }; match state.db.delete_user(&user_id).await { Ok(_) => (StatusCode::NO_CONTENT, ()).into_response(), Err(e) => { tracing::error!("Failed to delete user: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to delete account" })), ) .into_response() } } } #[derive(Debug, Deserialize, Validate)] pub struct ChangePasswordRequest { #[validate(length(min = 8))] pub current_password: String, #[validate(length(min = 8))] pub new_password: String, } pub async fn change_password( State(state): State, Extension(claims): Extension, Extension(client_ip): Extension, Json(req): Json, ) -> impl IntoResponse { if let Err(errors) = req.validate() { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "validation failed", "details": errors.to_string() })), ) .into_response(); } // The middleware already validated this against a real user, but guard // against a malformed subject anyway rather than panicking. let user_id = match ObjectId::parse_str(&claims.sub) { Ok(oid) => oid, Err(_) => { return ( StatusCode::UNAUTHORIZED, Json(serde_json::json!({ "error": "invalid token" })), ) .into_response() } }; let mut user = match state.db.find_user_by_id(&user_id).await { Ok(Some(u)) => u, Ok(None) => { return ( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "user not found" })), ) .into_response() } Err(e) => { tracing::error!("Failed to get user: {}", e); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "database error" })), ) .into_response(); } }; // Verify current password match user.verify_password(&req.current_password) { Ok(true) => {} Ok(false) => { return ( StatusCode::UNAUTHORIZED, Json(serde_json::json!({ "error": "current password is incorrect" })), ) .into_response() } Err(e) => { tracing::error!("Failed to verify password: {}", e); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to verify password" })), ) .into_response(); } } // Update password (this also bumps token_version in memory) match user.update_password(req.new_password) { Ok(_) => {} Err(e) => { tracing::error!("Failed to hash new password: {}", e); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to update password" })), ) .into_response(); } } match state.db.update_user(&user).await { Ok(_) => { // Invalidate all sessions: the bumped token_version rejects existing // access tokens, and we revoke every refresh token immediately so // they can't mint new access tokens. if let Some(ref repo) = state.refresh_token_repo { if let Err(e) = repo.revoke_all_by_user(&user_id.to_string()).await { tracing::warn!("Failed to revoke refresh tokens on password change: {}", e); } } state.token_version_cache.invalidate(&user_id).await; // Audit the credential change. if let Some(ref audit) = state.audit_logger { let _ = audit .log_event( AuditEventType::PasswordChanged, Some(user_id), Some(user.email.clone()), client_ip.as_str().to_string(), None, None, ) .await; } (StatusCode::NO_CONTENT, ()).into_response() } Err(e) => { tracing::error!("Failed to update user: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to update password" })), ) .into_response() } } } #[derive(Debug, Serialize)] pub struct UserSettingsResponse { pub recovery_enabled: bool, pub email_verified: bool, } impl From for UserSettingsResponse { fn from(user: User) -> Self { Self { recovery_enabled: user.recovery_enabled, email_verified: user.email_verified, } } } pub async fn get_settings( State(state): State, Extension(claims): Extension, ) -> impl IntoResponse { let user_id = match ObjectId::parse_str(&claims.sub) { Ok(oid) => oid, Err(_) => { return ( StatusCode::UNAUTHORIZED, Json(serde_json::json!({ "error": "invalid token" })), ) .into_response() } }; match state.db.find_user_by_id(&user_id).await { Ok(Some(user)) => { let response: UserSettingsResponse = user.into(); (StatusCode::OK, Json(response)).into_response() } Ok(None) => ( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "user not found" })), ) .into_response(), Err(e) => { tracing::error!("Failed to get user: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to get settings" })), ) .into_response() } } } #[derive(Debug, Deserialize, Validate)] pub struct UpdateSettingsRequest { pub recovery_enabled: Option, } pub async fn update_settings( State(state): State, Extension(claims): Extension, Json(req): Json, ) -> impl IntoResponse { let user_id = match ObjectId::parse_str(&claims.sub) { Ok(oid) => oid, Err(_) => { return ( StatusCode::UNAUTHORIZED, Json(serde_json::json!({ "error": "invalid token" })), ) .into_response() } }; let mut user = match state.db.find_user_by_id(&user_id).await { Ok(Some(u)) => u, Ok(None) => { return ( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "user not found" })), ) .into_response() } Err(e) => { tracing::error!("Failed to get user: {}", e); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "database error" })), ) .into_response(); } }; if let Some(recovery_enabled) = req.recovery_enabled { if !recovery_enabled { user.remove_recovery_phrase(); } // Note: Enabling recovery requires a separate endpoint to set the phrase } match state.db.update_user(&user).await { Ok(_) => { let response: UserSettingsResponse = user.into(); (StatusCode::OK, Json(response)).into_response() } Err(e) => { tracing::error!("Failed to update user: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to update settings" })), ) .into_response() } } }