feat(api): phase3e sessions/{id}+is_current, devices, access-key patch/rotate, notification filters+batch read, space @me join/leave
ZhenYi committed
Added lib/auth/src/handlers/devices.rs lib/auth/src/handlers/devices.rs +78 −0 View lib/auth/src/handlers/devices.rs
@@ -0,0 +1,78 @@
1use axum::extract::{Path, Query, State};
2use axum::Json;
3use export::rest::{AppResponseData, NoData};
4use session::Session;
5use uuid::Uuid;
6
7use crate::errors::ApiError;
8use crate::services::AuthService;
9use crate::services::devices::{DeviceDto, UpdateDeviceCmd};
10use crate::share::types::ListQuery;
11use session::share::session::require_uid;
12
13#[utoipa::path(
14 get,
15 path = "/me/devices",
16 tag = "account",
17 params(ListQuery),
18 responses(
19 (status = 200, description = "devices seen by your account", body = AppResponseData<Vec<DeviceDto>>)
20 )
21)]
22pub async fn list_devices(
23 State(service): State<AuthService>,
24 session: Session,
25 Query(query): Query<ListQuery>,
26) -> Result<AppResponseData<Vec<DeviceDto>>, ApiError> {
27 let uid = require_uid(&session)?;
28 let devices = service.list_devices(uid, query).await.map_err(ApiError::from)?;
29 Ok(AppResponseData::ok(devices))
30}
31
32#[utoipa::path(
33 patch,
34 path = "/me/devices/{device_id}",
35 tag = "account",
36 params(("device_id" = Uuid, Path)),
37 request_body = UpdateDeviceCmd,
38 responses(
39 (status = 200, description = "device updated", body = AppResponseData<NoData>),
40 (status = 404, description = "not found", body = AppResponseData<NoData>)
41 )
42)]
43pub async fn update_device(
44 State(service): State<AuthService>,
45 session: Session,
46 Path(device_id): Path<Uuid>,
47 Json(cmd): Json<UpdateDeviceCmd>,
48) -> Result<AppResponseData<NoData>, ApiError> {
49 let uid = require_uid(&session)?;
50 service
51 .update_device(uid, device_id, cmd)
52 .await
53 .map_err(ApiError::from)?;
54 Ok(AppResponseData::empty())
55}
56
57#[utoipa::path(
58 delete,
59 path = "/me/devices/{device_id}",
60 tag = "account",
61 params(("device_id" = Uuid, Path)),
62 responses(
63 (status = 200, description = "device removed and its sessions revoked", body = AppResponseData<NoData>),
64 (status = 404, description = "not found", body = AppResponseData<NoData>)
65 )
66)]
67pub async fn delete_device(
68 State(service): State<AuthService>,
69 session: Session,
70 Path(device_id): Path<Uuid>,
71) -> Result<AppResponseData<NoData>, ApiError> {
72 let uid = require_uid(&session)?;
73 service
74 .delete_device(uid, device_id)
75 .await
76 .map_err(ApiError::from)?;
77 Ok(AppResponseData::empty())
78}
Modified lib/auth/src/handlers/list_notifications.rs lib/auth/src/handlers/list_notifications.rs +10 −1 View lib/auth/src/handlers/list_notifications.rs
@@ -13,6 +13,9 @@
1313pub struct ListNotificationsQuery {
1414 pub limit: Option<u64>,
1515 pub offset: Option<u64>,
16 #[serde(rename = "type")]
17 pub notification_type: Option<models::users::user_notifications::NotificationType>,
18 pub unread: Option<bool>,
1619}
1720
1821#[utoipa::path(
@@ -31,7 +34,13 @@
3134) -> Result<AppResponseData<Vec<NotificationDto>>, ApiError> {
3235 let uid = require_uid(&session)?;
3336 let notifications = service
34 .list_notifications(uid, query.limit, query.offset)
37 .list_notifications(
38 uid,
39 query.limit,
40 query.offset,
41 query.notification_type,
42 query.unread,
43 )
3544 .await
3645 .map_err(ApiError::from)?;
3746 Ok(AppResponseData::ok(notifications))
Added lib/auth/src/handlers/mark_notifications_read.rs lib/auth/src/handlers/mark_notifications_read.rs +65 −0 View lib/auth/src/handlers/mark_notifications_read.rs
@@ -0,0 +1,65 @@
1use axum::Json;
2use axum::extract::State;
3use export::rest::{AppResponseData, NoData};
4use serde::Deserialize;
5use session::Session;
6use uuid::Uuid;
7
8use crate::errors::ApiError;
9use crate::services::AuthService;
10use session::share::session::require_uid;
11
12#[derive(Debug, Deserialize, utoipa::ToSchema)]
13pub struct MarkNotificationsReadCmd {
14 #[serde(default)]
15 pub ids: Option<Vec<Uuid>>,
16 #[serde(default)]
17 pub all: bool,
18}
19
20#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
21pub struct MarkedCountResp {
22 pub marked: u64,
23}
24
25#[utoipa::path(
26 put,
27 path = "/me/notifications/read",
28 tag = "account",
29 request_body = MarkNotificationsReadCmd,
30 responses(
31 (status = 200, description = "notifications marked read", body = AppResponseData<MarkedCountResp>),
32 (status = 400, description = "bad request", body = AppResponseData<NoData>)
33 )
34)]
35pub async fn mark_notifications_read(
36 State(service): State<AuthService>,
37 session: Session,
38 Json(cmd): Json<MarkNotificationsReadCmd>,
39) -> Result<AppResponseData<MarkedCountResp>, ApiError> {
40 let uid = require_uid(&session)?;
41 let marked = if cmd.all {
42 service
43 .mark_all_notifications_read(uid)
44 .await
45 .map_err(ApiError::from)?;
46 u64::MAX
47 } else if let Some(ids) = cmd.ids {
48 if ids.is_empty() {
49 return Err(ApiError::BadRequest(AppResponseData::err(
50 400,
51 "ids must not be empty",
52 )));
53 }
54 service
55 .mark_notifications_read(uid, ids)
56 .await
57 .map_err(ApiError::from)?
58 } else {
59 return Err(ApiError::BadRequest(AppResponseData::err(
60 400,
61 "provide ids or all",
62 )));
63 };
64 Ok(AppResponseData::ok(MarkedCountResp { marked }))
65}
Modified lib/auth/src/handlers/mod.rs lib/auth/src/handlers/mod.rs +4 −0 View lib/auth/src/handlers/mod.rs
@@ -22,7 +22,11 @@
2222pub mod login;
2323pub mod login_2fa;
2424pub mod logout;
25pub mod devices;
2526pub mod mark_all_notifications_read;
27pub mod mark_notifications_read;
28pub mod rotate_access_key;
29pub mod update_access_key;
2630pub mod mark_notification_read;
2731pub mod me;
2832pub mod pow_challenge;
Added lib/auth/src/handlers/rotate_access_key.rs lib/auth/src/handlers/rotate_access_key.rs +32 −0 View lib/auth/src/handlers/rotate_access_key.rs
@@ -0,0 +1,32 @@
1use axum::extract::{Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use uuid::Uuid;
5
6use crate::errors::ApiError;
7use crate::services::AuthService;
8use crate::services::access_keys::CreateAccessKeyResp;
9use session::share::session::require_uid;
10
11#[utoipa::path(
12 post,
13 path = "/auth/access-keys/{key_id}/rotate",
14 tag = "auth",
15 params(("key_id" = Uuid, Path)),
16 responses(
17 (status = 200, description = "access key rotated, new token returned", body = AppResponseData<CreateAccessKeyResp>),
18 (status = 404, description = "not found", body = AppResponseData<NoData>)
19 )
20)]
21pub async fn rotate_access_key(
22 State(service): State<AuthService>,
23 session: Session,
24 Path(key_id): Path<Uuid>,
25) -> Result<AppResponseData<CreateAccessKeyResp>, ApiError> {
26 let uid = require_uid(&session)?;
27 let resp = service
28 .rotate_access_key(uid, key_id)
29 .await
30 .map_err(ApiError::from)?;
31 Ok(AppResponseData::ok(resp))
32}
Added lib/auth/src/handlers/update_access_key.rs lib/auth/src/handlers/update_access_key.rs +35 −0 View lib/auth/src/handlers/update_access_key.rs
@@ -0,0 +1,35 @@
1use axum::extract::{Path, State};
2use axum::Json;
3use export::rest::{AppResponseData, NoData};
4use session::Session;
5use uuid::Uuid;
6
7use crate::errors::ApiError;
8use crate::services::AuthService;
9use crate::services::access_keys::{AccessKeyDto, UpdateAccessKeyCmd};
10use session::share::session::require_uid;
11
12#[utoipa::path(
13 patch,
14 path = "/auth/access-keys/{key_id}",
15 tag = "auth",
16 params(("key_id" = Uuid, Path)),
17 request_body = UpdateAccessKeyCmd,
18 responses(
19 (status = 200, description = "access key updated", body = AppResponseData<AccessKeyDto>),
20 (status = 404, description = "not found", body = AppResponseData<NoData>)
21 )
22)]
23pub async fn update_access_key(
24 State(service): State<AuthService>,
25 session: Session,
26 Path(key_id): Path<Uuid>,
27 Json(cmd): Json<UpdateAccessKeyCmd>,
28) -> Result<AppResponseData<AccessKeyDto>, ApiError> {
29 let uid = require_uid(&session)?;
30 let key = service
31 .update_access_key(uid, key_id, cmd)
32 .await
33 .map_err(ApiError::from)?;
34 Ok(AppResponseData::ok(key))
35}
Modified lib/auth/src/services/access_keys.rs lib/auth/src/services/access_keys.rs +85 −0 View lib/auth/src/services/access_keys.rs
@@ -38,6 +38,16 @@
3838 pub token: String,
3939}
4040
41#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
42pub struct UpdateAccessKeyCmd {
43 #[serde(default)]
44 pub name: Option<String>,
45 #[serde(default)]
46 pub scope: Option<AccessKeyScope>,
47 #[serde(default)]
48 pub expires_at: Option<Option<OffsetDateTime>>,
49}
50
4151#[derive(Debug, Clone, Serialize, Deserialize)]
4252pub struct VerifiedAccessKey {
4353 pub user_id: Uuid,
@@ -116,6 +126,81 @@
116126 .collect())
117127 }
118128
129 pub async fn update_access_key(
130 &self,
131 uid: Uuid,
132 key_id: Uuid,
133 cmd: UpdateAccessKeyCmd,
134 ) -> Result<AccessKeyDto, AuthError> {
135 let db = self.db();
136 let row = user_access_keys::Entity::find_by_id(key_id)
137 .one(db.read())
138 .await?
139 .filter(|k| k.user_id == uid)
140 .ok_or(AuthError::NotFound)?;
141 let mut am: user_access_keys::ActiveModel = row.into();
142 if let Some(name) = cmd.name {
143 let name = name.trim().to_string();
144 if name.is_empty() || name.len() > 64 {
145 return Err(AuthError::BadRequest(
146 "name must be between 1 and 64 characters".to_string(),
147 ));
148 }
149 am.name = Set(name);
150 }
151 if let Some(scope) = cmd.scope {
152 am.scope = Set(scope);
153 }
154 if let Some(expires_at) = cmd.expires_at {
155 am.expires_at = Set(expires_at);
156 }
157 am.updated_at = Set(OffsetDateTime::now_utc());
158 let updated = am.update(db.write()).await?;
159 Ok(AccessKeyDto {
160 id: updated.id,
161 name: updated.name,
162 scope: updated.scope,
163 created_at: updated.created_at,
164 expires_at: updated.expires_at,
165 last_used_at: updated.last_used_at,
166 })
167 }
168
169 pub async fn rotate_access_key(
170 &self,
171 uid: Uuid,
172 key_id: Uuid,
173 ) -> Result<CreateAccessKeyResp, AuthError> {
174 let db = self.db();
175 let row = user_access_keys::Entity::find_by_id(key_id)
176 .one(db.read())
177 .await?
178 .filter(|k| k.user_id == uid)
179 .ok_or(AuthError::NotFound)?;
180 let secret = random_token(ACCESS_TOKEN_BYTES);
181 let token = format!("{key_id}.{secret}");
182 let salt = random_token(8);
183 let now = OffsetDateTime::now_utc();
184 let mut am: user_access_keys::ActiveModel = row.into();
185 am.token_hash = Set(sha256_hex(format!("{secret}{salt}").as_bytes()));
186 am.token_salt = Set(salt);
187 am.token_last_eight = Set(token[token.len().saturating_sub(8)..].to_string());
188 am.version = Set(am.version.as_ref() + 1);
189 am.updated_at = Set(now);
190 let updated = am.update(db.write()).await?;
191 Ok(CreateAccessKeyResp {
192 key: AccessKeyDto {
193 id: updated.id,
194 name: updated.name,
195 scope: updated.scope,
196 created_at: updated.created_at,
197 expires_at: updated.expires_at,
198 last_used_at: updated.last_used_at,
199 },
200 token,
201 })
202 }
203
119204 pub async fn delete_access_key(&self, uid: Uuid, key_id: Uuid) -> Result<(), AuthError> {
120205 let db = self.db();
121206 let result = user_access_keys::Entity::delete_many()
Modified lib/auth/src/services/mod.rs lib/auth/src/services/mod.rs +1 −0 View lib/auth/src/services/mod.rs
@@ -14,6 +14,7 @@
1414pub mod access_keys;
1515pub mod agreements;
1616pub mod deletion;
17pub mod devices;
1718pub mod email;
1819pub mod login;
1920pub mod logout;
Modified lib/auth/src/services/notifications.rs lib/auth/src/services/notifications.rs +23 −0 View lib/auth/src/services/notifications.rs
@@ -30,6 +30,8 @@
3030 uid: Uuid,
3131 limit: Option<u64>,
3232 offset: Option<u64>,
33 notification_type: Option<NotificationType>,
34 unread: Option<bool>,
3335 ) -> Result<Vec<NotificationDto>, AuthError> {
3436 let max_limit = limit.unwrap_or(50).min(100);
3537 let mut query = user_notifications::Entity::find()
@@ -39,6 +41,12 @@
3941 if let Some(offset) = offset {
4042 query = query.offset(offset);
4143 }
44 if let Some(notification_type) = notification_type {
45 query = query.filter(user_notifications::Column::NotificationType.eq(notification_type));
46 }
47 if unread == Some(true) {
48 query = query.filter(user_notifications::Column::ReadAt.is_null());
49 }
4250 let rows = query.all(self.db().read()).await?;
4351 Ok(rows
4452 .into_iter()
@@ -80,6 +88,21 @@
8088 Ok(())
8189 }
8290
91 pub async fn mark_notifications_read(&self, uid: Uuid, ids: Vec<Uuid>) -> Result<u64, AuthError> {
92 let db = self.db();
93 let result = user_notifications::Entity::update_many()
94 .col_expr(
95 user_notifications::Column::ReadAt,
96 sea_orm::sea_query::Expr::value(OffsetDateTime::now_utc()),
97 )
98 .filter(user_notifications::Column::UserId.eq(uid))
99 .filter(user_notifications::Column::Id.is_in(ids))
100 .filter(user_notifications::Column::ReadAt.is_null())
101 .exec(db.write())
102 .await?;
103 Ok(result.rows_affected)
104 }
105
83106 pub async fn mark_all_notifications_read(&self, uid: Uuid) -> Result<(), AuthError> {
84107 let db = self.db();
85108 user_notifications::Entity::update_many()
Modified lib/auth/src/openapi.rs lib/auth/src/openapi.rs +13 −1 View lib/auth/src/openapi.rs
@@ -14,7 +14,8 @@
1414use crate::handlers::pow_challenge::PowChallengeResp;
1515use crate::handlers::request_deletion::RequestDeletionCmd;
1616use crate::handlers::unread_notifications_count::UnreadCountResp;
17use crate::services::access_keys::{AccessKeyDto, CreateAccessKeyCmd, CreateAccessKeyResp};
17use crate::services::access_keys::{AccessKeyDto, CreateAccessKeyCmd, CreateAccessKeyResp, UpdateAccessKeyCmd};
18use crate::services::devices::{DeviceDto, UpdateDeviceCmd};
1819use crate::services::agreements::AcceptAgreementResp;
1920use crate::services::deletion::DeletionDto;
2021use crate::services::email::{
@@ -53,6 +54,11 @@
5354 handlers::create_access_key::create_access_key,
5455 handlers::create_report::create_report,
5556 handlers::delete_access_key::delete_access_key,
57 handlers::update_access_key::update_access_key,
58 handlers::rotate_access_key::rotate_access_key,
59 handlers::devices::list_devices,
60 handlers::devices::update_device,
61 handlers::devices::delete_device,
5662 handlers::delete_notification::delete_notification,
5763 handlers::delete_ssh_key::delete_ssh_key,
5864 handlers::deletion_status::deletion_status,
@@ -71,6 +77,7 @@
7177 handlers::logout::logout,
7278 handlers::mark_all_notifications_read::mark_all_notifications_read,
7379 handlers::mark_notification_read::mark_notification_read,
80 handlers::mark_notifications_read::mark_notifications_read,
7481 handlers::me::me,
7582 handlers::pow_challenge::pow_challenge,
7683 handlers::reclaim::reclaim,
@@ -91,6 +98,8 @@
9198 handlers::verify_email::verify_email,
9299 ),
93100 components(schemas(
101 handlers::mark_notifications_read::MarkNotificationsReadCmd,
102 handlers::mark_notifications_read::MarkedCountResp,
94103 NoData,
95104 TempKey,
96105 UserPublic,
@@ -123,6 +132,9 @@
123132 CreateAccessKeyCmd,
124133 AccessKeyDto,
125134 CreateAccessKeyResp,
135 UpdateAccessKeyCmd,
136 DeviceDto,
137 UpdateDeviceCmd,
126138 SshKeyDto,
127139 AddSshKeyCmd,
128140 UserMeDto,
Modified lib/auth/src/routes.rs lib/auth/src/routes.rs +22 −1 View lib/auth/src/routes.rs
@@ -1,6 +1,6 @@
11use axum::Router;
22use axum::middleware;
3use axum::routing::{delete, get, post, put};
3use axum::routing::{delete, get, patch, post, put};
44
55use crate::handlers::accept_agreement::accept_agreement;
66use crate::handlers::active_suspension::active_suspension;
@@ -90,6 +90,14 @@
9090 get(list_access_keys).post(create_access_key),
9191 )
9292 .route("/auth/access-keys/{key_id}", delete(delete_access_key))
93 .route(
94 "/auth/access-keys/{key_id}",
95 patch(crate::handlers::update_access_key::update_access_key),
96 )
97 .route(
98 "/auth/access-keys/{key_id}/rotate",
99 post(crate::handlers::rotate_access_key::rotate_access_key),
100 )
93101 .route("/auth/ssh-keys", get(list_ssh_keys).post(add_ssh_key))
94102 .route("/auth/ssh-keys/{key_id}", delete(delete_ssh_key))
95103 .route("/me", get(me).patch(update_profile))
@@ -116,6 +124,19 @@
116124 "/me/notifications/{notification_id}/read",
117125 put(mark_notification_read),
118126 )
127 .route(
128 "/me/notifications/read",
129 put(crate::handlers::mark_notifications_read::mark_notifications_read),
130 )
131 .route(
132 "/me/devices",
133 get(crate::handlers::devices::list_devices),
134 )
135 .route(
136 "/me/devices/{device_id}",
137 patch(crate::handlers::devices::update_device)
138 .delete(crate::handlers::devices::delete_device),
139 )
119140 .route("/me/reports", post(create_report))
120141 .route(
121142 "/me/deletion",
Modified lib/space/src/handlers/mod.rs lib/space/src/handlers/mod.rs +1 −0 View lib/space/src/handlers/mod.rs
@@ -49,3 +49,4 @@
4949pub mod update_space;
5050pub mod upload_space_avatar;
5151pub mod upload_space_image;
52pub mod self_member;
Added lib/space/src/handlers/self_member.rs lib/space/src/handlers/self_member.rs +53 −0 View lib/space/src/handlers/self_member.rs
@@ -0,0 +1,53 @@
1use axum::extract::{Json, Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4
5use crate::errors::ApiError;
6use crate::services::members::{MemberDto, UpdateMemberCmd};
7use crate::services::service::SpaceService;
8use session::share::session::require_uid;
9
10#[utoipa::path(
11 patch,
12 path = "/spaces/{space_name}/members/@me",
13 tag = "space",
14 params(("space_name" = String, Path)),
15 request_body = UpdateMemberCmd,
16 responses(
17 (status = 200, description = "your membership settings updated", body = AppResponseData<MemberDto>),
18 (status = 404, description = "not a member", body = AppResponseData<NoData>)
19 )
20)]
21pub async fn update_self_member(
22 State(service): State<SpaceService>,
23 session: Session,
24 Path(space_name): Path<String>,
25 Json(cmd): Json<UpdateMemberCmd>,
26) -> Result<AppResponseData<MemberDto>, ApiError> {
27 let caller = require_uid(&session)?;
28 let member = service
29 .update_member(caller, &space_name, caller, cmd)
30 .await?;
31 Ok(AppResponseData::ok(member))
32}
33
34#[utoipa::path(
35 delete,
36 path = "/spaces/{space_name}/members/@me",
37 tag = "space",
38 params(("space_name" = String, Path)),
39 responses(
40 (status = 200, description = "left the space", body = AppResponseData<NoData>),
41 (status = 400, description = "owner cannot leave", body = AppResponseData<NoData>),
42 (status = 404, description = "not a member", body = AppResponseData<NoData>)
43 )
44)]
45pub async fn leave_space(
46 State(service): State<SpaceService>,
47 session: Session,
48 Path(space_name): Path<String>,
49) -> Result<AppResponseData<NoData>, ApiError> {
50 let caller = require_uid(&session)?;
51 service.leave_space(caller, &space_name).await?;
52 Ok(AppResponseData::empty())
53}
Modified lib/space/src/services/members.rs lib/space/src/services/members.rs +44 −0 View lib/space/src/services/members.rs
@@ -377,6 +377,50 @@
377377 })
378378 }
379379
380 pub async fn leave_space(&self, caller: Uuid, space_name: &str) -> Result<(), SpaceError> {
381 let space = lookup_space_by_name(self, space_name).await?;
382 if space.owner_id == caller {
383 return Err(SpaceError::BadRequest(
384 "space owner cannot leave; delete the space instead".to_string(),
385 ));
386 }
387 let member = space_members::Entity::find_by_id((space.id, caller))
388 .one(self.db().read())
389 .await?
390 .ok_or(SpaceError::NotFound)?;
391 if member.status != MemberStatus::Active {
392 return Ok(());
393 }
394 let now = OffsetDateTime::now_utc();
395 let mut am: space_members::ActiveModel = member.into();
396 am.status = Set(MemberStatus::Left);
397 am.left_at = Set(Some(now));
398 am.updated_at = Set(Some(now));
399 self.db()
400 .primary()
401 .transaction::<_, (), SpaceError>(|txn| {
402 Box::pin(async move {
403 space_member_groups::Entity::delete_many()
404 .filter(space_member_groups::Column::SpaceId.eq(space.id))
405 .filter(space_member_groups::Column::UserId.eq(caller))
406 .exec(txn)
407 .await?;
408 am.update(txn).await?;
409 spaces::Entity::update_many()
410 .filter(spaces::Column::Id.eq(space.id))
411 .col_expr(
412 spaces::Column::NumsMembers,
413 sea_orm::sea_query::Expr::cust("GREATEST(nums_members - 1, 0)"),
414 )
415 .exec(txn)
416 .await?;
417 Ok(())
418 })
419 })
420 .await?;
421 Ok(())
422 }
423
380424 pub async fn remove_member(
381425 &self,
382426 caller: Uuid,
Modified lib/space/src/openapi.rs lib/space/src/openapi.rs +2 −0 View lib/space/src/openapi.rs
@@ -31,6 +31,8 @@
3131 crate::handlers::list_members::list_members,
3232 crate::handlers::get_member::get_member,
3333 crate::handlers::update_member::update_member,
34 crate::handlers::self_member::update_self_member,
35 crate::handlers::self_member::leave_space,
3436 crate::handlers::remove_member::remove_member,
3537 crate::handlers::create_group::create_group,
3638 crate::handlers::list_groups::list_groups,
Modified lib/space/src/routes.rs lib/space/src/routes.rs +5 −0 View lib/space/src/routes.rs
@@ -113,6 +113,11 @@
113113 get(get_member).patch(update_member).delete(remove_member),
114114 )
115115 .route(
116 "/spaces/{space_name}/members/@me",
117 axum::routing::patch(crate::handlers::self_member::update_self_member)
118 .delete(crate::handlers::self_member::leave_space),
119 )
120 .route(
116121 "/spaces/{space_name}/groups",
117122 post(create_group).get(list_groups),
118123 )
Added lib/user/src/handlers/get_session.rs lib/user/src/handlers/get_session.rs +32 −0 View lib/user/src/handlers/get_session.rs
@@ -0,0 +1,32 @@
1use axum::extract::{Path, State};
2use export::rest::AppResponseData;
3use session::Session;
4use uuid::Uuid;
5
6use crate::errors::ApiError;
7use crate::services::service::UserService;
8use crate::services::sessions::SessionDto;
9use session::share::session::require_uid;
10
11#[utoipa::path(
12 get,
13 path = "/me/sessions/{session_id}",
14 tag = "user",
15 params(("session_id" = Uuid, Path)),
16 responses(
17 (status = 200, description = "session detail", body = AppResponseData<SessionDto>),
18 (status = 404, description = "not found")
19 )
20)]
21pub async fn get_session(
22 State(service): State<UserService>,
23 session: Session,
24 Path(session_id): Path<Uuid>,
25) -> Result<AppResponseData<SessionDto>, ApiError> {
26 let uid = require_uid(&session)?;
27 let token = session.get::<String>(&session::share::session::session_token_key(uid));
28 let dto = service
29 .get_session(uid, session_id, token.as_deref())
30 .await?;
31 Ok(AppResponseData::ok(dto))
32}
Modified lib/user/src/handlers/list_sessions.rs lib/user/src/handlers/list_sessions.rs +4 −1 View lib/user/src/handlers/list_sessions.rs
@@ -24,6 +24,9 @@
2424 Query(query): Query<ListSessionsQuery>,
2525) -> Result<AppResponseData<Vec<SessionDto>>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let sessions = service.list_sessions(uid, query).await?;
27 let token = session.get::<String>(&session::share::session::session_token_key(uid));
28 let sessions = service
29 .list_sessions(uid, query, token.as_deref())
30 .await?;
2831 Ok(AppResponseData::ok(sessions))
2932}
Modified lib/user/src/handlers/mod.rs lib/user/src/handlers/mod.rs +1 −0 View lib/user/src/handlers/mod.rs
@@ -11,6 +11,7 @@
1111pub mod list_memes;
1212pub mod list_push_subscriptions;
1313pub mod list_saved_messages;
14pub mod get_session;
1415pub mod list_sessions;
1516pub mod my_reports;
1617pub mod my_spaces;
Modified lib/user/src/services/sessions.rs lib/user/src/services/sessions.rs +26 −3 View lib/user/src/services/sessions.rs
@@ -24,10 +24,12 @@
2424 pub expires_at: OffsetDateTime,
2525 pub is_revoked: bool,
2626 pub revoked_at: Option<OffsetDateTime>,
27 pub is_current: bool,
2728}
2829
29impl From<user_sessions::Model> for SessionDto {
30 fn from(m: user_sessions::Model) -> Self {
30impl SessionDto {
31 fn build(m: user_sessions::Model, current_token: Option<&str>) -> Self {
32 let is_current = current_token.is_some_and(|t| t == m.token_hash);
3133 Self {
3234 id: m.id,
3335 kind: m.kind,
@@ -39,6 +41,7 @@
3941 expires_at: m.expires_at,
4042 is_revoked: m.is_revoked,
4143 revoked_at: m.revoked_at,
44 is_current,
4245 }
4346 }
4447}
@@ -61,6 +64,7 @@
6164 &self,
6265 uid: Uuid,
6366 q: ListSessionsQuery,
67 current_token: Option<&str>,
6468 ) -> Result<Vec<SessionDto>, UserError> {
6569 let mut sel = user_sessions::Entity::find()
6670 .filter(user_sessions::Column::UserId.eq(uid))
@@ -86,7 +90,26 @@
8690 )
8791 .all(self.db().read())
8892 .await?;
89 Ok(rows.into_iter().map(SessionDto::from).collect())
93 Ok(rows
94 .into_iter()
95 .map(|r| SessionDto::build(r, current_token))
96 .collect())
97 }
98
99 pub async fn get_session(
100 &self,
101 uid: Uuid,
102 session_id: Uuid,
103 current_token: Option<&str>,
104 ) -> Result<SessionDto, UserError> {
105 let row = user_sessions::Entity::find_by_id(session_id)
106 .one(self.db().read())
107 .await?
108 .ok_or(UserError::NotFound)?;
109 if row.user_id != uid {
110 return Err(UserError::NotFound);
111 }
112 Ok(SessionDto::build(row, current_token))
90113 }
91114
92115 pub async fn revoke_session(&self, uid: Uuid, session_id: Uuid) -> Result<(), UserError> {
Modified lib/user/src/routes.rs lib/user/src/routes.rs +2 −1 View lib/user/src/routes.rs
@@ -14,6 +14,7 @@
1414use crate::handlers::list_memes::list_memes;
1515use crate::handlers::list_push_subscriptions::list_push_subscriptions;
1616use crate::handlers::list_saved_messages::list_saved_messages;
17use crate::handlers::get_session::get_session;
1718use crate::handlers::list_sessions::list_sessions;
1819use crate::handlers::my_reports::my_reports;
1920use crate::handlers::my_spaces::my_spaces;
@@ -51,7 +52,7 @@
5152 "/me/sessions",
5253 get(list_sessions).delete(revoke_all_sessions),
5354 )
54 .route("/me/sessions/{session_id}", delete(revoke_session))
55 .route("/me/sessions/{session_id}", get(get_session).delete(revoke_session))
5556 .route(
5657 "/me/push-subscriptions",
5758 get(list_push_subscriptions).post(upsert_push_subscription),