use axum::{ Router, extract::State, routing::{delete, get, post, put}, }; use chrono::{DateTime, Duration, Utc}; use entity::users::{self}; use sea_orm::{ ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, IntoActiveModel, ModelTrait, QueryFilter, }; use serde::{Deserialize, Serialize}; use crate::{ ApiError, ApiResult, AppState, JwtClaims, SuccessResponse, create_jwt, create_password, extract::{ApiJson, Auth}, validate_password, }; #[derive(Serialize)] struct Account { id: i64, username: String, first_name: String, last_name: String, } #[derive(Serialize)] struct Token { token: String, expired_at: DateTime, } async fn me(Auth(user): Auth) -> ApiResult { return Ok(SuccessResponse(Account { id: user.id, username: user.username, first_name: user.first_name, last_name: user.last_name, })); } #[derive(Deserialize)] struct RegisterRequest { username: String, password: String, first_name: String, last_name: String, } async fn register( State(state): State, ApiJson(req): ApiJson, ) -> ApiResult { let user_exists = users::Entity::find() .filter(users::Column::Username.eq(&req.username)) .one(&state.db) .await? .is_some(); if user_exists { return Err(ApiError::UserAlreadyExists { username: req.username, }); } let user = users::ActiveModel { username: Set(req.username), password_hash: Set(create_password(&req.password)?), password_issue_date: Set(Utc::now().naive_utc()), first_name: Set(req.first_name), last_name: Set(req.last_name), ..Default::default() } .insert(&state.db) .await?; Ok(SuccessResponse(Account { id: user.id, username: user.username, first_name: user.first_name, last_name: user.last_name, })) } #[derive(Deserialize, Default)] enum TokenLifetime { Day = 1, #[default] Week = 7, Month = 31, } #[derive(Deserialize)] struct LoginRequest { username: String, password: String, #[serde(default)] token_lifetime: TokenLifetime, } async fn login( State(state): State, ApiJson(req): ApiJson, ) -> ApiResult { let user = users::Entity::find() .filter(users::Column::Username.eq(&req.username)) .one(&state.db) .await? .ok_or(ApiError::InvalidPassword)?; if !validate_password(&req.password, &user.password_hash)? { return Err(ApiError::InvalidPassword); } let expired_at = Utc::now() + Duration::days(req.token_lifetime as i64); let token = create_jwt( &JwtClaims { sub: user.id, iat: user.password_issue_date.and_utc().timestamp(), exp: expired_at.timestamp(), }, &state.secret, ) .map_err(|e| ApiError::InternalJwt(e.to_string()))?; Ok(SuccessResponse(Token { token, expired_at })) } #[derive(Deserialize)] struct ChangePasswordRequest { old_password: String, new_password: String, } async fn change_password( State(state): State, Auth(user): Auth, ApiJson(req): ApiJson, ) -> ApiResult { if !validate_password(&req.old_password, &user.password_hash)? { return Err(ApiError::InvalidPassword); } let mut active_user = user.into_active_model(); active_user.password_hash = Set(create_password(&req.new_password)?); active_user.password_issue_date = Set(Utc::now().naive_utc()); let user = active_user.update(&state.db).await?; Ok(SuccessResponse(Account { id: user.id, username: user.username, first_name: user.first_name, last_name: user.last_name, })) } #[derive(Deserialize)] struct DeleteUserRequest { password: String, } async fn delete_account( State(state): State, Auth(user): Auth, ApiJson(req): ApiJson, ) -> ApiResult { if !validate_password(&req.password, &user.password_hash)? { return Err(ApiError::InvalidPassword); } user.clone().delete(&state.db).await?; Ok(SuccessResponse(Account { id: user.id, username: user.username, first_name: user.first_name, last_name: user.last_name, })) } pub(crate) fn router() -> Router { Router::new() .route("/me", get(me)) .route("/register", post(register)) .route("/login", post(login)) .route("/change_password", put(change_password)) .route("/delete", delete(delete_account)) }