From 955598dce9aeb5626654c72b0ef94850123fa8ac Mon Sep 17 00:00:00 2001 From: Tolmachev Igor Date: Sun, 14 Sep 2025 23:27:25 +0300 Subject: Add openapi specs and docs --- src/routers/account.rs | 221 +++++++++++++++++++++++++++++++++++-------------- src/routers/mod.rs | 9 +- 2 files changed, 163 insertions(+), 67 deletions(-) (limited to 'src/routers') diff --git a/src/routers/account.rs b/src/routers/account.rs index 1498b73..0d6d4c1 100644 --- a/src/routers/account.rs +++ b/src/routers/account.rs @@ -1,8 +1,4 @@ -use axum::{ - Router, - extract::State, - routing::{delete, get, post, put}, -}; +use axum::extract::State; use chrono::{DateTime, Duration, Utc}; use entity::users::{self}; use sea_orm::{ @@ -10,29 +6,104 @@ use sea_orm::{ QueryFilter, }; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use utoipa_axum::{router::OpenApiRouter, routes}; use crate::{ - ApiError, ApiResult, AppState, JwtClaims, SuccessResponse, create_jwt, create_password, + ApiResult, AppState, ClientError, GlobalResponses, JwtClaims, ServerError, SuccessResponse, + create_jwt, create_password, extract::{ApiJson, Auth}, + tags::ACCOUNT, validate_password, }; -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] +#[schema(description = "Account information")] struct Account { + #[schema(examples(1))] id: i64, + #[schema(examples("john_doe", "ivanov_ivan"))] username: String, + #[schema(examples("John", "Иван"))] first_name: String, + #[schema(examples("Doe", "Иванов"))] last_name: String, } -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] +#[schema(description = "Authorization token information")] struct Token { token: String, expired_at: DateTime, } +#[derive(Deserialize, ToSchema)] +#[schema(description = "Account register data")] +struct RegisterRequest { + #[schema(examples("john_doe", "ivanov_ivan"))] + username: String, + #[schema(examples("secret-password"))] + password: String, + #[schema(examples("John", "Иван"))] + first_name: String, + #[schema(examples("Doe", "Иванов"))] + last_name: String, +} + +#[derive(Deserialize, ToSchema, Default)] +#[serde(rename_all = "UPPERCASE")] +#[schema(description = "Account token life time")] +enum TokenLifetime { + Day = 1, + #[default] + Week = 7, + Month = 31, +} + +#[derive(Deserialize, ToSchema)] +#[schema(description = "Account login data")] +struct LoginRequest { + #[schema(examples("john_doe", "ivanov_ivan"))] + username: String, + #[schema(examples("secret-password"))] + password: String, + #[serde(default)] + #[schema(default = "WEEK")] + token_lifetime: TokenLifetime, +} + +#[derive(Deserialize, ToSchema)] +#[schema(description = "Change account password data")] +struct ChangePasswordRequest { + #[schema(examples("secret-password"))] + old_password: String, + #[schema(examples("super-secret-password"))] + new_password: String, +} + +#[derive(Deserialize, ToSchema)] +#[schema(description = "Account delete data")] +struct DeleteUserRequest { + #[schema(examples("secret-password"))] + password: String, +} + +#[utoipa::path( + get, + path = "/me", + tag = ACCOUNT, + summary = "Get me", + responses( + ( + status = 200, body = SuccessResponse, + description = "Success response with your account data" + ), + GlobalResponses + ), + security(("auth" = [])), +)] async fn me(Auth(user): Auth) -> ApiResult { - return Ok(SuccessResponse(Account { + return Ok(SuccessResponse::ok(Account { id: user.id, username: user.username, first_name: user.first_name, @@ -40,14 +111,20 @@ async fn me(Auth(user): Auth) -> ApiResult { })); } -#[derive(Deserialize)] -struct RegisterRequest { - username: String, - password: String, - first_name: String, - last_name: String, -} - +#[utoipa::path( + post, + path = "/register", + tag = ACCOUNT, + summary = "Register", + responses( + ( + status = 200, body = SuccessResponse, + description = "Success response with created account data" + ), + GlobalResponses + ), + request_body = RegisterRequest +)] async fn register( State(state): State, ApiJson(req): ApiJson, @@ -59,9 +136,10 @@ async fn register( .is_some(); if user_exists { - return Err(ApiError::UserAlreadyExists { + return Err(ClientError::UserAlreadyExists { username: req.username, - }); + } + .into()); } let user = users::ActiveModel { @@ -75,7 +153,7 @@ async fn register( .insert(&state.db) .await?; - Ok(SuccessResponse(Account { + Ok(SuccessResponse::ok(Account { id: user.id, username: user.username, first_name: user.first_name, @@ -83,22 +161,20 @@ async fn register( })) } -#[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, -} - +#[utoipa::path( + post, + path = "/login", + tag = ACCOUNT, + summary = "Login", + responses( + ( + status = 200, body = SuccessResponse, + description = "Success response with auth token data" + ), + GlobalResponses + ), + request_body = LoginRequest +)] async fn login( State(state): State, ApiJson(req): ApiJson, @@ -107,10 +183,10 @@ async fn login( .filter(users::Column::Username.eq(&req.username)) .one(&state.db) .await? - .ok_or(ApiError::InvalidPassword)?; + .ok_or(ClientError::InvalidPassword)?; if !validate_password(&req.password, &user.password_hash)? { - return Err(ApiError::InvalidPassword); + return Err(ClientError::InvalidPassword.into()); } let expired_at = Utc::now() + Duration::days(req.token_lifetime as i64); @@ -123,24 +199,33 @@ async fn login( }, &state.secret, ) - .map_err(|e| ApiError::InternalJwt(e.to_string()))?; + .map_err(|e| ServerError::Token(e.to_string()))?; - Ok(SuccessResponse(Token { token, expired_at })) -} - -#[derive(Deserialize)] -struct ChangePasswordRequest { - old_password: String, - new_password: String, + Ok(SuccessResponse::ok(Token { token, expired_at })) } +#[utoipa::path( + put, + path = "/change/password", + tag = ACCOUNT, + summary = "Change password", + request_body = ChangePasswordRequest, + responses( + ( + status = 200, body = SuccessResponse, + description = "Success response with changed account data" + ), + GlobalResponses + ), + security(("auth" = [])) +)] 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); + return Err(ClientError::InvalidPassword.into()); } let mut active_user = user.into_active_model(); @@ -148,7 +233,7 @@ async fn change_password( active_user.password_issue_date = Set(Utc::now().naive_utc()); let user = active_user.update(&state.db).await?; - Ok(SuccessResponse(Account { + Ok(SuccessResponse::ok(Account { id: user.id, username: user.username, first_name: user.first_name, @@ -156,23 +241,33 @@ async fn change_password( })) } -#[derive(Deserialize)] -struct DeleteUserRequest { - password: String, -} - -async fn delete_account( +#[utoipa::path( + delete, + path = "/delete", + tag = ACCOUNT, + summary = "Delete", + request_body = DeleteUserRequest, + responses( + ( + status = 200, body = SuccessResponse, + description = "Success response with deleted account data" + ), + GlobalResponses + ), + security(("auth" = [])) +)] +async fn delete( State(state): State, Auth(user): Auth, ApiJson(req): ApiJson, ) -> ApiResult { if !validate_password(&req.password, &user.password_hash)? { - return Err(ApiError::InvalidPassword); + return Err(ClientError::InvalidPassword.into()); } user.clone().delete(&state.db).await?; - Ok(SuccessResponse(Account { + Ok(SuccessResponse::ok(Account { id: user.id, username: user.username, first_name: user.first_name, @@ -180,11 +275,11 @@ async fn delete_account( })) } -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)) +pub(crate) fn router() -> OpenApiRouter { + OpenApiRouter::new() + .routes(routes!(me)) + .routes(routes!(register)) + .routes(routes!(login)) + .routes(routes!(change_password)) + .routes(routes!(delete)) } diff --git a/src/routers/mod.rs b/src/routers/mod.rs index b57f71d..4623f8e 100644 --- a/src/routers/mod.rs +++ b/src/routers/mod.rs @@ -1,9 +1,10 @@ mod account; -use axum::Router; +use utoipa::OpenApi; +use utoipa_axum::router::OpenApiRouter; -use crate::state::AppState; +use crate::{AppOpenApi, AppState}; -pub(crate) fn router() -> Router { - Router::new().nest("/account", account::router()) +pub(crate) fn router() -> OpenApiRouter { + OpenApiRouter::with_openapi(AppOpenApi::openapi()).nest("/account", account::router()) } -- cgit v1.2.3