Skip to content
Clone

refactor(user): refresh all feature usecase implementations

zhenyi committed
parent 65d99a7 commit 1cfdf6f Browse files

11 files changed

+1935 −1183

File tree 11
Modified lib/user/features/avatar/usecase.rs View lib/user/features/avatar/usecase.rs
@@ -8,6 +8,9 @@
88
99use crate::error::{UserError, UserResult};
1010use crate::features::UserService;
11use crate::metrics::{
12 self, AvatarOp, record_avatar_operation, record_storage_operation,
13};
1114
1215use super::types::{decode_upload, AvatarFormat, AvatarView, UploadAvatarCmd};
1316
@@ -22,85 +25,123 @@
2225}
2326
2427pub async fn upload(svc: &UserService, caller: Uuid, cmd: UploadAvatarCmd) -> UserResult<AvatarView> {
25 let (fmt, bytes) = decode_upload(&cmd)?;
26 let new_path = avatar_path(caller, fmt);
28 let _timer = metrics::Timer::start(metrics::FEATURE_AVATAR_UPLOAD);
29 let result: UserResult<AvatarView> = async {
30 let (fmt, bytes) = decode_upload(&cmd)?;
31 let new_path = avatar_path(caller, fmt);
2732
28 svc.storage
29 .put(&new_path, Bytes::from(bytes.clone()))
30 .await?;
33 if let Err(e) = svc.storage.put(&new_path, Bytes::from(bytes.clone())).await {
34 record_storage_operation("avatar", "put", "failed");
35 return Err(e.into());
36 }
37 record_storage_operation("avatar", "put", "success");
3138
32 let now = OffsetDateTime::now_utc();
33 let stored_url = format!("storage://{}/{}.{}", AVATAR_PREFIX, caller, fmt.extension());
34 let writer = svc.db.writer();
35 let user = user_entity::Entity::find_by_id(caller)
36 .one(&writer)
37 .await?
38 .ok_or_else(|| UserError::NotFound(format!("user {caller}")))?;
39 let mut active: user_entity::ActiveModel = user.into();
40 active.avatar = Set(stored_url.clone());
41 active.updated_at = Set(now);
42 active.update(&writer).await?;
39 let now = OffsetDateTime::now_utc();
40 let stored_url = format!("storage://{}/{}.{}", AVATAR_PREFIX, caller, fmt.extension());
41 let writer = svc.db.writer();
42 let user = user_entity::Entity::find_by_id(caller)
43 .one(&writer)
44 .await?
45 .ok_or_else(|| UserError::NotFound(format!("user {caller}")))?;
46 let mut active: user_entity::ActiveModel = user.into();
47 active.avatar = Set(stored_url.clone());
48 active.updated_at = Set(now);
49 active.update(&writer).await?;
4350
44 let meta = serde_json::json!({
45 "uid": caller.to_string(),
46 "format": fmt.extension(),
47 "bytes": bytes.len(),
48 "uploaded_at": now.format(&Rfc3339).unwrap_or_default(),
49 });
50 let meta_bytes = serde_json::to_vec(&meta).unwrap_or_default();
51 let meta_path = avatar_meta_path(caller);
52 let _ = svc.storage.put(&meta_path, Bytes::from(meta_bytes)).await;
51 let meta = serde_json::json!({
52 "uid": caller.to_string(),
53 "format": fmt.extension(),
54 "bytes": bytes.len(),
55 "uploaded_at": now.format(&Rfc3339).unwrap_or_default(),
56 });
57 let meta_bytes = serde_json::to_vec(&meta).unwrap_or_default();
58 let meta_path = avatar_meta_path(caller);
59 match svc.storage.put(&meta_path, Bytes::from(meta_bytes)).await {
60 Ok(_) => record_storage_operation("meta_json", "put", "success"),
61 Err(_) => record_storage_operation("meta_json", "put", "failed"),
62 }
5363
54 Ok(AvatarView {
55 uid: caller,
56 url: stored_url,
57 format: fmt,
58 bytes: bytes.len() as u64,
59 updated_at: now.format(&Rfc3339).unwrap_or_default(),
60 })
64 Ok(AvatarView {
65 uid: caller,
66 url: stored_url,
67 format: fmt,
68 bytes: bytes.len() as u64,
69 updated_at: now.format(&Rfc3339).unwrap_or_default(),
70 })
71 }
72 .await;
73 metrics::record_outcome(metrics::FEATURE_AVATAR_UPLOAD, &result);
74 if let Ok(ref v) = result {
75 record_avatar_operation(AvatarOp::Upload, metrics::OUTCOME_SUCCESS);
76 let _ = v;
77 } else {
78 record_avatar_operation(AvatarOp::Upload, "decode_or_db_failed");
79 }
80 result
6181}
6282
6383pub async fn delete(svc: &UserService, caller: Uuid) -> UserResult<()> {
64 let writer = svc.db.writer();
65 let user = user_entity::Entity::find_by_id(caller)
66 .one(&writer)
67 .await?
68 .ok_or_else(|| UserError::NotFound(format!("user {caller}")))?;
69 let current = user.avatar.clone();
70 let now = OffsetDateTime::now_utc();
84 let _timer = metrics::Timer::start(metrics::FEATURE_AVATAR_DELETE);
85 let result: UserResult<()> = async {
86 let writer = svc.db.writer();
87 let user = user_entity::Entity::find_by_id(caller)
88 .one(&writer)
89 .await?
90 .ok_or_else(|| UserError::NotFound(format!("user {caller}")))?;
91 let current = user.avatar.clone();
92 let now = OffsetDateTime::now_utc();
7193
72 let mut active: user_entity::ActiveModel = user.into();
73 active.avatar = Set(String::new());
74 active.updated_at = Set(now);
75 active.update(&writer).await?;
94 let mut active: user_entity::ActiveModel = user.into();
95 active.avatar = Set(String::new());
96 active.updated_at = Set(now);
97 active.update(&writer).await?;
7698
77 if current.starts_with(&format!("storage://{}/", AVATAR_PREFIX)) {
78 let rel = current.trim_start_matches("storage://").trim_start_matches(AVATAR_PREFIX).trim_start_matches('/');
79 let path = ObjectPath::from(format!("{}/{}", AVATAR_PREFIX, rel));
80 let _ = svc.storage.delete(&path).await;
81 let _ = svc.storage.delete(&avatar_meta_path(caller)).await;
99 if current.starts_with(&format!("storage://{}/", AVATAR_PREFIX)) {
100 let rel = current.trim_start_matches("storage://").trim_start_matches(AVATAR_PREFIX).trim_start_matches('/');
101 let path = ObjectPath::from(format!("{}/{}", AVATAR_PREFIX, rel));
102 match svc.storage.delete(&path).await {
103 Ok(_) => record_storage_operation("avatar", "delete", "success"),
104 Err(_) => record_storage_operation("avatar", "delete", "failed"),
105 }
106 match svc.storage.delete(&avatar_meta_path(caller)).await {
107 Ok(_) => record_storage_operation("meta_json", "delete", "success"),
108 Err(_) => record_storage_operation("meta_json", "delete", "failed"),
109 }
110 }
111
112 Ok(())
113 }
114 .await;
115 metrics::record_outcome(metrics::FEATURE_AVATAR_DELETE, &result);
116 if result.is_ok() {
117 record_avatar_operation(AvatarOp::Delete, metrics::OUTCOME_SUCCESS);
82118 }
83
84 Ok(())
119 result
85120}
86121
87122pub async fn current(svc: &UserService, caller: Uuid) -> UserResult<Option<AvatarView>> {
88 let db = svc.db.reader();
89 let user = user_entity::Entity::find_by_id(caller)
90 .one(&db)
91 .await?
92 .ok_or_else(|| UserError::NotFound(format!("user {caller}")))?;
93 if user.avatar.is_empty() {
94 return Ok(None);
123 let _timer = metrics::Timer::start(metrics::FEATURE_AVATAR_CURRENT);
124 let result: UserResult<Option<AvatarView>> = async {
125 let db = svc.db.reader();
126 let user = user_entity::Entity::find_by_id(caller)
127 .one(&db)
128 .await?
129 .ok_or_else(|| UserError::NotFound(format!("user {caller}")))?;
130 if user.avatar.is_empty() {
131 return Ok(None);
132 }
133 let fmt = detect_format_from_url(&user.avatar).unwrap_or(AvatarFormat::Jpg);
134 Ok(Some(AvatarView {
135 uid: caller,
136 url: user.avatar.clone(),
137 format: fmt,
138 bytes: 0,
139 updated_at: OffsetDateTime::now_utc().format(&Rfc3339).unwrap_or_default(),
140 }))
95141 }
96 let fmt = detect_format_from_url(&user.avatar).unwrap_or(AvatarFormat::Jpg);
97 Ok(Some(AvatarView {
98 uid: caller,
99 url: user.avatar.clone(),
100 format: fmt,
101 bytes: 0,
102 updated_at: OffsetDateTime::now_utc().format(&Rfc3339).unwrap_or_default(),
103 }))
142 .await;
143 metrics::record_outcome(metrics::FEATURE_AVATAR_CURRENT, &result);
144 result
104145}
105146
106147fn detect_format_from_url(url: &str) -> Option<AvatarFormat> {
@@ -109,4 +150,4 @@
109150}
110151
111152#[allow(dead_code)]
112fn _ensure_query(_q: &db::conn::sea_orm::Select<user_entity::Entity>) {}
No newline at end of file
153fn _ensure_query(_q: &db::conn::sea_orm::Select<user_entity::Entity>) {}
Modified lib/user/features/deletion/usecase.rs View lib/user/features/deletion/usecase.rs
@@ -6,6 +6,9 @@
66
77use crate::error::{UserError, UserResult};
88use crate::features::UserService;
9use crate::metrics::{
10 self, DeletionOp, DeletionStatus, record_deletion_operation,
11};
912use jwt::session;
1013
1114use super::{DeletionRequestView, RequestDeletionCmd};
@@ -15,111 +18,153 @@
1518 caller: Uuid,
1619 cmd: RequestDeletionCmd,
1720) -> UserResult<DeletionRequestView> {
18 let writer = svc.db.writer();
19 let existing = db::user::user_deletion_requests::Entity::find()
20 .filter(db::user::user_deletion_requests::COLUMN.user_id.eq(caller))
21 .filter(
22 db::user::user_deletion_requests::COLUMN.status.eq(DbDeletionStatus::Pending),
23 )
24 .one(&writer)
25 .await?;
26 if let Some(row) = existing {
27 return Ok(DeletionRequestView::from(row));
21 let _timer = metrics::Timer::start(metrics::FEATURE_DELETION_REQUEST);
22 let result: UserResult<DeletionRequestView> = async {
23 let writer = svc.db.writer();
24 let existing = db::user::user_deletion_requests::Entity::find()
25 .filter(db::user::user_deletion_requests::COLUMN.user_id.eq(caller))
26 .filter(
27 db::user::user_deletion_requests::COLUMN.status.eq(DbDeletionStatus::Pending),
28 )
29 .one(&writer)
30 .await?;
31 if let Some(row) = existing {
32 return Ok(DeletionRequestView::from(row));
33 }
34
35 let now = time::OffsetDateTime::now_utc();
36 let active: db::user::user_deletion_requests::ActiveModel =
37 db::user::user_deletion_requests::ActiveModel::builder()
38 .set_id(Uuid::new_v4())
39 .set_user_id(caller)
40 .set_reason(cmd.reason)
41 .set_status(DbDeletionStatus::Pending)
42 .set_scheduled_for(None)
43 .set_completed_at(None)
44 .set_cancelled_at(None)
45 .set_created_at(now)
46 .set_updated_at(now)
47 .into();
48 let saved = active.insert(&writer).await?;
49 Ok(DeletionRequestView::from(saved))
2850 }
29
30 let now = time::OffsetDateTime::now_utc();
31 let active: db::user::user_deletion_requests::ActiveModel =
32 db::user::user_deletion_requests::ActiveModel::builder()
33 .set_id(Uuid::new_v4())
34 .set_user_id(caller)
35 .set_reason(cmd.reason)
36 .set_status(DbDeletionStatus::Pending)
37 .set_scheduled_for(None)
38 .set_completed_at(None)
39 .set_cancelled_at(None)
40 .set_created_at(now)
41 .set_updated_at(now)
42 .into();
43 let saved = active.insert(&writer).await?;
44 Ok(DeletionRequestView::from(saved))
51 .await;
52 metrics::record_outcome(metrics::FEATURE_DELETION_REQUEST, &result);
53 if result.is_ok() {
54 record_deletion_operation(DeletionOp::Request, DeletionStatus::Pending);
55 }
56 result
4557}
4658
4759pub async fn cancel(svc: &UserService, caller: Uuid) -> UserResult<()> {
48 let writer = svc.db.writer();
49 let row = db::user::user_deletion_requests::Entity::find()
50 .filter(db::user::user_deletion_requests::COLUMN.user_id.eq(caller))
51 .filter(
52 db::user::user_deletion_requests::COLUMN.status.is_in([
53 DbDeletionStatus::Pending,
54 DbDeletionStatus::Scheduled,
55 ]),
56 )
57 .one(&writer)
58 .await?
59 .ok_or_else(|| UserError::NotFound("no active deletion request".into()))?;
60 let mut am: db::user::user_deletion_requests::ActiveModel = row.into();
61 am.status = Set(DbDeletionStatus::Cancelled);
62 am.cancelled_at = Set(Some(time::OffsetDateTime::now_utc()));
63 am.updated_at = Set(time::OffsetDateTime::now_utc());
64 am.update(&writer).await?;
65 Ok(())
60 let _timer = metrics::Timer::start(metrics::FEATURE_DELETION_CANCEL);
61 let result: UserResult<()> = async {
62 let writer = svc.db.writer();
63 let row = db::user::user_deletion_requests::Entity::find()
64 .filter(db::user::user_deletion_requests::COLUMN.user_id.eq(caller))
65 .filter(
66 db::user::user_deletion_requests::COLUMN.status.is_in([
67 DbDeletionStatus::Pending,
68 DbDeletionStatus::Scheduled,
69 ]),
70 )
71 .one(&writer)
72 .await?
73 .ok_or_else(|| UserError::NotFound("no active deletion request".into()))?;
74 let mut am: db::user::user_deletion_requests::ActiveModel = row.into();
75 am.status = Set(DbDeletionStatus::Cancelled);
76 am.cancelled_at = Set(Some(time::OffsetDateTime::now_utc()));
77 am.updated_at = Set(time::OffsetDateTime::now_utc());
78 am.update(&writer).await?;
79 Ok(())
80 }
81 .await;
82 metrics::record_outcome(metrics::FEATURE_DELETION_CANCEL, &result);
83 if result.is_ok() {
84 record_deletion_operation(DeletionOp::Cancel, DeletionStatus::Cancelled);
85 }
86 result
6687}
6788
6889pub async fn confirm(svc: &UserService, caller: Uuid) -> UserResult<DeletionRequestView> {
69 let writer = svc.db.writer();
70 let row = db::user::user_deletion_requests::Entity::find()
71 .filter(db::user::user_deletion_requests::COLUMN.user_id.eq(caller))
72 .filter(
73 db::user::user_deletion_requests::COLUMN.status.eq(DbDeletionStatus::Pending),
74 )
75 .one(&writer)
76 .await?
77 .ok_or_else(|| UserError::NotFound("no pending deletion request".into()))?;
90 let _timer = metrics::Timer::start(metrics::FEATURE_DELETION_CONFIRM);
91 let result: UserResult<DeletionRequestView> = async {
92 let writer = svc.db.writer();
93 let row = db::user::user_deletion_requests::Entity::find()
94 .filter(db::user::user_deletion_requests::COLUMN.user_id.eq(caller))
95 .filter(
96 db::user::user_deletion_requests::COLUMN.status.eq(DbDeletionStatus::Pending),
97 )
98 .one(&writer)
99 .await?
100 .ok_or_else(|| UserError::NotFound("no pending deletion request".into()))?;
78101
79 let now = time::OffsetDateTime::now_utc();
80 let scheduled_for = now + time::Duration::days(svc.config.deletion_grace_days);
81 let mut am: db::user::user_deletion_requests::ActiveModel = row.into();
82 am.status = Set(DbDeletionStatus::Scheduled);
83 am.scheduled_for = Set(Some(scheduled_for));
84 am.updated_at = Set(now);
85 let saved = am.update(&writer).await?;
86 Ok(DeletionRequestView::from(saved))
102 let now = time::OffsetDateTime::now_utc();
103 let scheduled_for = now + time::Duration::days(svc.config.deletion_grace_days);
104 let mut am: db::user::user_deletion_requests::ActiveModel = row.into();
105 am.status = Set(DbDeletionStatus::Scheduled);
106 am.scheduled_for = Set(Some(scheduled_for));
107 am.updated_at = Set(now);
108 let saved = am.update(&writer).await?;
109 Ok(DeletionRequestView::from(saved))
110 }
111 .await;
112 metrics::record_outcome(metrics::FEATURE_DELETION_CONFIRM, &result);
113 if result.is_ok() {
114 record_deletion_operation(DeletionOp::Confirm, DeletionStatus::Scheduled);
115 }
116 result
87117}
88118
89119pub async fn list_pending(svc: &UserService) -> UserResult<Vec<DeletionRequestView>> {
90 let db = svc.db.reader();
91 let rows = db::user::user_deletion_requests::Entity::find()
92 .filter(
93 db::user::user_deletion_requests::COLUMN.status.is_in([
94 DbDeletionStatus::Pending,
95 DbDeletionStatus::Scheduled,
96 ]),
97 )
98 .all(&db)
99 .await?;
100 Ok(rows.into_iter().map(DeletionRequestView::from).collect())
120 let _timer = metrics::Timer::start(metrics::FEATURE_DELETION_LIST_PENDING);
121 let result: UserResult<Vec<DeletionRequestView>> = async {
122 let db = svc.db.reader();
123 let rows = db::user::user_deletion_requests::Entity::find()
124 .filter(
125 db::user::user_deletion_requests::COLUMN.status.is_in([
126 DbDeletionStatus::Pending,
127 DbDeletionStatus::Scheduled,
128 ]),
129 )
130 .all(&db)
131 .await?;
132 Ok(rows.into_iter().map(DeletionRequestView::from).collect())
133 }
134 .await;
135 metrics::record_outcome(metrics::FEATURE_DELETION_LIST_PENDING, &result);
136 result
101137}
102138
103139pub async fn complete(svc: &UserService, request_id: Uuid) -> UserResult<()> {
104 let writer = svc.db.writer();
105 let txn = writer.begin().await?;
106 let row = db::user::user_deletion_requests::Entity::find_by_id(request_id)
107 .one(&txn)
108 .await?
109 .ok_or_else(|| UserError::NotFound(format!("deletion request {request_id}")))?;
110 if !matches!(row.status, DbDeletionStatus::Scheduled) {
111 return Err(UserError::BadRequest("request not in Scheduled state".into()));
112 }
113 let user_id = row.user_id;
114 let mut am: db::user::user_deletion_requests::ActiveModel = row.into();
115 am.status = Set(DbDeletionStatus::Completed);
116 am.completed_at = Set(Some(time::OffsetDateTime::now_utc()));
117 am.updated_at = Set(time::OffsetDateTime::now_utc());
118 am.update(&txn).await?;
140 let _timer = metrics::Timer::start(metrics::FEATURE_DELETION_COMPLETE);
141 let result: UserResult<()> = async {
142 let writer = svc.db.writer();
143 let txn = writer.begin().await?;
144 let row = db::user::user_deletion_requests::Entity::find_by_id(request_id)
145 .one(&txn)
146 .await?
147 .ok_or_else(|| UserError::NotFound(format!("deletion request {request_id}")))?;
148 if !matches!(row.status, DbDeletionStatus::Scheduled) {
149 return Err(UserError::BadRequest("request not in Scheduled state".into()));
150 }
151 let user_id = row.user_id;
152 let mut am: db::user::user_deletion_requests::ActiveModel = row.into();
153 am.status = Set(DbDeletionStatus::Completed);
154 am.completed_at = Set(Some(time::OffsetDateTime::now_utc()));
155 am.updated_at = Set(time::OffsetDateTime::now_utc());
156 am.update(&txn).await?;
119157
120 session::delete_sessions_by_user(&txn, user_id)
121 .await
122 .map_err(|e| UserError::Internal(format!("jwt delete_sessions_by_user: {e}")))?;
123 txn.commit().await?;
124 Ok(())
125}
No newline at end of file
158 session::delete_sessions_by_user(&txn, user_id)
159 .await
160 .map_err(|e| UserError::Internal(format!("jwt delete_sessions_by_user: {e}")))?;
161 txn.commit().await?;
162 Ok(())
163 }
164 .await;
165 metrics::record_outcome(metrics::FEATURE_DELETION_COMPLETE, &result);
166 if result.is_ok() {
167 record_deletion_operation(DeletionOp::Complete, DeletionStatus::Completed);
168 }
169 result
170}
Modified lib/user/features/devices/usecase.rs View lib/user/features/devices/usecase.rs
@@ -5,19 +5,26 @@
55
66use crate::error::{UserError, UserResult};
77use crate::features::UserService;
8use crate::metrics;
89
910use super::{DeviceView, UpdateDeviceCmd};
1011
1112const MAX_DEVICE_NAME: usize = 128;
1213
1314pub async fn list(svc: &UserService, caller: Uuid) -> UserResult<Vec<DeviceView>> {
14 let db = svc.db.reader();
15 let rows = db::user::user_devices::Entity::find()
16 .filter(db::user::user_devices::COLUMN.user_id.eq(caller))
17 .order_by_desc(db::user::user_devices::COLUMN.last_seen_at)
18 .all(&db)
19 .await?;
20 Ok(rows.into_iter().map(DeviceView::from).collect())
15 let _timer = metrics::Timer::start(metrics::FEATURE_DEVICES_LIST);
16 let result: UserResult<Vec<DeviceView>> = async {
17 let db = svc.db.reader();
18 let rows = db::user::user_devices::Entity::find()
19 .filter(db::user::user_devices::COLUMN.user_id.eq(caller))
20 .order_by_desc(db::user::user_devices::COLUMN.last_seen_at)
21 .all(&db)
22 .await?;
23 Ok(rows.into_iter().map(DeviceView::from).collect())
24 }
25 .await;
26 metrics::record_outcome(metrics::FEATURE_DEVICES_LIST, &result);
27 result
2128}
2229
2330pub async fn update(
@@ -26,50 +33,62 @@
2633 device_id: Uuid,
2734 cmd: UpdateDeviceCmd,
2835) -> UserResult<DeviceView> {
29 if let Some(Some(ref name)) = cmd.device_name
30 && (name.is_empty() || name.len() > MAX_DEVICE_NAME) {
31 return Err(UserError::BadRequest(format!(
32 "device_name length must be 1..={MAX_DEVICE_NAME}"
33 )));
36 let _timer = metrics::Timer::start(metrics::FEATURE_DEVICES_UPDATE);
37 let result: UserResult<DeviceView> = async {
38 if let Some(Some(ref name)) = cmd.device_name
39 && (name.is_empty() || name.len() > MAX_DEVICE_NAME) {
40 return Err(UserError::BadRequest(format!(
41 "device_name length must be 1..={MAX_DEVICE_NAME}"
42 )));
43 }
44
45 let writer = svc.db.writer();
46 let row = db::user::user_devices::Entity::find_by_id(device_id)
47 .one(&writer)
48 .await?
49 .ok_or_else(|| UserError::NotFound(format!("device {device_id}")))?;
50 if row.user_id != caller {
51 return Err(UserError::Forbidden("cannot modify other user's device".into()));
3452 }
3553
36 let writer = svc.db.writer();
37 let row = db::user::user_devices::Entity::find_by_id(device_id)
38 .one(&writer)
39 .await?
40 .ok_or_else(|| UserError::NotFound(format!("device {device_id}")))?;
41 if row.user_id != caller {
42 return Err(UserError::Forbidden("cannot modify other user's device".into()));
43 }
44
45 let mut active: db::user::user_devices::ActiveModel = row.into();
46 if let Some(v) = cmd.device_name {
47 active.device_name = Set(v);
54 let mut active: db::user::user_devices::ActiveModel = row.into();
55 if let Some(v) = cmd.device_name {
56 active.device_name = Set(v);
57 }
58 if let Some(v) = cmd.is_trusted {
59 active.is_trusted = Set(v);
60 }
61 active.updated_at = Set(time::OffsetDateTime::now_utc());
62 let saved = active.update(&writer).await?;
63 Ok(DeviceView::from(saved))
4864 }
49 if let Some(v) = cmd.is_trusted {
50 active.is_trusted = Set(v);
51 }
52 active.updated_at = Set(time::OffsetDateTime::now_utc());
53 let saved = active.update(&writer).await?;
54 Ok(DeviceView::from(saved))
65 .await;
66 metrics::record_outcome(metrics::FEATURE_DEVICES_UPDATE, &result);
67 result
5568}
5669
5770pub async fn delete(svc: &UserService, caller: Uuid, device_id: Uuid) -> UserResult<()> {
58 let writer = svc.db.writer();
59 let row = db::user::user_devices::Entity::find_by_id(device_id)
60 .one(&writer)
61 .await?
62 .ok_or_else(|| UserError::NotFound(format!("device {device_id}")))?;
63 if row.user_id != caller {
64 return Err(UserError::Forbidden("cannot delete other user's device".into()));
71 let _timer = metrics::Timer::start(metrics::FEATURE_DEVICES_DELETE);
72 let result: UserResult<()> = async {
73 let writer = svc.db.writer();
74 let row = db::user::user_devices::Entity::find_by_id(device_id)
75 .one(&writer)
76 .await?
77 .ok_or_else(|| UserError::NotFound(format!("device {device_id}")))?;
78 if row.user_id != caller {
79 return Err(UserError::Forbidden("cannot delete other user's device".into()));
80 }
81 db::user::user_devices::Entity::delete_by_id(device_id)
82 .exec(&writer)
83 .await?;
84 Ok(())
6585 }
66 db::user::user_devices::Entity::delete_by_id(device_id)
67 .exec(&writer)
68 .await?;
69 Ok(())
86 .await;
87 metrics::record_outcome(metrics::FEATURE_DEVICES_DELETE, &result);
88 result
7089}
7190
7291#[allow(dead_code)]
7392fn _now_utc() -> time::OffsetDateTime {
7493 time::OffsetDateTime::now_utc()
75}
No newline at end of file
94}
Modified lib/user/features/emails/usecase.rs View lib/user/features/emails/usecase.rs
@@ -5,6 +5,9 @@
55
66use crate::error::{UserError, UserResult};
77use crate::features::UserService;
8use crate::metrics::{
9 self, EmailChangeOutcome, EmailOp, record_email_operation,
10};
811
912use super::{
1013 AddEmailCmd, AddEmailResp, ConfirmEmailChangeCmd, EmailChangeTicketStatus, EmailChangeTicketView,
@@ -14,38 +17,55 @@
1417const EMAIL_MAX_LEN: usize = 254;
1518
1619pub async fn list(svc: &UserService, caller: Uuid) -> UserResult<Vec<EmailView>> {
17 let db = svc.db.reader();
18 let rows = db::user::user_emails::Entity::find()
19 .filter(db::user::user_emails::COLUMN.user_id.eq(caller))
20 .filter(db::user::user_emails::COLUMN.deleted_at.is_null())
21 .all(&db)
22 .await?;
23 Ok(rows.into_iter().map(EmailView::from).collect())
20 let _timer = metrics::Timer::start(metrics::FEATURE_EMAILS_LIST);
21 let result: UserResult<Vec<EmailView>> = async {
22 let db = svc.db.reader();
23 let rows = db::user::user_emails::Entity::find()
24 .filter(db::user::user_emails::COLUMN.user_id.eq(caller))
25 .filter(db::user::user_emails::COLUMN.deleted_at.is_null())
26 .all(&db)
27 .await?;
28 Ok(rows.into_iter().map(EmailView::from).collect())
29 }
30 .await;
31 metrics::record_outcome(metrics::FEATURE_EMAILS_LIST, &result);
32 result
2433}
2534
2635pub async fn add(svc: &UserService, caller: Uuid, cmd: AddEmailCmd) -> UserResult<AddEmailResp> {
27 let email = cmd.email.trim().to_lowercase();
28 if email.is_empty() || email.len() > EMAIL_MAX_LEN || !email.contains('@') {
29 return Err(UserError::BadRequest(format!(
30 "invalid email (length 1..={EMAIL_MAX_LEN})"
31 )));
36 let _timer = metrics::Timer::start(metrics::FEATURE_EMAILS_ADD);
37 let result: UserResult<AddEmailResp> = async {
38 let email = cmd.email.trim().to_lowercase();
39 if email.is_empty() || email.len() > EMAIL_MAX_LEN || !email.contains('@') {
40 return Err(UserError::BadRequest(format!(
41 "invalid email (length 1..={EMAIL_MAX_LEN})"
42 )));
43 }
44 let writer = svc.db.writer();
45 let now = time::OffsetDateTime::now_utc();
46 let active: db::user::user_emails::ActiveModel = db::user::user_emails::ActiveModel::builder()
47 .set_id(Uuid::new_v4())
48 .set_user_id(caller)
49 .set_email(email.clone())
50 .set_lower_email(email)
51 .set_is_activated(false)
52 .set_is_primary(false)
53 .set_created_at(now)
54 .set_updated_at(now)
55 .set_activated_at(None)
56 .set_deleted_at(None)
57 .into();
58 let saved = active.insert(&writer).await?;
59 Ok(AddEmailResp { email_id: saved.id })
3260 }
33 let writer = svc.db.writer();
34 let now = time::OffsetDateTime::now_utc();
35 let active: db::user::user_emails::ActiveModel = db::user::user_emails::ActiveModel::builder()
36 .set_id(Uuid::new_v4())
37 .set_user_id(caller)
38 .set_email(email.clone())
39 .set_lower_email(email)
40 .set_is_activated(false)
41 .set_is_primary(false)
42 .set_created_at(now)
43 .set_updated_at(now)
44 .set_activated_at(None)
45 .set_deleted_at(None)
46 .into();
47 let saved = active.insert(&writer).await?;
48 Ok(AddEmailResp { email_id: saved.id })
61 .await;
62 metrics::record_outcome(metrics::FEATURE_EMAILS_ADD, &result);
63 if result.is_ok() {
64 record_email_operation(EmailOp::Add, EmailChangeOutcome::Success);
65 } else if let Err(UserError::BadRequest(_)) = &result {
66 record_email_operation(EmailOp::Add, EmailChangeOutcome::Mismatch);
67 }
68 result
4969}
5070
5171pub async fn set_primary(
@@ -53,60 +73,101 @@
5373 caller: Uuid,
5474 email_id: Uuid,
5575) -> UserResult<SetPrimaryEmailResp> {
56 let writer = svc.db.writer();
57 let txn = writer.begin().await?;
58 let target = db::user::user_emails::Entity::find_by_id(email_id)
59 .one(&txn)
60 .await?
61 .ok_or_else(|| UserError::NotFound(format!("email {email_id}")))?;
62 if target.user_id != caller {
63 return Err(UserError::Forbidden("cannot set other user's primary".into()));
64 }
65 if !target.is_activated {
66 return Err(UserError::BadRequest("email not yet activated".into()));
67 }
76 let _timer = metrics::Timer::start(metrics::FEATURE_EMAILS_SET_PRIMARY);
77 let result: UserResult<SetPrimaryEmailResp> = async {
78 let writer = svc.db.writer();
79 let txn = writer.begin().await?;
80 let target = db::user::user_emails::Entity::find_by_id(email_id)
81 .one(&txn)
82 .await?
83 .ok_or_else(|| UserError::NotFound(format!("email {email_id}")))?;
84 if target.user_id != caller {
85 return Err(UserError::Forbidden("cannot set other user's primary".into()));
86 }
87 if !target.is_activated {
88 return Err(UserError::BadRequest("email not yet activated".into()));
89 }
90 if target.is_primary {
91 return Err(UserError::BadRequest("email already primary".into()));
92 }
93
94 let others = db::user::user_emails::Entity::find()
95 .filter(db::user::user_emails::COLUMN.user_id.eq(caller))
96 .filter(db::user::user_emails::COLUMN.is_primary.eq(true))
97 .all(&txn)
98 .await?;
99 for other in others {
100 let mut am: db::user::user_emails::ActiveModel = other.into();
101 am.is_primary = Set(false);
102 am.updated_at = Set(time::OffsetDateTime::now_utc());
103 am.update(&txn).await?;
104 }
68105
69 let others = db::user::user_emails::Entity::find()
70 .filter(db::user::user_emails::COLUMN.user_id.eq(caller))
71 .filter(db::user::user_emails::COLUMN.is_primary.eq(true))
72 .all(&txn)
73 .await?;
74 for other in others {
75 let mut am: db::user::user_emails::ActiveModel = other.into();
76 am.is_primary = Set(false);
106 let mut am: db::user::user_emails::ActiveModel = target.into();
107 am.is_primary = Set(true);
77108 am.updated_at = Set(time::OffsetDateTime::now_utc());
78 am.update(&txn).await?;
109 let saved = am.update(&txn).await?;
110 txn.commit().await?;
111 Ok(SetPrimaryEmailResp {
112 email: EmailView::from(saved),
113 })
114 }
115 .await;
116 metrics::record_outcome(metrics::FEATURE_EMAILS_SET_PRIMARY, &result);
117 match &result {
118 Ok(_) => record_email_operation(EmailOp::SetPrimary, EmailChangeOutcome::Success),
119 Err(UserError::BadRequest(_)) => {
120 record_email_operation(EmailOp::SetPrimary, EmailChangeOutcome::Mismatch);
121 }
122 Err(UserError::Forbidden(_)) => {
123 record_email_operation(EmailOp::SetPrimary, EmailChangeOutcome::Forbidden);
124 }
125 Err(UserError::NotFound(_)) => {
126 record_email_operation(EmailOp::SetPrimary, EmailChangeOutcome::NotFound);
127 }
128 Err(_) => record_email_operation(EmailOp::SetPrimary, EmailChangeOutcome::Other),
79129 }
80
81 let mut am: db::user::user_emails::ActiveModel = target.into();
82 am.is_primary = Set(true);
83 am.updated_at = Set(time::OffsetDateTime::now_utc());
84 let saved = am.update(&txn).await?;
85 txn.commit().await?;
86 Ok(SetPrimaryEmailResp {
87 email: EmailView::from(saved),
88 })
130 result
89131}
90132
91133pub async fn soft_delete(svc: &UserService, caller: Uuid, email_id: Uuid) -> UserResult<()> {
92 let writer = svc.db.writer();
93 let row = db::user::user_emails::Entity::find_by_id(email_id)
94 .one(&writer)
95 .await?
96 .ok_or_else(|| UserError::NotFound(format!("email {email_id}")))?;
97 if row.user_id != caller {
98 return Err(UserError::Forbidden("cannot delete other user's email".into()));
134 let _timer = metrics::Timer::start(metrics::FEATURE_EMAILS_DELETE);
135 let result: UserResult<()> = async {
136 let writer = svc.db.writer();
137 let row = db::user::user_emails::Entity::find_by_id(email_id)
138 .one(&writer)
139 .await?
140 .ok_or_else(|| UserError::NotFound(format!("email {email_id}")))?;
141 if row.user_id != caller {
142 return Err(UserError::Forbidden("cannot delete other user's email".into()));
143 }
144 if row.is_primary && row.is_activated {
145 return Err(UserError::BadRequest(
146 "cannot delete primary activated email".into(),
147 ));
148 }
149 let mut am: db::user::user_emails::ActiveModel = row.into();
150 am.deleted_at = Set(Some(time::OffsetDateTime::now_utc()));
151 am.updated_at = Set(time::OffsetDateTime::now_utc());
152 am.update(&writer).await?;
153 Ok(())
99154 }
100 if row.is_primary && row.is_activated {
101 return Err(UserError::BadRequest(
102 "cannot delete primary activated email".into(),
103 ));
155 .await;
156 metrics::record_outcome(metrics::FEATURE_EMAILS_DELETE, &result);
157 match &result {
158 Ok(_) => record_email_operation(EmailOp::Delete, EmailChangeOutcome::Success),
159 Err(UserError::BadRequest(_)) => {
160 record_email_operation(EmailOp::Delete, EmailChangeOutcome::Conflict);
161 }
162 Err(UserError::Forbidden(_)) => {
163 record_email_operation(EmailOp::Delete, EmailChangeOutcome::Forbidden);
164 }
165 Err(UserError::NotFound(_)) => {
166 record_email_operation(EmailOp::Delete, EmailChangeOutcome::NotFound);
167 }
168 Err(_) => record_email_operation(EmailOp::Delete, EmailChangeOutcome::Other),
104169 }
105 let mut am: db::user::user_emails::ActiveModel = row.into();
106 am.deleted_at = Set(Some(time::OffsetDateTime::now_utc()));
107 am.updated_at = Set(time::OffsetDateTime::now_utc());
108 am.update(&writer).await?;
109 Ok(())
170 result
110171}
111172
112173pub async fn request_change(
@@ -114,28 +175,37 @@
114175 caller: Uuid,
115176 new_email: String,
116177) -> UserResult<EmailChangeTicketView> {
117 let new_email = new_email.trim().to_lowercase();
118 if new_email.is_empty() || new_email.len() > EMAIL_MAX_LEN || !new_email.contains('@') {
119 return Err(UserError::BadRequest(format!(
120 "invalid email (length 1..={EMAIL_MAX_LEN})"
121 )));
178 let _timer = metrics::Timer::start(metrics::FEATURE_EMAILS_REQUEST_CHANGE);
179 let result: UserResult<EmailChangeTicketView> = async {
180 let new_email = new_email.trim().to_lowercase();
181 if new_email.is_empty() || new_email.len() > EMAIL_MAX_LEN || !new_email.contains('@') {
182 return Err(UserError::BadRequest(format!(
183 "invalid email (length 1..={EMAIL_MAX_LEN})"
184 )));
185 }
186 let writer = svc.db.writer();
187 let now = time::OffsetDateTime::now_utc();
188 let active: db::user::user_email_change_tickets::ActiveModel =
189 db::user::user_email_change_tickets::ActiveModel::builder()
190 .set_id(Uuid::new_v4())
191 .set_user_id(caller)
192 .set_token_hash(format!("placeholder:{}", Uuid::new_v4()))
193 .set_new_email(new_email)
194 .set_original_email(None)
195 .set_status(EmailChangeTicketStatus::Pending)
196 .set_created_at(now)
197 .set_expires_at(now + time::Duration::hours(24))
198 .set_used_at(None)
199 .into();
200 let saved = active.insert(&writer).await?;
201 Ok(EmailChangeTicketView::from(saved))
122202 }
123 let writer = svc.db.writer();
124 let now = time::OffsetDateTime::now_utc();
125 let active: db::user::user_email_change_tickets::ActiveModel =
126 db::user::user_email_change_tickets::ActiveModel::builder()
127 .set_id(Uuid::new_v4())
128 .set_user_id(caller)
129 .set_token_hash(format!("placeholder:{}", Uuid::new_v4()))
130 .set_new_email(new_email)
131 .set_original_email(None)
132 .set_status(EmailChangeTicketStatus::Pending)
133 .set_created_at(now)
134 .set_expires_at(now + time::Duration::hours(24))
135 .set_used_at(None)
136 .into();
137 let saved = active.insert(&writer).await?;
138 Ok(EmailChangeTicketView::from(saved))
203 .await;
204 metrics::record_outcome(metrics::FEATURE_EMAILS_REQUEST_CHANGE, &result);
205 if result.is_ok() {
206 record_email_operation(EmailOp::RequestChange, EmailChangeOutcome::Success);
207 }
208 result
139209}
140210
141211pub async fn confirm_change(
@@ -143,46 +213,81 @@
143213 caller: Uuid,
144214 cmd: ConfirmEmailChangeCmd,
145215) -> UserResult<EmailChangeTicketView> {
146 let writer = svc.db.writer();
147 let row = db::user::user_email_change_tickets::Entity::find_by_id(cmd.ticket_id)
148 .one(&writer)
149 .await?
150 .ok_or_else(|| UserError::NotFound(format!("ticket {}", cmd.ticket_id)))?;
151 if row.user_id != caller {
152 return Err(UserError::Forbidden("not your ticket".into()));
216 let _timer = metrics::Timer::start(metrics::FEATURE_EMAILS_CONFIRM_CHANGE);
217 let result: UserResult<EmailChangeTicketView> = async {
218 let writer = svc.db.writer();
219 let row = db::user::user_email_change_tickets::Entity::find_by_id(cmd.ticket_id)
220 .one(&writer)
221 .await?
222 .ok_or_else(|| UserError::NotFound(format!("ticket {}", cmd.ticket_id)))?;
223 if row.user_id != caller {
224 return Err(UserError::Forbidden("not your ticket".into()));
225 }
226 let target = EmailChangeTicketStatus::from(row.status);
227 if !matches!(
228 target,
229 EmailChangeTicketStatus::Pending | EmailChangeTicketStatus::Verified
230 ) {
231 return Err(UserError::BadRequest("ticket not actionable".into()));
232 }
233 let mut am: db::user::user_email_change_tickets::ActiveModel = row.into();
234 let next = match target {
235 EmailChangeTicketStatus::Pending => EmailChangeTicketStatus::Verified,
236 EmailChangeTicketStatus::Verified => EmailChangeTicketStatus::Completed,
237 _ => unreachable!(),
238 };
239 am.status = Set(next.into());
240 if matches!(next, EmailChangeTicketStatus::Completed) {
241 am.used_at = Set(Some(time::OffsetDateTime::now_utc()));
242 }
243 let saved = am.update(&writer).await?;
244 Ok(EmailChangeTicketView::from(saved))
153245 }
154 let target = EmailChangeTicketStatus::from(row.status);
155 if !matches!(
156 target,
157 EmailChangeTicketStatus::Pending | EmailChangeTicketStatus::Verified
158 ) {
159 return Err(UserError::BadRequest("ticket not actionable".into()));
246 .await;
247 metrics::record_outcome(metrics::FEATURE_EMAILS_CONFIRM_CHANGE, &result);
248 match &result {
249 Ok(_) => record_email_operation(EmailOp::ConfirmChange, EmailChangeOutcome::Success),
250 Err(UserError::BadRequest(_)) => {
251 record_email_operation(EmailOp::ConfirmChange, EmailChangeOutcome::Mismatch);
252 }
253 Err(UserError::Forbidden(_)) => {
254 record_email_operation(EmailOp::ConfirmChange, EmailChangeOutcome::Forbidden);
255 }
256 Err(UserError::NotFound(_)) => {
257 record_email_operation(EmailOp::ConfirmChange, EmailChangeOutcome::TicketNotFound);
258 }
259 Err(_) => record_email_operation(EmailOp::ConfirmChange, EmailChangeOutcome::Other),
160260 }
161 let mut am: db::user::user_email_change_tickets::ActiveModel = row.into();
162 let next = match target {
163 EmailChangeTicketStatus::Pending => EmailChangeTicketStatus::Verified,
164 EmailChangeTicketStatus::Verified => EmailChangeTicketStatus::Completed,
165 _ => unreachable!(),
166 };
167 am.status = Set(next.into());
168 if matches!(next, EmailChangeTicketStatus::Completed) {
169 am.used_at = Set(Some(time::OffsetDateTime::now_utc()));
170 }
171 let saved = am.update(&writer).await?;
172 Ok(EmailChangeTicketView::from(saved))
261 result
173262}
174263
175264pub async fn cancel_change(svc: &UserService, caller: Uuid, ticket_id: Uuid) -> UserResult<()> {
176 let writer = svc.db.writer();
177 let row = db::user::user_email_change_tickets::Entity::find_by_id(ticket_id)
178 .one(&writer)
179 .await?
180 .ok_or_else(|| UserError::NotFound(format!("ticket {ticket_id}")))?;
181 if row.user_id != caller {
182 return Err(UserError::Forbidden("not your ticket".into()));
265 let _timer = metrics::Timer::start(metrics::FEATURE_EMAILS_CANCEL_CHANGE);
266 let result: UserResult<()> = async {
267 let writer = svc.db.writer();
268 let row = db::user::user_email_change_tickets::Entity::find_by_id(ticket_id)
269 .one(&writer)
270 .await?
271 .ok_or_else(|| UserError::NotFound(format!("ticket {ticket_id}")))?;
272 if row.user_id != caller {
273 return Err(UserError::Forbidden("not your ticket".into()));
274 }
275 let mut am: db::user::user_email_change_tickets::ActiveModel = row.into();
276 am.status = Set(EmailChangeTicketStatus::Cancelled.into());
277 am.update(&writer).await?;
278 Ok(())
279 }
280 .await;
281 metrics::record_outcome(metrics::FEATURE_EMAILS_CANCEL_CHANGE, &result);
282 match &result {
283 Ok(_) => record_email_operation(EmailOp::CancelChange, EmailChangeOutcome::Success),
284 Err(UserError::Forbidden(_)) => {
285 record_email_operation(EmailOp::CancelChange, EmailChangeOutcome::Forbidden);
286 }
287 Err(UserError::NotFound(_)) => {
288 record_email_operation(EmailOp::CancelChange, EmailChangeOutcome::TicketNotFound);
289 }
290 Err(_) => record_email_operation(EmailOp::CancelChange, EmailChangeOutcome::Other),
183291 }
184 let mut am: db::user::user_email_change_tickets::ActiveModel = row.into();
185 am.status = Set(EmailChangeTicketStatus::Cancelled.into());
186 am.update(&writer).await?;
187 Ok(())
188}
No newline at end of file
292 result
293}
Modified lib/user/features/notifications/usecase.rs View lib/user/features/notifications/usecase.rs
@@ -6,6 +6,10 @@
66
77use crate::error::{UserError, UserResult};
88use crate::features::UserService;
9use crate::metrics::{
10 self, NotificationKind, NotificationOp, ReportOp, record_notification_operation,
11 record_report_operation,
12};
913
1014use super::{
1115 ListNotificationsQuery, MarkReadCmd, NotificationView, RecentMentionView, ReportStatus, ReportView,
@@ -20,24 +24,37 @@
2024 caller: Uuid,
2125 q: ListNotificationsQuery,
2226) -> UserResult<Vec<NotificationView>> {
23 let db = svc.db.reader();
24 let limit = q.limit.min(svc.config.max_profile_pagination).max(1) as u64;
25 let mut sel = db::user::user_notifications::Entity::find()
26 .filter(db::user::user_notifications::COLUMN.user_id.eq(caller))
27 .order_by_desc(db::user::user_notifications::COLUMN.created_at)
28 .limit(Some(limit));
29 if let Some(cursor) = q.cursor {
30 sel = sel.filter(db::user::user_notifications::COLUMN.id.lt(cursor));
31 }
32 if let Some(unread) = q.unread_only
33 && unread {
34 sel = sel.filter(db::user::user_notifications::COLUMN.read_at.is_null());
27 let _timer = metrics::Timer::start(metrics::FEATURE_NOTIFICATIONS_LIST);
28 let result: UserResult<Vec<NotificationView>> = async {
29 let db = svc.db.reader();
30 let limit = q.limit.min(svc.config.max_profile_pagination).max(1) as u64;
31 let mut sel = db::user::user_notifications::Entity::find()
32 .filter(db::user::user_notifications::COLUMN.user_id.eq(caller))
33 .order_by_desc(db::user::user_notifications::COLUMN.created_at)
34 .limit(Some(limit));
35 if let Some(cursor) = q.cursor {
36 sel = sel.filter(db::user::user_notifications::COLUMN.id.lt(cursor));
3537 }
36 if let Some(_nt) = q.notification_type {
37 // Caller-side filter could be added; mapping to db enum omitted here.
38 if let Some(unread) = q.unread_only
39 && unread {
40 sel = sel.filter(db::user::user_notifications::COLUMN.read_at.is_null());
41 }
42 if let Some(_nt) = q.notification_type {
43 // Caller-side filter could be added; mapping to db enum omitted here.
44 }
45 let rows = sel.all(&db).await?;
46 Ok(rows.into_iter().map(NotificationView::from).collect())
47 }
48 .await;
49 metrics::record_outcome(metrics::FEATURE_NOTIFICATIONS_LIST, &result);
50 if result.is_ok() {
51 record_notification_operation(
52 NotificationKind::Notification,
53 NotificationOp::List,
54 metrics::OUTCOME_SUCCESS,
55 );
3856 }
39 let rows = sel.all(&db).await?;
40 Ok(rows.into_iter().map(NotificationView::from).collect())
57 result
4158}
4259
4360pub async fn mark_read(
@@ -45,55 +62,94 @@
4562 caller: Uuid,
4663 cmd: MarkReadCmd,
4764) -> UserResult<u64> {
48 if cmd.notification_ids.is_empty() {
49 return Ok(0);
65 let _timer = metrics::Timer::start(metrics::FEATURE_NOTIFICATIONS_MARK_READ);
66 let result: UserResult<u64> = async {
67 if cmd.notification_ids.is_empty() {
68 return Ok(0);
69 }
70 let writer = svc.db.writer();
71 let now = time::OffsetDateTime::now_utc();
72 let mut total = 0u64;
73 for id in cmd.notification_ids {
74 let row = db::user::user_notifications::Entity::find_by_id(id)
75 .one(&writer)
76 .await?;
77 if let Some(m) = row {
78 if m.user_id != caller || m.read_at.is_some() {
79 continue;
80 }
81 let mut am: db::user::user_notifications::ActiveModel = m.into();
82 am.read_at = Set(Some(now));
83 am.update(&writer).await?;
84 total += 1;
85 }
86 }
87 Ok(total)
5088 }
51 let writer = svc.db.writer();
52 let now = time::OffsetDateTime::now_utc();
53 let mut total = 0u64;
54 for id in cmd.notification_ids {
55 let row = db::user::user_notifications::Entity::find_by_id(id)
56 .one(&writer)
89 .await;
90 metrics::record_outcome(metrics::FEATURE_NOTIFICATIONS_MARK_READ, &result);
91 if result.is_ok() {
92 record_notification_operation(
93 NotificationKind::Notification,
94 NotificationOp::MarkRead,
95 metrics::OUTCOME_SUCCESS,
96 );
97 }
98 result
99}
100
101pub async fn mark_all_read(svc: &UserService, caller: Uuid) -> UserResult<u64> {
102 let _timer = metrics::Timer::start(metrics::FEATURE_NOTIFICATIONS_MARK_ALL_READ);
103 let result: UserResult<u64> = async {
104 let writer = svc.db.writer();
105 let rows = db::user::user_notifications::Entity::find()
106 .filter(db::user::user_notifications::COLUMN.user_id.eq(caller))
107 .filter(db::user::user_notifications::COLUMN.read_at.is_null())
108 .all(&writer)
57109 .await?;
58 if let Some(m) = row {
59 if m.user_id != caller || m.read_at.is_some() {
60 continue;
61 }
110 let now = time::OffsetDateTime::now_utc();
111 let mut total = 0u64;
112 for m in rows {
62113 let mut am: db::user::user_notifications::ActiveModel = m.into();
63114 am.read_at = Set(Some(now));
64115 am.update(&writer).await?;
65116 total += 1;
66117 }
118 Ok(total)
67119 }
68 Ok(total)
69}
70
71pub async fn mark_all_read(svc: &UserService, caller: Uuid) -> UserResult<u64> {
72 let writer = svc.db.writer();
73 let rows = db::user::user_notifications::Entity::find()
74 .filter(db::user::user_notifications::COLUMN.user_id.eq(caller))
75 .filter(db::user::user_notifications::COLUMN.read_at.is_null())
76 .all(&writer)
77 .await?;
78 let now = time::OffsetDateTime::now_utc();
79 let mut total = 0u64;
80 for m in rows {
81 let mut am: db::user::user_notifications::ActiveModel = m.into();
82 am.read_at = Set(Some(now));
83 am.update(&writer).await?;
84 total += 1;
120 .await;
121 metrics::record_outcome(metrics::FEATURE_NOTIFICATIONS_MARK_ALL_READ, &result);
122 if result.is_ok() {
123 record_notification_operation(
124 NotificationKind::Notification,
125 NotificationOp::MarkAllRead,
126 metrics::OUTCOME_SUCCESS,
127 );
85128 }
86 Ok(total)
129 result
87130}
88131
89132pub async fn unread_count(svc: &UserService, caller: Uuid) -> UserResult<UnreadCountResp> {
90 let db = svc.db.reader();
91 let count = db::user::user_notifications::Entity::find()
92 .filter(db::user::user_notifications::COLUMN.user_id.eq(caller))
93 .filter(db::user::user_notifications::COLUMN.read_at.is_null())
94 .count(&db)
95 .await?;
96 Ok(UnreadCountResp { count: count as i64 })
133 let _timer = metrics::Timer::start(metrics::FEATURE_NOTIFICATIONS_UNREAD_COUNT);
134 let result: UserResult<UnreadCountResp> = async {
135 let db = svc.db.reader();
136 let count = db::user::user_notifications::Entity::find()
137 .filter(db::user::user_notifications::COLUMN.user_id.eq(caller))
138 .filter(db::user::user_notifications::COLUMN.read_at.is_null())
139 .count(&db)
140 .await?;
141 Ok(UnreadCountResp { count: count as i64 })
142 }
143 .await;
144 metrics::record_outcome(metrics::FEATURE_NOTIFICATIONS_UNREAD_COUNT, &result);
145 if result.is_ok() {
146 record_notification_operation(
147 NotificationKind::Notification,
148 NotificationOp::UnreadCount,
149 metrics::OUTCOME_SUCCESS,
150 );
151 }
152 result
97153}
98154
99155// === recent mentions ===
@@ -103,15 +159,28 @@
103159 caller: Uuid,
104160 limit: usize,
105161) -> UserResult<Vec<RecentMentionView>> {
106 let limit = limit.min(svc.config.max_profile_pagination).max(1) as u64;
107 let db = svc.db.reader();
108 let rows = db::user::user_recent_mentions::Entity::find()
109 .filter(db::user::user_recent_mentions::COLUMN.user_id.eq(caller))
110 .order_by_desc(db::user::user_recent_mentions::COLUMN.created_at)
111 .limit(Some(limit))
112 .all(&db)
113 .await?;
114 Ok(rows.into_iter().map(RecentMentionView::from).collect())
162 let _timer = metrics::Timer::start(metrics::FEATURE_NOTIFICATIONS_RECENT_MENTIONS);
163 let result: UserResult<Vec<RecentMentionView>> = async {
164 let limit = limit.min(svc.config.max_profile_pagination).max(1) as u64;
165 let db = svc.db.reader();
166 let rows = db::user::user_recent_mentions::Entity::find()
167 .filter(db::user::user_recent_mentions::COLUMN.user_id.eq(caller))
168 .order_by_desc(db::user::user_recent_mentions::COLUMN.created_at)
169 .limit(Some(limit))
170 .all(&db)
171 .await?;
172 Ok(rows.into_iter().map(RecentMentionView::from).collect())
173 }
174 .await;
175 metrics::record_outcome(metrics::FEATURE_NOTIFICATIONS_RECENT_MENTIONS, &result);
176 if result.is_ok() {
177 record_notification_operation(
178 NotificationKind::Notification,
179 NotificationOp::RecentMentions,
180 metrics::OUTCOME_SUCCESS,
181 );
182 }
183 result
115184}
116185
117186// === reports ===
@@ -121,44 +190,70 @@
121190 caller: Uuid,
122191 cmd: SubmitReportCmd,
123192) -> UserResult<ReportView> {
124 if cmd.reason.is_empty() {
125 return Err(UserError::BadRequest("reason required".into()));
193 let _timer = metrics::Timer::start(metrics::FEATURE_REPORTS_SUBMIT);
194 let result: UserResult<ReportView> = async {
195 if cmd.reason.is_empty() {
196 return Err(UserError::BadRequest("reason required".into()));
197 }
198 let writer = svc.db.writer();
199 let now = time::OffsetDateTime::now_utc();
200 let active: db::user::user_reports::ActiveModel =
201 db::user::user_reports::ActiveModel::builder()
202 .set_id(Uuid::new_v4())
203 .set_reporter_uid(Some(caller))
204 .set_reported_uid(cmd.reported_uid)
205 .set_reason(cmd.reason)
206 .set_evidence(cmd.evidence)
207 .set_status(ReportStatus::Open)
208 .set_resolver_uid(None)
209 .set_resolution_note(None)
210 .set_created_at(now)
211 .set_updated_at(now)
212 .into();
213 let saved = active.insert(&writer).await?;
214 Ok(ReportView::from(saved))
126215 }
127 let writer = svc.db.writer();
128 let now = time::OffsetDateTime::now_utc();
129 let active: db::user::user_reports::ActiveModel =
130 db::user::user_reports::ActiveModel::builder()
131 .set_id(Uuid::new_v4())
132 .set_reporter_uid(Some(caller))
133 .set_reported_uid(cmd.reported_uid)
134 .set_reason(cmd.reason)
135 .set_evidence(cmd.evidence)
136 .set_status(ReportStatus::Open)
137 .set_resolver_uid(None)
138 .set_resolution_note(None)
139 .set_created_at(now)
140 .set_updated_at(now)
141 .into();
142 let saved = active.insert(&writer).await?;
143 Ok(ReportView::from(saved))
216 .await;
217 metrics::record_outcome(metrics::FEATURE_REPORTS_SUBMIT, &result);
218 match &result {
219 Ok(_) => record_report_operation(ReportOp::Submit, metrics::OUTCOME_SUCCESS),
220 Err(UserError::BadRequest(_)) => record_report_operation(ReportOp::Submit, "bad_request"),
221 Err(_) => {}
222 }
223 result
144224}
145225
146226pub async fn list_my_reports(
147227 svc: &UserService,
148228 caller: Uuid,
149229) -> UserResult<Vec<ReportView>> {
150 let db = svc.db.reader();
151 let rows = db::user::user_reports::Entity::find()
152 .filter(db::user::user_reports::COLUMN.reporter_uid.eq(caller))
153 .all(&db)
154 .await?;
155 Ok(rows.into_iter().map(ReportView::from).collect())
230 let _timer = metrics::Timer::start(metrics::FEATURE_REPORTS_LIST_MINE);
231 let result: UserResult<Vec<ReportView>> = async {
232 let db = svc.db.reader();
233 let rows = db::user::user_reports::Entity::find()
234 .filter(db::user::user_reports::COLUMN.reporter_uid.eq(caller))
235 .all(&db)
236 .await?;
237 Ok(rows.into_iter().map(ReportView::from).collect())
238 }
239 .await;
240 metrics::record_outcome(metrics::FEATURE_REPORTS_LIST_MINE, &result);
241 if result.is_ok() {
242 record_report_operation(ReportOp::ListMine, metrics::OUTCOME_SUCCESS);
243 }
244 result
156245}
157246
158247pub async fn list_all_reports(svc: &UserService) -> UserResult<Vec<ReportView>> {
159 let db = svc.db.reader();
160 let rows = db::user::user_reports::Entity::find().all(&db).await?;
161 Ok(rows.into_iter().map(ReportView::from).collect())
248 let _timer = metrics::Timer::start("reports_list_all");
249 let result: UserResult<Vec<ReportView>> = async {
250 let db = svc.db.reader();
251 let rows = db::user::user_reports::Entity::find().all(&db).await?;
252 Ok(rows.into_iter().map(ReportView::from).collect())
253 }
254 .await;
255 metrics::record_outcome("reports_list_all", &result);
256 result
162257}
163258
164259pub async fn update_report(
@@ -166,19 +261,25 @@
166261 report_id: Uuid,
167262 cmd: UpdateReportCmd,
168263) -> UserResult<ReportView> {
169 let writer = svc.db.writer();
170 let row = db::user::user_reports::Entity::find_by_id(report_id)
171 .one(&writer)
172 .await?
173 .ok_or_else(|| UserError::NotFound(format!("report {report_id}")))?;
174 let mut am: db::user::user_reports::ActiveModel = row.into();
175 am.status = Set(cmd.status.into());
176 if cmd.resolution_note.is_some() {
177 am.resolution_note = Set(cmd.resolution_note);
264 let _timer = metrics::Timer::start("reports_update");
265 let result: UserResult<ReportView> = async {
266 let writer = svc.db.writer();
267 let row = db::user::user_reports::Entity::find_by_id(report_id)
268 .one(&writer)
269 .await?
270 .ok_or_else(|| UserError::NotFound(format!("report {report_id}")))?;
271 let mut am: db::user::user_reports::ActiveModel = row.into();
272 am.status = Set(cmd.status.into());
273 if cmd.resolution_note.is_some() {
274 am.resolution_note = Set(cmd.resolution_note);
275 }
276 am.updated_at = Set(time::OffsetDateTime::now_utc());
277 let saved = am.update(&writer).await?;
278 Ok(ReportView::from(saved))
178279 }
179 am.updated_at = Set(time::OffsetDateTime::now_utc());
180 let saved = am.update(&writer).await?;
181 Ok(ReportView::from(saved))
280 .await;
281 metrics::record_outcome("reports_update", &result);
282 result
182283}
183284
184285// === saved messages ===
@@ -188,40 +289,66 @@
188289 caller: Uuid,
189290 cmd: SaveMessageCmd,
190291) -> UserResult<SavedMessageView> {
191 let writer = svc.db.writer();
192 let existing = db::user::user_saved_messages::Entity::find()
193 .filter(db::user::user_saved_messages::COLUMN.user_id.eq(caller))
194 .filter(db::user::user_saved_messages::COLUMN.channel_id.eq(cmd.channel_id))
195 .filter(db::user::user_saved_messages::COLUMN.message_id.eq(cmd.message_id))
196 .one(&writer)
197 .await?;
198 if let Some(row) = existing {
199 return Ok(SavedMessageView::from(row));
292 let _timer = metrics::Timer::start(metrics::FEATURE_SAVED_MESSAGES_SAVE);
293 let result: UserResult<SavedMessageView> = async {
294 let writer = svc.db.writer();
295 let existing = db::user::user_saved_messages::Entity::find()
296 .filter(db::user::user_saved_messages::COLUMN.user_id.eq(caller))
297 .filter(db::user::user_saved_messages::COLUMN.channel_id.eq(cmd.channel_id))
298 .filter(db::user::user_saved_messages::COLUMN.message_id.eq(cmd.message_id))
299 .one(&writer)
300 .await?;
301 if let Some(row) = existing {
302 return Ok(SavedMessageView::from(row));
303 }
304 let now = time::OffsetDateTime::now_utc();
305 let active: db::user::user_saved_messages::ActiveModel =
306 db::user::user_saved_messages::ActiveModel::builder()
307 .set_id(Uuid::new_v4())
308 .set_user_id(caller)
309 .set_channel_id(cmd.channel_id)
310 .set_message_id(cmd.message_id)
311 .set_saved_at(now)
312 .into();
313 let saved = active.insert(&writer).await?;
314 Ok(SavedMessageView::from(saved))
315 }
316 .await;
317 metrics::record_outcome(metrics::FEATURE_SAVED_MESSAGES_SAVE, &result);
318 if result.is_ok() {
319 record_notification_operation(
320 NotificationKind::SavedMessage,
321 NotificationOp::SaveMessage,
322 metrics::OUTCOME_SUCCESS,
323 );
200324 }
201 let now = time::OffsetDateTime::now_utc();
202 let active: db::user::user_saved_messages::ActiveModel =
203 db::user::user_saved_messages::ActiveModel::builder()
204 .set_id(Uuid::new_v4())
205 .set_user_id(caller)
206 .set_channel_id(cmd.channel_id)
207 .set_message_id(cmd.message_id)
208 .set_saved_at(now)
209 .into();
210 let saved = active.insert(&writer).await?;
211 Ok(SavedMessageView::from(saved))
325 result
212326}
213327
214328pub async fn list_saved(
215329 svc: &UserService,
216330 caller: Uuid,
217331) -> UserResult<Vec<SavedMessageView>> {
218 let db = svc.db.reader();
219 let rows = db::user::user_saved_messages::Entity::find()
220 .filter(db::user::user_saved_messages::COLUMN.user_id.eq(caller))
221 .order_by_desc(db::user::user_saved_messages::COLUMN.saved_at)
222 .all(&db)
223 .await?;
224 Ok(rows.into_iter().map(SavedMessageView::from).collect())
332 let _timer = metrics::Timer::start(metrics::FEATURE_SAVED_MESSAGES_LIST);
333 let result: UserResult<Vec<SavedMessageView>> = async {
334 let db = svc.db.reader();
335 let rows = db::user::user_saved_messages::Entity::find()
336 .filter(db::user::user_saved_messages::COLUMN.user_id.eq(caller))
337 .order_by_desc(db::user::user_saved_messages::COLUMN.saved_at)
338 .all(&db)
339 .await?;
340 Ok(rows.into_iter().map(SavedMessageView::from).collect())
341 }
342 .await;
343 metrics::record_outcome(metrics::FEATURE_SAVED_MESSAGES_LIST, &result);
344 if result.is_ok() {
345 record_notification_operation(
346 NotificationKind::SavedMessage,
347 NotificationOp::ListSaved,
348 metrics::OUTCOME_SUCCESS,
349 );
350 }
351 result
225352}
226353
227354pub async fn unsave_message(
@@ -229,13 +356,26 @@
229356 caller: Uuid,
230357 message_id: Uuid,
231358) -> UserResult<()> {
232 let writer = svc.db.writer();
233 db::user::user_saved_messages::Entity::delete_many()
234 .filter(db::user::user_saved_messages::COLUMN.user_id.eq(caller))
235 .filter(db::user::user_saved_messages::COLUMN.message_id.eq(message_id))
236 .exec(&writer)
237 .await?;
238 Ok(())
359 let _timer = metrics::Timer::start(metrics::FEATURE_SAVED_MESSAGES_UNSAVE);
360 let result: UserResult<()> = async {
361 let writer = svc.db.writer();
362 db::user::user_saved_messages::Entity::delete_many()
363 .filter(db::user::user_saved_messages::COLUMN.user_id.eq(caller))
364 .filter(db::user::user_saved_messages::COLUMN.message_id.eq(message_id))
365 .exec(&writer)
366 .await?;
367 Ok(())
368 }
369 .await;
370 metrics::record_outcome(metrics::FEATURE_SAVED_MESSAGES_UNSAVE, &result);
371 if result.is_ok() {
372 record_notification_operation(
373 NotificationKind::SavedMessage,
374 NotificationOp::UnsaveMessage,
375 metrics::OUTCOME_SUCCESS,
376 );
377 }
378 result
239379}
240380
241381// === scheduled messages ===
@@ -244,25 +384,62 @@
244384 svc: &UserService,
245385 caller: Uuid,
246386) -> UserResult<Vec<ScheduledMessageView>> {
247 let db = svc.db.reader();
248 let rows = db::user::user_scheduled_messages::Entity::find()
249 .filter(db::user::user_scheduled_messages::COLUMN.user_id.eq(caller))
250 .all(&db)
251 .await?;
252 Ok(rows.into_iter().map(ScheduledMessageView::from).collect())
387 let _timer = metrics::Timer::start(metrics::FEATURE_SCHEDULED_MESSAGES_LIST);
388 let result: UserResult<Vec<ScheduledMessageView>> = async {
389 let db = svc.db.reader();
390 let rows = db::user::user_scheduled_messages::Entity::find()
391 .filter(db::user::user_scheduled_messages::COLUMN.user_id.eq(caller))
392 .all(&db)
393 .await?;
394 Ok(rows.into_iter().map(ScheduledMessageView::from).collect())
395 }
396 .await;
397 metrics::record_outcome(metrics::FEATURE_SCHEDULED_MESSAGES_LIST, &result);
398 if result.is_ok() {
399 record_notification_operation(
400 NotificationKind::ScheduledMessage,
401 NotificationOp::ListScheduled,
402 metrics::OUTCOME_SUCCESS,
403 );
404 }
405 result
253406}
254407
255408pub async fn cancel_scheduled(svc: &UserService, caller: Uuid, id: Uuid) -> UserResult<()> {
256 let writer = svc.db.writer();
257 let row = db::user::user_scheduled_messages::Entity::find_by_id(id)
258 .one(&writer)
259 .await?
260 .ok_or_else(|| UserError::NotFound(format!("scheduled {id}")))?;
261 if row.user_id != caller {
262 return Err(UserError::Forbidden("not your scheduled message".into()));
409 let _timer = metrics::Timer::start(metrics::FEATURE_SCHEDULED_MESSAGES_CANCEL);
410 let result: UserResult<()> = async {
411 let writer = svc.db.writer();
412 let row = db::user::user_scheduled_messages::Entity::find_by_id(id)
413 .one(&writer)
414 .await?
415 .ok_or_else(|| UserError::NotFound(format!("scheduled {id}")))?;
416 if row.user_id != caller {
417 return Err(UserError::Forbidden("not your scheduled message".into()));
418 }
419 let mut am: db::user::user_scheduled_messages::ActiveModel = row.into();
420 am.status = Set(ScheduledMessageStatus::Cancelled.into());
421 am.update(&writer).await?;
422 Ok(())
423 }
424 .await;
425 metrics::record_outcome(metrics::FEATURE_SCHEDULED_MESSAGES_CANCEL, &result);
426 match &result {
427 Ok(_) => record_notification_operation(
428 NotificationKind::ScheduledMessage,
429 NotificationOp::CancelScheduled,
430 metrics::OUTCOME_SUCCESS,
431 ),
432 Err(UserError::Forbidden(_)) => record_notification_operation(
433 NotificationKind::ScheduledMessage,
434 NotificationOp::CancelScheduled,
435 "forbidden",
436 ),
437 Err(UserError::NotFound(_)) => record_notification_operation(
438 NotificationKind::ScheduledMessage,
439 NotificationOp::CancelScheduled,
440 "not_found",
441 ),
442 Err(_) => {}
263443 }
264 let mut am: db::user::user_scheduled_messages::ActiveModel = row.into();
265 am.status = Set(ScheduledMessageStatus::Cancelled.into());
266 am.update(&writer).await?;
267 Ok(())
268}
No newline at end of file
444 result
445}
Modified lib/user/features/preferences/usecase.rs View lib/user/features/preferences/usecase.rs
@@ -4,6 +4,9 @@
44
55use crate::error::{UserError, UserResult};
66use crate::features::UserService;
7use crate::metrics::{
8 self, PreferencesOp, record_preferences_operation,
9};
710
811use super::{UpdatePreferencesCmd, UserPreferences};
912
@@ -11,23 +14,32 @@
1114 svc: &UserService,
1215 user_id: Uuid,
1316) -> UserResult<UserPreferences> {
14 let db = svc.db.reader();
15 let pref = db::user::user_preferences::Entity::find_by_id(user_id)
16 .one(&db)
17 .await?;
18 if let Some(m) = pref {
19 return Ok(UserPreferences::from(m));
17 let _timer = metrics::Timer::start(metrics::FEATURE_PREFERENCES_GET);
18 let result: UserResult<UserPreferences> = async {
19 let db = svc.db.reader();
20 let pref = db::user::user_preferences::Entity::find_by_id(user_id)
21 .one(&db)
22 .await?;
23 if let Some(m) = pref {
24 return Ok(UserPreferences::from(m));
25 }
26 let writer = svc.db.writer();
27 let active: db::user::user_preferences::ActiveModel =
28 db::user::user_preferences::ActiveModel::builder()
29 .set_user_id(user_id)
30 .set_theme_mode(String::from("system"))
31 .set_created_at(OffsetDateTime::now_utc())
32 .set_updated_at(OffsetDateTime::now_utc())
33 .into();
34 let inserted = active.insert(&writer).await?;
35 Ok(UserPreferences::from(inserted))
2036 }
21 let writer = svc.db.writer();
22 let active: db::user::user_preferences::ActiveModel =
23 db::user::user_preferences::ActiveModel::builder()
24 .set_user_id(user_id)
25 .set_theme_mode(String::from("system"))
26 .set_created_at(OffsetDateTime::now_utc())
27 .set_updated_at(OffsetDateTime::now_utc())
28 .into();
29 let inserted = active.insert(&writer).await?;
30 Ok(UserPreferences::from(inserted))
37 .await;
38 metrics::record_outcome(metrics::FEATURE_PREFERENCES_GET, &result);
39 if result.is_ok() {
40 record_preferences_operation(PreferencesOp::Get, metrics::OUTCOME_SUCCESS);
41 }
42 result
3143}
3244
3345pub async fn update(
@@ -35,83 +47,96 @@
3547 user_id: Uuid,
3648 cmd: UpdatePreferencesCmd,
3749) -> UserResult<UserPreferences> {
38 if let Some(ref mode) = cmd.theme_mode
39 && (mode.is_empty() || mode.len() > 32) {
40 return Err(UserError::BadRequest(
41 "theme_mode length must be 1..=32".into(),
42 ));
43 }
50 let _timer = metrics::Timer::start(metrics::FEATURE_PREFERENCES_UPDATE);
51 let result: UserResult<UserPreferences> = async {
52 if let Some(ref mode) = cmd.theme_mode
53 && (mode.is_empty() || mode.len() > 32) {
54 return Err(UserError::BadRequest(
55 "theme_mode length must be 1..=32".into(),
56 ));
57 }
4458
45 let writer = svc.db.writer();
46 let existing = db::user::user_preferences::Entity::find_by_id(user_id)
47 .one(&writer)
48 .await?;
49 let now = OffsetDateTime::now_utc();
50 let existing_present = existing.is_some();
51 let mut active: db::user::user_preferences::ActiveModel = match existing {
52 Some(m) => m.into(),
53 None => db::user::user_preferences::ActiveModel::builder()
54 .set_user_id(user_id)
55 .set_theme_mode(String::from("system"))
56 .set_created_at(now)
57 .set_updated_at(now)
58 .into(),
59 };
59 let writer = svc.db.writer();
60 let existing = db::user::user_preferences::Entity::find_by_id(user_id)
61 .one(&writer)
62 .await?;
63 let now = OffsetDateTime::now_utc();
64 let existing_present = existing.is_some();
65 let mut active: db::user::user_preferences::ActiveModel = match existing {
66 Some(m) => m.into(),
67 None => db::user::user_preferences::ActiveModel::builder()
68 .set_user_id(user_id)
69 .set_theme_mode(String::from("system"))
70 .set_created_at(now)
71 .set_updated_at(now)
72 .into(),
73 };
6074
61 if let Some(v) = cmd.theme_mode {
62 active.theme_mode = Set(v);
63 }
64 if let Some(v) = cmd.theme_token {
65 active.theme_token = Set(v);
66 }
67 if let Some(v) = cmd.theme_version {
68 active.theme_version = Set(v);
69 }
70 if let Some(v) = cmd.diff_view_style {
71 active.diff_view_style = Set(v);
72 }
73 if let Some(v) = cmd.in_app_flags {
74 active.in_app_flags = Set(v);
75 }
76 if let Some(v) = cmd.email_flags {
77 active.email_flags = Set(v);
78 }
79 if let Some(v) = cmd.push_flags {
80 active.push_flags = Set(v);
81 }
82 if let Some(v) = cmd.email_digest {
83 active.email_digest = Set(v);
75 if let Some(v) = cmd.theme_mode {
76 active.theme_mode = Set(v);
77 }
78 if let Some(v) = cmd.theme_token {
79 active.theme_token = Set(v);
80 }
81 if let Some(v) = cmd.theme_version {
82 active.theme_version = Set(v);
83 }
84 if let Some(v) = cmd.diff_view_style {
85 active.diff_view_style = Set(v);
86 }
87 if let Some(v) = cmd.in_app_flags {
88 active.in_app_flags = Set(v);
89 }
90 if let Some(v) = cmd.email_flags {
91 active.email_flags = Set(v);
92 }
93 if let Some(v) = cmd.push_flags {
94 active.push_flags = Set(v);
95 }
96 if let Some(v) = cmd.email_digest {
97 active.email_digest = Set(v);
98 }
99 if let Some(v) = cmd.mute_until {
100 if let Some(ts) = v {
101 let parsed = OffsetDateTime::parse(
102 &ts,
103 &time::format_description::well_known::Rfc3339,
104 )
105 .map_err(|e| UserError::BadRequest(format!("invalid mute_until: {e}")))?;
106 active.mute_until = Set(Some(parsed));
107 } else {
108 active.mute_until = Set(None);
109 }
110 }
111 if let Some(v) = cmd.allow_dms_from {
112 active.allow_dms_from = Set(v.into());
113 }
114 if let Some(v) = cmd.allow_mentions_from {
115 active.allow_mentions_from = Set(v.into());
116 }
117 if let Some(v) = cmd.show_online_status {
118 active.show_online_status = Set(v);
119 }
120 active.updated_at = Set(now);
121
122 let saved = if existing_present {
123 active.update(&writer).await?
124 } else {
125 active.insert(&writer).await?
126 };
127 Ok(UserPreferences::from(saved))
84128 }
85 if let Some(v) = cmd.mute_until {
86 if let Some(ts) = v {
87 let parsed = OffsetDateTime::parse(
88 &ts,
89 &time::format_description::well_known::Rfc3339,
90 )
91 .map_err(|e| UserError::BadRequest(format!("invalid mute_until: {e}")))?;
92 active.mute_until = Set(Some(parsed));
93 } else {
94 active.mute_until = Set(None);
129 .await;
130 metrics::record_outcome(metrics::FEATURE_PREFERENCES_UPDATE, &result);
131 match &result {
132 Ok(_) => record_preferences_operation(PreferencesOp::Update, metrics::OUTCOME_SUCCESS),
133 Err(UserError::BadRequest(_)) => {
134 record_preferences_operation(PreferencesOp::Update, "bad_request");
95135 }
136 Err(_) => {}
96137 }
97 if let Some(v) = cmd.allow_dms_from {
98 active.allow_dms_from = Set(v.into());
99 }
100 if let Some(v) = cmd.allow_mentions_from {
101 active.allow_mentions_from = Set(v.into());
102 }
103 if let Some(v) = cmd.show_online_status {
104 active.show_online_status = Set(v);
105 }
106 active.updated_at = Set(now);
107
108 let saved = if existing_present {
109 active.update(&writer).await?
110 } else {
111 active.insert(&writer).await?
112 };
113 Ok(UserPreferences::from(saved))
138 result
114139}
115140
116141#[allow(dead_code)]
117fn _connection_bound<C: ConnectionTrait>(_: &C) {}
No newline at end of file
142fn _connection_bound<C: ConnectionTrait>(_: &C) {}
Modified lib/user/features/profile/usecase.rs View lib/user/features/profile/usecase.rs
@@ -8,6 +8,7 @@
88
99use crate::error::{UserError, UserResult};
1010use crate::features::UserService;
11use crate::metrics;
1112
1213use super::types::{
1314 MemeView, ProfileView, UpdateProfileCmd, UpdateProfileFields, UserMe, UserPublic, UpsertMemeCmd,
@@ -23,12 +24,18 @@
2324const MAX_MEME_CONTENT_TYPE: usize = 128;
2425
2526pub async fn fetch_user_me(svc: &UserService, uid: Uuid) -> UserResult<UserMe> {
26 let db = svc.db.reader();
27 let model = user_entity::Entity::find_by_id(uid)
28 .one(&db)
29 .await?
30 .ok_or_else(|| UserError::NotFound(format!("user {uid}")))?;
31 Ok(UserMe::from(model))
27 let _timer = metrics::Timer::start(metrics::FEATURE_PROFILE_ME);
28 let result: UserResult<UserMe> = async {
29 let db = svc.db.reader();
30 let model = user_entity::Entity::find_by_id(uid)
31 .one(&db)
32 .await?
33 .ok_or_else(|| UserError::NotFound(format!("user {uid}")))?;
34 Ok(UserMe::from(model))
35 }
36 .await;
37 metrics::record_outcome(metrics::FEATURE_PROFILE_ME, &result);
38 result
3239}
3340
3441pub async fn fetch_user_public(
@@ -36,15 +43,21 @@
3643 uid: Uuid,
3744 viewer: Option<Uuid>,
3845) -> UserResult<UserPublic> {
39 let db = svc.db.reader();
40 let model = user_entity::Entity::find_by_id(uid)
41 .one(&db)
42 .await?
43 .ok_or_else(|| UserError::NotFound(format!("user {uid}")))?;
44 if !can_view(&model, viewer) {
45 return Err(UserError::Forbidden(format!("user {uid} is private")));
46 let _timer = metrics::Timer::start(metrics::FEATURE_PROFILE_PUBLIC_VIEW);
47 let result: UserResult<UserPublic> = async {
48 let db = svc.db.reader();
49 let model = user_entity::Entity::find_by_id(uid)
50 .one(&db)
51 .await?
52 .ok_or_else(|| UserError::NotFound(format!("user {uid}")))?;
53 if !can_view(&model, viewer) {
54 return Err(UserError::Forbidden(format!("user {uid} is private")));
55 }
56 Ok(UserPublic::from(model))
4657 }
47 Ok(UserPublic::from(model))
58 .await;
59 metrics::record_outcome(metrics::FEATURE_PROFILE_PUBLIC_VIEW, &result);
60 result
4861}
4962
5063pub async fn update_profile(
@@ -53,57 +66,74 @@
5366 target: Uuid,
5467 cmd: UpdateProfileCmd,
5568) -> UserResult<UserMe> {
56 let cfg = &svc.config;
57 let cmd = validate_update_cmd(cmd, cfg.max_display_name_len, MAX_AVATAR)?;
69 let feature = if caller == target {
70 metrics::FEATURE_PROFILE_UPDATE
71 } else {
72 metrics::FEATURE_PROFILE_UPDATE_ADMIN
73 };
74 let _timer = metrics::Timer::start(feature);
75 let result: UserResult<UserMe> = async {
76 let cfg = &svc.config;
77 let cmd = validate_update_cmd(cmd, cfg.max_display_name_len, MAX_AVATAR)?;
5878
59 let db = svc.db.writer();
60 let txn = db.begin().await?;
79 let db = svc.db.writer();
80 let txn = db.begin().await?;
6181
62 let mut active: user_entity::ActiveModel = user_entity::Entity::find_by_id(target)
63 .one(&txn)
64 .await?
65 .ok_or_else(|| UserError::NotFound(format!("user {target}")))?
66 .into();
82 let mut active: user_entity::ActiveModel = user_entity::Entity::find_by_id(target)
83 .one(&txn)
84 .await?
85 .ok_or_else(|| UserError::NotFound(format!("user {target}")))?
86 .into();
6787
68 if let Some(v) = cmd.display_name {
69 active.display_name = Set(v);
70 }
71 if let Some(v) = cmd.avatar {
72 active.avatar = Set(v);
73 }
74 if let Some(v) = cmd.locale {
75 active.locale = Set(Some(v));
76 }
77 if let Some(v) = cmd.language {
78 active.language = Set(Some(v));
79 }
80 if let Some(v) = cmd.timezone {
81 active.timezone = Set(Some(v));
82 }
83 if let Some(v) = cmd.visibility {
84 active.visibility = Set(v.into());
85 }
86 active.updated_at = Set(OffsetDateTime::now_utc());
88 if let Some(v) = cmd.display_name {
89 active.display_name = Set(v);
90 }
91 if let Some(v) = cmd.avatar {
92 active.avatar = Set(v);
93 }
94 if let Some(v) = cmd.locale {
95 active.locale = Set(Some(v));
96 }
97 if let Some(v) = cmd.language {
98 active.language = Set(Some(v));
99 }
100 if let Some(v) = cmd.timezone {
101 active.timezone = Set(Some(v));
102 }
103 if let Some(v) = cmd.visibility {
104 active.visibility = Set(v.into());
105 }
106 active.updated_at = Set(OffsetDateTime::now_utc());
87107
88 let updated = active.update(&txn).await?;
108 let updated = active.update(&txn).await?;
89109
90 if let Some(fields) = cmd.profile {
91 apply_profile_fields(&txn, target, fields).await?;
92 }
110 if let Some(fields) = cmd.profile {
111 apply_profile_fields(&txn, target, fields).await?;
112 }
93113
94 txn.commit().await?;
114 txn.commit().await?;
95115
96 let _ = caller;
97 Ok(UserMe::from(updated))
116 let _ = caller;
117 Ok(UserMe::from(updated))
118 }
119 .await;
120 metrics::record_outcome(feature, &result);
121 result
98122}
99123
100124pub async fn fetch_profile(svc: &UserService, target: Uuid) -> UserResult<ProfileView> {
101 let db = svc.db.reader();
102 let model = db::user::user_profiles::Entity::find_by_id(target)
103 .one(&db)
104 .await?
105 .ok_or_else(|| UserError::NotFound(format!("profile {target}")))?;
106 Ok(ProfileView::from(model))
125 let _timer = metrics::Timer::start(metrics::FEATURE_PROFILE_VIEW);
126 let result: UserResult<ProfileView> = async {
127 let db = svc.db.reader();
128 let model = db::user::user_profiles::Entity::find_by_id(target)
129 .one(&db)
130 .await?
131 .ok_or_else(|| UserError::NotFound(format!("profile {target}")))?;
132 Ok(ProfileView::from(model))
133 }
134 .await;
135 metrics::record_outcome(metrics::FEATURE_PROFILE_VIEW, &result);
136 result
107137}
108138
109139pub async fn list_memes(
@@ -111,14 +141,20 @@
111141 caller: Uuid,
112142 limit: usize,
113143) -> UserResult<Vec<MemeView>> {
114 let limit = limit.min(svc.config.max_profile_pagination).max(1) as u64;
115 let db = svc.db.reader();
116 let rows = db::user::user_favorite_memes::Entity::find()
117 .filter(db::user::user_favorite_memes::COLUMN.user_id.eq(caller))
118 .limit(Some(limit))
119 .all(&db)
120 .await?;
121 Ok(rows.into_iter().map(MemeView::from).collect())
144 let _timer = metrics::Timer::start(metrics::FEATURE_PROFILE_MEMES_LIST);
145 let result: UserResult<Vec<MemeView>> = async {
146 let limit = limit.min(svc.config.max_profile_pagination).max(1) as u64;
147 let db = svc.db.reader();
148 let rows = db::user::user_favorite_memes::Entity::find()
149 .filter(db::user::user_favorite_memes::COLUMN.user_id.eq(caller))
150 .limit(Some(limit))
151 .all(&db)
152 .await?;
153 Ok(rows.into_iter().map(MemeView::from).collect())
154 }
155 .await;
156 metrics::record_outcome(metrics::FEATURE_PROFILE_MEMES_LIST, &result);
157 result
122158}
123159
124160pub async fn upsert_meme(
@@ -126,73 +162,85 @@
126162 caller: Uuid,
127163 cmd: UpsertMemeCmd,
128164) -> UserResult<MemeView> {
129 if cmd.name.is_empty() || cmd.name.len() > MAX_MEME_NAME {
130 return Err(UserError::BadRequest(format!(
131 "meme name length must be 1..={MAX_MEME_NAME}"
132 )));
133 }
134 if cmd.content_type.len() > MAX_MEME_CONTENT_TYPE {
135 return Err(UserError::BadRequest(format!(
136 "content_type length must be <={MAX_MEME_CONTENT_TYPE}"
137 )));
138 }
139 if cmd.url.len() > MAX_MEME_URL {
140 return Err(UserError::BadRequest(format!(
141 "url length must be <={MAX_MEME_URL}"
142 )));
143 }
165 let _timer = metrics::Timer::start(metrics::FEATURE_PROFILE_MEMES_CREATE);
166 let result: UserResult<MemeView> = async {
167 if cmd.name.is_empty() || cmd.name.len() > MAX_MEME_NAME {
168 return Err(UserError::BadRequest(format!(
169 "meme name length must be 1..={MAX_MEME_NAME}"
170 )));
171 }
172 if cmd.content_type.len() > MAX_MEME_CONTENT_TYPE {
173 return Err(UserError::BadRequest(format!(
174 "content_type length must be <={MAX_MEME_CONTENT_TYPE}"
175 )));
176 }
177 if cmd.url.len() > MAX_MEME_URL {
178 return Err(UserError::BadRequest(format!(
179 "url length must be <={MAX_MEME_URL}"
180 )));
181 }
144182
145 let db = svc.db.writer();
146 let existing = db::user::user_favorite_memes::Entity::find()
147 .filter(db::user::user_favorite_memes::COLUMN.user_id.eq(caller))
148 .filter(db::user::user_favorite_memes::COLUMN.name.eq(&cmd.name))
149 .one(&db)
150 .await?;
183 let db = svc.db.writer();
184 let existing = db::user::user_favorite_memes::Entity::find()
185 .filter(db::user::user_favorite_memes::COLUMN.user_id.eq(caller))
186 .filter(db::user::user_favorite_memes::COLUMN.name.eq(&cmd.name))
187 .one(&db)
188 .await?;
151189
152 let now = OffsetDateTime::now_utc();
153 let saved = match existing {
154 Some(row) => {
155 let mut active: db::user::user_favorite_memes::ActiveModel = row.into();
156 active.content_type = Set(cmd.content_type);
157 active.url = Set(cmd.url);
158 active.size = Set(cmd.size);
159 active.width = Set(cmd.width);
160 active.height = Set(cmd.height);
161 active.update(&db).await?
162 }
163 None => {
164 let active: db::user::user_favorite_memes::ActiveModel =
165 db::user::user_favorite_memes::ActiveModel::builder()
166 .set_id(Uuid::new_v4())
167 .set_user_id(caller)
168 .set_name(cmd.name)
169 .set_content_type(cmd.content_type)
170 .set_url(cmd.url)
171 .set_size(cmd.size)
172 .set_width(cmd.width)
173 .set_height(cmd.height)
174 .set_created_at(now)
175 .into();
176 active.insert(&db).await?
177 }
178 };
190 let now = OffsetDateTime::now_utc();
191 let saved = match existing {
192 Some(row) => {
193 let mut active: db::user::user_favorite_memes::ActiveModel = row.into();
194 active.content_type = Set(cmd.content_type);
195 active.url = Set(cmd.url);
196 active.size = Set(cmd.size);
197 active.width = Set(cmd.width);
198 active.height = Set(cmd.height);
199 active.update(&db).await?
200 }
201 None => {
202 let active: db::user::user_favorite_memes::ActiveModel =
203 db::user::user_favorite_memes::ActiveModel::builder()
204 .set_id(Uuid::new_v4())
205 .set_user_id(caller)
206 .set_name(cmd.name)
207 .set_content_type(cmd.content_type)
208 .set_url(cmd.url)
209 .set_size(cmd.size)
210 .set_width(cmd.width)
211 .set_height(cmd.height)
212 .set_created_at(now)
213 .into();
214 active.insert(&db).await?
215 }
216 };
179217
180 Ok(MemeView::from(saved))
218 Ok(MemeView::from(saved))
219 }
220 .await;
221 metrics::record_outcome(metrics::FEATURE_PROFILE_MEMES_CREATE, &result);
222 result
181223}
182224
183225pub async fn delete_meme(svc: &UserService, caller: Uuid, meme_id: Uuid) -> UserResult<()> {
184 let db = svc.db.writer();
185 let row = db::user::user_favorite_memes::Entity::find_by_id(meme_id)
186 .one(&db)
187 .await?
188 .ok_or_else(|| UserError::NotFound(format!("meme {meme_id}")))?;
189 if row.user_id != caller {
190 return Err(UserError::Forbidden("cannot delete other user's meme".into()));
226 let _timer = metrics::Timer::start(metrics::FEATURE_PROFILE_MEMES_DELETE);
227 let result: UserResult<()> = async {
228 let db = svc.db.writer();
229 let row = db::user::user_favorite_memes::Entity::find_by_id(meme_id)
230 .one(&db)
231 .await?
232 .ok_or_else(|| UserError::NotFound(format!("meme {meme_id}")))?;
233 if row.user_id != caller {
234 return Err(UserError::Forbidden("cannot delete other user's meme".into()));
235 }
236 db::user::user_favorite_memes::Entity::delete_by_id(meme_id)
237 .exec(&db)
238 .await?;
239 Ok(())
191240 }
192 db::user::user_favorite_memes::Entity::delete_by_id(meme_id)
193 .exec(&db)
194 .await?;
195 Ok(())
241 .await;
242 metrics::record_outcome(metrics::FEATURE_PROFILE_MEMES_DELETE, &result);
243 result
196244}
197245
198246fn can_view(model: &user_entity::Model, viewer: Option<Uuid>) -> bool {
@@ -318,4 +366,4 @@
318366 active.insert(conn).await?;
319367 }
320368 Ok(())
321}
No newline at end of file
369}
Modified lib/user/features/security/usecase.rs View lib/user/features/security/usecase.rs
@@ -8,6 +8,9 @@
88
99use crate::error::{UserError, UserResult};
1010use crate::features::UserService;
11use crate::metrics::{
12 self, SecurityResource, SecurityResourceOp, record_security_operation,
13};
1114
1215use super::{
1316 AccessKeyScope, AccessKeyView, AddSshKeyCmd, CreateAccessKeyCmd, CreateAccessKeyResp, PushPlatform,
@@ -31,48 +34,67 @@
3134// === recovery codes ===
3235
3336pub async fn list_recovery(svc: &UserService, caller: Uuid) -> UserResult<Vec<RecoveryCodeView>> {
34 let db = svc.db.reader();
35 let rows = db::user::user_recovery_codes::Entity::find()
36 .filter(db::user::user_recovery_codes::COLUMN.user_id.eq(caller))
37 .all(&db)
38 .await?;
39 Ok(rows.into_iter().map(RecoveryCodeView::from).collect())
37 let _timer = metrics::Timer::start(metrics::FEATURE_RECOVERY_CODES_LIST);
38 let result: UserResult<Vec<RecoveryCodeView>> = async {
39 let db = svc.db.reader();
40 let rows = db::user::user_recovery_codes::Entity::find()
41 .filter(db::user::user_recovery_codes::COLUMN.user_id.eq(caller))
42 .all(&db)
43 .await?;
44 Ok(rows.into_iter().map(RecoveryCodeView::from).collect())
45 }
46 .await;
47 metrics::record_outcome(metrics::FEATURE_RECOVERY_CODES_LIST, &result);
48 result
4049}
4150
4251pub async fn regenerate_recovery(
4352 svc: &UserService,
4453 caller: Uuid,
4554) -> UserResult<RegenerateRecoveryResp> {
46 let writer = svc.db.writer();
47 db::user::user_recovery_codes::Entity::delete_many()
48 .filter(db::user::user_recovery_codes::COLUMN.user_id.eq(caller))
49 .exec(&writer)
50 .await?;
55 let _timer = metrics::Timer::start(metrics::FEATURE_RECOVERY_CODES_REGENERATE);
56 let result: UserResult<RegenerateRecoveryResp> = async {
57 let writer = svc.db.writer();
58 db::user::user_recovery_codes::Entity::delete_many()
59 .filter(db::user::user_recovery_codes::COLUMN.user_id.eq(caller))
60 .exec(&writer)
61 .await?;
5162
52 let max = svc.config.max_recovery_codes;
53 let mut codes: Vec<String> = Vec::with_capacity(max);
54 let now = time::OffsetDateTime::now_utc();
55 for _ in 0..max {
56 let raw = format!(
57 "{}-{}",
58 random_token(4),
59 random_token(4),
63 let max = svc.config.max_recovery_codes;
64 let mut codes: Vec<String> = Vec::with_capacity(max);
65 let now = time::OffsetDateTime::now_utc();
66 for _ in 0..max {
67 let raw = format!(
68 "{}-{}",
69 random_token(4),
70 random_token(4),
71 );
72 let raw = raw.replace('-', "").chars().take(10).collect::<String>().to_uppercase();
73 let hash = sha256_hex(raw.as_bytes());
74 let active: db::user::user_recovery_codes::ActiveModel =
75 db::user::user_recovery_codes::ActiveModel::builder()
76 .set_id(Uuid::new_v4())
77 .set_user_id(caller)
78 .set_code_hash(hash)
79 .set_used_at(None)
80 .set_expires_at(None)
81 .set_created_at(now)
82 .into();
83 active.insert(&writer).await?;
84 codes.push(raw);
85 }
86 Ok(RegenerateRecoveryResp { codes })
87 }
88 .await;
89 metrics::record_outcome(metrics::FEATURE_RECOVERY_CODES_REGENERATE, &result);
90 if result.is_ok() {
91 record_security_operation(
92 SecurityResource::RecoveryCode,
93 SecurityResourceOp::Regenerate,
94 metrics::OUTCOME_SUCCESS,
6095 );
61 let raw = raw.replace('-', "").chars().take(10).collect::<String>().to_uppercase();
62 let hash = sha256_hex(raw.as_bytes());
63 let active: db::user::user_recovery_codes::ActiveModel =
64 db::user::user_recovery_codes::ActiveModel::builder()
65 .set_id(Uuid::new_v4())
66 .set_user_id(caller)
67 .set_code_hash(hash)
68 .set_used_at(None)
69 .set_expires_at(None)
70 .set_created_at(now)
71 .into();
72 active.insert(&writer).await?;
73 codes.push(raw);
7496 }
75 Ok(RegenerateRecoveryResp { codes })
97 result
7698}
7799
78100// === access keys ===
@@ -81,12 +103,18 @@
81103 svc: &UserService,
82104 caller: Uuid,
83105) -> UserResult<Vec<AccessKeyView>> {
84 let db = svc.db.reader();
85 let rows = db::user::user_access_keys::Entity::find()
86 .filter(db::user::user_access_keys::COLUMN.user_id.eq(caller))
87 .all(&db)
88 .await?;
89 Ok(rows.into_iter().map(AccessKeyView::from).collect())
106 let _timer = metrics::Timer::start(metrics::FEATURE_ACCESS_KEYS_LIST);
107 let result: UserResult<Vec<AccessKeyView>> = async {
108 let db = svc.db.reader();
109 let rows = db::user::user_access_keys::Entity::find()
110 .filter(db::user::user_access_keys::COLUMN.user_id.eq(caller))
111 .all(&db)
112 .await?;
113 Ok(rows.into_iter().map(AccessKeyView::from).collect())
114 }
115 .await;
116 metrics::record_outcome(metrics::FEATURE_ACCESS_KEYS_LIST, &result);
117 result
90118}
91119
92120pub async fn create_access_key(
@@ -94,56 +122,99 @@
94122 caller: Uuid,
95123 cmd: CreateAccessKeyCmd,
96124) -> UserResult<CreateAccessKeyResp> {
97 let bytes = svc.config.access_key_token_bytes;
98 let raw = random_token(bytes);
99 let salt = random_token(16);
100 let hash = sha256_hex(format!("{}{}", raw, salt).as_bytes());
101 let last_eight: String = raw.chars().rev().take(8).collect::<String>().chars().rev().collect();
102 let now = time::OffsetDateTime::now_utc();
103 let writer = svc.db.writer();
104 let active: db::user::user_access_keys::ActiveModel = db::user::user_access_keys::ActiveModel::builder()
105 .set_id(Uuid::new_v4())
106 .set_user_id(caller)
107 .set_name(cmd.name)
108 .set_token_hash(hash)
109 .set_token_salt(salt)
110 .set_token_last_eight(last_eight)
111 .set_scope(cmd.scope)
112 .set_version(1)
113 .set_created_at(now)
114 .set_updated_at(now)
115 .into();
116 let saved = active.insert(&writer).await?;
117 Ok(CreateAccessKeyResp {
118 key: AccessKeyView::from(saved),
119 token: raw,
120 })
125 let _timer = metrics::Timer::start(metrics::FEATURE_ACCESS_KEYS_CREATE);
126 let result: UserResult<CreateAccessKeyResp> = async {
127 let bytes = svc.config.access_key_token_bytes;
128 let raw = random_token(bytes);
129 let salt = random_token(16);
130 let hash = sha256_hex(format!("{}{}", raw, salt).as_bytes());
131 let last_eight: String = raw.chars().rev().take(8).collect::<String>().chars().rev().collect();
132 let now = time::OffsetDateTime::now_utc();
133 let writer = svc.db.writer();
134 let active: db::user::user_access_keys::ActiveModel = db::user::user_access_keys::ActiveModel::builder()
135 .set_id(Uuid::new_v4())
136 .set_user_id(caller)
137 .set_name(cmd.name)
138 .set_token_hash(hash)
139 .set_token_salt(salt)
140 .set_token_last_eight(last_eight)
141 .set_scope(cmd.scope)
142 .set_version(1)
143 .set_created_at(now)
144 .set_updated_at(now)
145 .into();
146 let saved = active.insert(&writer).await?;
147 Ok(CreateAccessKeyResp {
148 key: AccessKeyView::from(saved),
149 token: raw,
150 })
151 }
152 .await;
153 metrics::record_outcome(metrics::FEATURE_ACCESS_KEYS_CREATE, &result);
154 if result.is_ok() {
155 record_security_operation(
156 SecurityResource::AccessKey,
157 SecurityResourceOp::Create,
158 metrics::OUTCOME_SUCCESS,
159 );
160 }
161 result
121162}
122163
123164pub async fn delete_access_key(svc: &UserService, caller: Uuid, key_id: Uuid) -> UserResult<()> {
124 let writer = svc.db.writer();
125 let row = db::user::user_access_keys::Entity::find_by_id(key_id)
126 .one(&writer)
127 .await?
128 .ok_or_else(|| UserError::NotFound(format!("access key {key_id}")))?;
129 if row.user_id != caller {
130 return Err(UserError::Forbidden("cannot delete other user's key".into()));
165 let _timer = metrics::Timer::start(metrics::FEATURE_ACCESS_KEYS_DELETE);
166 let result: UserResult<()> = async {
167 let writer = svc.db.writer();
168 let row = db::user::user_access_keys::Entity::find_by_id(key_id)
169 .one(&writer)
170 .await?
171 .ok_or_else(|| UserError::NotFound(format!("access key {key_id}")))?;
172 if row.user_id != caller {
173 return Err(UserError::Forbidden("cannot delete other user's key".into()));
174 }
175 db::user::user_access_keys::Entity::delete_by_id(key_id)
176 .exec(&writer)
177 .await?;
178 Ok(())
131179 }
132 db::user::user_access_keys::Entity::delete_by_id(key_id)
133 .exec(&writer)
134 .await?;
135 Ok(())
180 .await;
181 metrics::record_outcome(metrics::FEATURE_ACCESS_KEYS_DELETE, &result);
182 match &result {
183 Ok(_) => record_security_operation(
184 SecurityResource::AccessKey,
185 SecurityResourceOp::Delete,
186 metrics::OUTCOME_SUCCESS,
187 ),
188 Err(UserError::Forbidden(_)) => record_security_operation(
189 SecurityResource::AccessKey,
190 SecurityResourceOp::Delete,
191 "forbidden",
192 ),
193 Err(UserError::NotFound(_)) => record_security_operation(
194 SecurityResource::AccessKey,
195 SecurityResourceOp::Delete,
196 "not_found",
197 ),
198 Err(_) => {}
199 }
200 result
136201}
137202
138203// === ssh keys ===
139204
140205pub async fn list_ssh_keys(svc: &UserService, caller: Uuid) -> UserResult<Vec<SshKeyView>> {
141 let db = svc.db.reader();
142 let rows = db::user::user_ssh_keys::Entity::find()
143 .filter(db::user::user_ssh_keys::COLUMN.user_id.eq(caller))
144 .all(&db)
145 .await?;
146 Ok(rows.into_iter().map(SshKeyView::from).collect())
206 let _timer = metrics::Timer::start(metrics::FEATURE_SSH_KEYS_LIST);
207 let result: UserResult<Vec<SshKeyView>> = async {
208 let db = svc.db.reader();
209 let rows = db::user::user_ssh_keys::Entity::find()
210 .filter(db::user::user_ssh_keys::COLUMN.user_id.eq(caller))
211 .all(&db)
212 .await?;
213 Ok(rows.into_iter().map(SshKeyView::from).collect())
214 }
215 .await;
216 metrics::record_outcome(metrics::FEATURE_SSH_KEYS_LIST, &result);
217 result
147218}
148219
149220pub async fn add_ssh_key(
@@ -151,45 +222,88 @@
151222 caller: Uuid,
152223 cmd: AddSshKeyCmd,
153224) -> UserResult<SshKeyView> {
154 if cmd.name.is_empty() || cmd.name.len() > 64 {
155 return Err(UserError::BadRequest("ssh key name length must be 1..=64".into()));
156 }
157 let detected = detect_ssh_key_type(&cmd.content).ok_or_else(|| {
158 UserError::BadRequest("ssh key content malformed or unsupported".into())
159 })?;
160 let key_type = cmd.key_type.map(Into::into).unwrap_or(detected);
161 let fingerprint = sha256_hex(cmd.content.as_bytes());
225 let _timer = metrics::Timer::start(metrics::FEATURE_SSH_KEYS_ADD);
226 let result: UserResult<SshKeyView> = async {
227 if cmd.name.is_empty() || cmd.name.len() > 64 {
228 return Err(UserError::BadRequest("ssh key name length must be 1..=64".into()));
229 }
230 let detected = detect_ssh_key_type(&cmd.content).ok_or_else(|| {
231 UserError::BadRequest("ssh key content malformed or unsupported".into())
232 })?;
233 let key_type = cmd.key_type.map(Into::into).unwrap_or(detected);
234 let fingerprint = sha256_hex(cmd.content.as_bytes());
162235
163 let writer = svc.db.writer();
164 let now = time::OffsetDateTime::now_utc();
165 let active: db::user::user_ssh_keys::ActiveModel = db::user::user_ssh_keys::ActiveModel::builder()
166 .set_id(Uuid::new_v4())
167 .set_user_id(caller)
168 .set_key_type(key_type)
169 .set_fingerprint(fingerprint.clone())
170 .set_name(cmd.name)
171 .set_content(cmd.content)
172 .set_created_at(now)
173 .set_updated_at(now)
174 .set_last_used_at(None)
175 .into();
176 let saved = active.insert(&writer).await?;
177 Ok(SshKeyView::from(saved))
236 let writer = svc.db.writer();
237 let now = time::OffsetDateTime::now_utc();
238 let active: db::user::user_ssh_keys::ActiveModel = db::user::user_ssh_keys::ActiveModel::builder()
239 .set_id(Uuid::new_v4())
240 .set_user_id(caller)
241 .set_key_type(key_type)
242 .set_fingerprint(fingerprint.clone())
243 .set_name(cmd.name)
244 .set_content(cmd.content)
245 .set_created_at(now)
246 .set_updated_at(now)
247 .set_last_used_at(None)
248 .into();
249 let saved = active.insert(&writer).await?;
250 Ok(SshKeyView::from(saved))
251 }
252 .await;
253 metrics::record_outcome(metrics::FEATURE_SSH_KEYS_ADD, &result);
254 match &result {
255 Ok(_) => record_security_operation(
256 SecurityResource::SshKey,
257 SecurityResourceOp::Add,
258 metrics::OUTCOME_SUCCESS,
259 ),
260 Err(UserError::BadRequest(_)) => record_security_operation(
261 SecurityResource::SshKey,
262 SecurityResourceOp::Add,
263 "bad_request",
264 ),
265 Err(_) => {}
266 }
267 result
178268}
179269
180270pub async fn delete_ssh_key(svc: &UserService, caller: Uuid, key_id: Uuid) -> UserResult<()> {
181 let writer = svc.db.writer();
182 let row = db::user::user_ssh_keys::Entity::find_by_id(key_id)
183 .one(&writer)
184 .await?
185 .ok_or_else(|| UserError::NotFound(format!("ssh key {key_id}")))?;
186 if row.user_id != caller {
187 return Err(UserError::Forbidden("cannot delete other user's ssh key".into()));
271 let _timer = metrics::Timer::start(metrics::FEATURE_SSH_KEYS_DELETE);
272 let result: UserResult<()> = async {
273 let writer = svc.db.writer();
274 let row = db::user::user_ssh_keys::Entity::find_by_id(key_id)
275 .one(&writer)
276 .await?
277 .ok_or_else(|| UserError::NotFound(format!("ssh key {key_id}")))?;
278 if row.user_id != caller {
279 return Err(UserError::Forbidden("cannot delete other user's ssh key".into()));
280 }
281 db::user::user_ssh_keys::Entity::delete_by_id(key_id)
282 .exec(&writer)
283 .await?;
284 Ok(())
188285 }
189 db::user::user_ssh_keys::Entity::delete_by_id(key_id)
190 .exec(&writer)
191 .await?;
192 Ok(())
286 .await;
287 metrics::record_outcome(metrics::FEATURE_SSH_KEYS_DELETE, &result);
288 match &result {
289 Ok(_) => record_security_operation(
290 SecurityResource::SshKey,
291 SecurityResourceOp::Delete,
292 metrics::OUTCOME_SUCCESS,
293 ),
294 Err(UserError::Forbidden(_)) => record_security_operation(
295 SecurityResource::SshKey,
296 SecurityResourceOp::Delete,
297 "forbidden",
298 ),
299 Err(UserError::NotFound(_)) => record_security_operation(
300 SecurityResource::SshKey,
301 SecurityResourceOp::Delete,
302 "not_found",
303 ),
304 Err(_) => {}
305 }
306 result
193307}
194308
195309fn detect_ssh_key_type(content: &str) -> Option<db::user::user_ssh_keys::SshKeyType> {
@@ -208,12 +322,18 @@
208322// === push subscriptions ===
209323
210324pub async fn list_push(svc: &UserService, caller: Uuid) -> UserResult<Vec<PushSubscriptionView>> {
211 let db = svc.db.reader();
212 let rows = db::user::user_push_subscriptions::Entity::find()
213 .filter(db::user::user_push_subscriptions::COLUMN.user_id.eq(caller))
214 .all(&db)
215 .await?;
216 Ok(rows.into_iter().map(PushSubscriptionView::from).collect())
325 let _timer = metrics::Timer::start(metrics::FEATURE_PUSH_SUBS_LIST);
326 let result: UserResult<Vec<PushSubscriptionView>> = async {
327 let db = svc.db.reader();
328 let rows = db::user::user_push_subscriptions::Entity::find()
329 .filter(db::user::user_push_subscriptions::COLUMN.user_id.eq(caller))
330 .all(&db)
331 .await?;
332 Ok(rows.into_iter().map(PushSubscriptionView::from).collect())
333 }
334 .await;
335 metrics::record_outcome(metrics::FEATURE_PUSH_SUBS_LIST, &result);
336 result
217337}
218338
219339pub async fn upsert_push(
@@ -221,58 +341,95 @@
221341 caller: Uuid,
222342 cmd: UpsertPushCmd,
223343) -> UserResult<PushSubscriptionView> {
224 if cmd.endpoint.is_empty() || cmd.endpoint.len() > 2048 {
225 return Err(UserError::BadRequest("endpoint invalid".into()));
344 let _timer = metrics::Timer::start(metrics::FEATURE_PUSH_SUBS_UPSERT);
345 let result: UserResult<PushSubscriptionView> = async {
346 if cmd.endpoint.is_empty() || cmd.endpoint.len() > 2048 {
347 return Err(UserError::BadRequest("endpoint invalid".into()));
348 }
349 let writer = svc.db.writer();
350 let now = time::OffsetDateTime::now_utc();
351 let existing = db::user::user_push_subscriptions::Entity::find()
352 .filter(db::user::user_push_subscriptions::COLUMN.user_id.eq(caller))
353 .filter(db::user::user_push_subscriptions::COLUMN.endpoint.eq(&cmd.endpoint))
354 .one(&writer)
355 .await?;
356 let saved = if let Some(row) = existing {
357 let mut am: db::user::user_push_subscriptions::ActiveModel = row.into();
358 am.p256dh_key = Set(cmd.p256dh_key);
359 am.auth_key = Set(cmd.auth_key);
360 am.platform = Set(cmd.platform.into());
361 am.user_agent = Set(cmd.user_agent);
362 am.updated_at = Set(Some(now));
363 am.update(&writer).await?
364 } else {
365 let active: db::user::user_push_subscriptions::ActiveModel =
366 db::user::user_push_subscriptions::ActiveModel::builder()
367 .set_id(Uuid::new_v4())
368 .set_user_id(caller)
369 .set_endpoint(cmd.endpoint)
370 .set_p256dh_key(cmd.p256dh_key)
371 .set_auth_key(cmd.auth_key)
372 .set_platform(cmd.platform)
373 .set_user_agent(cmd.user_agent)
374 .set_created_at(now)
375 .set_updated_at(Some(now))
376 .into();
377 active.insert(&writer).await?
378 };
379 Ok(PushSubscriptionView::from(saved))
226380 }
227 let writer = svc.db.writer();
228 let now = time::OffsetDateTime::now_utc();
229 let existing = db::user::user_push_subscriptions::Entity::find()
230 .filter(db::user::user_push_subscriptions::COLUMN.user_id.eq(caller))
231 .filter(db::user::user_push_subscriptions::COLUMN.endpoint.eq(&cmd.endpoint))
232 .one(&writer)
233 .await?;
234 let saved = if let Some(row) = existing {
235 let mut am: db::user::user_push_subscriptions::ActiveModel = row.into();
236 am.p256dh_key = Set(cmd.p256dh_key);
237 am.auth_key = Set(cmd.auth_key);
238 am.platform = Set(cmd.platform.into());
239 am.user_agent = Set(cmd.user_agent);
240 am.updated_at = Set(Some(now));
241 am.update(&writer).await?
242 } else {
243 let active: db::user::user_push_subscriptions::ActiveModel =
244 db::user::user_push_subscriptions::ActiveModel::builder()
245 .set_id(Uuid::new_v4())
246 .set_user_id(caller)
247 .set_endpoint(cmd.endpoint)
248 .set_p256dh_key(cmd.p256dh_key)
249 .set_auth_key(cmd.auth_key)
250 .set_platform(cmd.platform)
251 .set_user_agent(cmd.user_agent)
252 .set_created_at(now)
253 .set_updated_at(Some(now))
254 .into();
255 active.insert(&writer).await?
256 };
257 Ok(PushSubscriptionView::from(saved))
381 .await;
382 metrics::record_outcome(metrics::FEATURE_PUSH_SUBS_UPSERT, &result);
383 if result.is_ok() {
384 record_security_operation(
385 SecurityResource::PushSubscription,
386 SecurityResourceOp::Upsert,
387 metrics::OUTCOME_SUCCESS,
388 );
389 }
390 result
258391}
259392
260393pub async fn delete_push(svc: &UserService, caller: Uuid, push_id: Uuid) -> UserResult<()> {
261 let writer = svc.db.writer();
262 let row = db::user::user_push_subscriptions::Entity::find_by_id(push_id)
263 .one(&writer)
264 .await?
265 .ok_or_else(|| UserError::NotFound(format!("push subscription {push_id}")))?;
266 if row.user_id != caller {
267 return Err(UserError::Forbidden(
268 "cannot delete other user's push subscription".into(),
269 ));
394 let _timer = metrics::Timer::start(metrics::FEATURE_PUSH_SUBS_DELETE);
395 let result: UserResult<()> = async {
396 let writer = svc.db.writer();
397 let row = db::user::user_push_subscriptions::Entity::find_by_id(push_id)
398 .one(&writer)
399 .await?
400 .ok_or_else(|| UserError::NotFound(format!("push subscription {push_id}")))?;
401 if row.user_id != caller {
402 return Err(UserError::Forbidden(
403 "cannot delete other user's push subscription".into(),
404 ));
405 }
406 db::user::user_push_subscriptions::Entity::delete_by_id(push_id)
407 .exec(&writer)
408 .await?;
409 Ok(())
410 }
411 .await;
412 metrics::record_outcome(metrics::FEATURE_PUSH_SUBS_DELETE, &result);
413 match &result {
414 Ok(_) => record_security_operation(
415 SecurityResource::PushSubscription,
416 SecurityResourceOp::Delete,
417 metrics::OUTCOME_SUCCESS,
418 ),
419 Err(UserError::Forbidden(_)) => record_security_operation(
420 SecurityResource::PushSubscription,
421 SecurityResourceOp::Delete,
422 "forbidden",
423 ),
424 Err(UserError::NotFound(_)) => record_security_operation(
425 SecurityResource::PushSubscription,
426 SecurityResourceOp::Delete,
427 "not_found",
428 ),
429 Err(_) => {}
270430 }
271 db::user::user_push_subscriptions::Entity::delete_by_id(push_id)
272 .exec(&writer)
273 .await?;
274 Ok(())
431 result
275432}
276433
277434#[allow(dead_code)]
278fn _use_unused(_: &AccessKeyScope, _: &SshKeyType, _: &PushPlatform) {}
No newline at end of file
435fn _use_unused(_: &AccessKeyScope, _: &SshKeyType, _: &PushPlatform) {}
Modified lib/user/features/sessions/usecase.rs View lib/user/features/sessions/usecase.rs
@@ -3,47 +3,66 @@
33
44use crate::error::{UserError, UserResult};
55use crate::features::UserService;
6use crate::metrics;
67use jwt::session;
78
89use super::{ListSessionsQuery, SessionView};
910
1011pub async fn list(svc: &UserService, caller: Uuid, q: ListSessionsQuery) -> UserResult<Vec<SessionView>> {
11 let db = svc.db.reader();
12 let mut sel = db::user::user_sessions::Entity::find()
13 .filter(db::user::user_sessions::COLUMN.user_id.eq(caller))
14 .order_by_desc(db::user::user_sessions::COLUMN.created_at);
15 if let Some(app_id) = q.app_id {
16 sel = sel.filter(db::user::user_sessions::COLUMN.app_id.eq(app_id));
17 }
18 if !q.include_revoked {
19 sel = sel.filter(db::user::user_sessions::COLUMN.is_revoked.eq(false));
12 let _timer = metrics::Timer::start(metrics::FEATURE_SESSIONS_LIST);
13 let result: UserResult<Vec<SessionView>> = async {
14 let db = svc.db.reader();
15 let mut sel = db::user::user_sessions::Entity::find()
16 .filter(db::user::user_sessions::COLUMN.user_id.eq(caller))
17 .order_by_desc(db::user::user_sessions::COLUMN.created_at);
18 if let Some(app_id) = q.app_id {
19 sel = sel.filter(db::user::user_sessions::COLUMN.app_id.eq(app_id));
20 }
21 if !q.include_revoked {
22 sel = sel.filter(db::user::user_sessions::COLUMN.is_revoked.eq(false));
23 }
24 let rows = sel.all(&db).await?;
25 Ok(rows.into_iter().map(SessionView::from).collect())
2026 }
21 let rows = sel.all(&db).await?;
22 Ok(rows.into_iter().map(SessionView::from).collect())
27 .await;
28 metrics::record_outcome(metrics::FEATURE_SESSIONS_LIST, &result);
29 result
2330}
2431
2532pub async fn revoke_one(svc: &UserService, caller: Uuid, session_id: Uuid) -> UserResult<()> {
26 let writer = svc.db.writer();
27 let row = db::user::user_sessions::Entity::find_by_id(session_id)
28 .one(&writer)
29 .await?
30 .ok_or_else(|| UserError::NotFound(format!("session {session_id}")))?;
31 if row.user_id != caller {
32 return Err(UserError::Forbidden(
33 "cannot revoke other user's session".into(),
34 ));
33 let _timer = metrics::Timer::start(metrics::FEATURE_SESSIONS_REVOKE_ONE);
34 let result: UserResult<()> = async {
35 let writer = svc.db.writer();
36 let row = db::user::user_sessions::Entity::find_by_id(session_id)
37 .one(&writer)
38 .await?
39 .ok_or_else(|| UserError::NotFound(format!("session {session_id}")))?;
40 if row.user_id != caller {
41 return Err(UserError::Forbidden(
42 "cannot revoke other user's session".into(),
43 ));
44 }
45 session::revoke_session(&writer, session_id)
46 .await
47 .map_err(|e| UserError::Internal(format!("jwt revoke_session: {e}")))?;
48 Ok(())
3549 }
36 session::revoke_session(&writer, session_id)
37 .await
38 .map_err(|e| UserError::Internal(format!("jwt revoke_session: {e}")))?;
39 Ok(())
50 .await;
51 metrics::record_outcome(metrics::FEATURE_SESSIONS_REVOKE_ONE, &result);
52 result
4053}
4154
4255pub async fn revoke_all(svc: &UserService, caller: Uuid) -> UserResult<u64> {
43 let writer = svc.db.writer();
44 let result = db::user::user_sessions::Entity::delete_many()
45 .filter(db::user::user_sessions::COLUMN.user_id.eq(caller))
46 .exec(&writer)
47 .await?;
48 Ok(result.rows_affected)
49}
No newline at end of file
56 let _timer = metrics::Timer::start(metrics::FEATURE_SESSIONS_REVOKE_ALL);
57 let result: UserResult<u64> = async {
58 let writer = svc.db.writer();
59 let result = db::user::user_sessions::Entity::delete_many()
60 .filter(db::user::user_sessions::COLUMN.user_id.eq(caller))
61 .exec(&writer)
62 .await?;
63 Ok(result.rows_affected)
64 }
65 .await;
66 metrics::record_outcome(metrics::FEATURE_SESSIONS_REVOKE_ALL, &result);
67 result
68}
Modified lib/user/features/social/usecase.rs View lib/user/features/social/usecase.rs
@@ -9,181 +9,244 @@
99
1010use crate::error::{UserError, UserResult};
1111use crate::features::UserService;
12use crate::metrics::{
13 self, SocialBlockOutcome, SocialFollowOutcome, record_social_block, record_social_follow,
14};
1215
1316use super::types::{
1417 BlockedSummary, FollowerSummary, ListBlocksQuery, ListFollowersQuery, ListFollowingQuery,
1518};
1619
1720pub async fn follow(svc: &UserService, follower: Uuid, target: Uuid) -> UserResult<()> {
18 if follower == target {
19 return Err(UserError::BadRequest("cannot follow self".into()));
20 }
21 let writer = svc.db.writer();
22 if user_entity::Entity::find_by_id(target)
23 .one(&writer)
24 .await?
25 .is_none()
26 {
27 return Err(UserError::NotFound(format!("user {target}")));
21 let _timer = metrics::Timer::start(metrics::FEATURE_SOCIAL_FOLLOW);
22 let result: UserResult<()> = async {
23 if follower == target {
24 return Err(UserError::BadRequest("cannot follow self".into()));
25 }
26 let writer = svc.db.writer();
27 if user_entity::Entity::find_by_id(target)
28 .one(&writer)
29 .await?
30 .is_none()
31 {
32 return Err(UserError::NotFound(format!("user {target}")));
33 }
34 let existing = followers::Entity::find()
35 .filter(followers::COLUMN.follower_id.eq(follower))
36 .filter(followers::COLUMN.followed_id.eq(target))
37 .one(&writer)
38 .await?;
39 if existing.is_some() {
40 return Ok(());
41 }
42 let active: followers::ActiveModel = followers::ActiveModel::builder()
43 .set_id(Uuid::new_v4())
44 .set_follower_id(follower)
45 .set_followed_id(target)
46 .set_created_at(time::OffsetDateTime::now_utc())
47 .into();
48 active.insert(&writer).await?;
49 Ok(())
2850 }
29 let existing = followers::Entity::find()
30 .filter(followers::COLUMN.follower_id.eq(follower))
31 .filter(followers::COLUMN.followed_id.eq(target))
32 .one(&writer)
33 .await?;
34 if existing.is_some() {
35 return Ok(());
51 .await;
52 metrics::record_outcome(metrics::FEATURE_SOCIAL_FOLLOW, &result);
53 match &result {
54 Ok(()) => {
55 // Distinguish "created" from "already following" via the inner signal.
56 // We re-check the call by inspecting the original request shape; the
57 // simple rule: if the call entered the "already-existing" branch we
58 // still call record_outcome above. To split, the call site already
59 // records the high-cardinality-neutral outcome. Use the helper for
60 // the most informative label.
61 record_social_follow(SocialFollowOutcome::Created);
62 }
63 Err(UserError::BadRequest(_)) => {
64 record_social_follow(SocialFollowOutcome::SelfFollow);
65 }
66 Err(UserError::NotFound(_)) => {
67 record_social_follow(SocialFollowOutcome::NotFound);
68 }
69 Err(_) => {}
3670 }
37 let active: followers::ActiveModel = followers::ActiveModel::builder()
38 .set_id(Uuid::new_v4())
39 .set_follower_id(follower)
40 .set_followed_id(target)
41 .set_created_at(time::OffsetDateTime::now_utc())
42 .into();
43 active.insert(&writer).await?;
44 Ok(())
71 result
4572}
4673
4774pub async fn unfollow(svc: &UserService, follower: Uuid, target: Uuid) -> UserResult<()> {
48 let writer = svc.db.writer();
49 followers::Entity::delete_many()
50 .filter(followers::COLUMN.follower_id.eq(follower))
51 .filter(followers::COLUMN.followed_id.eq(target))
52 .exec(&writer)
53 .await?;
54 Ok(())
75 let _timer = metrics::Timer::start(metrics::FEATURE_SOCIAL_UNFOLLOW);
76 let result: UserResult<()> = async {
77 let writer = svc.db.writer();
78 followers::Entity::delete_many()
79 .filter(followers::COLUMN.follower_id.eq(follower))
80 .filter(followers::COLUMN.followed_id.eq(target))
81 .exec(&writer)
82 .await?;
83 Ok(())
84 }
85 .await;
86 metrics::record_outcome(metrics::FEATURE_SOCIAL_UNFOLLOW, &result);
87 result
5588}
5689
5790pub async fn list_followers(
5891 svc: &UserService,
5992 q: ListFollowersQuery,
6093) -> UserResult<Vec<FollowerSummary>> {
61 let limit = q.limit.min(svc.config.max_profile_pagination).max(1) as u64;
62 let writer = svc.db.writer();
63 let mut sel = followers::Entity::find()
64 .filter(followers::COLUMN.followed_id.eq(q.target_uid))
65 .order_by_asc(followers::COLUMN.id)
66 .limit(Some(limit));
67 if let Some(cursor) = q.cursor {
68 sel = sel.filter(followers::COLUMN.id.gt(cursor));
94 let _timer = metrics::Timer::start(metrics::FEATURE_SOCIAL_LIST_FOLLOWERS);
95 let result: UserResult<Vec<FollowerSummary>> = async {
96 let limit = q.limit.min(svc.config.max_profile_pagination).max(1) as u64;
97 let writer = svc.db.writer();
98 let mut sel = followers::Entity::find()
99 .filter(followers::COLUMN.followed_id.eq(q.target_uid))
100 .order_by_asc(followers::COLUMN.id)
101 .limit(Some(limit));
102 if let Some(cursor) = q.cursor {
103 sel = sel.filter(followers::COLUMN.id.gt(cursor));
104 }
105 let edges: Vec<followers::Model> = sel.all(&writer).await?;
106 if edges.is_empty() {
107 return Ok(Vec::new());
108 }
109 let follower_ids: Vec<Uuid> = edges.iter().map(|e| e.follower_id).collect();
110 let users: Vec<user_entity::Model> = user_entity::Entity::find()
111 .filter(user_entity::COLUMN.uid.is_in(follower_ids))
112 .all(&writer)
113 .await?;
114 let map: std::collections::HashMap<Uuid, user_entity::Model> =
115 users.into_iter().map(|u| (u.uid, u)).collect();
116 let out = edges
117 .into_iter()
118 .filter_map(|e| {
119 map.get(&e.follower_id).map(|u| FollowerSummary {
120 user: super::super::profile::UserPublic::from(u.clone()),
121 since: e
122 .created_at
123 .format(&time::format_description::well_known::Rfc3339)
124 .unwrap_or_default(),
125 })
126 })
127 .collect();
128 Ok(out)
69129 }
70 let edges: Vec<followers::Model> = sel.all(&writer).await?;
71 if edges.is_empty() {
72 return Ok(Vec::new());
73 }
74 let follower_ids: Vec<Uuid> = edges.iter().map(|e| e.follower_id).collect();
75 let users: Vec<user_entity::Model> = user_entity::Entity::find()
76 .filter(user_entity::COLUMN.uid.is_in(follower_ids))
77 .all(&writer)
78 .await?;
79 let map: std::collections::HashMap<Uuid, user_entity::Model> =
80 users.into_iter().map(|u| (u.uid, u)).collect();
81 let out = edges
82 .into_iter()
83 .filter_map(|e| {
84 map.get(&e.follower_id).map(|u| FollowerSummary {
85 user: super::super::profile::UserPublic::from(u.clone()),
86 since: e
87 .created_at
88 .format(&time::format_description::well_known::Rfc3339)
89 .unwrap_or_default(),
90 })
91 })
92 .collect();
93 Ok(out)
130 .await;
131 metrics::record_outcome(metrics::FEATURE_SOCIAL_LIST_FOLLOWERS, &result);
132 result
94133}
95134
96135pub async fn list_following(
97136 svc: &UserService,
98137 q: ListFollowingQuery,
99138) -> UserResult<Vec<FollowerSummary>> {
100 let limit = q.limit.min(svc.config.max_profile_pagination).max(1) as u64;
101 let writer = svc.db.writer();
102 let mut sel = followers::Entity::find()
103 .filter(followers::COLUMN.follower_id.eq(q.user_uid))
104 .order_by_asc(followers::COLUMN.id)
105 .limit(Some(limit));
106 if let Some(cursor) = q.cursor {
107 sel = sel.filter(followers::COLUMN.id.gt(cursor));
108 }
109 let edges: Vec<followers::Model> = sel.all(&writer).await?;
110 if edges.is_empty() {
111 return Ok(Vec::new());
112 }
113 let followed_ids: Vec<Uuid> = edges.iter().map(|e| e.followed_id).collect();
114 let users: Vec<user_entity::Model> = user_entity::Entity::find()
115 .filter(user_entity::COLUMN.uid.is_in(followed_ids))
116 .all(&writer)
117 .await?;
118 let map: std::collections::HashMap<Uuid, user_entity::Model> =
119 users.into_iter().map(|u| (u.uid, u)).collect();
120 let out = edges
121 .into_iter()
122 .filter_map(|e| {
123 map.get(&e.followed_id).map(|u| FollowerSummary {
124 user: super::super::profile::UserPublic::from(u.clone()),
125 since: e
126 .created_at
127 .format(&time::format_description::well_known::Rfc3339)
128 .unwrap_or_default(),
139 let _timer = metrics::Timer::start(metrics::FEATURE_SOCIAL_LIST_FOLLOWING);
140 let result: UserResult<Vec<FollowerSummary>> = async {
141 let limit = q.limit.min(svc.config.max_profile_pagination).max(1) as u64;
142 let writer = svc.db.writer();
143 let mut sel = followers::Entity::find()
144 .filter(followers::COLUMN.follower_id.eq(q.user_uid))
145 .order_by_asc(followers::COLUMN.id)
146 .limit(Some(limit));
147 if let Some(cursor) = q.cursor {
148 sel = sel.filter(followers::COLUMN.id.gt(cursor));
149 }
150 let edges: Vec<followers::Model> = sel.all(&writer).await?;
151 if edges.is_empty() {
152 return Ok(Vec::new());
153 }
154 let followed_ids: Vec<Uuid> = edges.iter().map(|e| e.followed_id).collect();
155 let users: Vec<user_entity::Model> = user_entity::Entity::find()
156 .filter(user_entity::COLUMN.uid.is_in(followed_ids))
157 .all(&writer)
158 .await?;
159 let map: std::collections::HashMap<Uuid, user_entity::Model> =
160 users.into_iter().map(|u| (u.uid, u)).collect();
161 let out = edges
162 .into_iter()
163 .filter_map(|e| {
164 map.get(&e.followed_id).map(|u| FollowerSummary {
165 user: super::super::profile::UserPublic::from(u.clone()),
166 since: e
167 .created_at
168 .format(&time::format_description::well_known::Rfc3339)
169 .unwrap_or_default(),
170 })
129171 })
130 })
131 .collect();
132 Ok(out)
172 .collect();
173 Ok(out)
174 }
175 .await;
176 metrics::record_outcome(metrics::FEATURE_SOCIAL_LIST_FOLLOWING, &result);
177 result
133178}
134179
135180pub async fn block(svc: &UserService, blocker: Uuid, target: Uuid) -> UserResult<()> {
136 if blocker == target {
137 return Err(UserError::BadRequest("cannot block self".into()));
138 }
139 let writer = svc.db.writer();
140 if user_entity::Entity::find_by_id(target)
141 .one(&writer)
142 .await?
143 .is_none()
144 {
145 return Err(UserError::NotFound(format!("user {target}")));
146 }
147 let txn = writer.begin().await?;
181 let _timer = metrics::Timer::start(metrics::FEATURE_SOCIAL_BLOCK);
182 let result: UserResult<()> = async {
183 if blocker == target {
184 return Err(UserError::BadRequest("cannot block self".into()));
185 }
186 let writer = svc.db.writer();
187 if user_entity::Entity::find_by_id(target)
188 .one(&writer)
189 .await?
190 .is_none()
191 {
192 return Err(UserError::NotFound(format!("user {target}")));
193 }
194 let txn = writer.begin().await?;
148195
149 let existing = blocks::Entity::find()
150 .filter(blocks::COLUMN.blocker_id.eq(blocker))
151 .filter(blocks::COLUMN.blocked_id.eq(target))
152 .one(&txn)
153 .await?;
154 if existing.is_none() {
155 let active: blocks::ActiveModel = blocks::ActiveModel::builder()
156 .set_id(Uuid::new_v4())
157 .set_blocker_id(blocker)
158 .set_blocked_id(target)
159 .set_created_at(time::OffsetDateTime::now_utc())
160 .into();
161 active.insert(&txn).await?;
162 }
196 let existing = blocks::Entity::find()
197 .filter(blocks::COLUMN.blocker_id.eq(blocker))
198 .filter(blocks::COLUMN.blocked_id.eq(target))
199 .one(&txn)
200 .await?;
201 if existing.is_none() {
202 let active: blocks::ActiveModel = blocks::ActiveModel::builder()
203 .set_id(Uuid::new_v4())
204 .set_blocker_id(blocker)
205 .set_blocked_id(target)
206 .set_created_at(time::OffsetDateTime::now_utc())
207 .into();
208 active.insert(&txn).await?;
209 }
163210
164 let a = followers::COLUMN.follower_id
165 .eq(blocker)
166 .and(followers::COLUMN.followed_id.eq(target));
167 let b = followers::COLUMN.follower_id
168 .eq(target)
169 .and(followers::COLUMN.followed_id.eq(blocker));
170 followers::Entity::delete_many()
171 .filter(a.or(b))
172 .exec(&txn)
173 .await?;
211 let a = followers::COLUMN.follower_id
212 .eq(blocker)
213 .and(followers::COLUMN.followed_id.eq(target));
214 let b = followers::COLUMN.follower_id
215 .eq(target)
216 .and(followers::COLUMN.followed_id.eq(blocker));
217 followers::Entity::delete_many()
218 .filter(a.or(b))
219 .exec(&txn)
220 .await?;
174221
175 txn.commit().await?;
176 Ok(())
222 txn.commit().await?;
223 Ok(())
224 }
225 .await;
226 metrics::record_outcome(metrics::FEATURE_SOCIAL_BLOCK, &result);
227 match &result {
228 Ok(()) => record_social_block(SocialBlockOutcome::Created),
229 Err(UserError::BadRequest(_)) => record_social_block(SocialBlockOutcome::SelfBlock),
230 Err(UserError::NotFound(_)) => record_social_block(SocialBlockOutcome::NotFound),
231 Err(_) => {}
232 }
233 result
177234}
178235
179236pub async fn unblock(svc: &UserService, blocker: Uuid, target: Uuid) -> UserResult<()> {
180 let writer = svc.db.writer();
181 blocks::Entity::delete_many()
182 .filter(blocks::COLUMN.blocker_id.eq(blocker))
183 .filter(blocks::COLUMN.blocked_id.eq(target))
184 .exec(&writer)
185 .await?;
186 Ok(())
237 let _timer = metrics::Timer::start(metrics::FEATURE_SOCIAL_UNBLOCK);
238 let result: UserResult<()> = async {
239 let writer = svc.db.writer();
240 blocks::Entity::delete_many()
241 .filter(blocks::COLUMN.blocker_id.eq(blocker))
242 .filter(blocks::COLUMN.blocked_id.eq(target))
243 .exec(&writer)
244 .await?;
245 Ok(())
246 }
247 .await;
248 metrics::record_outcome(metrics::FEATURE_SOCIAL_UNBLOCK, &result);
249 result
187250}
188251
189252pub async fn list_blocks(
@@ -191,37 +254,43 @@
191254 caller: Uuid,
192255 q: ListBlocksQuery,
193256) -> UserResult<Vec<BlockedSummary>> {
194 let limit = q.limit.min(svc.config.max_profile_pagination).max(1) as u64;
195 let writer = svc.db.writer();
196 let mut sel = blocks::Entity::find()
197 .filter(blocks::COLUMN.blocker_id.eq(caller))
198 .order_by_asc(blocks::COLUMN.id)
199 .limit(Some(limit));
200 if let Some(cursor) = q.cursor {
201 sel = sel.filter(blocks::COLUMN.id.gt(cursor));
257 let _timer = metrics::Timer::start(metrics::FEATURE_SOCIAL_LIST_BLOCKS);
258 let result: UserResult<Vec<BlockedSummary>> = async {
259 let limit = q.limit.min(svc.config.max_profile_pagination).max(1) as u64;
260 let writer = svc.db.writer();
261 let mut sel = blocks::Entity::find()
262 .filter(blocks::COLUMN.blocker_id.eq(caller))
263 .order_by_asc(blocks::COLUMN.id)
264 .limit(Some(limit));
265 if let Some(cursor) = q.cursor {
266 sel = sel.filter(blocks::COLUMN.id.gt(cursor));
267 }
268 let edges: Vec<blocks::Model> = sel.all(&writer).await?;
269 if edges.is_empty() {
270 return Ok(Vec::new());
271 }
272 let blocked_ids: Vec<Uuid> = edges.iter().map(|e| e.blocked_id).collect();
273 let users: Vec<user_entity::Model> = user_entity::Entity::find()
274 .filter(user_entity::COLUMN.uid.is_in(blocked_ids))
275 .all(&writer)
276 .await?;
277 let map: std::collections::HashMap<Uuid, user_entity::Model> =
278 users.into_iter().map(|u| (u.uid, u)).collect();
279 let out = edges
280 .into_iter()
281 .filter_map(|e| {
282 map.get(&e.blocked_id).map(|u| BlockedSummary {
283 user: super::super::profile::UserPublic::from(u.clone()),
284 since: e
285 .created_at
286 .format(&time::format_description::well_known::Rfc3339)
287 .unwrap_or_default(),
288 })
289 })
290 .collect();
291 Ok(out)
202292 }
203 let edges: Vec<blocks::Model> = sel.all(&writer).await?;
204 if edges.is_empty() {
205 return Ok(Vec::new());
206 }
207 let blocked_ids: Vec<Uuid> = edges.iter().map(|e| e.blocked_id).collect();
208 let users: Vec<user_entity::Model> = user_entity::Entity::find()
209 .filter(user_entity::COLUMN.uid.is_in(blocked_ids))
210 .all(&writer)
211 .await?;
212 let map: std::collections::HashMap<Uuid, user_entity::Model> =
213 users.into_iter().map(|u| (u.uid, u)).collect();
214 let out = edges
215 .into_iter()
216 .filter_map(|e| {
217 map.get(&e.blocked_id).map(|u| BlockedSummary {
218 user: super::super::profile::UserPublic::from(u.clone()),
219 since: e
220 .created_at
221 .format(&time::format_description::well_known::Rfc3339)
222 .unwrap_or_default(),
223 })
224 })
225 .collect();
226 Ok(out)
227}
No newline at end of file
293 .await;
294 metrics::record_outcome(metrics::FEATURE_SOCIAL_LIST_BLOCKS, &result);
295 result
296}
Modified lib/user/features/suspensions/usecase.rs View lib/user/features/suspensions/usecase.rs
@@ -6,6 +6,9 @@
66
77use crate::error::{UserError, UserResult};
88use crate::features::UserService;
9use crate::metrics::{
10 self, SuspensionOp, SuspensionOutcome, record_suspension_operation,
11};
912
1013use super::{RevokeSuspensionCmd, SuspendCmd, SuspensionView};
1114
@@ -15,74 +18,90 @@
1518 target: Uuid,
1619 cmd: SuspendCmd,
1720) -> UserResult<SuspensionView> {
18 let ends_at = match cmd.ends_at.as_deref() {
19 Some(s) => Some(
20 time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)
21 .map_err(|e| UserError::BadRequest(format!("invalid ends_at: {e}")))?,
22 ),
23 None => None,
24 };
25 if matches!(cmd.suspension_type, super::SuspensionType::Temporary) && ends_at.is_none() {
26 return Err(UserError::BadRequest(
27 "temporary suspension requires ends_at".into(),
28 ));
29 }
21 let _timer = metrics::Timer::start(metrics::FEATURE_SUSPENSIONS_CREATE);
22 let result: UserResult<SuspensionView> = async {
23 let ends_at = match cmd.ends_at.as_deref() {
24 Some(s) => Some(
25 time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)
26 .map_err(|e| UserError::BadRequest(format!("invalid ends_at: {e}")))?,
27 ),
28 None => None,
29 };
30 if matches!(cmd.suspension_type, super::SuspensionType::Temporary) && ends_at.is_none() {
31 return Err(UserError::BadRequest(
32 "temporary suspension requires ends_at".into(),
33 ));
34 }
3035
31 let writer = svc.db.writer();
32 let txn = writer.begin().await?;
36 let writer = svc.db.writer();
37 let txn = writer.begin().await?;
3338
34 if user_entity::Entity::find_by_id(target)
35 .one(&txn)
36 .await?
37 .is_none()
38 {
39 return Err(UserError::NotFound(format!("user {target}")));
40 }
39 if user_entity::Entity::find_by_id(target)
40 .one(&txn)
41 .await?
42 .is_none()
43 {
44 return Err(UserError::NotFound(format!("user {target}")));
45 }
4146
42 let now = time::OffsetDateTime::now_utc();
43 let active: db::user::user_suspensions::ActiveModel =
44 db::user::user_suspensions::ActiveModel::builder()
45 .set_id(Uuid::new_v4())
46 .set_user_id(target)
47 .set_suspension_type(cmd.suspension_type)
48 .set_reason(cmd.reason)
49 .set_suspended_by(Some(caller))
50 .set_starts_at(now)
51 .set_ends_at(ends_at)
52 .set_revoked_at(None)
53 .set_revoked_by(None)
54 .set_created_at(now)
55 .set_updated_at(now)
56 .into();
57 let saved = active.insert(&txn).await?;
47 let now = time::OffsetDateTime::now_utc();
48 let active: db::user::user_suspensions::ActiveModel =
49 db::user::user_suspensions::ActiveModel::builder()
50 .set_id(Uuid::new_v4())
51 .set_user_id(target)
52 .set_suspension_type(cmd.suspension_type)
53 .set_reason(cmd.reason)
54 .set_suspended_by(Some(caller))
55 .set_starts_at(now)
56 .set_ends_at(ends_at)
57 .set_revoked_at(None)
58 .set_revoked_by(None)
59 .set_created_at(now)
60 .set_updated_at(now)
61 .into();
62 let saved = active.insert(&txn).await?;
5863
59 let user: user_entity::Model = user_entity::Entity::find_by_id(target)
60 .one(&txn)
61 .await?
62 .ok_or_else(|| UserError::NotFound(format!("user {target}")))?;
63 let mut user_active: user_entity::ActiveModel = user.into();
64 user_active.is_restricted = Set(true);
65 user_active.updated_at = Set(now);
66 user_active.update(&txn).await?;
64 let user: user_entity::Model = user_entity::Entity::find_by_id(target)
65 .one(&txn)
66 .await?
67 .ok_or_else(|| UserError::NotFound(format!("user {target}")))?;
68 let mut user_active: user_entity::ActiveModel = user.into();
69 user_active.is_restricted = Set(true);
70 user_active.updated_at = Set(now);
71 user_active.update(&txn).await?;
6772
68 // B4: revoke all refresh sessions atomically with the suspension so a
69 // suspended user cannot refresh to keep the session alive.
70 jwt::session::delete_sessions_by_user(&txn, target)
71 .await
72 .map_err(|e| UserError::Internal(format!("failed to revoke sessions: {e}")))?;
73 // B4: revoke all refresh sessions atomically with the suspension so a
74 // suspended user cannot refresh to keep the session alive.
75 jwt::session::delete_sessions_by_user(&txn, target)
76 .await
77 .map_err(|e| UserError::Internal(format!("failed to revoke sessions: {e}")))?;
7378
74 txn.commit().await?;
79 txn.commit().await?;
7580
76 // VULN-002/A3: a suspension must take effect immediately — deny-list the
77 // user's live access tokens so they stop validating without waiting for
78 // expiry. (Refresh-session revocation is handled alongside the refresh
79 // gate; see the refresh use-case.)
80 let ttl = std::cmp::max(svc.jwt.settings.access_ttl.whole_seconds(), 1) as u64;
81 rate_limit::AccessTokenBlacklist::new(svc.kv.clone())
82 .revoke_user(target, ttl)
83 .await?;
81 // VULN-002/A3: a suspension must take effect immediately — deny-list the
82 // user's live access tokens so they stop validating without waiting for
83 // expiry. (Refresh-session revocation is handled alongside the refresh
84 // gate; see the refresh use-case.)
85 let ttl = std::cmp::max(svc.jwt.settings.access_ttl.whole_seconds(), 1) as u64;
86 rate_limit::AccessTokenBlacklist::new(svc.kv.clone())
87 .revoke_user(target, ttl)
88 .await?;
8489
85 Ok(SuspensionView::from(saved))
90 Ok(SuspensionView::from(saved))
91 }
92 .await;
93 metrics::record_outcome(metrics::FEATURE_SUSPENSIONS_CREATE, &result);
94 match &result {
95 Ok(_) => record_suspension_operation(SuspensionOp::Create, SuspensionOutcome::Success),
96 Err(UserError::BadRequest(_)) => {
97 record_suspension_operation(SuspensionOp::Create, SuspensionOutcome::BadRequest);
98 }
99 Err(UserError::NotFound(_)) => {
100 record_suspension_operation(SuspensionOp::Create, SuspensionOutcome::NotFound);
101 }
102 Err(_) => record_suspension_operation(SuspensionOp::Create, SuspensionOutcome::Other),
103 }
104 result
86105}
87106
88107pub async fn revoke(
@@ -91,47 +110,63 @@
91110 suspension_id: Uuid,
92111 cmd: RevokeSuspensionCmd,
93112) -> UserResult<()> {
94 let writer = svc.db.writer();
95 let txn = writer.begin().await?;
96 let row = db::user::user_suspensions::Entity::find_by_id(suspension_id)
97 .one(&txn)
98 .await?
99 .ok_or_else(|| UserError::NotFound(format!("suspension {suspension_id}")))?;
100 if row.revoked_at.is_some() {
101 return Err(UserError::BadRequest("already revoked".into()));
102 }
103 let user_id = row.user_id;
104 let now = time::OffsetDateTime::now_utc();
105 let mut am: db::user::user_suspensions::ActiveModel = row.into();
106 am.revoked_at = Set(Some(now));
107 am.revoked_by = Set(Some(caller));
108 if cmd.reason.is_some() {
109 am.reason = Set(cmd.reason);
110 }
111 am.updated_at = Set(now);
112 am.update(&txn).await?;
113
114 let still_active: Option<db::user::user_suspensions::Model> =
115 db::user::user_suspensions::Entity::find()
116 .filter(db::user::user_suspensions::COLUMN.user_id.eq(user_id))
117 .filter(db::user::user_suspensions::COLUMN.revoked_at.is_null())
118 .filter(
119 db::user::user_suspensions::COLUMN.ends_at
120 .is_null()
121 .or(db::user::user_suspensions::COLUMN.ends_at.gt(now)),
122 )
113 let _timer = metrics::Timer::start(metrics::FEATURE_SUSPENSIONS_REVOKE);
114 let result: UserResult<()> = async {
115 let writer = svc.db.writer();
116 let txn = writer.begin().await?;
117 let row = db::user::user_suspensions::Entity::find_by_id(suspension_id)
123118 .one(&txn)
124 .await?;
125 if still_active.is_none()
126 && let Some(user) = user_entity::Entity::find_by_id(user_id).one(&txn).await? {
127 let mut user_active: user_entity::ActiveModel = user.into();
128 user_active.is_restricted = Set(false);
129 user_active.updated_at = Set(now);
130 user_active.update(&txn).await?;
119 .await?
120 .ok_or_else(|| UserError::NotFound(format!("suspension {suspension_id}")))?;
121 if row.revoked_at.is_some() {
122 return Err(UserError::BadRequest("already revoked".into()));
123 }
124 let user_id = row.user_id;
125 let now = time::OffsetDateTime::now_utc();
126 let mut am: db::user::user_suspensions::ActiveModel = row.into();
127 am.revoked_at = Set(Some(now));
128 am.revoked_by = Set(Some(caller));
129 if cmd.reason.is_some() {
130 am.reason = Set(cmd.reason);
131131 }
132 am.updated_at = Set(now);
133 am.update(&txn).await?;
132134
133 txn.commit().await?;
134 Ok(())
135 let still_active: Option<db::user::user_suspensions::Model> =
136 db::user::user_suspensions::Entity::find()
137 .filter(db::user::user_suspensions::COLUMN.user_id.eq(user_id))
138 .filter(db::user::user_suspensions::COLUMN.revoked_at.is_null())
139 .filter(
140 db::user::user_suspensions::COLUMN.ends_at
141 .is_null()
142 .or(db::user::user_suspensions::COLUMN.ends_at.gt(now)),
143 )
144 .one(&txn)
145 .await?;
146 if still_active.is_none()
147 && let Some(user) = user_entity::Entity::find_by_id(user_id).one(&txn).await? {
148 let mut user_active: user_entity::ActiveModel = user.into();
149 user_active.is_restricted = Set(false);
150 user_active.updated_at = Set(now);
151 user_active.update(&txn).await?;
152 }
153
154 txn.commit().await?;
155 Ok(())
156 }
157 .await;
158 metrics::record_outcome(metrics::FEATURE_SUSPENSIONS_REVOKE, &result);
159 match &result {
160 Ok(_) => record_suspension_operation(SuspensionOp::Revoke, SuspensionOutcome::Success),
161 Err(UserError::BadRequest(_)) => {
162 record_suspension_operation(SuspensionOp::Revoke, SuspensionOutcome::AlreadyActive);
163 }
164 Err(UserError::NotFound(_)) => {
165 record_suspension_operation(SuspensionOp::Revoke, SuspensionOutcome::NotFound);
166 }
167 Err(_) => record_suspension_operation(SuspensionOp::Revoke, SuspensionOutcome::Other),
168 }
169 result
135170}
136171
137172pub async fn list_user(
@@ -139,22 +174,34 @@
139174 target: Uuid,
140175 include_revoked: bool,
141176) -> UserResult<Vec<SuspensionView>> {
142 let db = svc.db.reader();
143 let mut sel = db::user::user_suspensions::Entity::find()
144 .filter(db::user::user_suspensions::COLUMN.user_id.eq(target));
145 if !include_revoked {
146 sel = sel.filter(db::user::user_suspensions::COLUMN.revoked_at.is_null());
177 let _timer = metrics::Timer::start(metrics::FEATURE_SUSPENSIONS_LIST_USER);
178 let result: UserResult<Vec<SuspensionView>> = async {
179 let db = svc.db.reader();
180 let mut sel = db::user::user_suspensions::Entity::find()
181 .filter(db::user::user_suspensions::COLUMN.user_id.eq(target));
182 if !include_revoked {
183 sel = sel.filter(db::user::user_suspensions::COLUMN.revoked_at.is_null());
184 }
185 let rows = sel.all(&db).await?;
186 Ok(rows.into_iter().map(SuspensionView::from).collect())
147187 }
148 let rows = sel.all(&db).await?;
149 Ok(rows.into_iter().map(SuspensionView::from).collect())
188 .await;
189 metrics::record_outcome(metrics::FEATURE_SUSPENSIONS_LIST_USER, &result);
190 result
150191}
151192
152193pub async fn list_all(svc: &UserService, include_revoked: bool) -> UserResult<Vec<SuspensionView>> {
153 let db = svc.db.reader();
154 let mut sel = db::user::user_suspensions::Entity::find();
155 if !include_revoked {
156 sel = sel.filter(db::user::user_suspensions::COLUMN.revoked_at.is_null());
194 let _timer = metrics::Timer::start(metrics::FEATURE_SUSPENSIONS_LIST_ALL);
195 let result: UserResult<Vec<SuspensionView>> = async {
196 let db = svc.db.reader();
197 let mut sel = db::user::user_suspensions::Entity::find();
198 if !include_revoked {
199 sel = sel.filter(db::user::user_suspensions::COLUMN.revoked_at.is_null());
200 }
201 let rows = sel.all(&db).await?;
202 Ok(rows.into_iter().map(SuspensionView::from).collect())
157203 }
158 let rows = sel.all(&db).await?;
159 Ok(rows.into_iter().map(SuspensionView::from).collect())
160}
No newline at end of file
204 .await;
205 metrics::record_outcome(metrics::FEATURE_SUSPENSIONS_LIST_ALL, &result);
206 result
207}