refactor(user): refresh user crate to support TOTP 2FA (features, handler, metrics)
zhenyi committed
Modified lib/user/features/deletion/usecase.rs lib/user/features/deletion/usecase.rs +17 −4 View lib/user/features/deletion/usecase.rs
@@ -2,6 +2,8 @@
22 ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, TransactionTrait,
33};
44use db::user::user_deletion_requests::UserDeletionRequestStatus as DbDeletionStatus;
5use db::user::users_loader::{load_user, load_users};
6use export::user::basic::UserBasicInfo;
57use uuid::Uuid;
68
79use crate::error::{UserError, UserResult};
@@ -29,7 +31,8 @@
2931 .one(&writer)
3032 .await?;
3133 if let Some(row) = existing {
32 return Ok(DeletionRequestView::from(row));
34 let user = load_user(&svc.db, caller).await?.map(UserBasicInfo::from);
35 return Ok(DeletionRequestView::from_model(row, user));
3336 }
3437
3538 let now = time::OffsetDateTime::now_utc();
@@ -46,7 +49,8 @@
4649 .set_updated_at(now)
4750 .into();
4851 let saved = active.insert(&writer).await?;
49 Ok(DeletionRequestView::from(saved))
52 let user = load_user(&svc.db, caller).await?.map(UserBasicInfo::from);
53 Ok(DeletionRequestView::from_model(saved, user))
5054 }
5155 .await;
5256 metrics::record_outcome(metrics::FEATURE_DELETION_REQUEST, &result);
@@ -107,7 +111,8 @@
107111 am.scheduled_for = Set(Some(scheduled_for));
108112 am.updated_at = Set(now);
109113 let saved = am.update(&writer).await?;
110 Ok(DeletionRequestView::from(saved))
114 let user = load_user(&svc.db, caller).await?.map(UserBasicInfo::from);
115 Ok(DeletionRequestView::from_model(saved, user))
111116 }
112117 .await;
113118 metrics::record_outcome(metrics::FEATURE_DELETION_CONFIRM, &result);
@@ -129,7 +134,15 @@
129134 )
130135 .all(&db)
131136 .await?;
132 Ok(rows.into_iter().map(DeletionRequestView::from).collect())
137 let user_ids: Vec<Uuid> = rows.iter().map(|r| r.user_id).collect();
138 let users = load_users(&svc.db, &user_ids).await?;
139 Ok(rows
140 .into_iter()
141 .map(|r| {
142 let user_id = r.user_id;
143 DeletionRequestView::from_model(r, users.get(&user_id).map(UserBasicInfo::from))
144 })
145 .collect())
133146 }
134147 .await;
135148 metrics::record_outcome(metrics::FEATURE_DELETION_LIST_PENDING, &result);
Modified lib/user/features/notifications/usecase.rs lib/user/features/notifications/usecase.rs +79 −5 View lib/user/features/notifications/usecase.rs
@@ -1,6 +1,8 @@
11use db::conn::sea_orm::{
22 ActiveModelTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Set,
33};
4use db::user::users_loader::load_users;
5use export::user::basic::UserBasicInfo;
46use uuid::Uuid;
57
68use crate::error::{UserError, UserResult};
@@ -39,7 +41,18 @@
3941 }
4042 if let Some(_nt) = q.notification_type {}
4143 let rows = sel.all(&db).await?;
42 Ok(rows.into_iter().map(NotificationView::from).collect())
44 let actor_ids: Vec<Uuid> = rows.iter().filter_map(|r| r.actor_uid).collect();
45 let users = load_users(&svc.db, &actor_ids).await?;
46 Ok(rows
47 .into_iter()
48 .map(|r| {
49 let actor_uid = r.actor_uid;
50 NotificationView::from_model(
51 r,
52 actor_uid.and_then(|id| users.get(&id).map(UserBasicInfo::from)),
53 )
54 })
55 .collect())
4356 }
4457 .await;
4558 metrics::record_outcome(metrics::FEATURE_NOTIFICATIONS_LIST, &result);
@@ -201,7 +214,20 @@
201214 .set_updated_at(now)
202215 .into();
203216 let saved = active.insert(&writer).await?;
204 Ok(ReportView::from(saved))
217 let mut ids = vec![caller, cmd.reported_uid];
218 if let Some(id) = saved.resolver_uid {
219 ids.push(id);
220 }
221 let users = load_users(&svc.db, &ids).await?;
222 let reporter_uid = saved.reporter_uid;
223 let reported_uid = saved.reported_uid;
224 let resolver_uid = saved.resolver_uid;
225 Ok(ReportView::from_model(
226 saved,
227 reporter_uid.and_then(|id| users.get(&id).map(UserBasicInfo::from)),
228 users.get(&reported_uid).map(UserBasicInfo::from),
229 resolver_uid.and_then(|id| users.get(&id).map(UserBasicInfo::from)),
230 ))
205231 }
206232 .await;
207233 metrics::record_outcome(metrics::FEATURE_REPORTS_SUBMIT, &result);
@@ -221,7 +247,24 @@
221247 .filter(db::user::user_reports::COLUMN.reporter_uid.eq(caller))
222248 .all(&db)
223249 .await?;
224 Ok(rows.into_iter().map(ReportView::from).collect())
250 let mut ids: Vec<Uuid> = rows.iter().filter_map(|r| r.reporter_uid).collect();
251 ids.extend(rows.iter().map(|r| r.reported_uid));
252 ids.extend(rows.iter().filter_map(|r| r.resolver_uid));
253 let users = load_users(&svc.db, &ids).await?;
254 Ok(rows
255 .into_iter()
256 .map(|r| {
257 let reporter_uid = r.reporter_uid;
258 let reported_uid = r.reported_uid;
259 let resolver_uid = r.resolver_uid;
260 ReportView::from_model(
261 r,
262 reporter_uid.and_then(|id| users.get(&id).map(UserBasicInfo::from)),
263 users.get(&reported_uid).map(UserBasicInfo::from),
264 resolver_uid.and_then(|id| users.get(&id).map(UserBasicInfo::from)),
265 )
266 })
267 .collect())
225268 }
226269 .await;
227270 metrics::record_outcome(metrics::FEATURE_REPORTS_LIST_MINE, &result);
@@ -236,7 +279,24 @@
236279 let result: UserResult<Vec<ReportView>> = async {
237280 let db = svc.db.reader();
238281 let rows = db::user::user_reports::Entity::find().all(&db).await?;
239 Ok(rows.into_iter().map(ReportView::from).collect())
282 let mut ids: Vec<Uuid> = rows.iter().filter_map(|r| r.reporter_uid).collect();
283 ids.extend(rows.iter().map(|r| r.reported_uid));
284 ids.extend(rows.iter().filter_map(|r| r.resolver_uid));
285 let users = load_users(&svc.db, &ids).await?;
286 Ok(rows
287 .into_iter()
288 .map(|r| {
289 let reporter_uid = r.reporter_uid;
290 let reported_uid = r.reported_uid;
291 let resolver_uid = r.resolver_uid;
292 ReportView::from_model(
293 r,
294 reporter_uid.and_then(|id| users.get(&id).map(UserBasicInfo::from)),
295 users.get(&reported_uid).map(UserBasicInfo::from),
296 resolver_uid.and_then(|id| users.get(&id).map(UserBasicInfo::from)),
297 )
298 })
299 .collect())
240300 }
241301 .await;
242302 metrics::record_outcome("reports_list_all", &result);
@@ -262,7 +322,21 @@
262322 }
263323 am.updated_at = Set(time::OffsetDateTime::now_utc());
264324 let saved = am.update(&writer).await?;
265 Ok(ReportView::from(saved))
325 let mut ids: Vec<Uuid> = saved.reporter_uid.into_iter().collect();
326 ids.push(saved.reported_uid);
327 if let Some(id) = saved.resolver_uid {
328 ids.push(id);
329 }
330 let users = load_users(&svc.db, &ids).await?;
331 let reporter_uid = saved.reporter_uid;
332 let reported_uid = saved.reported_uid;
333 let resolver_uid = saved.resolver_uid;
334 Ok(ReportView::from_model(
335 saved,
336 reporter_uid.and_then(|id| users.get(&id).map(UserBasicInfo::from)),
337 users.get(&reported_uid).map(UserBasicInfo::from),
338 resolver_uid.and_then(|id| users.get(&id).map(UserBasicInfo::from)),
339 ))
266340 }
267341 .await;
268342 metrics::record_outcome("reports_update", &result);
Modified lib/user/features/profile/impls.rs lib/user/features/profile/impls.rs +1 −4 View lib/user/features/profile/impls.rs
@@ -43,10 +43,7 @@
4343 usecase::delete_meme(self, caller, meme_id).await
4444 }
4545
46 pub async fn get_my_spaces(
47 &self,
48 uid: Uuid,
49 ) -> UserResult<Vec<UserSpacePermissionView>> {
46 pub async fn get_my_spaces(&self, uid: Uuid) -> UserResult<Vec<UserSpacePermissionView>> {
5047 usecase::fetch_my_spaces(self, uid).await
5148 }
5249}
Modified lib/user/features/profile/types.rs lib/user/features/profile/types.rs +5 −27 View lib/user/features/profile/types.rs
@@ -7,6 +7,7 @@
77
88use config::user::ProfileVisibility;
99use export::space::spaces::SpaceView;
10use export::user::basic::UserBasicInfo;
1011
1112fn fmt_ts(ts: OffsetDateTime) -> String {
1213 ts.format(&Rfc3339).unwrap_or_default()
@@ -118,7 +119,7 @@
118119
119120#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
120121pub struct ProfileView {
121 pub user_id: Uuid,
122 pub user: Option<UserBasicInfo>,
122123 pub real_name: Option<String>,
123124 pub bio: Option<String>,
124125 pub pronouns: Option<String>,
@@ -135,36 +136,13 @@
135136 pub show_real_name: bool,
136137 pub show_activity: bool,
137138 pub updated_at: String,
138}
139
140impl From<db::user::user_profiles::Model> for ProfileView {
141 fn from(m: db::user::user_profiles::Model) -> Self {
142 Self {
143 user_id: m.user_id,
144 real_name: m.real_name,
145 bio: m.bio,
146 pronouns: m.pronouns,
147 pronouns_privacy: m.pronouns_privacy.map(visibility_from_entity),
148 website: m.website,
149 location: m.location,
150 company: m.company,
151 date_of_birth: m.date_of_birth,
152 banner_url: m.banner_url,
153 banner_color: m.banner_color,
154 accent_color: m.accent_color,
155 show_email: m.show_email,
156 show_location: m.show_location,
157 show_real_name: m.show_real_name,
158 show_activity: m.show_activity,
159 updated_at: fmt_ts(m.updated_at),
160 }
161 }
162139}
163140
164141impl ProfileView {
165 pub fn for_viewer(
142 pub fn for_viewer(
166143 m: db::user::user_profiles::Model,
167144 visibility: user_entity::ProfileVisibility,
145 user: &user_entity::Model,
168146 viewer: Option<Uuid>,
169147 ) -> Self {
170148 let is_owner = viewer == Some(m.user_id);
@@ -180,7 +158,7 @@
180158 let emit_dob = can_see && is_owner;
181159
182160 Self {
183 user_id: m.user_id,
161 user: Some(UserBasicInfo::from(user)),
184162 real_name: if emit_real_name { m.real_name } else { None },
185163 bio: if can_see { m.bio } else { None },
186164 pronouns: if can_see { m.pronouns } else { None },
Modified lib/user/features/profile/usecase.rs lib/user/features/profile/usecase.rs +29 −4 View lib/user/features/profile/usecase.rs
@@ -9,7 +9,9 @@
99use db::space::space_role_permissions as role_perm_entity;
1010use db::space::spaces as space_entity;
1111use db::user::users as user_entity;
12use db::user::users_loader::load_user;
1213use export::space::spaces::SpaceView;
14use export::user::basic::UserBasicInfo;
1315use time::OffsetDateTime;
1416use uuid::Uuid;
1517
@@ -89,8 +91,11 @@
8991 let mut views = Vec::with_capacity(models.len());
9092 for model in models {
9193 let permissions = effective_permissions(&db, &model, uid).await?;
94 let owner = load_user(&svc.db, model.owner_id)
95 .await?
96 .map(UserBasicInfo::from);
9297 views.push(UserSpacePermissionView {
93 space: SpaceView::from(model),
98 space: SpaceView::from_model(model, owner),
9499 permissions,
95100 });
96101 }
@@ -181,7 +186,12 @@
181186 .one(&db)
182187 .await?
183188 .ok_or_else(|| UserError::NotFound(format!("profile {target}")))?;
184 Ok(ProfileView::for_viewer(profile, user.visibility, viewer))
189 Ok(ProfileView::for_viewer(
190 profile,
191 user.visibility,
192 &user,
193 viewer,
194 ))
185195 }
186196 .await;
187197 metrics::record_outcome(metrics::FEATURE_PROFILE_VIEW, &result);
@@ -345,7 +355,9 @@
345355 if space.owner_id == user_id {
346356 return Ok(OWNER_PERMISSIONS.iter().map(|s| s.to_string()).collect());
347357 }
348 let member = member_entity::Entity::find_by_id((space.id, user_id)).one(db).await?;
358 let member = member_entity::Entity::find_by_id((space.id, user_id))
359 .one(db)
360 .await?;
349361 match member {
350362 Some(m) if m.status == member_entity::MemberStatus::Active => {}
351363 _ => return Ok(Vec::new()),
@@ -381,7 +393,20 @@
381393 .order_by_asc(space_entity::Column::LowerName)
382394 .all(db)
383395 .await?;
384 Ok(models.into_iter().map(SpaceView::from).collect())
396 let owner_ids: Vec<Uuid> = models.iter().map(|m| m.owner_id).collect();
397 let owners = user_entity::Entity::find()
398 .filter(user_entity::Column::Uid.is_in(owner_ids))
399 .all(db)
400 .await?;
401 let owner_map: std::collections::HashMap<Uuid, user_entity::Model> =
402 owners.into_iter().map(|u| (u.uid, u)).collect();
403 Ok(models
404 .into_iter()
405 .map(|m| {
406 let owner_id = m.owner_id;
407 SpaceView::from_model(m, owner_map.get(&owner_id).map(UserBasicInfo::from))
408 })
409 .collect())
385410}
386411
387412async fn fetch_common_spaces<C>(
Modified lib/user/features/security/impls.rs lib/user/features/security/impls.rs +23 −2 View lib/user/features/security/impls.rs
@@ -5,11 +5,32 @@
55
66use super::usecase;
77use super::{
8 AccessKeyView, AddSshKeyCmd, CreateAccessKeyCmd, CreateAccessKeyResp, PushSubscriptionView,
9 RecoveryCodeView, RegenerateRecoveryResp, SshKeyView, UpsertPushCmd,
8 AccessKeyView, AddSshKeyCmd, CreateAccessKeyCmd, CreateAccessKeyResp, DisableTwoFactorCmd,
9 EnableTwoFactorCmd, PushSubscriptionView, RecoveryCodeView, RegenerateRecoveryResp, SshKeyView,
10 TwoFactorSetupResp, TwoFactorStatusResp, UpsertPushCmd,
1011};
1112
1213impl UserService {
14 pub async fn two_factor_status(&self, caller: Uuid) -> UserResult<TwoFactorStatusResp> {
15 usecase::totp_status(self, caller).await
16 }
17
18 pub async fn two_factor_setup(&self, caller: Uuid) -> UserResult<TwoFactorSetupResp> {
19 usecase::totp_setup(self, caller).await
20 }
21
22 pub async fn enable_two_factor(&self, caller: Uuid, cmd: EnableTwoFactorCmd) -> UserResult<()> {
23 usecase::totp_enable(self, caller, cmd).await
24 }
25
26 pub async fn disable_two_factor(
27 &self,
28 caller: Uuid,
29 cmd: DisableTwoFactorCmd,
30 ) -> UserResult<()> {
31 usecase::totp_disable(self, caller, cmd).await
32 }
33
1334 pub async fn list_recovery_codes(&self, caller: Uuid) -> UserResult<Vec<RecoveryCodeView>> {
1435 usecase::list_recovery(self, caller).await
1536 }
Modified lib/user/features/security/usecase.rs lib/user/features/security/usecase.rs +179 −2 View lib/user/features/security/usecase.rs
@@ -2,6 +2,7 @@
22use db::conn::sea_orm::{ActiveModelTrait, EntityTrait, QueryFilter, Set};
33use rand::TryRng;
44use sha2::{Digest, Sha256};
5use totp::{self, TotpError};
56use uuid::Uuid;
67
78use crate::error::{UserError, UserResult};
@@ -10,8 +11,9 @@
1011
1112use super::{
1213 AccessKeyScope, AccessKeyView, AddSshKeyCmd, CreateAccessKeyCmd, CreateAccessKeyResp,
13 PushPlatform, PushSubscriptionView, RecoveryCodeView, RegenerateRecoveryResp, SshKeyType,
14 SshKeyView, UpsertPushCmd,
14 DisableTwoFactorCmd, EnableTwoFactorCmd, PushPlatform, PushSubscriptionView, RecoveryCodeView,
15 RegenerateRecoveryResp, SshKeyType, SshKeyView, TwoFactorSetupResp, TwoFactorStatusResp,
16 UpsertPushCmd,
1517};
1618
1719fn random_token(bytes: usize) -> String {
@@ -436,5 +438,180 @@
436438 result
437439}
438440
441fn map_totp(e: TotpError) -> UserError {
442 match e {
443 TotpError::InvalidCode => UserError::BadRequest("invalid code".into()),
444 TotpError::InvalidOrExpiredCode => UserError::BadRequest("invalid or expired code".into()),
445 _ => UserError::Internal(e.to_string()),
446 }
447}
448
449pub async fn totp_status(svc: &UserService, caller: Uuid) -> UserResult<TwoFactorStatusResp> {
450 let _timer = metrics::Timer::start(metrics::FEATURE_TWO_FACTOR_STATUS);
451 let result: UserResult<TwoFactorStatusResp> = async {
452 let db = svc.db.reader();
453 let row = db::user::users::Entity::find_by_id(caller)
454 .one(&db)
455 .await?
456 .ok_or_else(|| UserError::NotFound(format!("user {caller}")))?;
457 Ok(TwoFactorStatusResp {
458 enabled: row.totp_enabled,
459 })
460 }
461 .await;
462 metrics::record_outcome(metrics::FEATURE_TWO_FACTOR_STATUS, &result);
463 result
464}
465
466pub async fn totp_setup(svc: &UserService, caller: Uuid) -> UserResult<TwoFactorSetupResp> {
467 let _timer = metrics::Timer::start(metrics::FEATURE_TWO_FACTOR_SETUP);
468 let result: UserResult<TwoFactorSetupResp> = async {
469 let db = svc.db.reader();
470 let row = db::user::users::Entity::find_by_id(caller)
471 .one(&db)
472 .await?
473 .ok_or_else(|| UserError::NotFound(format!("user {caller}")))?;
474 if row.totp_enabled {
475 return Err(UserError::Conflict("two-factor already enabled".into()));
476 }
477 let key = svc
478 .config
479 .totp_encryption_key
480 .as_deref()
481 .ok_or_else(|| UserError::Internal("totp encryption key not configured".into()))?;
482 let mut raw = vec![0u8; TOTP_SECRET_BYTES];
483 rand::rng().try_fill_bytes(&mut raw).expect("rng");
484 let secret = base32_encode(&raw);
485 let uri = format!(
486 "otpauth://totp/{issuer}:{user}?secret={secret}&issuer={issuer}&algorithm=SHA1&digits={digits}&period={period}",
487 issuer = TOTP_ISSUER,
488 user = row.username,
489 digits = TOTP_DIGITS,
490 period = TOTP_PERIOD_SECS,
491 );
492 let stored = totp_encrypt_secret(key, &secret)?;
493 let now = time::OffsetDateTime::now_utc();
494 let writer = svc.db.writer();
495 let mut am: db::user::users::ActiveModel = row.into();
496 am.totp_encrypted_secret = Set(Some(stored));
497 am.totp_enabled = Set(false);
498 am.totp_last_used_at = Set(None);
499 am.totp_last_counter = Set(None);
500 am.updated_at = Set(now);
501 am.update(&writer).await?;
502 Ok(TwoFactorSetupResp {
503 secret,
504 otpauth_uri: uri,
505 })
506 }
507 .await;
508 metrics::record_outcome(metrics::FEATURE_TWO_FACTOR_SETUP, &result);
509 if result.is_ok() {
510 record_security_operation(
511 SecurityResource::TwoFactor,
512 SecurityResourceOp::Setup,
513 metrics::OUTCOME_SUCCESS,
514 );
515 }
516 result
517}
518
519pub async fn totp_enable(
520 svc: &UserService,
521 caller: Uuid,
522 cmd: EnableTwoFactorCmd,
523) -> UserResult<()> {
524 let _timer = metrics::Timer::start(metrics::FEATURE_TWO_FACTOR_ENABLE);
525 let result: UserResult<()> = async {
526 let key = svc
527 .config
528 .totp_encryption_key
529 .as_deref()
530 .ok_or_else(|| UserError::Internal("totp encryption key not configured".into()))?;
531 let db = svc.db.reader();
532 let row = db::user::users::Entity::find_by_id(caller)
533 .one(&db)
534 .await?
535 .ok_or_else(|| UserError::NotFound(format!("user {caller}")))?;
536 if row.totp_enabled {
537 return Err(UserError::Conflict("two-factor already enabled".into()));
538 }
539 let stored = row
540 .totp_encrypted_secret
541 .as_deref()
542 .ok_or_else(|| UserError::BadRequest("run two-factor setup first".into()))?;
543 let secret = totp_decrypt_secret(key, stored)?;
544 let (_, counter) = verify_totp_code(&secret, &cmd.code, row.totp_last_counter)?;
545 let now = time::OffsetDateTime::now_utc();
546 let writer = svc.db.writer();
547 let mut am: db::user::users::ActiveModel = row.into();
548 am.totp_enabled = Set(true);
549 am.totp_last_used_at = Set(Some(now));
550 am.totp_last_counter = Set(Some(counter));
551 am.updated_at = Set(now);
552 am.update(&writer).await?;
553 Ok(())
554 }
555 .await;
556 metrics::record_outcome(metrics::FEATURE_TWO_FACTOR_ENABLE, &result);
557 if result.is_ok() {
558 record_security_operation(
559 SecurityResource::TwoFactor,
560 SecurityResourceOp::Enable,
561 metrics::OUTCOME_SUCCESS,
562 );
563 }
564 result
565}
566
567pub async fn totp_disable(
568 svc: &UserService,
569 caller: Uuid,
570 cmd: DisableTwoFactorCmd,
571) -> UserResult<()> {
572 let _timer = metrics::Timer::start(metrics::FEATURE_TWO_FACTOR_DISABLE);
573 let result: UserResult<()> = async {
574 let key = svc
575 .config
576 .totp_encryption_key
577 .as_deref()
578 .ok_or_else(|| UserError::Internal("totp encryption key not configured".into()))?;
579 let db = svc.db.reader();
580 let row = db::user::users::Entity::find_by_id(caller)
581 .one(&db)
582 .await?
583 .ok_or_else(|| UserError::NotFound(format!("user {caller}")))?;
584 if !row.totp_enabled {
585 return Err(UserError::Conflict("two-factor not enabled".into()));
586 }
587 let stored = row
588 .totp_encrypted_secret
589 .as_deref()
590 .ok_or_else(|| UserError::Internal("stored totp secret missing".into()))?;
591 let secret = totp_decrypt_secret(key, stored)?;
592 let _ = verify_totp_code(&secret, &cmd.code, row.totp_last_counter)?;
593 let now = time::OffsetDateTime::now_utc();
594 let writer = svc.db.writer();
595 let mut am: db::user::users::ActiveModel = row.into();
596 am.totp_encrypted_secret = Set(None);
597 am.totp_enabled = Set(false);
598 am.totp_last_used_at = Set(None);
599 am.totp_last_counter = Set(None);
600 am.updated_at = Set(now);
601 am.update(&writer).await?;
602 Ok(())
603 }
604 .await;
605 metrics::record_outcome(metrics::FEATURE_TWO_FACTOR_DISABLE, &result);
606 if result.is_ok() {
607 record_security_operation(
608 SecurityResource::TwoFactor,
609 SecurityResourceOp::Disable,
610 metrics::OUTCOME_SUCCESS,
611 );
612 }
613 result
614}
615
439616#[allow(dead_code)]
440617fn _use_unused(_: &AccessKeyScope, _: &SshKeyType, _: &PushPlatform) {}
Modified lib/user/features/social/types.rs lib/user/features/social/types.rs +3 −2 View lib/user/features/social/types.rs
@@ -1,3 +1,4 @@
1use export::user::basic::UserBasicInfo;
12use serde::{Deserialize, Serialize};
23use time::format_description::well_known::Rfc3339;
34use utoipa::ToSchema;
@@ -5,13 +6,13 @@
56
67#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
78pub struct FollowerSummary {
8 pub user: super::super::profile::UserPublic,
9 pub user: UserBasicInfo,
910 pub since: String,
1011}
1112
1213#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
1314pub struct BlockedSummary {
14 pub user: super::super::profile::UserPublic,
15 pub user: UserBasicInfo,
1516 pub since: String,
1617}
1718
Modified lib/user/features/social/usecase.rs lib/user/features/social/usecase.rs +4 −3 View lib/user/features/social/usecase.rs
@@ -5,6 +5,7 @@
55use db::user::user_blocks as blocks;
66use db::user::user_followers as followers;
77use db::user::users as user_entity;
8use export::user::basic::UserBasicInfo;
89use uuid::Uuid;
910
1011use crate::error::{UserError, UserResult};
@@ -111,7 +112,7 @@
111112 .into_iter()
112113 .filter_map(|e| {
113114 map.get(&e.follower_id).map(|u| FollowerSummary {
114 user: super::super::profile::UserPublic::from(u.clone()),
115 user: UserBasicInfo::from(u),
115116 since: e
116117 .created_at
117118 .format(&time::format_description::well_known::Rfc3339)
@@ -156,7 +157,7 @@
156157 .into_iter()
157158 .filter_map(|e| {
158159 map.get(&e.followed_id).map(|u| FollowerSummary {
159 user: super::super::profile::UserPublic::from(u.clone()),
160 user: UserBasicInfo::from(u),
160161 since: e
161162 .created_at
162163 .format(&time::format_description::well_known::Rfc3339)
@@ -276,7 +277,7 @@
276277 .into_iter()
277278 .filter_map(|e| {
278279 map.get(&e.blocked_id).map(|u| BlockedSummary {
279 user: super::super::profile::UserPublic::from(u.clone()),
280 user: UserBasicInfo::from(u),
280281 since: e
281282 .created_at
282283 .format(&time::format_description::well_known::Rfc3339)
Modified lib/user/features/suspensions/usecase.rs lib/user/features/suspensions/usecase.rs +55 −3 View lib/user/features/suspensions/usecase.rs
@@ -2,6 +2,8 @@
22 ActiveModelTrait, EntityTrait, ExprTrait, QueryFilter, Set, TransactionTrait,
33};
44use db::user::users as user_entity;
5use db::user::users_loader::{load_user, load_users};
6use export::user::basic::UserBasicInfo;
57use uuid::Uuid;
68
79use crate::error::{UserError, UserResult};
@@ -81,7 +83,9 @@
8183 .revoke_user(target, ttl)
8284 .await?;
8385
84 Ok(SuspensionView::from(saved))
86 let user = load_user(&svc.db, target).await?.map(UserBasicInfo::from);
87 let suspended_by = load_user(&svc.db, caller).await?.map(UserBasicInfo::from);
88 Ok(SuspensionView::from_model(saved, user, suspended_by, None))
8589 }
8690 .await;
8791 metrics::record_outcome(metrics::FEATURE_SUSPENSIONS_CREATE, &result);
@@ -180,7 +184,31 @@
180184 sel = sel.filter(db::user::user_suspensions::COLUMN.revoked_at.is_null());
181185 }
182186 let rows = sel.all(&db).await?;
183 Ok(rows.into_iter().map(SuspensionView::from).collect())
187 let mut ids: Vec<Uuid> = Vec::new();
188 for row in &rows {
189 ids.push(row.user_id);
190 if let Some(id) = row.suspended_by {
191 ids.push(id);
192 }
193 if let Some(id) = row.revoked_by {
194 ids.push(id);
195 }
196 }
197 let users = load_users(&svc.db, &ids).await?;
198 Ok(rows
199 .into_iter()
200 .map(|r| {
201 let user_id = r.user_id;
202 let suspended_by = r.suspended_by;
203 let revoked_by = r.revoked_by;
204 SuspensionView::from_model(
205 r,
206 users.get(&user_id).map(UserBasicInfo::from),
207 suspended_by.and_then(|id| users.get(&id).map(UserBasicInfo::from)),
208 revoked_by.and_then(|id| users.get(&id).map(UserBasicInfo::from)),
209 )
210 })
211 .collect())
184212 }
185213 .await;
186214 metrics::record_outcome(metrics::FEATURE_SUSPENSIONS_LIST_USER, &result);
@@ -196,7 +224,31 @@
196224 sel = sel.filter(db::user::user_suspensions::COLUMN.revoked_at.is_null());
197225 }
198226 let rows = sel.all(&db).await?;
199 Ok(rows.into_iter().map(SuspensionView::from).collect())
227 let mut ids: Vec<Uuid> = Vec::new();
228 for row in &rows {
229 ids.push(row.user_id);
230 if let Some(id) = row.suspended_by {
231 ids.push(id);
232 }
233 if let Some(id) = row.revoked_by {
234 ids.push(id);
235 }
236 }
237 let users = load_users(&svc.db, &ids).await?;
238 Ok(rows
239 .into_iter()
240 .map(|r| {
241 let user_id = r.user_id;
242 let suspended_by = r.suspended_by;
243 let revoked_by = r.revoked_by;
244 SuspensionView::from_model(
245 r,
246 users.get(&user_id).map(UserBasicInfo::from),
247 suspended_by.and_then(|id| users.get(&id).map(UserBasicInfo::from)),
248 revoked_by.and_then(|id| users.get(&id).map(UserBasicInfo::from)),
249 )
250 })
251 .collect())
200252 }
201253 .await;
202254 metrics::record_outcome(metrics::FEATURE_SUSPENSIONS_LIST_ALL, &result);
Modified lib/user/handler/profile/profile_view.rs lib/user/handler/profile/profile_view.rs +5 −1 View lib/user/handler/profile/profile_view.rs
@@ -25,7 +25,11 @@
2525 AuthUser { claims }: AuthUser,
2626 Path(uid): Path<Uuid>,
2727) -> UserResult<AppResponseData<ProfileView>> {
28 let viewer = if claims.sub.is_nil() { None } else { Some(claims.sub) };
28 let viewer = if claims.sub.is_nil() {
29 None
30 } else {
31 Some(claims.sub)
32 };
2933 let view = svc.get_profile(uid, viewer).await?;
3034 Ok(AppResponseData::ok(view))
3135}
Modified lib/user/handler/security/mod.rs lib/user/handler/security/mod.rs +1 −0 View lib/user/handler/security/mod.rs
@@ -2,3 +2,4 @@
22pub(crate) mod push_subscriptions;
33pub(crate) mod recovery_codes;
44pub(crate) mod ssh_keys;
5pub(crate) mod totp;
Modified lib/user/handler/openapi.rs lib/user/handler/openapi.rs +8 −2 View lib/user/handler/openapi.rs
@@ -23,8 +23,9 @@
2323use export::user::preferences::{Audience, UpdatePreferencesCmd, UserPreferences};
2424use export::user::security::{
2525 AccessKeyScope, AccessKeyView, AddSshKeyCmd, CreateAccessKeyCmd, CreateAccessKeyResp,
26 PushPlatform, PushSubscriptionView, RecoveryCodeView, RegenerateRecoveryResp, SshKeyType,
27 SshKeyView, UpsertPushCmd,
26 DisableTwoFactorCmd, EnableTwoFactorCmd, PushPlatform, PushSubscriptionView, RecoveryCodeView,
27 RegenerateRecoveryResp, SshKeyType, SshKeyView, TwoFactorSetupResp, TwoFactorStatusResp,
28 UpsertPushCmd,
2829};
2930use export::user::sessions::{ListSessionsQuery, SessionKind, SessionView};
3031use export::user::suspensions::{RevokeSuspensionCmd, SuspendCmd, SuspensionType, SuspensionView};
@@ -86,6 +87,10 @@
8687 crate::handler::suspensions::list_self::handler,
8788 crate::handler::security::recovery_codes::list::handler,
8889 crate::handler::security::recovery_codes::regenerate::handler,
90 crate::handler::security::totp::status::handler,
91 crate::handler::security::totp::setup::handler,
92 crate::handler::security::totp::enable::handler,
93 crate::handler::security::totp::disable::handler,
8994 crate::handler::security::access_keys::list_keys::handler,
9095 crate::handler::security::access_keys::create_key::handler,
9196 crate::handler::security::access_keys::delete_key::handler,
@@ -121,6 +126,7 @@
121126 DeletionRequestView, DeletionStatus, RequestDeletionCmd,
122127 SuspensionView, SuspensionType, SuspendCmd, RevokeSuspensionCmd,
123128 RecoveryCodeView, RegenerateRecoveryResp,
129 TwoFactorStatusResp, TwoFactorSetupResp, EnableTwoFactorCmd, DisableTwoFactorCmd,
124130 AccessKeyView, AccessKeyScope, CreateAccessKeyCmd, CreateAccessKeyResp,
125131 SshKeyView, SshKeyType, AddSshKeyCmd,
126132 PushSubscriptionView, PushPlatform, UpsertPushCmd,
Modified lib/user/handler/routes.rs lib/user/handler/routes.rs +16 −0 View lib/user/handler/routes.rs
@@ -113,6 +113,22 @@
113113 post(super::security::recovery_codes::regenerate::handler),
114114 )
115115 .route(
116 "/me/two-factor",
117 get(super::security::totp::status::handler),
118 )
119 .route(
120 "/me/two-factor/setup",
121 post(super::security::totp::setup::handler),
122 )
123 .route(
124 "/me/two-factor/enable",
125 post(super::security::totp::enable::handler),
126 )
127 .route(
128 "/me/two-factor/disable",
129 post(super::security::totp::disable::handler),
130 )
131 .route(
116132 "/me/access-keys",
117133 get(super::security::access_keys::list_keys::handler)
118134 .post(super::security::access_keys::create_key::handler),
Modified lib/user/Cargo.toml lib/user/Cargo.toml +4 −0 View lib/user/Cargo.toml
@@ -34,9 +34,13 @@
3434metrics.workspace = true
3535utoipa.workspace = true
3636validator.workspace = true
37aes-gcm.workspace = true
3738base64.workspace = true
39hex.workspace = true
40hmac.workspace = true
3841rand.workspace = true
3942rand_core = "0.10"
43sha1.workspace = true
4044sha2.workspace = true
4145url.workspace = true
4246http.workspace = true
Modified lib/user/metrics.rs lib/user/metrics.rs +12 −0 View lib/user/metrics.rs
@@ -44,6 +44,10 @@
4444
4545pub const FEATURE_RECOVERY_CODES_LIST: &str = "recovery_codes_list";
4646pub const FEATURE_RECOVERY_CODES_REGENERATE: &str = "recovery_codes_regenerate";
47pub const FEATURE_TWO_FACTOR_STATUS: &str = "two_factor_status";
48pub const FEATURE_TWO_FACTOR_SETUP: &str = "two_factor_setup";
49pub const FEATURE_TWO_FACTOR_ENABLE: &str = "two_factor_enable";
50pub const FEATURE_TWO_FACTOR_DISABLE: &str = "two_factor_disable";
4751pub const FEATURE_ACCESS_KEYS_LIST: &str = "access_keys_list";
4852pub const FEATURE_ACCESS_KEYS_CREATE: &str = "access_keys_create";
4953pub const FEATURE_ACCESS_KEYS_DELETE: &str = "access_keys_delete";
@@ -348,6 +352,9 @@
348352 Regenerate,
349353 Upsert,
350354 Add,
355 Setup,
356 Enable,
357 Disable,
351358}
352359
353360impl SecurityResourceOp {
@@ -359,6 +366,9 @@
359366 SecurityResourceOp::Regenerate => "regenerate",
360367 SecurityResourceOp::Upsert => "upsert",
361368 SecurityResourceOp::Add => "add",
369 SecurityResourceOp::Setup => "setup",
370 SecurityResourceOp::Enable => "enable",
371 SecurityResourceOp::Disable => "disable",
362372 }
363373 }
364374}
@@ -369,6 +379,7 @@
369379 AccessKey,
370380 SshKey,
371381 PushSubscription,
382 TwoFactor,
372383}
373384
374385impl SecurityResource {
@@ -378,6 +389,7 @@
378389 SecurityResource::AccessKey => "access_key",
379390 SecurityResource::SshKey => "ssh_key",
380391 SecurityResource::PushSubscription => "push_subscription",
392 SecurityResource::TwoFactor => "two_factor",
381393 }
382394 }
383395}