From b9d75e22db72aabf47815e381aa6432c1bff3877 Mon Sep 17 00:00:00 2001 From: Tolmachev Igor Date: Mon, 1 Sep 2025 13:32:05 +0300 Subject: Add account endpoints --- src/routers/account.rs | 184 ++++++++++++++++++++++++++++++++++++++++++++++--- src/routers/mod.rs | 12 +--- 2 files changed, 177 insertions(+), 19 deletions(-) (limited to 'src/routers') diff --git a/src/routers/account.rs b/src/routers/account.rs index 8192133..98ee61d 100644 --- a/src/routers/account.rs +++ b/src/routers/account.rs @@ -1,24 +1,188 @@ use axum::Router; -use axum::response::IntoResponse; -use axum::routing::{get, post}; +use axum::extract::State; +use axum::routing::{delete, get, post, put}; +use chrono::{DateTime, Duration, Utc}; +use entity::users::{self}; +use sea_orm::ActiveValue::Set; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, ModelTrait, QueryFilter, +}; +use serde::{Deserialize, Serialize}; -use crate::response::ApiResponse; +use crate::extract::{ApiJson, Auth}; +use crate::{ + ApiError, ApiResult, AppState, JwtClaims, SuccessResponse, create_jwt, create_password, + validate_password, +}; -async fn me() -> impl IntoResponse { - ApiResponse::Success("Me") +#[derive(Serialize)] +struct Account { + id: i64, + username: String, + first_name: String, + last_name: String, } -async fn register() -> impl IntoResponse { - ApiResponse::Success("Register") +#[derive(Serialize)] +struct Token { + token: String, + expired_at: DateTime, } -async fn login() -> impl IntoResponse { - ApiResponse::Success("Login") +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, + })); } -pub(crate) fn router() -> Router { +#[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)) } diff --git a/src/routers/mod.rs b/src/routers/mod.rs index ee925f0..b57f71d 100644 --- a/src/routers/mod.rs +++ b/src/routers/mod.rs @@ -1,15 +1,9 @@ mod account; use axum::Router; -use tower_http::trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer}; -use tracing::Level; -pub(crate) fn router() -> Router { - let trace_layer = TraceLayer::new_for_http() - .on_request(DefaultOnRequest::new().level(Level::INFO)) - .on_response(DefaultOnResponse::new().level(Level::INFO)); +use crate::state::AppState; - Router::new() - .layer(trace_layer) - .nest("/account", account::router()) +pub(crate) fn router() -> Router { + Router::new().nest("/account", account::router()) } -- cgit v1.3