diff options
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | entity/src/invite_tokens.rs | 2 | ||||
| -rw-r--r-- | migration/src/m0_init_tables.rs | 4 | ||||
| -rw-r--r-- | src/api.rs | 4 | ||||
| -rw-r--r-- | src/error/client.rs | 8 | ||||
| -rw-r--r-- | src/main.rs | 3 | ||||
| -rw-r--r-- | src/models.rs | 29 | ||||
| -rw-r--r-- | src/routers/account.rs | 30 | ||||
| -rw-r--r-- | src/routers/queue/access.rs | 257 | ||||
| -rw-r--r-- | src/routers/queue/manage.rs | 78 | ||||
| -rw-r--r-- | src/routers/queue/mod.rs | 2 | ||||
| -rw-r--r-- | src/util.rs | 51 |
12 files changed, 405 insertions, 65 deletions
| @@ -23,7 +23,7 @@ tower = "0.5.2" | |||
| 23 | tower-http = { version = "0.6.6", features = ["trace"] } | 23 | tower-http = { version = "0.6.6", features = ["trace"] } |
| 24 | tracing = "0.1.41" | 24 | tracing = "0.1.41" |
| 25 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } | 25 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } |
| 26 | utoipa = { version = "5.4.0", features = ["axum_extras", "chrono", "preserve_order", "preserve_path_order"] } | 26 | utoipa = { version = "5.4.0", features = ["axum_extras", "chrono", "preserve_order", "preserve_path_order", "uuid"] } |
| 27 | utoipa-axum = "0.2.0" | 27 | utoipa-axum = "0.2.0" |
| 28 | utoipa-scalar = { version = "0.3.0", features = ["axum"] } | 28 | utoipa-scalar = { version = "0.3.0", features = ["axum"] } |
| 29 | uuid = { version = "1.18.1", features = ["v4", "serde"] } | 29 | uuid = { version = "1.18.1", features = ["v4", "serde"] } |
diff --git a/entity/src/invite_tokens.rs b/entity/src/invite_tokens.rs index c0b59f7..51f09ff 100644 --- a/entity/src/invite_tokens.rs +++ b/entity/src/invite_tokens.rs | |||
| @@ -10,7 +10,7 @@ pub struct Model { | |||
| 10 | pub token: Uuid, | 10 | pub token: Uuid, |
| 11 | pub queue_id: i64, | 11 | pub queue_id: i64, |
| 12 | pub name: String, | 12 | pub name: String, |
| 13 | pub is_revoked: bool, | 13 | pub expiration_date: Option<DateTime>, |
| 14 | } | 14 | } |
| 15 | 15 | ||
| 16 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] | 16 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] |
diff --git a/migration/src/m0_init_tables.rs b/migration/src/m0_init_tables.rs index 5e9a621..6ce86e1 100644 --- a/migration/src/m0_init_tables.rs +++ b/migration/src/m0_init_tables.rs | |||
| @@ -30,7 +30,7 @@ enum InviteTokens { | |||
| 30 | Token, | 30 | Token, |
| 31 | QueueId, | 31 | QueueId, |
| 32 | Name, | 32 | Name, |
| 33 | IsRevoked, | 33 | ExpirationDate, |
| 34 | } | 34 | } |
| 35 | 35 | ||
| 36 | #[derive(DeriveIden)] | 36 | #[derive(DeriveIden)] |
| @@ -121,7 +121,7 @@ impl MigrationTrait for Migration { | |||
| 121 | .on_update(ForeignKeyAction::Cascade), | 121 | .on_update(ForeignKeyAction::Cascade), |
| 122 | ) | 122 | ) |
| 123 | .col(string(InviteTokens::Name)) | 123 | .col(string(InviteTokens::Name)) |
| 124 | .col(boolean(InviteTokens::IsRevoked).default(false)) | 124 | .col(timestamp_null(InviteTokens::ExpirationDate)) |
| 125 | .to_owned(), | 125 | .to_owned(), |
| 126 | ) | 126 | ) |
| 127 | .await?; | 127 | .await?; |
| @@ -8,7 +8,7 @@ use crate::ErrorResponse; | |||
| 8 | pub mod tags { | 8 | pub mod tags { |
| 9 | pub const ACCOUNT: &str = "Account"; | 9 | pub const ACCOUNT: &str = "Account"; |
| 10 | pub const QUEUE: &str = "Queue"; | 10 | pub const QUEUE: &str = "Queue"; |
| 11 | pub const QUEUE_ACCESS: &str = "Queue access"; | 11 | pub const INVITE_TOKEN: &str = "Invite token"; |
| 12 | } | 12 | } |
| 13 | 13 | ||
| 14 | struct AuthModifier; | 14 | struct AuthModifier; |
| @@ -53,7 +53,7 @@ impl Modify for AuthModifier { | |||
| 53 | tags( | 53 | tags( |
| 54 | (name=tags::ACCOUNT, description="Account management methods"), | 54 | (name=tags::ACCOUNT, description="Account management methods"), |
| 55 | (name=tags::QUEUE, description="Queue management methods"), | 55 | (name=tags::QUEUE, description="Queue management methods"), |
| 56 | (name=tags::QUEUE_ACCESS, description="Queue access management methods") | 56 | (name=tags::INVITE_TOKEN, description="Invite token management methods") |
| 57 | ), | 57 | ), |
| 58 | components( | 58 | components( |
| 59 | schemas(ErrorResponse) | 59 | schemas(ErrorResponse) |
diff --git a/src/error/client.rs b/src/error/client.rs index 9b2c89b..b678d66 100644 --- a/src/error/client.rs +++ b/src/error/client.rs | |||
| @@ -10,6 +10,8 @@ pub enum ClientError { | |||
| 10 | UserNotFound { id: i64 }, | 10 | UserNotFound { id: i64 }, |
| 11 | NotQueueOwner { id: i64 }, | 11 | NotQueueOwner { id: i64 }, |
| 12 | QueueNotFound { id: i64 }, | 12 | QueueNotFound { id: i64 }, |
| 13 | NoInviteTokenAccess { id: i64 }, | ||
| 14 | InviteTokenNotFound { id: i64 }, | ||
| 13 | } | 15 | } |
| 14 | 16 | ||
| 15 | impl ClientError { | 17 | impl ClientError { |
| @@ -26,6 +28,8 @@ impl ClientError { | |||
| 26 | Self::UserNotFound { .. } => "UserNotFound", | 28 | Self::UserNotFound { .. } => "UserNotFound", |
| 27 | Self::NotQueueOwner { .. } => "NotQueueOwner", | 29 | Self::NotQueueOwner { .. } => "NotQueueOwner", |
| 28 | Self::QueueNotFound { .. } => "QueueNotFound", | 30 | Self::QueueNotFound { .. } => "QueueNotFound", |
| 31 | Self::NoInviteTokenAccess { .. } => "NoInviteTokenAccess", | ||
| 32 | Self::InviteTokenNotFound { .. } => "InviteTokenNotFound", | ||
| 29 | } | 33 | } |
| 30 | .to_string() | 34 | .to_string() |
| 31 | } | 35 | } |
| @@ -50,6 +54,10 @@ impl ClientError { | |||
| 50 | format!("you are not the owner of the queue with id `{}`", id) | 54 | format!("you are not the owner of the queue with id `{}`", id) |
| 51 | } | 55 | } |
| 52 | Self::QueueNotFound { id } => format!("queue with id `{}` not found", id), | 56 | Self::QueueNotFound { id } => format!("queue with id `{}` not found", id), |
| 57 | Self::NoInviteTokenAccess { id } => { | ||
| 58 | format!("you don't have access to the invite token with id `{}`", id) | ||
| 59 | } | ||
| 60 | Self::InviteTokenNotFound { id } => format!("invite token with id `{}` not found", id), | ||
| 53 | } | 61 | } |
| 54 | } | 62 | } |
| 55 | } | 63 | } |
diff --git a/src/main.rs b/src/main.rs index 4541646..7ed0fdd 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -2,10 +2,11 @@ mod api; | |||
| 2 | mod auth; | 2 | mod auth; |
| 3 | mod error; | 3 | mod error; |
| 4 | mod extract; | 4 | mod extract; |
| 5 | pub mod models; | 5 | mod models; |
| 6 | mod response; | 6 | mod response; |
| 7 | mod routers; | 7 | mod routers; |
| 8 | mod state; | 8 | mod state; |
| 9 | mod util; | ||
| 9 | 10 | ||
| 10 | pub use api::{AppOpenApi, tags}; | 11 | pub use api::{AppOpenApi, tags}; |
| 11 | pub use auth::{JwtClaims, create_jwt, create_password, validate_jwt, validate_password}; | 12 | pub use auth::{JwtClaims, create_jwt, create_password, validate_jwt, validate_password}; |
diff --git a/src/models.rs b/src/models.rs index 4821ec3..e815b73 100644 --- a/src/models.rs +++ b/src/models.rs | |||
| @@ -1,6 +1,8 @@ | |||
| 1 | use entity::{queues, users}; | 1 | use chrono::NaiveDateTime; |
| 2 | use entity::{invite_tokens, queues, users}; | ||
| 2 | use serde::Serialize; | 3 | use serde::Serialize; |
| 3 | use utoipa::ToSchema; | 4 | use utoipa::ToSchema; |
| 5 | use uuid::Uuid; | ||
| 4 | 6 | ||
| 5 | #[derive(Serialize, ToSchema)] | 7 | #[derive(Serialize, ToSchema)] |
| 6 | #[schema(description = "Account information")] | 8 | #[schema(description = "Account information")] |
| @@ -42,3 +44,28 @@ impl From<queues::Model> for Queue { | |||
| 42 | } | 44 | } |
| 43 | } | 45 | } |
| 44 | } | 46 | } |
| 47 | |||
| 48 | #[derive(Serialize, ToSchema)] | ||
| 49 | pub struct InviteToken { | ||
| 50 | #[schema(examples(1))] | ||
| 51 | pub id: i64, | ||
| 52 | pub token: Uuid, | ||
| 53 | #[schema(examples(1))] | ||
| 54 | pub queue_id: i64, | ||
| 55 | #[schema(examples("For classmates", "Для однокурсников"))] | ||
| 56 | pub name: String, | ||
| 57 | #[schema(examples(false))] | ||
| 58 | pub expiration_date: Option<NaiveDateTime>, | ||
| 59 | } | ||
| 60 | |||
| 61 | impl From<invite_tokens::Model> for InviteToken { | ||
| 62 | fn from(value: invite_tokens::Model) -> Self { | ||
| 63 | Self { | ||
| 64 | id: value.id, | ||
| 65 | token: value.token, | ||
| 66 | queue_id: value.queue_id, | ||
| 67 | name: value.name, | ||
| 68 | expiration_date: value.expiration_date, | ||
| 69 | } | ||
| 70 | } | ||
| 71 | } | ||
diff --git a/src/routers/account.rs b/src/routers/account.rs index 51ce911..e39327a 100644 --- a/src/routers/account.rs +++ b/src/routers/account.rs | |||
| @@ -2,30 +2,23 @@ use axum::extract::State; | |||
| 2 | use chrono::{DateTime, Duration, Utc}; | 2 | use chrono::{DateTime, Duration, Utc}; |
| 3 | use entity::users::{self}; | 3 | use entity::users::{self}; |
| 4 | use sea_orm::{ | 4 | use sea_orm::{ |
| 5 | ActiveModelTrait, ActiveValue::Set, ColumnTrait, DatabaseConnection, EntityTrait, | 5 | ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, IntoActiveModel, ModelTrait, |
| 6 | IntoActiveModel, ModelTrait, QueryFilter, | 6 | QueryFilter, |
| 7 | }; | 7 | }; |
| 8 | use serde::{Deserialize, Serialize}; | 8 | use serde::{Deserialize, Serialize}; |
| 9 | use utoipa::ToSchema; | 9 | use utoipa::ToSchema; |
| 10 | use utoipa_axum::{router::OpenApiRouter, routes}; | 10 | use utoipa_axum::{router::OpenApiRouter, routes}; |
| 11 | 11 | ||
| 12 | use crate::{ | 12 | use crate::{ |
| 13 | ApiError, ApiResult, AppState, ClientError, GlobalResponses, JwtClaims, ServerError, | 13 | ApiResult, AppState, ClientError, GlobalResponses, JwtClaims, ServerError, SuccessResponse, |
| 14 | SuccessResponse, create_jwt, create_password, | 14 | create_jwt, create_password, |
| 15 | extract::{ApiJson, Auth}, | 15 | extract::{ApiJson, Auth}, |
| 16 | models::Account, | 16 | models::Account, |
| 17 | tags::ACCOUNT, | 17 | tags::ACCOUNT, |
| 18 | util::username_exists, | ||
| 18 | validate_password, | 19 | validate_password, |
| 19 | }; | 20 | }; |
| 20 | 21 | ||
| 21 | async fn username_exists(username: &str, db: &DatabaseConnection) -> Result<bool, ApiError> { | ||
| 22 | Ok(users::Entity::find() | ||
| 23 | .filter(users::Column::Username.eq(username)) | ||
| 24 | .one(db) | ||
| 25 | .await? | ||
| 26 | .is_some()) | ||
| 27 | } | ||
| 28 | |||
| 29 | #[derive(Serialize, ToSchema)] | 22 | #[derive(Serialize, ToSchema)] |
| 30 | #[schema(description = "Authorization token information")] | 23 | #[schema(description = "Authorization token information")] |
| 31 | struct Token { | 24 | struct Token { |
| @@ -205,7 +198,7 @@ async fn login( | |||
| 205 | } | 198 | } |
| 206 | 199 | ||
| 207 | #[utoipa::path( | 200 | #[utoipa::path( |
| 208 | put, | 201 | patch, |
| 209 | path = "/update/password", | 202 | path = "/update/password", |
| 210 | tag = ACCOUNT, | 203 | tag = ACCOUNT, |
| 211 | summary = "Change password", | 204 | summary = "Change password", |
| @@ -214,7 +207,7 @@ async fn login( | |||
| 214 | responses( | 207 | responses( |
| 215 | ( | 208 | ( |
| 216 | status = 200, body = SuccessResponse<Account>, | 209 | status = 200, body = SuccessResponse<Account>, |
| 217 | description = "Success response with the updated account data" | 210 | description = "Success response with the changed account data" |
| 218 | ), | 211 | ), |
| 219 | GlobalResponses | 212 | GlobalResponses |
| 220 | ), | 213 | ), |
| @@ -238,7 +231,7 @@ async fn update_password( | |||
| 238 | } | 231 | } |
| 239 | 232 | ||
| 240 | #[utoipa::path( | 233 | #[utoipa::path( |
| 241 | put, | 234 | patch, |
| 242 | path = "/update/username", | 235 | path = "/update/username", |
| 243 | tag = ACCOUNT, | 236 | tag = ACCOUNT, |
| 244 | summary = "Change username", | 237 | summary = "Change username", |
| @@ -247,7 +240,7 @@ async fn update_password( | |||
| 247 | responses( | 240 | responses( |
| 248 | ( | 241 | ( |
| 249 | status = 200, body = SuccessResponse<Account>, | 242 | status = 200, body = SuccessResponse<Account>, |
| 250 | description = "Success response with the updated account data" | 243 | description = "Success response with the changed account data" |
| 251 | ), | 244 | ), |
| 252 | GlobalResponses | 245 | GlobalResponses |
| 253 | ), | 246 | ), |
| @@ -273,7 +266,7 @@ async fn update_username( | |||
| 273 | } | 266 | } |
| 274 | 267 | ||
| 275 | #[utoipa::path( | 268 | #[utoipa::path( |
| 276 | put, | 269 | patch, |
| 277 | path = "/update/name", | 270 | path = "/update/name", |
| 278 | tag = ACCOUNT, | 271 | tag = ACCOUNT, |
| 279 | summary = "Change name", | 272 | summary = "Change name", |
| @@ -282,7 +275,7 @@ async fn update_username( | |||
| 282 | responses( | 275 | responses( |
| 283 | ( | 276 | ( |
| 284 | status = 200, body = SuccessResponse<Account>, | 277 | status = 200, body = SuccessResponse<Account>, |
| 285 | description = "Success response with the updated account data" | 278 | description = "Success response with the changed account data" |
| 286 | ), | 279 | ), |
| 287 | GlobalResponses | 280 | GlobalResponses |
| 288 | ), | 281 | ), |
| @@ -332,7 +325,6 @@ async fn delete( | |||
| 332 | } | 325 | } |
| 333 | 326 | ||
| 334 | user.clone().delete(&state.db).await?; | 327 | user.clone().delete(&state.db).await?; |
| 335 | |||
| 336 | Ok(SuccessResponse::ok(user.into())) | 328 | Ok(SuccessResponse::ok(user.into())) |
| 337 | } | 329 | } |
| 338 | 330 | ||
diff --git a/src/routers/queue/access.rs b/src/routers/queue/access.rs index 1cba0b1..1aee8da 100644 --- a/src/routers/queue/access.rs +++ b/src/routers/queue/access.rs | |||
| @@ -1,7 +1,260 @@ | |||
| 1 | use utoipa_axum::router::OpenApiRouter; | 1 | use axum::extract::State; |
| 2 | use chrono::{DateTime, Utc}; | ||
| 3 | use entity::invite_tokens; | ||
| 4 | use sea_orm::{ActiveModelTrait, ActiveValue::Set, IntoActiveModel, ModelTrait}; | ||
| 5 | use serde::Deserialize; | ||
| 6 | use utoipa::{IntoParams, ToSchema}; | ||
| 7 | use utoipa_axum::{router::OpenApiRouter, routes}; | ||
| 2 | 8 | ||
| 3 | use crate::AppState; | 9 | use crate::{ |
| 10 | ApiResult, AppState, GlobalResponses, SuccessResponse, | ||
| 11 | extract::{ApiJson, ApiQuery, Auth}, | ||
| 12 | models::InviteToken, | ||
| 13 | tags::INVITE_TOKEN, | ||
| 14 | util::{get_owned_invite_token, get_owned_queue}, | ||
| 15 | }; | ||
| 16 | |||
| 17 | #[derive(Deserialize, IntoParams)] | ||
| 18 | #[into_params(parameter_in = Query)] | ||
| 19 | struct GetInviteTokenByIdQuery { | ||
| 20 | #[param(example = 1)] | ||
| 21 | id: i64, | ||
| 22 | } | ||
| 23 | |||
| 24 | #[derive(Deserialize, IntoParams)] | ||
| 25 | #[into_params(parameter_in = Query)] | ||
| 26 | struct GetInviteTokenByQueueIdQuery { | ||
| 27 | #[param(example = 1)] | ||
| 28 | queue_id: i64, | ||
| 29 | } | ||
| 30 | |||
| 31 | #[derive(Deserialize, ToSchema)] | ||
| 32 | #[schema(description = "Body of the create invite token request")] | ||
| 33 | struct CreateInviteTokenRequest { | ||
| 34 | #[schema(examples(1))] | ||
| 35 | queue_id: i64, | ||
| 36 | #[schema(examples("For classmates", "Для однокурсников"))] | ||
| 37 | name: String, | ||
| 38 | expiration_date: Option<DateTime<Utc>>, | ||
| 39 | } | ||
| 40 | |||
| 41 | #[derive(Deserialize, ToSchema)] | ||
| 42 | #[schema(description = "Body of the expire invite token request")] | ||
| 43 | struct ExpireInviteTokenRequest { | ||
| 44 | #[schema(examples(1))] | ||
| 45 | id: i64, | ||
| 46 | } | ||
| 47 | |||
| 48 | #[derive(Deserialize, ToSchema)] | ||
| 49 | #[schema(description = "Body of the change invite token expiration date request")] | ||
| 50 | struct ChangeInviteTokenExpirationDateRequest { | ||
| 51 | #[schema(examples(1))] | ||
| 52 | id: i64, | ||
| 53 | #[schema(examples("2000-01-01 00:00:00Z", "2000-01-01 03:00:00+03:00", json!(null)))] | ||
| 54 | expiration_date: Option<DateTime<Utc>>, | ||
| 55 | } | ||
| 56 | |||
| 57 | #[derive(Deserialize, ToSchema)] | ||
| 58 | #[schema(description = "Body of the delete invite token request")] | ||
| 59 | struct DeleteInviteTokenRequest { | ||
| 60 | #[schema(examples(1))] | ||
| 61 | id: i64, | ||
| 62 | } | ||
| 63 | |||
| 64 | #[utoipa::path( | ||
| 65 | get, | ||
| 66 | path = "/get/by_id", | ||
| 67 | tag = INVITE_TOKEN, | ||
| 68 | summary = "Get by id", | ||
| 69 | description = "Get the invite token by id", | ||
| 70 | params(GetInviteTokenByIdQuery), | ||
| 71 | responses( | ||
| 72 | ( | ||
| 73 | status = 200, body = SuccessResponse<InviteToken>, | ||
| 74 | description = "Success response with the requested invite token" | ||
| 75 | ), | ||
| 76 | GlobalResponses | ||
| 77 | ), | ||
| 78 | security(("auth" = [])), | ||
| 79 | )] | ||
| 80 | async fn get_by_id( | ||
| 81 | State(state): State<AppState>, | ||
| 82 | Auth(user): Auth, | ||
| 83 | ApiQuery(req): ApiQuery<GetInviteTokenByIdQuery>, | ||
| 84 | ) -> ApiResult<InviteToken> { | ||
| 85 | Ok(SuccessResponse::ok( | ||
| 86 | get_owned_invite_token(req.id, user.id, &state.db) | ||
| 87 | .await? | ||
| 88 | .into(), | ||
| 89 | )) | ||
| 90 | } | ||
| 91 | |||
| 92 | #[utoipa::path( | ||
| 93 | get, | ||
| 94 | path = "/get/by_queue_id", | ||
| 95 | tag = INVITE_TOKEN, | ||
| 96 | summary = "Get by queue id", | ||
| 97 | description = "Get the invite token by the queue id", | ||
| 98 | params(GetInviteTokenByQueueIdQuery), | ||
| 99 | responses( | ||
| 100 | ( | ||
| 101 | status = 200, body = SuccessResponse<Vec<InviteToken>>, | ||
| 102 | description = "Success response with the requested invite tokens" | ||
| 103 | ), | ||
| 104 | GlobalResponses | ||
| 105 | ), | ||
| 106 | security(("auth" = [])), | ||
| 107 | )] | ||
| 108 | async fn get_by_queue_id( | ||
| 109 | State(state): State<AppState>, | ||
| 110 | Auth(user): Auth, | ||
| 111 | ApiQuery(req): ApiQuery<GetInviteTokenByQueueIdQuery>, | ||
| 112 | ) -> ApiResult<Vec<InviteToken>> { | ||
| 113 | let queue = get_owned_queue(req.queue_id, user.id, &state.db).await?; | ||
| 114 | |||
| 115 | Ok(SuccessResponse::ok( | ||
| 116 | queue | ||
| 117 | .find_related(invite_tokens::Entity) | ||
| 118 | .all(&state.db) | ||
| 119 | .await? | ||
| 120 | .into_iter() | ||
| 121 | .map(Into::into) | ||
| 122 | .collect(), | ||
| 123 | )) | ||
| 124 | } | ||
| 125 | |||
| 126 | #[utoipa::path( | ||
| 127 | post, | ||
| 128 | path = "/create", | ||
| 129 | tag = INVITE_TOKEN, | ||
| 130 | summary = "Create", | ||
| 131 | description = "Create a new invite token", | ||
| 132 | request_body = CreateInviteTokenRequest, | ||
| 133 | responses( | ||
| 134 | ( | ||
| 135 | status = 200, body = SuccessResponse<InviteToken>, | ||
| 136 | description = "Success response with the created invite token" | ||
| 137 | ), | ||
| 138 | GlobalResponses | ||
| 139 | ), | ||
| 140 | security(("auth" = [])), | ||
| 141 | )] | ||
| 142 | async fn create( | ||
| 143 | State(state): State<AppState>, | ||
| 144 | Auth(user): Auth, | ||
| 145 | ApiJson(req): ApiJson<CreateInviteTokenRequest>, | ||
| 146 | ) -> ApiResult<InviteToken> { | ||
| 147 | let queue = get_owned_queue(req.queue_id, user.id, &state.db).await?; | ||
| 148 | |||
| 149 | Ok(SuccessResponse::ok( | ||
| 150 | invite_tokens::ActiveModel { | ||
| 151 | token: Set(uuid::Uuid::new_v4()), | ||
| 152 | queue_id: Set(queue.id), | ||
| 153 | name: Set(req.name), | ||
| 154 | expiration_date: Set(req.expiration_date.as_ref().map(DateTime::naive_utc)), | ||
| 155 | ..Default::default() | ||
| 156 | } | ||
| 157 | .insert(&state.db) | ||
| 158 | .await? | ||
| 159 | .into(), | ||
| 160 | )) | ||
| 161 | } | ||
| 162 | |||
| 163 | #[utoipa::path( | ||
| 164 | patch, | ||
| 165 | path = "/expire", | ||
| 166 | tag = INVITE_TOKEN, | ||
| 167 | summary = "Expire", | ||
| 168 | description = "Expire the invite token", | ||
| 169 | request_body = ExpireInviteTokenRequest, | ||
| 170 | responses( | ||
| 171 | ( | ||
| 172 | status = 200, body = SuccessResponse<InviteToken>, | ||
| 173 | description = "Success response with the changed invite token data" | ||
| 174 | ), | ||
| 175 | GlobalResponses | ||
| 176 | ), | ||
| 177 | security(("auth" = [])), | ||
| 178 | )] | ||
| 179 | async fn expire( | ||
| 180 | State(state): State<AppState>, | ||
| 181 | Auth(user): Auth, | ||
| 182 | ApiJson(req): ApiJson<ExpireInviteTokenRequest>, | ||
| 183 | ) -> ApiResult<InviteToken> { | ||
| 184 | let mut active_invite_token = get_owned_invite_token(req.id, user.id, &state.db) | ||
| 185 | .await? | ||
| 186 | .into_active_model(); | ||
| 187 | |||
| 188 | active_invite_token.expiration_date = Set(Some(Utc::now().naive_utc())); | ||
| 189 | |||
| 190 | let invite_token = active_invite_token.update(&state.db).await?; | ||
| 191 | Ok(SuccessResponse::ok(invite_token.into())) | ||
| 192 | } | ||
| 193 | |||
| 194 | #[utoipa::path( | ||
| 195 | patch, | ||
| 196 | path = "/update/expiration_date", | ||
| 197 | tag = INVITE_TOKEN, | ||
| 198 | summary = "Change expiration date", | ||
| 199 | description = "Change invite token expiration date", | ||
| 200 | request_body = ChangeInviteTokenExpirationDateRequest, | ||
| 201 | responses( | ||
| 202 | ( | ||
| 203 | status = 200, body = SuccessResponse<InviteToken>, | ||
| 204 | description = "Success response with the changed invite token data" | ||
| 205 | ), | ||
| 206 | GlobalResponses | ||
| 207 | ), | ||
| 208 | security(("auth" = [])), | ||
| 209 | )] | ||
| 210 | async fn update_expiration_date( | ||
| 211 | State(state): State<AppState>, | ||
| 212 | Auth(user): Auth, | ||
| 213 | ApiJson(req): ApiJson<ChangeInviteTokenExpirationDateRequest>, | ||
| 214 | ) -> ApiResult<InviteToken> { | ||
| 215 | let mut active_invite_token = get_owned_invite_token(req.id, user.id, &state.db) | ||
| 216 | .await? | ||
| 217 | .into_active_model(); | ||
| 218 | |||
| 219 | active_invite_token.expiration_date = | ||
| 220 | Set(req.expiration_date.as_ref().map(DateTime::naive_utc)); | ||
| 221 | |||
| 222 | let invite_token = active_invite_token.update(&state.db).await?; | ||
| 223 | Ok(SuccessResponse::ok(invite_token.into())) | ||
| 224 | } | ||
| 225 | |||
| 226 | #[utoipa::path( | ||
| 227 | delete, | ||
| 228 | path = "/delete", | ||
| 229 | tag = INVITE_TOKEN, | ||
| 230 | summary = "Delete", | ||
| 231 | description = "Delete the invite token", | ||
| 232 | request_body = DeleteInviteTokenRequest, | ||
| 233 | responses( | ||
| 234 | ( | ||
| 235 | status = 200, body = SuccessResponse<InviteToken>, | ||
| 236 | description = "Success response with the deleted invite token data" | ||
| 237 | ), | ||
| 238 | GlobalResponses | ||
| 239 | ), | ||
| 240 | security(("auth" = [])), | ||
| 241 | )] | ||
| 242 | async fn delete( | ||
| 243 | State(state): State<AppState>, | ||
| 244 | Auth(user): Auth, | ||
| 245 | ApiJson(req): ApiJson<DeleteInviteTokenRequest>, | ||
| 246 | ) -> ApiResult<InviteToken> { | ||
| 247 | let invite_token = get_owned_invite_token(req.id, user.id, &state.db).await?; | ||
| 248 | invite_token.clone().delete(&state.db).await?; | ||
| 249 | Ok(SuccessResponse::ok(invite_token.into())) | ||
| 250 | } | ||
| 4 | 251 | ||
| 5 | pub fn router() -> OpenApiRouter<AppState> { | 252 | pub fn router() -> OpenApiRouter<AppState> { |
| 6 | OpenApiRouter::new() | 253 | OpenApiRouter::new() |
| 254 | .routes(routes!(get_by_id)) | ||
| 255 | .routes(routes!(get_by_queue_id)) | ||
| 256 | .routes(routes!(create)) | ||
| 257 | .routes(routes!(expire)) | ||
| 258 | .routes(routes!(update_expiration_date)) | ||
| 259 | .routes(routes!(delete)) | ||
| 7 | } | 260 | } |
diff --git a/src/routers/queue/manage.rs b/src/routers/queue/manage.rs index 8f42e07..b799458 100644 --- a/src/routers/queue/manage.rs +++ b/src/routers/queue/manage.rs | |||
| @@ -1,50 +1,32 @@ | |||
| 1 | use axum::extract::State; | 1 | use axum::extract::State; |
| 2 | use entity::{queues, users}; | 2 | use entity::queues; |
| 3 | use sea_orm::{ | 3 | use sea_orm::{ |
| 4 | ActiveModelTrait, ActiveValue::Set, ColumnTrait, DatabaseConnection, EntityTrait, | 4 | ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, IntoActiveModel, ModelTrait, |
| 5 | IntoActiveModel, ModelTrait, QueryFilter, | 5 | QueryFilter, |
| 6 | }; | 6 | }; |
| 7 | use serde::Deserialize; | 7 | use serde::Deserialize; |
| 8 | use utoipa::{IntoParams, ToSchema}; | 8 | use utoipa::{IntoParams, ToSchema}; |
| 9 | use utoipa_axum::{router::OpenApiRouter, routes}; | 9 | use utoipa_axum::{router::OpenApiRouter, routes}; |
| 10 | 10 | ||
| 11 | use crate::{ | 11 | use crate::{ |
| 12 | ApiError, ApiResult, AppState, ClientError, GlobalResponses, SuccessResponse, | 12 | ApiResult, AppState, ClientError, GlobalResponses, SuccessResponse, |
| 13 | extract::{ApiJson, ApiQuery, Auth}, | 13 | extract::{ApiJson, ApiQuery, Auth}, |
| 14 | models::Queue, | 14 | models::Queue, |
| 15 | tags::QUEUE, | 15 | tags::QUEUE, |
| 16 | util::{get_owned_queue, user_exists}, | ||
| 16 | }; | 17 | }; |
| 17 | 18 | ||
| 18 | async fn user_exists(id: i64, db: &DatabaseConnection) -> Result<bool, ApiError> { | ||
| 19 | Ok(users::Entity::find_by_id(id).one(db).await?.is_some()) | ||
| 20 | } | ||
| 21 | |||
| 22 | async fn get_owned_queue( | ||
| 23 | id: i64, | ||
| 24 | owner_id: i64, | ||
| 25 | db: &DatabaseConnection, | ||
| 26 | ) -> Result<queues::Model, ApiError> { | ||
| 27 | let queue = queues::Entity::find_by_id(id) | ||
| 28 | .one(db) | ||
| 29 | .await? | ||
| 30 | .ok_or(ClientError::QueueNotFound { id })?; | ||
| 31 | |||
| 32 | if queue.owner_id != owner_id { | ||
| 33 | return Err(ClientError::NotQueueOwner { id: queue.id }.into()); | ||
| 34 | } | ||
| 35 | |||
| 36 | Ok(queue) | ||
| 37 | } | ||
| 38 | |||
| 39 | #[derive(Deserialize, IntoParams)] | 19 | #[derive(Deserialize, IntoParams)] |
| 40 | #[into_params(parameter_in = Query)] | 20 | #[into_params(parameter_in = Query)] |
| 41 | struct GetByIdQueueQuery { | 21 | struct GetQueueByIdQuery { |
| 22 | #[param(example = 1)] | ||
| 42 | id: i64, | 23 | id: i64, |
| 43 | } | 24 | } |
| 44 | 25 | ||
| 45 | #[derive(Deserialize, IntoParams)] | 26 | #[derive(Deserialize, IntoParams)] |
| 46 | #[into_params(parameter_in = Query)] | 27 | #[into_params(parameter_in = Query)] |
| 47 | struct GetByOwnerIdQuery { | 28 | struct GetByOwnerIdQuery { |
| 29 | #[param(example = 1)] | ||
| 48 | owner_id: i64, | 30 | owner_id: i64, |
| 49 | } | 31 | } |
| 50 | 32 | ||
| @@ -82,11 +64,11 @@ struct DeleteQueueRequest { | |||
| 82 | 64 | ||
| 83 | #[utoipa::path( | 65 | #[utoipa::path( |
| 84 | get, | 66 | get, |
| 85 | path = "/get/id", | 67 | path = "/get/by_id", |
| 86 | tag = QUEUE, | 68 | tag = QUEUE, |
| 87 | summary = "Get by id", | 69 | summary = "Get by id", |
| 88 | description = "Get the queue by id", | 70 | description = "Get the queue by id", |
| 89 | params(GetByIdQueueQuery), | 71 | params(GetQueueByIdQuery), |
| 90 | responses( | 72 | responses( |
| 91 | ( | 73 | ( |
| 92 | status = 200, body = SuccessResponse<Option<Queue>>, | 74 | status = 200, body = SuccessResponse<Option<Queue>>, |
| @@ -97,7 +79,7 @@ struct DeleteQueueRequest { | |||
| 97 | )] | 79 | )] |
| 98 | async fn get_by_id( | 80 | async fn get_by_id( |
| 99 | State(state): State<AppState>, | 81 | State(state): State<AppState>, |
| 100 | ApiQuery(req): ApiQuery<GetByIdQueueQuery>, | 82 | ApiQuery(req): ApiQuery<GetQueueByIdQuery>, |
| 101 | ) -> ApiResult<Option<Queue>> { | 83 | ) -> ApiResult<Option<Queue>> { |
| 102 | Ok(SuccessResponse::ok( | 84 | Ok(SuccessResponse::ok( |
| 103 | queues::Entity::find_by_id(req.id) | 85 | queues::Entity::find_by_id(req.id) |
| @@ -109,10 +91,10 @@ async fn get_by_id( | |||
| 109 | 91 | ||
| 110 | #[utoipa::path( | 92 | #[utoipa::path( |
| 111 | get, | 93 | get, |
| 112 | path = "/get/owner", | 94 | path = "/get/by_owner", |
| 113 | tag = QUEUE, | 95 | tag = QUEUE, |
| 114 | summary = "Get by owner", | 96 | summary = "Get by owner", |
| 115 | description = "Get queues for a given owner", | 97 | description = "Get queues by the owner id", |
| 116 | params(GetByOwnerIdQuery), | 98 | params(GetByOwnerIdQuery), |
| 117 | responses( | 99 | responses( |
| 118 | ( | 100 | ( |
| @@ -126,7 +108,7 @@ async fn get_by_owner( | |||
| 126 | State(state): State<AppState>, | 108 | State(state): State<AppState>, |
| 127 | ApiQuery(req): ApiQuery<GetByOwnerIdQuery>, | 109 | ApiQuery(req): ApiQuery<GetByOwnerIdQuery>, |
| 128 | ) -> ApiResult<Vec<Queue>> { | 110 | ) -> ApiResult<Vec<Queue>> { |
| 129 | return Ok(SuccessResponse::ok( | 111 | Ok(SuccessResponse::ok( |
| 130 | queues::Entity::find() | 112 | queues::Entity::find() |
| 131 | .filter(queues::Column::OwnerId.eq(req.owner_id)) | 113 | .filter(queues::Column::OwnerId.eq(req.owner_id)) |
| 132 | .all(&state.db) | 114 | .all(&state.db) |
| @@ -134,7 +116,32 @@ async fn get_by_owner( | |||
| 134 | .into_iter() | 116 | .into_iter() |
| 135 | .map(Into::into) | 117 | .map(Into::into) |
| 136 | .collect(), | 118 | .collect(), |
| 137 | )); | 119 | )) |
| 120 | } | ||
| 121 | |||
| 122 | #[utoipa::path( | ||
| 123 | get, | ||
| 124 | path = "/get/owned", | ||
| 125 | tag = QUEUE, | ||
| 126 | summary = "Get owned", | ||
| 127 | description = "Get your queues", | ||
| 128 | responses( | ||
| 129 | ( | ||
| 130 | status = 200, body = SuccessResponse<Vec<Queue>>, | ||
| 131 | description = "Success response with queues owned by you" | ||
| 132 | ), | ||
| 133 | GlobalResponses | ||
| 134 | ), | ||
| 135 | )] | ||
| 136 | async fn get_owned(State(state): State<AppState>, Auth(user): Auth) -> ApiResult<Vec<Queue>> { | ||
| 137 | Ok(SuccessResponse::ok( | ||
| 138 | user.find_related(queues::Entity) | ||
| 139 | .all(&state.db) | ||
| 140 | .await? | ||
| 141 | .into_iter() | ||
| 142 | .map(Into::into) | ||
| 143 | .collect(), | ||
| 144 | )) | ||
| 138 | } | 145 | } |
| 139 | 146 | ||
| 140 | #[utoipa::path( | 147 | #[utoipa::path( |
| @@ -171,7 +178,7 @@ async fn create( | |||
| 171 | } | 178 | } |
| 172 | 179 | ||
| 173 | #[utoipa::path( | 180 | #[utoipa::path( |
| 174 | put, | 181 | patch, |
| 175 | path = "/update/name", | 182 | path = "/update/name", |
| 176 | tag = QUEUE, | 183 | tag = QUEUE, |
| 177 | summary = "Change name", | 184 | summary = "Change name", |
| @@ -202,7 +209,7 @@ async fn update_name( | |||
| 202 | } | 209 | } |
| 203 | 210 | ||
| 204 | #[utoipa::path( | 211 | #[utoipa::path( |
| 205 | put, | 212 | patch, |
| 206 | path = "/update/owner", | 213 | path = "/update/owner", |
| 207 | tag = QUEUE, | 214 | tag = QUEUE, |
| 208 | summary = "Change owner", | 215 | summary = "Change owner", |
| @@ -269,6 +276,7 @@ pub fn router() -> OpenApiRouter<AppState> { | |||
| 269 | OpenApiRouter::new() | 276 | OpenApiRouter::new() |
| 270 | .routes(routes!(get_by_id)) | 277 | .routes(routes!(get_by_id)) |
| 271 | .routes(routes!(get_by_owner)) | 278 | .routes(routes!(get_by_owner)) |
| 279 | .routes(routes!(get_owned)) | ||
| 272 | .routes(routes!(create)) | 280 | .routes(routes!(create)) |
| 273 | .routes(routes!(update_name)) | 281 | .routes(routes!(update_name)) |
| 274 | .routes(routes!(update_owner)) | 282 | .routes(routes!(update_owner)) |
diff --git a/src/routers/queue/mod.rs b/src/routers/queue/mod.rs index dd03956..9af1f01 100644 --- a/src/routers/queue/mod.rs +++ b/src/routers/queue/mod.rs | |||
| @@ -8,5 +8,5 @@ use crate::AppState; | |||
| 8 | pub fn router() -> OpenApiRouter<AppState> { | 8 | pub fn router() -> OpenApiRouter<AppState> { |
| 9 | OpenApiRouter::new() | 9 | OpenApiRouter::new() |
| 10 | .merge(manage::router()) | 10 | .merge(manage::router()) |
| 11 | .merge(access::router()) | 11 | .nest("/access", access::router()) |
| 12 | } | 12 | } |
diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..1d93085 --- /dev/null +++ b/src/util.rs | |||
| @@ -0,0 +1,51 @@ | |||
| 1 | use entity::{invite_tokens, queues, users}; | ||
| 2 | use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; | ||
| 3 | |||
| 4 | use crate::{ApiError, ClientError}; | ||
| 5 | |||
| 6 | pub async fn user_exists(id: i64, db: &DatabaseConnection) -> Result<bool, ApiError> { | ||
| 7 | Ok(users::Entity::find_by_id(id).one(db).await?.is_some()) | ||
| 8 | } | ||
| 9 | |||
| 10 | pub async fn username_exists(username: &str, db: &DatabaseConnection) -> Result<bool, ApiError> { | ||
| 11 | Ok(users::Entity::find() | ||
| 12 | .filter(users::Column::Username.eq(username)) | ||
| 13 | .one(db) | ||
| 14 | .await? | ||
| 15 | .is_some()) | ||
| 16 | } | ||
| 17 | |||
| 18 | pub async fn get_owned_queue( | ||
| 19 | id: i64, | ||
| 20 | owner_id: i64, | ||
| 21 | db: &DatabaseConnection, | ||
| 22 | ) -> Result<queues::Model, ApiError> { | ||
| 23 | let queue = queues::Entity::find_by_id(id) | ||
| 24 | .one(db) | ||
| 25 | .await? | ||
| 26 | .ok_or(ClientError::QueueNotFound { id })?; | ||
| 27 | |||
| 28 | if queue.owner_id != owner_id { | ||
| 29 | return Err(ClientError::NotQueueOwner { id: queue.id }.into()); | ||
| 30 | } | ||
| 31 | |||
| 32 | Ok(queue) | ||
| 33 | } | ||
| 34 | |||
| 35 | pub async fn get_owned_invite_token( | ||
| 36 | id: i64, | ||
| 37 | owner_id: i64, | ||
| 38 | db: &DatabaseConnection, | ||
| 39 | ) -> Result<invite_tokens::Model, ApiError> { | ||
| 40 | let (invite_token, queue) = invite_tokens::Entity::find_by_id(id) | ||
| 41 | .find_also_related(queues::Entity) | ||
| 42 | .one(db) | ||
| 43 | .await? | ||
| 44 | .ok_or(ClientError::InviteTokenNotFound { id })?; | ||
| 45 | |||
| 46 | if queue.unwrap().owner_id != owner_id { | ||
| 47 | return Err(ClientError::NoInviteTokenAccess { id }.into()); | ||
| 48 | } | ||
| 49 | |||
| 50 | Ok(invite_token) | ||
| 51 | } | ||
