aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTolmachev Igor <me@igorek.dev>2025-10-22 17:00:28 +0300
committerTolmachev Igor <me@igorek.dev>2025-10-22 17:00:28 +0300
commite864d2653d50ba1c920776aaa14a1625c9fc9da4 (patch)
tree4e4089d3fc249f9b4a64a9866b48a0e1f5de844b
parent67ed4e6bb82ac2645f9b7a014a8d635d7b80e821 (diff)
downloadqueue_server-e864d2653d50ba1c920776aaa14a1625c9fc9da4.tar.gz
queue_server-e864d2653d50ba1c920776aaa14a1625c9fc9da4.zip
Add invite tokens
-rw-r--r--Cargo.toml2
-rw-r--r--entity/src/invite_tokens.rs2
-rw-r--r--migration/src/m0_init_tables.rs4
-rw-r--r--src/api.rs4
-rw-r--r--src/error/client.rs8
-rw-r--r--src/main.rs3
-rw-r--r--src/models.rs29
-rw-r--r--src/routers/account.rs30
-rw-r--r--src/routers/queue/access.rs257
-rw-r--r--src/routers/queue/manage.rs78
-rw-r--r--src/routers/queue/mod.rs2
-rw-r--r--src/util.rs51
12 files changed, 405 insertions, 65 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 505232c..7232c2c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,7 +23,7 @@ tower = "0.5.2"
23tower-http = { version = "0.6.6", features = ["trace"] } 23tower-http = { version = "0.6.6", features = ["trace"] }
24tracing = "0.1.41" 24tracing = "0.1.41"
25tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 25tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
26utoipa = { version = "5.4.0", features = ["axum_extras", "chrono", "preserve_order", "preserve_path_order"] } 26utoipa = { version = "5.4.0", features = ["axum_extras", "chrono", "preserve_order", "preserve_path_order", "uuid"] }
27utoipa-axum = "0.2.0" 27utoipa-axum = "0.2.0"
28utoipa-scalar = { version = "0.3.0", features = ["axum"] } 28utoipa-scalar = { version = "0.3.0", features = ["axum"] }
29uuid = { version = "1.18.1", features = ["v4", "serde"] } 29uuid = { 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?;
diff --git a/src/api.rs b/src/api.rs
index eb77237..dd50b77 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -8,7 +8,7 @@ use crate::ErrorResponse;
8pub mod tags { 8pub 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
14struct AuthModifier; 14struct 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
15impl ClientError { 17impl 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;
2mod auth; 2mod auth;
3mod error; 3mod error;
4mod extract; 4mod extract;
5pub mod models; 5mod models;
6mod response; 6mod response;
7mod routers; 7mod routers;
8mod state; 8mod state;
9mod util;
9 10
10pub use api::{AppOpenApi, tags}; 11pub use api::{AppOpenApi, tags};
11pub use auth::{JwtClaims, create_jwt, create_password, validate_jwt, validate_password}; 12pub 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 @@
1use entity::{queues, users}; 1use chrono::NaiveDateTime;
2use entity::{invite_tokens, queues, users};
2use serde::Serialize; 3use serde::Serialize;
3use utoipa::ToSchema; 4use utoipa::ToSchema;
5use 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)]
49pub 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
61impl 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;
2use chrono::{DateTime, Duration, Utc}; 2use chrono::{DateTime, Duration, Utc};
3use entity::users::{self}; 3use entity::users::{self};
4use sea_orm::{ 4use 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};
8use serde::{Deserialize, Serialize}; 8use serde::{Deserialize, Serialize};
9use utoipa::ToSchema; 9use utoipa::ToSchema;
10use utoipa_axum::{router::OpenApiRouter, routes}; 10use utoipa_axum::{router::OpenApiRouter, routes};
11 11
12use crate::{ 12use 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
21async 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")]
31struct Token { 24struct 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 @@
1use utoipa_axum::router::OpenApiRouter; 1use axum::extract::State;
2use chrono::{DateTime, Utc};
3use entity::invite_tokens;
4use sea_orm::{ActiveModelTrait, ActiveValue::Set, IntoActiveModel, ModelTrait};
5use serde::Deserialize;
6use utoipa::{IntoParams, ToSchema};
7use utoipa_axum::{router::OpenApiRouter, routes};
2 8
3use crate::AppState; 9use 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)]
19struct GetInviteTokenByIdQuery {
20 #[param(example = 1)]
21 id: i64,
22}
23
24#[derive(Deserialize, IntoParams)]
25#[into_params(parameter_in = Query)]
26struct 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")]
33struct 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")]
43struct 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")]
50struct 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")]
59struct 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)]
80async 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)]
108async 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)]
142async 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)]
179async 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)]
210async 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)]
242async 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
5pub fn router() -> OpenApiRouter<AppState> { 252pub 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 @@
1use axum::extract::State; 1use axum::extract::State;
2use entity::{queues, users}; 2use entity::queues;
3use sea_orm::{ 3use 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};
7use serde::Deserialize; 7use serde::Deserialize;
8use utoipa::{IntoParams, ToSchema}; 8use utoipa::{IntoParams, ToSchema};
9use utoipa_axum::{router::OpenApiRouter, routes}; 9use utoipa_axum::{router::OpenApiRouter, routes};
10 10
11use crate::{ 11use 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
18async 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
22async 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)]
41struct GetByIdQueueQuery { 21struct 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)]
47struct GetByOwnerIdQuery { 28struct 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)]
98async fn get_by_id( 80async 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)]
136async 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;
8pub fn router() -> OpenApiRouter<AppState> { 8pub 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 @@
1use entity::{invite_tokens, queues, users};
2use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
3
4use crate::{ApiError, ClientError};
5
6pub 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
10pub 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
18pub 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
35pub 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}