aboutsummaryrefslogtreecommitdiff
path: root/src/routers
diff options
context:
space:
mode:
Diffstat (limited to 'src/routers')
-rw-r--r--src/routers/account.rs221
-rw-r--r--src/routers/mod.rs9
2 files changed, 163 insertions, 67 deletions
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 @@
1use axum::{ 1use axum::extract::State;
2 Router,
3 extract::State,
4 routing::{delete, get, post, put},
5};
6use chrono::{DateTime, Duration, Utc}; 2use chrono::{DateTime, Duration, Utc};
7use entity::users::{self}; 3use entity::users::{self};
8use sea_orm::{ 4use sea_orm::{
@@ -10,29 +6,104 @@ use sea_orm::{
10 QueryFilter, 6 QueryFilter,
11}; 7};
12use serde::{Deserialize, Serialize}; 8use serde::{Deserialize, Serialize};
9use utoipa::ToSchema;
10use utoipa_axum::{router::OpenApiRouter, routes};
13 11
14use crate::{ 12use crate::{
15 ApiError, ApiResult, AppState, JwtClaims, SuccessResponse, create_jwt, create_password, 13 ApiResult, AppState, ClientError, GlobalResponses, JwtClaims, ServerError, SuccessResponse,
14 create_jwt, create_password,
16 extract::{ApiJson, Auth}, 15 extract::{ApiJson, Auth},
16 tags::ACCOUNT,
17 validate_password, 17 validate_password,
18}; 18};
19 19
20#[derive(Serialize)] 20#[derive(Serialize, ToSchema)]
21#[schema(description = "Account information")]
21struct Account { 22struct Account {
23 #[schema(examples(1))]
22 id: i64, 24 id: i64,
25 #[schema(examples("john_doe", "ivanov_ivan"))]
23 username: String, 26 username: String,
27 #[schema(examples("John", "Иван"))]
24 first_name: String, 28 first_name: String,
29 #[schema(examples("Doe", "Иванов"))]
25 last_name: String, 30 last_name: String,
26} 31}
27 32
28#[derive(Serialize)] 33#[derive(Serialize, ToSchema)]
34#[schema(description = "Authorization token information")]
29struct Token { 35struct Token {
30 token: String, 36 token: String,
31 expired_at: DateTime<Utc>, 37 expired_at: DateTime<Utc>,
32} 38}
33 39
40#[derive(Deserialize, ToSchema)]
41#[schema(description = "Account register data")]
42struct RegisterRequest {
43 #[schema(examples("john_doe", "ivanov_ivan"))]
44 username: String,
45 #[schema(examples("secret-password"))]
46 password: String,
47 #[schema(examples("John", "Иван"))]
48 first_name: String,
49 #[schema(examples("Doe", "Иванов"))]
50 last_name: String,
51}
52
53#[derive(Deserialize, ToSchema, Default)]
54#[serde(rename_all = "UPPERCASE")]
55#[schema(description = "Account token life time")]
56enum TokenLifetime {
57 Day = 1,
58 #[default]
59 Week = 7,
60 Month = 31,
61}
62
63#[derive(Deserialize, ToSchema)]
64#[schema(description = "Account login data")]
65struct LoginRequest {
66 #[schema(examples("john_doe", "ivanov_ivan"))]
67 username: String,
68 #[schema(examples("secret-password"))]
69 password: String,
70 #[serde(default)]
71 #[schema(default = "WEEK")]
72 token_lifetime: TokenLifetime,
73}
74
75#[derive(Deserialize, ToSchema)]
76#[schema(description = "Change account password data")]
77struct ChangePasswordRequest {
78 #[schema(examples("secret-password"))]
79 old_password: String,
80 #[schema(examples("super-secret-password"))]
81 new_password: String,
82}
83
84#[derive(Deserialize, ToSchema)]
85#[schema(description = "Account delete data")]
86struct DeleteUserRequest {
87 #[schema(examples("secret-password"))]
88 password: String,
89}
90
91#[utoipa::path(
92 get,
93 path = "/me",
94 tag = ACCOUNT,
95 summary = "Get me",
96 responses(
97 (
98 status = 200, body = SuccessResponse<Account>,
99 description = "Success response with your account data"
100 ),
101 GlobalResponses
102 ),
103 security(("auth" = [])),
104)]
34async fn me(Auth(user): Auth) -> ApiResult<Account> { 105async fn me(Auth(user): Auth) -> ApiResult<Account> {
35 return Ok(SuccessResponse(Account { 106 return Ok(SuccessResponse::ok(Account {
36 id: user.id, 107 id: user.id,
37 username: user.username, 108 username: user.username,
38 first_name: user.first_name, 109 first_name: user.first_name,
@@ -40,14 +111,20 @@ async fn me(Auth(user): Auth) -> ApiResult<Account> {
40 })); 111 }));
41} 112}
42 113
43#[derive(Deserialize)] 114#[utoipa::path(
44struct RegisterRequest { 115 post,
45 username: String, 116 path = "/register",
46 password: String, 117 tag = ACCOUNT,
47 first_name: String, 118 summary = "Register",
48 last_name: String, 119 responses(
49} 120 (
50 121 status = 200, body = SuccessResponse<Account>,
122 description = "Success response with created account data"
123 ),
124 GlobalResponses
125 ),
126 request_body = RegisterRequest
127)]
51async fn register( 128async fn register(
52 State(state): State<AppState>, 129 State(state): State<AppState>,
53 ApiJson(req): ApiJson<RegisterRequest>, 130 ApiJson(req): ApiJson<RegisterRequest>,
@@ -59,9 +136,10 @@ async fn register(
59 .is_some(); 136 .is_some();
60 137
61 if user_exists { 138 if user_exists {
62 return Err(ApiError::UserAlreadyExists { 139 return Err(ClientError::UserAlreadyExists {
63 username: req.username, 140 username: req.username,
64 }); 141 }
142 .into());
65 } 143 }
66 144
67 let user = users::ActiveModel { 145 let user = users::ActiveModel {
@@ -75,7 +153,7 @@ async fn register(
75 .insert(&state.db) 153 .insert(&state.db)
76 .await?; 154 .await?;
77 155
78 Ok(SuccessResponse(Account { 156 Ok(SuccessResponse::ok(Account {
79 id: user.id, 157 id: user.id,
80 username: user.username, 158 username: user.username,
81 first_name: user.first_name, 159 first_name: user.first_name,
@@ -83,22 +161,20 @@ async fn register(
83 })) 161 }))
84} 162}
85 163
86#[derive(Deserialize, Default)] 164#[utoipa::path(
87enum TokenLifetime { 165 post,
88 Day = 1, 166 path = "/login",
89 #[default] 167 tag = ACCOUNT,
90 Week = 7, 168 summary = "Login",
91 Month = 31, 169 responses(
92} 170 (
93 171 status = 200, body = SuccessResponse<Token>,
94#[derive(Deserialize)] 172 description = "Success response with auth token data"
95struct LoginRequest { 173 ),
96 username: String, 174 GlobalResponses
97 password: String, 175 ),
98 #[serde(default)] 176 request_body = LoginRequest
99 token_lifetime: TokenLifetime, 177)]
100}
101
102async fn login( 178async fn login(
103 State(state): State<AppState>, 179 State(state): State<AppState>,
104 ApiJson(req): ApiJson<LoginRequest>, 180 ApiJson(req): ApiJson<LoginRequest>,
@@ -107,10 +183,10 @@ async fn login(
107 .filter(users::Column::Username.eq(&req.username)) 183 .filter(users::Column::Username.eq(&req.username))
108 .one(&state.db) 184 .one(&state.db)
109 .await? 185 .await?
110 .ok_or(ApiError::InvalidPassword)?; 186 .ok_or(ClientError::InvalidPassword)?;
111 187
112 if !validate_password(&req.password, &user.password_hash)? { 188 if !validate_password(&req.password, &user.password_hash)? {
113 return Err(ApiError::InvalidPassword); 189 return Err(ClientError::InvalidPassword.into());
114 } 190 }
115 191
116 let expired_at = Utc::now() + Duration::days(req.token_lifetime as i64); 192 let expired_at = Utc::now() + Duration::days(req.token_lifetime as i64);
@@ -123,24 +199,33 @@ async fn login(
123 }, 199 },
124 &state.secret, 200 &state.secret,
125 ) 201 )
126 .map_err(|e| ApiError::InternalJwt(e.to_string()))?; 202 .map_err(|e| ServerError::Token(e.to_string()))?;
127 203
128 Ok(SuccessResponse(Token { token, expired_at })) 204 Ok(SuccessResponse::ok(Token { token, expired_at }))
129}
130
131#[derive(Deserialize)]
132struct ChangePasswordRequest {
133 old_password: String,
134 new_password: String,
135} 205}
136 206
207#[utoipa::path(
208 put,
209 path = "/change/password",
210 tag = ACCOUNT,
211 summary = "Change password",
212 request_body = ChangePasswordRequest,
213 responses(
214 (
215 status = 200, body = SuccessResponse<Account>,
216 description = "Success response with changed account data"
217 ),
218 GlobalResponses
219 ),
220 security(("auth" = []))
221)]
137async fn change_password( 222async fn change_password(
138 State(state): State<AppState>, 223 State(state): State<AppState>,
139 Auth(user): Auth, 224 Auth(user): Auth,
140 ApiJson(req): ApiJson<ChangePasswordRequest>, 225 ApiJson(req): ApiJson<ChangePasswordRequest>,
141) -> ApiResult<Account> { 226) -> ApiResult<Account> {
142 if !validate_password(&req.old_password, &user.password_hash)? { 227 if !validate_password(&req.old_password, &user.password_hash)? {
143 return Err(ApiError::InvalidPassword); 228 return Err(ClientError::InvalidPassword.into());
144 } 229 }
145 230
146 let mut active_user = user.into_active_model(); 231 let mut active_user = user.into_active_model();
@@ -148,7 +233,7 @@ async fn change_password(
148 active_user.password_issue_date = Set(Utc::now().naive_utc()); 233 active_user.password_issue_date = Set(Utc::now().naive_utc());
149 234
150 let user = active_user.update(&state.db).await?; 235 let user = active_user.update(&state.db).await?;
151 Ok(SuccessResponse(Account { 236 Ok(SuccessResponse::ok(Account {
152 id: user.id, 237 id: user.id,
153 username: user.username, 238 username: user.username,
154 first_name: user.first_name, 239 first_name: user.first_name,
@@ -156,23 +241,33 @@ async fn change_password(
156 })) 241 }))
157} 242}
158 243
159#[derive(Deserialize)] 244#[utoipa::path(
160struct DeleteUserRequest { 245 delete,
161 password: String, 246 path = "/delete",
162} 247 tag = ACCOUNT,
163 248 summary = "Delete",
164async fn delete_account( 249 request_body = DeleteUserRequest,
250 responses(
251 (
252 status = 200, body = SuccessResponse<Account>,
253 description = "Success response with deleted account data"
254 ),
255 GlobalResponses
256 ),
257 security(("auth" = []))
258)]
259async fn delete(
165 State(state): State<AppState>, 260 State(state): State<AppState>,
166 Auth(user): Auth, 261 Auth(user): Auth,
167 ApiJson(req): ApiJson<DeleteUserRequest>, 262 ApiJson(req): ApiJson<DeleteUserRequest>,
168) -> ApiResult<Account> { 263) -> ApiResult<Account> {
169 if !validate_password(&req.password, &user.password_hash)? { 264 if !validate_password(&req.password, &user.password_hash)? {
170 return Err(ApiError::InvalidPassword); 265 return Err(ClientError::InvalidPassword.into());
171 } 266 }
172 267
173 user.clone().delete(&state.db).await?; 268 user.clone().delete(&state.db).await?;
174 269
175 Ok(SuccessResponse(Account { 270 Ok(SuccessResponse::ok(Account {
176 id: user.id, 271 id: user.id,
177 username: user.username, 272 username: user.username,
178 first_name: user.first_name, 273 first_name: user.first_name,
@@ -180,11 +275,11 @@ async fn delete_account(
180 })) 275 }))
181} 276}
182 277
183pub(crate) fn router() -> Router<AppState> { 278pub(crate) fn router() -> OpenApiRouter<AppState> {
184 Router::new() 279 OpenApiRouter::new()
185 .route("/me", get(me)) 280 .routes(routes!(me))
186 .route("/register", post(register)) 281 .routes(routes!(register))
187 .route("/login", post(login)) 282 .routes(routes!(login))
188 .route("/change_password", put(change_password)) 283 .routes(routes!(change_password))
189 .route("/delete", delete(delete_account)) 284 .routes(routes!(delete))
190} 285}
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 @@
1mod account; 1mod account;
2 2
3use axum::Router; 3use utoipa::OpenApi;
4use utoipa_axum::router::OpenApiRouter;
4 5
5use crate::state::AppState; 6use crate::{AppOpenApi, AppState};
6 7
7pub(crate) fn router() -> Router<AppState> { 8pub(crate) fn router() -> OpenApiRouter<AppState> {
8 Router::new().nest("/account", account::router()) 9 OpenApiRouter::with_openapi(AppOpenApi::openapi()).nest("/account", account::router())
9} 10}