Skip to content
Clone

refactor(space): refresh all feature usecase implementations

zhenyi committed
parent 59e464c commit 7a46188 Browse files

8 files changed

+1162 −894

File tree 8
Modified lib/space/features/bans/usecase.rs View lib/space/features/bans/usecase.rs
@@ -6,6 +6,7 @@
66
77use crate::error::{SpaceError, SpaceResult};
88use crate::features::SpaceService;
9use crate::metrics;
910
1011use super::{CreateBanCmd, SpaceBanPage, SpaceBanView, UpdateBanCmd};
1112
@@ -31,103 +32,133 @@
3132}
3233
3334pub async fn ban_user(svc: &SpaceService, caller: Uuid, space_id: Uuid, cmd: CreateBanCmd) -> SpaceResult<SpaceBanView> {
34 owner_space(svc, caller, space_id).await?;
35 let expires_at = parse_ts(cmd.expires_at.as_deref())?;
36 let db = svc.db.writer();
37 let now = OffsetDateTime::now_utc();
38 let existing = ban_entity::Entity::find_by_id((space_id, cmd.user_id)).one(&db).await?;
39 if existing.is_some() {
40 return Err(SpaceError::Conflict(format!("user {} is already banned in space {space_id}", cmd.user_id)));
35 let _timer = metrics::Timer::start(metrics::FEATURE_BANS_CREATE);
36 let result: SpaceResult<SpaceBanView> = async {
37 owner_space(svc, caller, space_id).await?;
38 let expires_at = parse_ts(cmd.expires_at.as_deref())?;
39 let db = svc.db.writer();
40 let now = OffsetDateTime::now_utc();
41 let existing = ban_entity::Entity::find_by_id((space_id, cmd.user_id)).one(&db).await?;
42 if existing.is_some() {
43 return Err(SpaceError::Conflict(format!("user {} is already banned in space {space_id}", cmd.user_id)));
44 }
45 let active: ban_entity::ActiveModel = ban_entity::ActiveModel::builder()
46 .set_space_id(space_id)
47 .set_user_id(cmd.user_id)
48 .set_reason(cmd.reason)
49 .set_banned_by(caller)
50 .set_created_at(now)
51 .set_updated_at(now)
52 .set_expires_at(expires_at)
53 .set_allow_agent_join(cmd.allow_agent_join.unwrap_or(false))
54 .set_allow_speak(cmd.allow_speak.unwrap_or(false))
55 .set_allow_push_repo(cmd.allow_push_repo.unwrap_or(false))
56 .set_allow_comment_any(cmd.allow_comment_any.unwrap_or(false))
57 .into();
58 let saved = active.insert(&db).await?;
59 Ok(saved.into())
4160 }
42 let active: ban_entity::ActiveModel = ban_entity::ActiveModel::builder()
43 .set_space_id(space_id)
44 .set_user_id(cmd.user_id)
45 .set_reason(cmd.reason)
46 .set_banned_by(caller)
47 .set_created_at(now)
48 .set_updated_at(now)
49 .set_expires_at(expires_at)
50 .set_allow_agent_join(cmd.allow_agent_join.unwrap_or(false))
51 .set_allow_speak(cmd.allow_speak.unwrap_or(false))
52 .set_allow_push_repo(cmd.allow_push_repo.unwrap_or(false))
53 .set_allow_comment_any(cmd.allow_comment_any.unwrap_or(false))
54 .into();
55 let saved = active.insert(&db).await?;
56 Ok(saved.into())
61 .await;
62 metrics::record_outcome(metrics::FEATURE_BANS_CREATE, &result);
63 result
5764}
5865
5966pub async fn get_ban(svc: &SpaceService, caller: Uuid, space_id: Uuid, user_id: Uuid) -> SpaceResult<SpaceBanView> {
60 owner_space(svc, caller, space_id).await?;
61 let db = svc.db.reader();
62 let ban = ban_entity::Entity::find_by_id((space_id, user_id))
63 .one(&db)
64 .await?
65 .ok_or_else(|| SpaceError::NotFound(format!("ban {user_id} in space {space_id}")))?;
66 Ok(ban.into())
67 let _timer = metrics::Timer::start(metrics::FEATURE_BANS_GET);
68 let result: SpaceResult<SpaceBanView> = async {
69 owner_space(svc, caller, space_id).await?;
70 let db = svc.db.reader();
71 let ban = ban_entity::Entity::find_by_id((space_id, user_id))
72 .one(&db)
73 .await?
74 .ok_or_else(|| SpaceError::NotFound(format!("ban {user_id} in space {space_id}")))?;
75 Ok(ban.into())
76 }
77 .await;
78 metrics::record_outcome(metrics::FEATURE_BANS_GET, &result);
79 result
6780}
6881
6982pub async fn update_ban(svc: &SpaceService, caller: Uuid, space_id: Uuid, user_id: Uuid, cmd: UpdateBanCmd) -> SpaceResult<SpaceBanView> {
70 owner_space(svc, caller, space_id).await?;
71 let db = svc.db.writer();
72 let txn = db.begin().await?;
73 let ban = ban_entity::Entity::find_by_id((space_id, user_id))
74 .one(&txn)
75 .await?
76 .ok_or_else(|| SpaceError::NotFound(format!("ban {user_id} in space {space_id}")))?;
77 let mut active: ban_entity::ActiveModel = ban.into();
78 if let Some(reason) = cmd.reason {
79 active.reason = Set(reason);
80 }
81 if let Some(expires_at) = cmd.expires_at {
82 active.expires_at = Set(parse_ts(expires_at.as_deref())?);
83 }
84 if let Some(v) = cmd.allow_agent_join {
85 active.allow_agent_join = Set(v);
86 }
87 if let Some(v) = cmd.allow_speak {
88 active.allow_speak = Set(v);
89 }
90 if let Some(v) = cmd.allow_push_repo {
91 active.allow_push_repo = Set(v);
92 }
93 if let Some(v) = cmd.allow_comment_any {
94 active.allow_comment_any = Set(v);
83 let _timer = metrics::Timer::start(metrics::FEATURE_BANS_UPDATE);
84 let result: SpaceResult<SpaceBanView> = async {
85 owner_space(svc, caller, space_id).await?;
86 let db = svc.db.writer();
87 let txn = db.begin().await?;
88 let ban = ban_entity::Entity::find_by_id((space_id, user_id))
89 .one(&txn)
90 .await?
91 .ok_or_else(|| SpaceError::NotFound(format!("ban {user_id} in space {space_id}")))?;
92 let mut active: ban_entity::ActiveModel = ban.into();
93 if let Some(reason) = cmd.reason {
94 active.reason = Set(reason);
95 }
96 if let Some(expires_at) = cmd.expires_at {
97 active.expires_at = Set(parse_ts(expires_at.as_deref())?);
98 }
99 if let Some(v) = cmd.allow_agent_join {
100 active.allow_agent_join = Set(v);
101 }
102 if let Some(v) = cmd.allow_speak {
103 active.allow_speak = Set(v);
104 }
105 if let Some(v) = cmd.allow_push_repo {
106 active.allow_push_repo = Set(v);
107 }
108 if let Some(v) = cmd.allow_comment_any {
109 active.allow_comment_any = Set(v);
110 }
111 active.updated_at = Set(OffsetDateTime::now_utc());
112 let saved = active.update(&txn).await?;
113 txn.commit().await?;
114 Ok(saved.into())
95115 }
96 active.updated_at = Set(OffsetDateTime::now_utc());
97 let saved = active.update(&txn).await?;
98 txn.commit().await?;
99 Ok(saved.into())
116 .await;
117 metrics::record_outcome(metrics::FEATURE_BANS_UPDATE, &result);
118 result
100119}
101120
102121pub async fn unban_user(svc: &SpaceService, caller: Uuid, space_id: Uuid, user_id: Uuid) -> SpaceResult<()> {
103 owner_space(svc, caller, space_id).await?;
104 let db = svc.db.writer();
105 let txn = db.begin().await?;
106 let deleted = ban_entity::Entity::delete_by_id((space_id, user_id)).exec(&txn).await?;
107 if deleted.rows_affected == 0 {
108 return Err(SpaceError::NotFound(format!("ban {user_id} in space {space_id}")));
122 let _timer = metrics::Timer::start(metrics::FEATURE_BANS_REMOVE);
123 let result: SpaceResult<()> = async {
124 owner_space(svc, caller, space_id).await?;
125 let db = svc.db.writer();
126 let txn = db.begin().await?;
127 let deleted = ban_entity::Entity::delete_by_id((space_id, user_id)).exec(&txn).await?;
128 if deleted.rows_affected == 0 {
129 return Err(SpaceError::NotFound(format!("ban {user_id} in space {space_id}")));
130 }
131 txn.commit().await?;
132 Ok(())
109133 }
110 txn.commit().await?;
111 Ok(())
134 .await;
135 metrics::record_outcome(metrics::FEATURE_BANS_REMOVE, &result);
136 result
112137}
113138
114139pub async fn list_bans(svc: &SpaceService, caller: Uuid, space_id: Uuid, limit: u64, offset: u64) -> SpaceResult<SpaceBanPage> {
115 owner_space(svc, caller, space_id).await?;
116 let db = svc.db.reader();
117 let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1);
118 let items = ban_entity::Entity::find()
119 .filter(ban_entity::Column::SpaceId.eq(space_id))
120 .order_by_desc(ban_entity::Column::CreatedAt)
121 .limit(Some(limit))
122 .offset(Some(offset))
123 .all(&db)
124 .await?;
125 let total = ban_entity::Entity::find()
126 .filter(ban_entity::Column::SpaceId.eq(space_id))
127 .count(&db)
128 .await?;
129 Ok(SpaceBanPage {
130 items: items.into_iter().map(SpaceBanView::from).collect(),
131 total: total as i64,
132 })
140 let _timer = metrics::Timer::start(metrics::FEATURE_BANS_LIST);
141 let result: SpaceResult<SpaceBanPage> = async {
142 owner_space(svc, caller, space_id).await?;
143 let db = svc.db.reader();
144 let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1);
145 let items = ban_entity::Entity::find()
146 .filter(ban_entity::Column::SpaceId.eq(space_id))
147 .order_by_desc(ban_entity::Column::CreatedAt)
148 .limit(Some(limit))
149 .offset(Some(offset))
150 .all(&db)
151 .await?;
152 let total = ban_entity::Entity::find()
153 .filter(ban_entity::Column::SpaceId.eq(space_id))
154 .count(&db)
155 .await?;
156 Ok(SpaceBanPage {
157 items: items.into_iter().map(SpaceBanView::from).collect(),
158 total: total as i64,
159 })
160 }
161 .await;
162 metrics::record_outcome(metrics::FEATURE_BANS_LIST, &result);
163 result
133164}
No newline at end of file
Modified lib/space/features/channel_members/usecase.rs View lib/space/features/channel_members/usecase.rs
@@ -9,6 +9,7 @@
99use crate::error::{SpaceError, SpaceResult};
1010use crate::features::authz;
1111use crate::features::SpaceService;
12use crate::metrics;
1213
1314use super::{AddChannelMemberCmd, ChannelMemberPage, ChannelMemberView, UpdateChannelMemberCmd};
1415
@@ -69,32 +70,38 @@
6970 channel_id: Uuid,
7071 cmd: AddChannelMemberCmd,
7172) -> SpaceResult<ChannelMemberView> {
72 authz::require_perm(svc, caller, space_id, authz::MANAGE_CHANNELS).await?;
73 let db = svc.db.writer();
74 channel_in_space(&db, space_id, channel_id).await?;
73 let _timer = metrics::Timer::start(metrics::FEATURE_CHANNEL_MEMBERS_ADD);
74 let result: SpaceResult<ChannelMemberView> = async {
75 authz::require_perm(svc, caller, space_id, authz::MANAGE_CHANNELS).await?;
76 let db = svc.db.writer();
77 channel_in_space(&db, space_id, channel_id).await?;
7578
76 let exists = member_entity::Entity::find_by_id((channel_id, cmd.user_id))
77 .one(&db)
78 .await?;
79 if exists.is_some() {
80 return Err(SpaceError::Conflict(format!(
81 "user {} is already a member of channel {channel_id}",
82 cmd.user_id
83 )));
84 }
79 let exists = member_entity::Entity::find_by_id((channel_id, cmd.user_id))
80 .one(&db)
81 .await?;
82 if exists.is_some() {
83 return Err(SpaceError::Conflict(format!(
84 "user {} is already a member of channel {channel_id}",
85 cmd.user_id
86 )));
87 }
8588
86 let now = OffsetDateTime::now_utc();
87 let active: member_entity::ActiveModel = member_entity::ActiveModel::builder()
88 .set_channel_id(channel_id)
89 .set_user_id(cmd.user_id)
90 .set_role(member_entity::ChannelMemberRole::from(cmd.role.unwrap_or_default()))
91 .set_muted(cmd.muted.unwrap_or(false))
92 .set_muted_until(None)
93 .set_last_read_message_id(None)
94 .set_joined_at(now)
95 .into();
96 let saved = active.insert(&db).await?;
97 Ok(saved.into())
89 let now = OffsetDateTime::now_utc();
90 let active: member_entity::ActiveModel = member_entity::ActiveModel::builder()
91 .set_channel_id(channel_id)
92 .set_user_id(cmd.user_id)
93 .set_role(member_entity::ChannelMemberRole::from(cmd.role.unwrap_or_default()))
94 .set_muted(cmd.muted.unwrap_or(false))
95 .set_muted_until(None)
96 .set_last_read_message_id(None)
97 .set_joined_at(now)
98 .into();
99 let saved = active.insert(&db).await?;
100 Ok(saved.into())
101 }
102 .await;
103 metrics::record_outcome(metrics::FEATURE_CHANNEL_MEMBERS_ADD, &result);
104 result
98105}
99106
100107pub async fn get_channel_member(
@@ -104,13 +111,19 @@
104111 channel_id: Uuid,
105112 user_id: Uuid,
106113) -> SpaceResult<ChannelMemberView> {
107 member_space(svc, caller, space_id).await?;
108 let db = svc.db.reader();
109 let member = member_entity::Entity::find_by_id((channel_id, user_id))
110 .one(&db)
111 .await?
112 .ok_or_else(|| SpaceError::NotFound(format!("member {user_id} in channel {channel_id}")))?;
113 Ok(member.into())
114 let _timer = metrics::Timer::start(metrics::FEATURE_CHANNEL_MEMBERS_GET);
115 let result: SpaceResult<ChannelMemberView> = async {
116 member_space(svc, caller, space_id).await?;
117 let db = svc.db.reader();
118 let member = member_entity::Entity::find_by_id((channel_id, user_id))
119 .one(&db)
120 .await?
121 .ok_or_else(|| SpaceError::NotFound(format!("member {user_id} in channel {channel_id}")))?;
122 Ok(member.into())
123 }
124 .await;
125 metrics::record_outcome(metrics::FEATURE_CHANNEL_MEMBERS_GET, &result);
126 result
114127}
115128
116129pub async fn update_channel_member(
@@ -121,29 +134,35 @@
121134 user_id: Uuid,
122135 cmd: UpdateChannelMemberCmd,
123136) -> SpaceResult<ChannelMemberView> {
124 authz::require_perm(svc, caller, space_id, authz::MANAGE_CHANNELS).await?;
125 let db = svc.db.writer();
126 let txn = db.begin().await?;
127 channel_in_space(&txn, space_id, channel_id).await?;
137 let _timer = metrics::Timer::start(metrics::FEATURE_CHANNEL_MEMBERS_UPDATE);
138 let result: SpaceResult<ChannelMemberView> = async {
139 authz::require_perm(svc, caller, space_id, authz::MANAGE_CHANNELS).await?;
140 let db = svc.db.writer();
141 let txn = db.begin().await?;
142 channel_in_space(&txn, space_id, channel_id).await?;
128143
129 let member = member_entity::Entity::find_by_id((channel_id, user_id))
130 .one(&txn)
131 .await?
132 .ok_or_else(|| SpaceError::NotFound(format!("member {user_id} in channel {channel_id}")))?;
144 let member = member_entity::Entity::find_by_id((channel_id, user_id))
145 .one(&txn)
146 .await?
147 .ok_or_else(|| SpaceError::NotFound(format!("member {user_id} in channel {channel_id}")))?;
133148
134 let mut active: member_entity::ActiveModel = member.into();
135 if let Some(role) = cmd.role {
136 active.role = Set(role.into());
137 }
138 if let Some(muted) = cmd.muted {
139 active.muted = Set(muted);
149 let mut active: member_entity::ActiveModel = member.into();
150 if let Some(role) = cmd.role {
151 active.role = Set(role.into());
152 }
153 if let Some(muted) = cmd.muted {
154 active.muted = Set(muted);
155 }
156 if let Some(mute_until) = cmd.muted_until {
157 active.muted_until = Set(parse_ts(mute_until.as_deref())?);
158 }
159 let saved = active.update(&txn).await?;
160 txn.commit().await?;
161 Ok(saved.into())
140162 }
141 if let Some(mute_until) = cmd.muted_until {
142 active.muted_until = Set(parse_ts(mute_until.as_deref())?);
143 }
144 let saved = active.update(&txn).await?;
145 txn.commit().await?;
146 Ok(saved.into())
163 .await;
164 metrics::record_outcome(metrics::FEATURE_CHANNEL_MEMBERS_UPDATE, &result);
165 result
147166}
148167
149168pub async fn remove_channel_member(
@@ -153,15 +172,21 @@
153172 channel_id: Uuid,
154173 user_id: Uuid,
155174) -> SpaceResult<()> {
156 authz::require_perm(svc, caller, space_id, authz::MANAGE_CHANNELS).await?;
157 let db = svc.db.writer();
158 let res = member_entity::Entity::delete_by_id((channel_id, user_id)).exec(&db).await?;
159 if res.rows_affected == 0 {
160 return Err(SpaceError::NotFound(format!(
161 "member {user_id} in channel {channel_id}"
162 )));
175 let _timer = metrics::Timer::start(metrics::FEATURE_CHANNEL_MEMBERS_REMOVE);
176 let result: SpaceResult<()> = async {
177 authz::require_perm(svc, caller, space_id, authz::MANAGE_CHANNELS).await?;
178 let db = svc.db.writer();
179 let res = member_entity::Entity::delete_by_id((channel_id, user_id)).exec(&db).await?;
180 if res.rows_affected == 0 {
181 return Err(SpaceError::NotFound(format!(
182 "member {user_id} in channel {channel_id}"
183 )));
184 }
185 Ok(())
163186 }
164 Ok(())
187 .await;
188 metrics::record_outcome(metrics::FEATURE_CHANNEL_MEMBERS_REMOVE, &result);
189 result
165190}
166191
167192pub async fn list_channel_members(
@@ -172,24 +197,30 @@
172197 limit: u64,
173198 offset: u64,
174199) -> SpaceResult<ChannelMemberPage> {
175 member_space(svc, caller, space_id).await?;
176 let db = svc.db.reader();
177 let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1);
200 let _timer = metrics::Timer::start(metrics::FEATURE_CHANNEL_MEMBERS_LIST);
201 let result: SpaceResult<ChannelMemberPage> = async {
202 member_space(svc, caller, space_id).await?;
203 let db = svc.db.reader();
204 let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1);
178205
179 let items = member_entity::Entity::find()
180 .filter(member_entity::Column::ChannelId.eq(channel_id))
181 .order_by_asc(member_entity::Column::JoinedAt)
182 .limit(Some(limit))
183 .offset(Some(offset))
184 .all(&db)
185 .await?
186 .into_iter()
187 .map(ChannelMemberView::from)
188 .collect();
189 let total = member_entity::Entity::find()
190 .filter(member_entity::Column::ChannelId.eq(channel_id))
191 .count(&db)
192 .await?;
206 let items = member_entity::Entity::find()
207 .filter(member_entity::Column::ChannelId.eq(channel_id))
208 .order_by_asc(member_entity::Column::JoinedAt)
209 .limit(Some(limit))
210 .offset(Some(offset))
211 .all(&db)
212 .await?
213 .into_iter()
214 .map(ChannelMemberView::from)
215 .collect();
216 let total = member_entity::Entity::find()
217 .filter(member_entity::Column::ChannelId.eq(channel_id))
218 .count(&db)
219 .await?;
193220
194 Ok(ChannelMemberPage { items, total: total as i64 })
221 Ok(ChannelMemberPage { items, total: total as i64 })
222 }
223 .await;
224 metrics::record_outcome(metrics::FEATURE_CHANNEL_MEMBERS_LIST, &result);
225 result
195226}
No newline at end of file
Modified lib/space/features/channels/usecase.rs View lib/space/features/channels/usecase.rs
@@ -7,6 +7,7 @@
77
88use crate::error::{SpaceError, SpaceResult};
99use crate::features::SpaceService;
10use crate::metrics;
1011
1112use super::{ChannelPage, ChannelView};
1213
@@ -40,16 +41,22 @@
4041 space_id: Uuid,
4142 channel_id: Uuid,
4243) -> SpaceResult<ChannelView> {
43 member_space(svc, caller, space_id).await?;
44 let db = svc.db.reader();
45 let channel = channel_entity::Entity::find_by_id(channel_id)
46 .one(&db)
47 .await?
48 .ok_or_else(|| SpaceError::NotFound(format!("channel {channel_id}")))?;
49 if channel.space_id != Some(space_id) {
50 return Err(SpaceError::NotFound(format!("channel {channel_id}")));
44 let _timer = metrics::Timer::start(metrics::FEATURE_CHANNELS_GET);
45 let result: SpaceResult<ChannelView> = async {
46 member_space(svc, caller, space_id).await?;
47 let db = svc.db.reader();
48 let channel = channel_entity::Entity::find_by_id(channel_id)
49 .one(&db)
50 .await?
51 .ok_or_else(|| SpaceError::NotFound(format!("channel {channel_id}")))?;
52 if channel.space_id != Some(space_id) {
53 return Err(SpaceError::NotFound(format!("channel {channel_id}")));
54 }
55 Ok(ChannelView::from(channel))
5156 }
52 Ok(ChannelView::from(channel))
57 .await;
58 metrics::record_outcome(metrics::FEATURE_CHANNELS_GET, &result);
59 result
5360}
5461
5562pub async fn list_channels(
@@ -59,24 +66,30 @@
5966 limit: u64,
6067 offset: u64,
6168) -> SpaceResult<ChannelPage> {
62 member_space(svc, caller, space_id).await?;
63 let db = svc.db.reader();
64 let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1);
65 let items = channel_entity::Entity::find()
66 .filter(channel_entity::Column::SpaceId.eq(space_id))
67 .order_by_asc(channel_entity::Column::Position)
68 .limit(Some(limit))
69 .offset(Some(offset))
70 .all(&db)
71 .await?
72 .into_iter()
73 .map(ChannelView::from)
74 .collect();
75 let total = channel_entity::Entity::find()
76 .filter(channel_entity::Column::SpaceId.eq(space_id))
77 .count(&db)
78 .await?;
79 Ok(ChannelPage { items, total: total as i64 })
69 let _timer = metrics::Timer::start(metrics::FEATURE_CHANNELS_LIST);
70 let result: SpaceResult<ChannelPage> = async {
71 member_space(svc, caller, space_id).await?;
72 let db = svc.db.reader();
73 let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1);
74 let items = channel_entity::Entity::find()
75 .filter(channel_entity::Column::SpaceId.eq(space_id))
76 .order_by_asc(channel_entity::Column::Position)
77 .limit(Some(limit))
78 .offset(Some(offset))
79 .all(&db)
80 .await?
81 .into_iter()
82 .map(ChannelView::from)
83 .collect();
84 let total = channel_entity::Entity::find()
85 .filter(channel_entity::Column::SpaceId.eq(space_id))
86 .count(&db)
87 .await?;
88 Ok(ChannelPage { items, total: total as i64 })
89 }
90 .await;
91 metrics::record_outcome(metrics::FEATURE_CHANNELS_LIST, &result);
92 result
8093}
8194
8295pub async fn ensure_channel_for_resource(
@@ -85,25 +98,31 @@
8598 name: &str,
8699 created_by: Uuid,
87100) -> SpaceResult<ChannelView> {
88 let db = svc.db.writer();
89 let now = OffsetDateTime::now_utc();
90 let active: channel_entity::ActiveModel = channel_entity::ActiveModel::builder()
91 .set_id(Uuid::new_v4())
92 .set_space_id(Some(space_id))
93 .set_name(name.to_string())
94 .set_parent_id(None)
95 .set_color(None)
96 .set_position(0)
97 .set_icon(None)
98 .set_topic(None)
99 .set_purpose(None)
100 .set_is_archived(false)
101 .set_archived_at(None)
102 .set_archived_by(None)
103 .set_created_at(now)
104 .set_updated_at(now)
105 .set_created_by(created_by)
106 .into();
107 let saved = active.insert(&db).await?;
108 Ok(ChannelView::from(saved))
101 let _timer = metrics::Timer::start(metrics::FEATURE_CHANNELS_ENSURE);
102 let result: SpaceResult<ChannelView> = async {
103 let db = svc.db.writer();
104 let now = OffsetDateTime::now_utc();
105 let active: channel_entity::ActiveModel = channel_entity::ActiveModel::builder()
106 .set_id(Uuid::new_v4())
107 .set_space_id(Some(space_id))
108 .set_name(name.to_string())
109 .set_parent_id(None)
110 .set_color(None)
111 .set_position(0)
112 .set_icon(None)
113 .set_topic(None)
114 .set_purpose(None)
115 .set_is_archived(false)
116 .set_archived_at(None)
117 .set_archived_by(None)
118 .set_created_at(now)
119 .set_updated_at(now)
120 .set_created_by(created_by)
121 .into();
122 let saved = active.insert(&db).await?;
123 Ok(ChannelView::from(saved))
124 }
125 .await;
126 metrics::record_outcome(metrics::FEATURE_CHANNELS_ENSURE, &result);
127 result
109128}
No newline at end of file
Modified lib/space/features/invites/usecase.rs View lib/space/features/invites/usecase.rs
@@ -8,6 +8,7 @@
88use crate::error::{SpaceError, SpaceResult};
99use crate::features::SpaceService;
1010use crate::features::authz;
11use crate::metrics;
1112
1213use super::{AcceptInviteCmd, CreateInviteCmd, SpaceInvitePage, SpaceInviteView};
1314
@@ -44,59 +45,65 @@
4445 space_id: Uuid,
4546 cmd: CreateInviteCmd,
4647) -> SpaceResult<SpaceInviteView> {
47 owner_space(svc, caller, space_id).await?;
48 if cmd.user_id.is_none() && cmd.email.is_none() {
49 return Err(SpaceError::BadRequest(
50 "either user_id or email must be provided".to_string(),
51 ));
52 }
53 if let Some(user_id) = cmd.user_id {
54 let db = svc.db.reader();
55 let existing = invite_entity::Entity::find()
56 .filter(invite_entity::Column::SpaceId.eq(space_id))
57 .filter(invite_entity::Column::UserId.eq(user_id))
58 .filter(invite_entity::Column::AcceptedAt.is_null())
59 .filter(invite_entity::Column::RevokedAt.is_null())
60 .one(&db)
61 .await?;
62 if existing.is_some() {
63 return Err(SpaceError::Conflict(format!(
64 "an active invite already exists for user {user_id} in space {space_id}"
48 let _timer = metrics::Timer::start(metrics::FEATURE_INVITES_CREATE);
49 let result: SpaceResult<SpaceInviteView> = async {
50 owner_space(svc, caller, space_id).await?;
51 if cmd.user_id.is_none() && cmd.email.is_none() {
52 return Err(SpaceError::BadRequest(
53 "either user_id or email must be provided".to_string(),
54 ));
55 }
56 if let Some(user_id) = cmd.user_id {
57 let db = svc.db.reader();
58 let existing = invite_entity::Entity::find()
59 .filter(invite_entity::Column::SpaceId.eq(space_id))
60 .filter(invite_entity::Column::UserId.eq(user_id))
61 .filter(invite_entity::Column::AcceptedAt.is_null())
62 .filter(invite_entity::Column::RevokedAt.is_null())
63 .one(&db)
64 .await?;
65 if existing.is_some() {
66 return Err(SpaceError::Conflict(format!(
67 "an active invite already exists for user {user_id} in space {space_id}"
68 )));
69 }
70 }
71 let email = cmd.email.unwrap_or_default();
72 if email.chars().count() > svc.config.max_invite_email_len {
73 return Err(SpaceError::BadRequest(format!(
74 "email exceeds {} characters",
75 svc.config.max_invite_email_len
6576 )));
6677 }
78 let expires_at = parse_ts(cmd.expires_at.as_deref())?;
79 let db = svc.db.writer();
80 let now = OffsetDateTime::now_utc();
81 let id = Uuid::new_v4();
82 let accept_code = format!("{}-{}", Uuid::new_v4().simple(), Uuid::new_v4().simple());
83 let role_ids = cmd
84 .role_ids
85 .iter()
86 .map(|r| r.to_string())
87 .collect::<Vec<_>>()
88 .join(",");
89 let invite: invite_entity::ActiveModel = invite_entity::ActiveModel::builder()
90 .set_id(id)
91 .set_space_id(space_id)
92 .set_user_id(cmd.user_id.unwrap_or_default())
93 .set_email(email)
94 .set_role_ids(role_ids)
95 .set_accept_code(accept_code)
96 .set_created_at(now)
97 .set_expires_at(expires_at)
98 .set_accepted_at(None)
99 .set_revoked_at(None)
100 .into();
101 let saved = invite.insert(&db).await?;
102 Ok(SpaceInviteView::from(saved))
67103 }
68 let email = cmd.email.unwrap_or_default();
69 if email.chars().count() > svc.config.max_invite_email_len {
70 return Err(SpaceError::BadRequest(format!(
71 "email exceeds {} characters",
72 svc.config.max_invite_email_len
73 )));
74 }
75 let expires_at = parse_ts(cmd.expires_at.as_deref())?;
76 let db = svc.db.writer();
77 let now = OffsetDateTime::now_utc();
78 let id = Uuid::new_v4();
79 let accept_code = format!("{}-{}", Uuid::new_v4().simple(), Uuid::new_v4().simple());
80 let role_ids = cmd
81 .role_ids
82 .iter()
83 .map(|r| r.to_string())
84 .collect::<Vec<_>>()
85 .join(",");
86 let invite: invite_entity::ActiveModel = invite_entity::ActiveModel::builder()
87 .set_id(id)
88 .set_space_id(space_id)
89 .set_user_id(cmd.user_id.unwrap_or_default())
90 .set_email(email)
91 .set_role_ids(role_ids)
92 .set_accept_code(accept_code)
93 .set_created_at(now)
94 .set_expires_at(expires_at)
95 .set_accepted_at(None)
96 .set_revoked_at(None)
97 .into();
98 let saved = invite.insert(&db).await?;
99 Ok(SpaceInviteView::from(saved))
104 .await;
105 metrics::record_outcome(metrics::FEATURE_INVITES_CREATE, &result);
106 result
100107}
101108
102109pub async fn revoke_invite(
@@ -105,19 +112,25 @@
105112 space_id: Uuid,
106113 invite_id: Uuid,
107114) -> SpaceResult<SpaceInviteView> {
108 owner_space(svc, caller, space_id).await?;
109 let db = svc.db.writer();
110 let invite = invite_entity::Entity::find_by_id(invite_id)
111 .one(&db)
112 .await?
113 .ok_or_else(|| SpaceError::NotFound(format!("invite {invite_id}")))?;
114 if invite.space_id != space_id {
115 return Err(SpaceError::NotFound(format!("invite {invite_id}")));
115 let _timer = metrics::Timer::start(metrics::FEATURE_INVITES_REVOKE);
116 let result: SpaceResult<SpaceInviteView> = async {
117 owner_space(svc, caller, space_id).await?;
118 let db = svc.db.writer();
119 let invite = invite_entity::Entity::find_by_id(invite_id)
120 .one(&db)
121 .await?
122 .ok_or_else(|| SpaceError::NotFound(format!("invite {invite_id}")))?;
123 if invite.space_id != space_id {
124 return Err(SpaceError::NotFound(format!("invite {invite_id}")));
125 }
126 let mut active: invite_entity::ActiveModel = invite.into();
127 active.revoked_at = Set(Some(OffsetDateTime::now_utc()));
128 let saved = active.update(&db).await?;
129 Ok(SpaceInviteView::from(saved))
116130 }
117 let mut active: invite_entity::ActiveModel = invite.into();
118 active.revoked_at = Set(Some(OffsetDateTime::now_utc()));
119 let saved = active.update(&db).await?;
120 Ok(SpaceInviteView::from(saved))
131 .await;
132 metrics::record_outcome(metrics::FEATURE_INVITES_REVOKE, &result);
133 result
121134}
122135
123136pub async fn accept_invite(
@@ -125,46 +138,72 @@
125138 caller: Uuid,
126139 cmd: AcceptInviteCmd,
127140) -> SpaceResult<()> {
128 let db = svc.db.writer();
129 let invite = invite_entity::Entity::find()
130 .filter(invite_entity::Column::AcceptCode.eq(&cmd.accept_code))
131 .one(&db)
132 .await?
133 .ok_or_else(|| SpaceError::NotFound("invite".to_string()))?;
134 if invite.user_id != caller {
135 return Err(SpaceError::Forbidden(
136 "this invite does not belong to you".to_string(),
137 ));
138 }
139 if invite.revoked_at.is_some() {
140 return Err(SpaceError::Conflict("invite has been revoked".to_string()));
141 }
142 if invite.accepted_at.is_some() {
143 return Err(SpaceError::Conflict("invite already accepted".to_string()));
141 let _timer = metrics::Timer::start(metrics::FEATURE_INVITES_ACCEPT);
142 let result: SpaceResult<()> = async {
143 let db = svc.db.writer();
144 let invite = invite_entity::Entity::find()
145 .filter(invite_entity::Column::AcceptCode.eq(&cmd.accept_code))
146 .one(&db)
147 .await?
148 .ok_or_else(|| SpaceError::NotFound("invite".to_string()))?;
149 if invite.user_id != caller {
150 return Err(SpaceError::Forbidden(
151 "this invite does not belong to you".to_string(),
152 ));
153 }
154 if invite.revoked_at.is_some() {
155 return Err(SpaceError::Conflict("invite has been revoked".to_string()));
156 }
157 if invite.accepted_at.is_some() {
158 return Err(SpaceError::Conflict("invite already accepted".to_string()));
159 }
160 if let Some(exp) = invite.expires_at
161 && OffsetDateTime::now_utc() > exp {
162 return Err(SpaceError::Conflict("invite has expired".to_string()));
163 }
164 let role_ids = invite
165 .role_ids
166 .split(',')
167 .filter_map(|s| Uuid::parse_str(s).ok())
168 .collect::<Vec<_>>();
169 let now = OffsetDateTime::now_utc();
170 for role_id in &role_ids {
171 let mr: member_role_entity::ActiveModel = member_role_entity::ActiveModel::builder()
172 .set_space_id(invite.space_id)
173 .set_user_id(caller)
174 .set_role_id(*role_id)
175 .set_created_at(now)
176 .into();
177 mr.insert(&db).await?;
178 }
179 let mut active: invite_entity::ActiveModel = invite.into();
180 active.accepted_at = Set(Some(now));
181 active.update(&db).await?;
182 Ok(())
144183 }
145 if let Some(exp) = invite.expires_at
146 && OffsetDateTime::now_utc() > exp {
147 return Err(SpaceError::Conflict("invite has expired".to_string()));
184 .await;
185 match &result {
186 Ok(_) => metrics::record_invite_accept(metrics::InviteAcceptOutcome::Success),
187 Err(SpaceError::NotFound(_)) => {
188 metrics::record_invite_accept(metrics::InviteAcceptOutcome::NotFound)
148189 }
149 let role_ids = invite
150 .role_ids
151 .split(',')
152 .filter_map(|s| Uuid::parse_str(s).ok())
153 .collect::<Vec<_>>();
154 let now = OffsetDateTime::now_utc();
155 for role_id in &role_ids {
156 let mr: member_role_entity::ActiveModel = member_role_entity::ActiveModel::builder()
157 .set_space_id(invite.space_id)
158 .set_user_id(caller)
159 .set_role_id(*role_id)
160 .set_created_at(now)
161 .into();
162 mr.insert(&db).await?;
190 Err(SpaceError::Forbidden(_)) => {
191 metrics::record_invite_accept(metrics::InviteAcceptOutcome::Forbidden)
192 }
193 Err(SpaceError::Conflict(msg)) => {
194 if msg.contains("revoked") {
195 metrics::record_invite_accept(metrics::InviteAcceptOutcome::Revoked)
196 } else if msg.contains("already accepted") {
197 metrics::record_invite_accept(metrics::InviteAcceptOutcome::AlreadyAccepted)
198 } else if msg.contains("expired") {
199 metrics::record_invite_accept(metrics::InviteAcceptOutcome::Expired)
200 } else {
201 metrics::record_invite_accept(metrics::InviteAcceptOutcome::Other)
202 }
203 }
204 Err(_) => metrics::record_invite_accept(metrics::InviteAcceptOutcome::Other),
163205 }
164 let mut active: invite_entity::ActiveModel = invite.into();
165 active.accepted_at = Set(Some(now));
166 active.update(&db).await?;
167 Ok(())
206 result
168207}
169208
170209pub async fn list_invites(
@@ -174,22 +213,28 @@
174213 limit: u64,
175214 offset: u64,
176215) -> SpaceResult<SpaceInvitePage> {
177 authz::require_perm(svc, caller, space_id, authz::MANAGE_INVITES).await?;
178 let db = svc.db.reader();
179 let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1);
180 let items = invite_entity::Entity::find()
181 .filter(invite_entity::Column::SpaceId.eq(space_id))
182 .order_by_desc(invite_entity::Column::CreatedAt)
183 .limit(Some(limit as u64))
184 .offset(Some(offset))
185 .all(&db)
186 .await?;
187 let total = invite_entity::Entity::find()
188 .filter(invite_entity::Column::SpaceId.eq(space_id))
189 .count(&db)
190 .await?;
191 Ok(SpaceInvitePage {
192 items: items.into_iter().map(SpaceInviteView::from).collect(),
193 total: total as i64,
194 })
216 let _timer = metrics::Timer::start(metrics::FEATURE_INVITES_LIST);
217 let result: SpaceResult<SpaceInvitePage> = async {
218 authz::require_perm(svc, caller, space_id, authz::MANAGE_INVITES).await?;
219 let db = svc.db.reader();
220 let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1);
221 let items = invite_entity::Entity::find()
222 .filter(invite_entity::Column::SpaceId.eq(space_id))
223 .order_by_desc(invite_entity::Column::CreatedAt)
224 .limit(Some(limit as u64))
225 .offset(Some(offset))
226 .all(&db)
227 .await?;
228 let total = invite_entity::Entity::find()
229 .filter(invite_entity::Column::SpaceId.eq(space_id))
230 .count(&db)
231 .await?;
232 Ok(SpaceInvitePage {
233 items: items.into_iter().map(SpaceInviteView::from).collect(),
234 total: total as i64,
235 })
236 }
237 .await;
238 metrics::record_outcome(metrics::FEATURE_INVITES_LIST, &result);
239 result
195240}
No newline at end of file
Modified lib/space/features/members/usecase.rs View lib/space/features/members/usecase.rs
@@ -6,6 +6,7 @@
66
77use crate::error::{SpaceError, SpaceResult};
88use crate::features::SpaceService;
9use crate::metrics;
910
1011use super::{AddMemberCmd, SpaceMemberPage, SpaceMemberView, UpdateMemberCmd};
1112
@@ -30,42 +31,48 @@
3031 space_id: Uuid,
3132 cmd: AddMemberCmd,
3233) -> SpaceResult<SpaceMemberView> {
33 space(caller, space_id, svc).await?;
34 let db = svc.db.writer();
35 let now = OffsetDateTime::now_utc();
36 let existing = member_entity::Entity::find_by_id((space_id, cmd.user_id))
37 .one(&db)
38 .await?;
39 if existing.is_some() {
40 return Err(SpaceError::Conflict(format!(
41 "user {} is already a member of space {space_id}",
42 cmd.user_id
43 )));
34 let _timer = metrics::Timer::start(metrics::FEATURE_MEMBERS_ADD);
35 let result: SpaceResult<SpaceMemberView> = async {
36 space(caller, space_id, svc).await?;
37 let db = svc.db.writer();
38 let now = OffsetDateTime::now_utc();
39 let existing = member_entity::Entity::find_by_id((space_id, cmd.user_id))
40 .one(&db)
41 .await?;
42 if existing.is_some() {
43 return Err(SpaceError::Conflict(format!(
44 "user {} is already a member of space {space_id}",
45 cmd.user_id
46 )));
47 }
48 let mute_until = parse_mute_until(cmd.mute_until.as_deref())?;
49 let active: member_entity::ActiveModel = member_entity::ActiveModel::builder()
50 .set_space_id(space_id)
51 .set_user_id(cmd.user_id)
52 .set_alias_name(cmd.alias_name)
53 .set_alias_avatar_url(cmd.alias_avatar_url)
54 .set_status(member_entity::MemberStatus::Active)
55 .set_invited_at(Option::<OffsetDateTime>::None)
56 .set_invited_by(Option::<Uuid>::None)
57 .set_joined_at(now)
58 .set_created_at(now)
59 .set_updated_at(Option::<OffsetDateTime>::Some(now))
60 .set_message_notifications(member_entity::MessageNotifications::from(
61 cmd.message_notifications.unwrap_or_default(),
62 ))
63 .set_muted(cmd.muted.unwrap_or(false))
64 .set_mute_until(mute_until)
65 .set_mobile_push(cmd.mobile_push.unwrap_or(false))
66 .set_suppress_everyone(cmd.suppress_everyone.unwrap_or(false))
67 .set_suppress_roles(cmd.suppress_roles.unwrap_or(false))
68 .set_hide_muted_channels(cmd.hide_muted_channels.unwrap_or(false))
69 .into();
70 let saved: member_entity::Model = active.insert(&db).await?;
71 Ok(saved.into())
4472 }
45 let mute_until = parse_mute_until(cmd.mute_until.as_deref())?;
46 let active: member_entity::ActiveModel = member_entity::ActiveModel::builder()
47 .set_space_id(space_id)
48 .set_user_id(cmd.user_id)
49 .set_alias_name(cmd.alias_name)
50 .set_alias_avatar_url(cmd.alias_avatar_url)
51 .set_status(member_entity::MemberStatus::Active)
52 .set_invited_at(Option::<OffsetDateTime>::None)
53 .set_invited_by(Option::<Uuid>::None)
54 .set_joined_at(now)
55 .set_created_at(now)
56 .set_updated_at(Option::<OffsetDateTime>::Some(now))
57 .set_message_notifications(member_entity::MessageNotifications::from(
58 cmd.message_notifications.unwrap_or_default(),
59 ))
60 .set_muted(cmd.muted.unwrap_or(false))
61 .set_mute_until(mute_until)
62 .set_mobile_push(cmd.mobile_push.unwrap_or(false))
63 .set_suppress_everyone(cmd.suppress_everyone.unwrap_or(false))
64 .set_suppress_roles(cmd.suppress_roles.unwrap_or(false))
65 .set_hide_muted_channels(cmd.hide_muted_channels.unwrap_or(false))
66 .into();
67 let saved: member_entity::Model = active.insert(&db).await?;
68 Ok(saved.into())
73 .await;
74 metrics::record_outcome(metrics::FEATURE_MEMBERS_ADD, &result);
75 result
6976}
7077
7178pub async fn get_member(
@@ -74,13 +81,19 @@
7481 space_id: Uuid,
7582 user_id: Uuid,
7683) -> SpaceResult<SpaceMemberView> {
77 space(caller, space_id, svc).await?;
78 let db = svc.db.reader();
79 let m = member_entity::Entity::find_by_id((space_id, user_id))
80 .one(&db)
81 .await?
82 .ok_or_else(|| SpaceError::NotFound(format!("member {user_id} in space {space_id}")))?;
83 Ok(m.into())
84 let _timer = metrics::Timer::start(metrics::FEATURE_MEMBERS_GET);
85 let result: SpaceResult<SpaceMemberView> = async {
86 space(caller, space_id, svc).await?;
87 let db = svc.db.reader();
88 let m = member_entity::Entity::find_by_id((space_id, user_id))
89 .one(&db)
90 .await?
91 .ok_or_else(|| SpaceError::NotFound(format!("member {user_id} in space {space_id}")))?;
92 Ok(m.into())
93 }
94 .await;
95 metrics::record_outcome(metrics::FEATURE_MEMBERS_GET, &result);
96 result
8497}
8598
8699fn parse_mute_until(s: Option<&str>) -> SpaceResult<Option<OffsetDateTime>> {
@@ -100,56 +113,62 @@
100113 user_id: Uuid,
101114 cmd: UpdateMemberCmd,
102115) -> SpaceResult<SpaceMemberView> {
103 space(caller, space_id, svc).await?;
104 let db = svc.db.writer();
105 let txn = db.begin().await?;
106 let m = member_entity::Entity::find_by_id((space_id, user_id))
107 .one(&txn)
108 .await?
109 .ok_or_else(|| SpaceError::NotFound(format!("member {user_id} in space {space_id}")))?;
110 let mut active: member_entity::ActiveModel = m.into();
111 if let Some(v) = cmd.alias_name {
112 match v {
113 None => active.alias_name = Set(None),
114 Some(s) => {
115 if s.chars().count() > svc.config.max_alias_name_len {
116 return Err(SpaceError::BadRequest(format!(
117 "alias_name exceeds {} characters",
118 svc.config.max_alias_name_len
119 )));
116 let _timer = metrics::Timer::start(metrics::FEATURE_MEMBERS_UPDATE);
117 let result: SpaceResult<SpaceMemberView> = async {
118 space(caller, space_id, svc).await?;
119 let db = svc.db.writer();
120 let txn = db.begin().await?;
121 let m = member_entity::Entity::find_by_id((space_id, user_id))
122 .one(&txn)
123 .await?
124 .ok_or_else(|| SpaceError::NotFound(format!("member {user_id} in space {space_id}")))?;
125 let mut active: member_entity::ActiveModel = m.into();
126 if let Some(v) = cmd.alias_name {
127 match v {
128 None => active.alias_name = Set(None),
129 Some(s) => {
130 if s.chars().count() > svc.config.max_alias_name_len {
131 return Err(SpaceError::BadRequest(format!(
132 "alias_name exceeds {} characters",
133 svc.config.max_alias_name_len
134 )));
135 }
136 active.alias_name = Set(Some(s));
120137 }
121 active.alias_name = Set(Some(s));
122138 }
123139 }
124 }
125 if let Some(v) = cmd.alias_avatar_url {
126 active.alias_avatar_url = Set(v);
127 }
128 if let Some(v) = cmd.message_notifications {
129 active.message_notifications = Set(v.into());
140 if let Some(v) = cmd.alias_avatar_url {
141 active.alias_avatar_url = Set(v);
142 }
143 if let Some(v) = cmd.message_notifications {
144 active.message_notifications = Set(v.into());
145 }
146 if let Some(v) = cmd.muted {
147 active.muted = Set(v);
148 }
149 if let Some(v) = cmd.mute_until {
150 active.mute_until = Set(parse_mute_until(v.as_deref())?);
151 }
152 if let Some(v) = cmd.mobile_push {
153 active.mobile_push = Set(v);
154 }
155 if let Some(v) = cmd.suppress_everyone {
156 active.suppress_everyone = Set(v);
157 }
158 if let Some(v) = cmd.suppress_roles {
159 active.suppress_roles = Set(v);
160 }
161 if let Some(v) = cmd.hide_muted_channels {
162 active.hide_muted_channels = Set(v);
163 }
164 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
165 let saved = active.update(&txn).await?;
166 txn.commit().await?;
167 Ok(saved.into())
130168 }
131 if let Some(v) = cmd.muted {
132 active.muted = Set(v);
133 }
134 if let Some(v) = cmd.mute_until {
135 active.mute_until = Set(parse_mute_until(v.as_deref())?);
136 }
137 if let Some(v) = cmd.mobile_push {
138 active.mobile_push = Set(v);
139 }
140 if let Some(v) = cmd.suppress_everyone {
141 active.suppress_everyone = Set(v);
142 }
143 if let Some(v) = cmd.suppress_roles {
144 active.suppress_roles = Set(v);
145 }
146 if let Some(v) = cmd.hide_muted_channels {
147 active.hide_muted_channels = Set(v);
148 }
149 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
150 let saved = active.update(&txn).await?;
151 txn.commit().await?;
152 Ok(saved.into())
169 .await;
170 metrics::record_outcome(metrics::FEATURE_MEMBERS_UPDATE, &result);
171 result
153172}
154173
155174pub async fn remove_member(
@@ -158,25 +177,31 @@
158177 space_id: Uuid,
159178 user_id: Uuid,
160179) -> SpaceResult<()> {
161 space(caller, space_id, svc).await?;
162 if caller == user_id {
163 return Err(SpaceError::BadRequest(
164 "owner cannot remove themselves".to_string(),
165 ));
180 let _timer = metrics::Timer::start(metrics::FEATURE_MEMBERS_REMOVE);
181 let result: SpaceResult<()> = async {
182 space(caller, space_id, svc).await?;
183 if caller == user_id {
184 return Err(SpaceError::BadRequest(
185 "owner cannot remove themselves".to_string(),
186 ));
187 }
188 let db = svc.db.writer();
189 let txn = db.begin().await?;
190 let m = member_entity::Entity::find_by_id((space_id, user_id))
191 .one(&txn)
192 .await?
193 .ok_or_else(|| SpaceError::NotFound(format!("member {user_id} in space {space_id}")))?;
194 let mut active: member_entity::ActiveModel = m.into();
195 active.status = Set(member_entity::MemberStatus::Removed);
196 active.left_at = Set(Some(OffsetDateTime::now_utc()));
197 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
198 active.update(&txn).await?;
199 txn.commit().await?;
200 Ok(())
166201 }
167 let db = svc.db.writer();
168 let txn = db.begin().await?;
169 let m = member_entity::Entity::find_by_id((space_id, user_id))
170 .one(&txn)
171 .await?
172 .ok_or_else(|| SpaceError::NotFound(format!("member {user_id} in space {space_id}")))?;
173 let mut active: member_entity::ActiveModel = m.into();
174 active.status = Set(member_entity::MemberStatus::Removed);
175 active.left_at = Set(Some(OffsetDateTime::now_utc()));
176 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
177 active.update(&txn).await?;
178 txn.commit().await?;
179 Ok(())
202 .await;
203 metrics::record_outcome(metrics::FEATURE_MEMBERS_REMOVE, &result);
204 result
180205}
181206
182207pub async fn list_members(
@@ -186,22 +211,28 @@
186211 limit: u64,
187212 offset: u64,
188213) -> SpaceResult<SpaceMemberPage> {
189 space(caller, space_id, svc).await?;
190 let db = svc.db.reader();
191 let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1) as u64;
192 let items = member_entity::Entity::find()
193 .filter(member_entity::Column::SpaceId.eq(space_id))
194 .order_by_asc(member_entity::Column::JoinedAt)
195 .limit(Some(limit))
196 .offset(Some(offset))
197 .all(&db)
198 .await?;
199 let total = member_entity::Entity::find()
200 .filter(member_entity::Column::SpaceId.eq(space_id))
201 .count(&db)
202 .await?;
203 Ok(SpaceMemberPage {
204 items: items.into_iter().map(SpaceMemberView::from).collect(),
205 total: total as i64,
206 })
214 let _timer = metrics::Timer::start(metrics::FEATURE_MEMBERS_LIST);
215 let result: SpaceResult<SpaceMemberPage> = async {
216 space(caller, space_id, svc).await?;
217 let db = svc.db.reader();
218 let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1) as u64;
219 let items = member_entity::Entity::find()
220 .filter(member_entity::Column::SpaceId.eq(space_id))
221 .order_by_asc(member_entity::Column::JoinedAt)
222 .limit(Some(limit))
223 .offset(Some(offset))
224 .all(&db)
225 .await?;
226 let total = member_entity::Entity::find()
227 .filter(member_entity::Column::SpaceId.eq(space_id))
228 .count(&db)
229 .await?;
230 Ok(SpaceMemberPage {
231 items: items.into_iter().map(SpaceMemberView::from).collect(),
232 total: total as i64,
233 })
234 }
235 .await;
236 metrics::record_outcome(metrics::FEATURE_MEMBERS_LIST, &result);
237 result
207238}
No newline at end of file
Modified lib/space/features/overwrites/usecase.rs View lib/space/features/overwrites/usecase.rs
@@ -10,6 +10,7 @@
1010use crate::error::{SpaceError, SpaceResult};
1111use crate::features::authz;
1212use crate::features::SpaceService;
13use crate::metrics;
1314
1415use super::{RoleOverwriteView, SetRoleOverwriteCmd, SetUserOverwriteCmd, UserOverwriteView};
1516
@@ -55,44 +56,50 @@
5556 channel_id: Uuid,
5657 cmd: SetRoleOverwriteCmd,
5758) -> SpaceResult<RoleOverwriteView> {
58 authz::require_perm(svc, caller, space_id, authz::MANAGE_OVERWRITES).await?;
59 let db = svc.db.writer();
60 channel_in_space(&db, space_id, channel_id).await?;
59 let _timer = metrics::Timer::start(metrics::FEATURE_OVERWRITES_SET_ROLE);
60 let result: SpaceResult<RoleOverwriteView> = async {
61 authz::require_perm(svc, caller, space_id, authz::MANAGE_OVERWRITES).await?;
62 let db = svc.db.writer();
63 channel_in_space(&db, space_id, channel_id).await?;
6164
62 let role = space_role_entity::Entity::find_by_id(cmd.role_id)
63 .one(&db)
64 .await?
65 .ok_or_else(|| SpaceError::NotFound(format!("role {}", cmd.role_id)))?;
66 if role.space_id != space_id {
67 return Err(SpaceError::NotFound(format!("role {}", cmd.role_id)));
68 }
65 let role = space_role_entity::Entity::find_by_id(cmd.role_id)
66 .one(&db)
67 .await?
68 .ok_or_else(|| SpaceError::NotFound(format!("role {}", cmd.role_id)))?;
69 if role.space_id != space_id {
70 return Err(SpaceError::NotFound(format!("role {}", cmd.role_id)));
71 }
6972
70 let existing = role_ow_entity::Entity::find()
71 .filter(
72 role_ow_entity::Column::ChannelId
73 .eq(channel_id)
74 .and(role_ow_entity::Column::RoleId.eq(cmd.role_id)),
75 )
76 .one(&db)
77 .await?;
73 let existing = role_ow_entity::Entity::find()
74 .filter(
75 role_ow_entity::Column::ChannelId
76 .eq(channel_id)
77 .and(role_ow_entity::Column::RoleId.eq(cmd.role_id)),
78 )
79 .one(&db)
80 .await?;
7881
79 let saved: role_ow_entity::Model = if let Some(ow) = existing {
80 let mut active: role_ow_entity::ActiveModel = ow.into();
81 active.allow_mask = Set(cmd.allow_mask);
82 active.deny_mask = Set(cmd.deny_mask);
83 active.update(&db).await?
84 } else {
85 let active: role_ow_entity::ActiveModel = role_ow_entity::ActiveModel::builder()
86 .set_id(Uuid::new_v4())
87 .set_space_id(space_id)
88 .set_channel_id(channel_id)
89 .set_role_id(cmd.role_id)
90 .set_allow_mask(cmd.allow_mask)
91 .set_deny_mask(cmd.deny_mask)
92 .into();
93 active.insert(&db).await?
94 };
95 Ok(saved.into())
82 let saved: role_ow_entity::Model = if let Some(ow) = existing {
83 let mut active: role_ow_entity::ActiveModel = ow.into();
84 active.allow_mask = Set(cmd.allow_mask);
85 active.deny_mask = Set(cmd.deny_mask);
86 active.update(&db).await?
87 } else {
88 let active: role_ow_entity::ActiveModel = role_ow_entity::ActiveModel::builder()
89 .set_id(Uuid::new_v4())
90 .set_space_id(space_id)
91 .set_channel_id(channel_id)
92 .set_role_id(cmd.role_id)
93 .set_allow_mask(cmd.allow_mask)
94 .set_deny_mask(cmd.deny_mask)
95 .into();
96 active.insert(&db).await?
97 };
98 Ok(saved.into())
99 }
100 .await;
101 metrics::record_outcome(metrics::FEATURE_OVERWRITES_SET_ROLE, &result);
102 result
96103}
97104
98105pub async fn remove_role_overwrite(
@@ -102,24 +109,30 @@
102109 channel_id: Uuid,
103110 role_id: Uuid,
104111) -> SpaceResult<()> {
105 authz::require_perm(svc, caller, space_id, authz::MANAGE_OVERWRITES).await?;
106 let db = svc.db.writer();
107 channel_in_space(&db, space_id, channel_id).await?;
112 let _timer = metrics::Timer::start(metrics::FEATURE_OVERWRITES_REMOVE_ROLE);
113 let result: SpaceResult<()> = async {
114 authz::require_perm(svc, caller, space_id, authz::MANAGE_OVERWRITES).await?;
115 let db = svc.db.writer();
116 channel_in_space(&db, space_id, channel_id).await?;
108117
109 let res = role_ow_entity::Entity::delete_many()
110 .filter(
111 role_ow_entity::Column::ChannelId
112 .eq(channel_id)
113 .and(role_ow_entity::Column::RoleId.eq(role_id)),
114 )
115 .exec(&db)
116 .await?;
117 if res.rows_affected == 0 {
118 return Err(SpaceError::NotFound(format!(
119 "role overwrite for role {role_id} in channel {channel_id}"
120 )));
118 let res = role_ow_entity::Entity::delete_many()
119 .filter(
120 role_ow_entity::Column::ChannelId
121 .eq(channel_id)
122 .and(role_ow_entity::Column::RoleId.eq(role_id)),
123 )
124 .exec(&db)
125 .await?;
126 if res.rows_affected == 0 {
127 return Err(SpaceError::NotFound(format!(
128 "role overwrite for role {role_id} in channel {channel_id}"
129 )));
130 }
131 Ok(())
121132 }
122 Ok(())
133 .await;
134 metrics::record_outcome(metrics::FEATURE_OVERWRITES_REMOVE_ROLE, &result);
135 result
123136}
124137
125138pub async fn list_role_overwrites(
@@ -128,15 +141,21 @@
128141 space_id: Uuid,
129142 channel_id: Uuid,
130143) -> SpaceResult<Vec<RoleOverwriteView>> {
131 member_space(svc, caller, space_id).await?;
132 let db = svc.db.reader();
133 channel_in_space(&db, space_id, channel_id).await?;
144 let _timer = metrics::Timer::start(metrics::FEATURE_OVERWRITES_LIST_ROLE);
145 let result: SpaceResult<Vec<RoleOverwriteView>> = async {
146 member_space(svc, caller, space_id).await?;
147 let db = svc.db.reader();
148 channel_in_space(&db, space_id, channel_id).await?;
134149
135 let items = role_ow_entity::Entity::find()
136 .filter(role_ow_entity::Column::ChannelId.eq(channel_id))
137 .all(&db)
138 .await?;
139 Ok(items.into_iter().map(RoleOverwriteView::from).collect())
150 let items = role_ow_entity::Entity::find()
151 .filter(role_ow_entity::Column::ChannelId.eq(channel_id))
152 .all(&db)
153 .await?;
154 Ok(items.into_iter().map(RoleOverwriteView::from).collect())
155 }
156 .await;
157 metrics::record_outcome(metrics::FEATURE_OVERWRITES_LIST_ROLE, &result);
158 result
140159}
141160
142161pub async fn set_user_overwrite(
@@ -146,29 +165,35 @@
146165 channel_id: Uuid,
147166 cmd: SetUserOverwriteCmd,
148167) -> SpaceResult<UserOverwriteView> {
149 authz::require_perm(svc, caller, space_id, authz::MANAGE_OVERWRITES).await?;
150 let db = svc.db.writer();
151 channel_in_space(&db, space_id, channel_id).await?;
168 let _timer = metrics::Timer::start(metrics::FEATURE_OVERWRITES_SET_USER);
169 let result: SpaceResult<UserOverwriteView> = async {
170 authz::require_perm(svc, caller, space_id, authz::MANAGE_OVERWRITES).await?;
171 let db = svc.db.writer();
172 channel_in_space(&db, space_id, channel_id).await?;
152173
153 let existing = user_ow_entity::Entity::find_by_id((channel_id, cmd.user_id))
154 .one(&db)
155 .await?;
174 let existing = user_ow_entity::Entity::find_by_id((channel_id, cmd.user_id))
175 .one(&db)
176 .await?;
156177
157 let saved: user_ow_entity::Model = if let Some(ow) = existing {
158 let mut active: user_ow_entity::ActiveModel = ow.into();
159 active.allow_mask = Set(cmd.allow_mask);
160 active.deny_mask = Set(cmd.deny_mask);
161 active.update(&db).await?
162 } else {
163 let active: user_ow_entity::ActiveModel = user_ow_entity::ActiveModel::builder()
164 .set_channel_id(channel_id)
165 .set_user_id(cmd.user_id)
166 .set_allow_mask(cmd.allow_mask)
167 .set_deny_mask(cmd.deny_mask)
168 .into();
169 active.insert(&db).await?
170 };
171 Ok(saved.into())
178 let saved: user_ow_entity::Model = if let Some(ow) = existing {
179 let mut active: user_ow_entity::ActiveModel = ow.into();
180 active.allow_mask = Set(cmd.allow_mask);
181 active.deny_mask = Set(cmd.deny_mask);
182 active.update(&db).await?
183 } else {
184 let active: user_ow_entity::ActiveModel = user_ow_entity::ActiveModel::builder()
185 .set_channel_id(channel_id)
186 .set_user_id(cmd.user_id)
187 .set_allow_mask(cmd.allow_mask)
188 .set_deny_mask(cmd.deny_mask)
189 .into();
190 active.insert(&db).await?
191 };
192 Ok(saved.into())
193 }
194 .await;
195 metrics::record_outcome(metrics::FEATURE_OVERWRITES_SET_USER, &result);
196 result
172197}
173198
174199pub async fn remove_user_overwrite(
@@ -178,19 +203,25 @@
178203 channel_id: Uuid,
179204 user_id: Uuid,
180205) -> SpaceResult<()> {
181 authz::require_perm(svc, caller, space_id, authz::MANAGE_OVERWRITES).await?;
182 let db = svc.db.writer();
183 channel_in_space(&db, space_id, channel_id).await?;
206 let _timer = metrics::Timer::start(metrics::FEATURE_OVERWRITES_REMOVE_USER);
207 let result: SpaceResult<()> = async {
208 authz::require_perm(svc, caller, space_id, authz::MANAGE_OVERWRITES).await?;
209 let db = svc.db.writer();
210 channel_in_space(&db, space_id, channel_id).await?;
184211
185 let res = user_ow_entity::Entity::delete_by_id((channel_id, user_id))
186 .exec(&db)
187 .await?;
188 if res.rows_affected == 0 {
189 return Err(SpaceError::NotFound(format!(
190 "user overwrite for user {user_id} in channel {channel_id}"
191 )));
212 let res = user_ow_entity::Entity::delete_by_id((channel_id, user_id))
213 .exec(&db)
214 .await?;
215 if res.rows_affected == 0 {
216 return Err(SpaceError::NotFound(format!(
217 "user overwrite for user {user_id} in channel {channel_id}"
218 )));
219 }
220 Ok(())
192221 }
193 Ok(())
222 .await;
223 metrics::record_outcome(metrics::FEATURE_OVERWRITES_REMOVE_USER, &result);
224 result
194225}
195226
196227pub async fn list_user_overwrites(
@@ -199,13 +230,19 @@
199230 space_id: Uuid,
200231 channel_id: Uuid,
201232) -> SpaceResult<Vec<UserOverwriteView>> {
202 member_space(svc, caller, space_id).await?;
203 let db = svc.db.reader();
204 channel_in_space(&db, space_id, channel_id).await?;
233 let _timer = metrics::Timer::start(metrics::FEATURE_OVERWRITES_LIST_USER);
234 let result: SpaceResult<Vec<UserOverwriteView>> = async {
235 member_space(svc, caller, space_id).await?;
236 let db = svc.db.reader();
237 channel_in_space(&db, space_id, channel_id).await?;
205238
206 let items = user_ow_entity::Entity::find()
207 .filter(user_ow_entity::Column::ChannelId.eq(channel_id))
208 .all(&db)
209 .await?;
210 Ok(items.into_iter().map(UserOverwriteView::from).collect())
239 let items = user_ow_entity::Entity::find()
240 .filter(user_ow_entity::Column::ChannelId.eq(channel_id))
241 .all(&db)
242 .await?;
243 Ok(items.into_iter().map(UserOverwriteView::from).collect())
244 }
245 .await;
246 metrics::record_outcome(metrics::FEATURE_OVERWRITES_LIST_USER, &result);
247 result
211248}
No newline at end of file
Modified lib/space/features/roles/usecase.rs View lib/space/features/roles/usecase.rs
@@ -9,6 +9,7 @@
99use crate::error::{SpaceError, SpaceResult};
1010use crate::features::authz;
1111use crate::features::SpaceService;
12use crate::metrics;
1213
1314use super::{CreateRoleCmd, RolePage, RoleView, UpdateRoleCmd};
1415
@@ -77,72 +78,78 @@
7778 space_id: Uuid,
7879 cmd: CreateRoleCmd,
7980) -> SpaceResult<RoleView> {
80 let name = cmd.name.trim().to_string();
81 if name.is_empty() {
82 return Err(SpaceError::BadRequest("role name cannot be empty".to_string()));
83 }
84 if name.chars().count() > svc.config.max_role_name_len {
85 return Err(SpaceError::BadRequest(format!(
86 "role name exceeds {} characters",
87 svc.config.max_role_name_len
88 )));
89 }
81 let _timer = metrics::Timer::start(metrics::FEATURE_ROLES_CREATE);
82 let result: SpaceResult<RoleView> = async {
83 let name = cmd.name.trim().to_string();
84 if name.is_empty() {
85 return Err(SpaceError::BadRequest("role name cannot be empty".to_string()));
86 }
87 if name.chars().count() > svc.config.max_role_name_len {
88 return Err(SpaceError::BadRequest(format!(
89 "role name exceeds {} characters",
90 svc.config.max_role_name_len
91 )));
92 }
9093
91 owner_space(svc, caller, space_id).await?;
94 owner_space(svc, caller, space_id).await?;
9295
93 let db = svc.db.writer();
94 let txn = db.begin().await?;
96 let db = svc.db.writer();
97 let txn = db.begin().await?;
9598
96 let existing = role_entity::Entity::find()
97 .filter(role_entity::Column::SpaceId.eq(space_id))
98 .filter(role_entity::Column::Name.eq(&name))
99 .one(&txn)
100 .await?;
101 if existing.is_some() {
102 return Err(SpaceError::Conflict(format!(
103 "role with name {name} already exists in space {space_id}"
104 )));
105 }
99 let existing = role_entity::Entity::find()
100 .filter(role_entity::Column::SpaceId.eq(space_id))
101 .filter(role_entity::Column::Name.eq(&name))
102 .one(&txn)
103 .await?;
104 if existing.is_some() {
105 return Err(SpaceError::Conflict(format!(
106 "role with name {name} already exists in space {space_id}"
107 )));
108 }
106109
107 let now = OffsetDateTime::now_utc();
108 let role_id = Uuid::new_v4();
109 let color = cmd.color.clone().unwrap_or_default();
110 let unicode_emoji = cmd.unicode_emoji.clone();
111 let icon_hash = cmd.icon_hash.clone();
112 let position = cmd.position.unwrap_or(0);
113 let role: role_entity::ActiveModel = role_entity::ActiveModel::builder()
114 .set_id(role_id)
115 .set_space_id(space_id)
116 .set_name(name.clone())
117 .set_position(position)
118 .set_color(color.clone())
119 .set_unicode_emoji(unicode_emoji.clone())
120 .set_icon_hash(icon_hash.clone())
121 .set_version(1)
122 .set_created_at(now)
123 .set_updated_at(Some(now))
124 .set_created_by(caller)
125 .into();
126 role.insert(&txn).await?;
110 let now = OffsetDateTime::now_utc();
111 let role_id = Uuid::new_v4();
112 let color = cmd.color.clone().unwrap_or_default();
113 let unicode_emoji = cmd.unicode_emoji.clone();
114 let icon_hash = cmd.icon_hash.clone();
115 let position = cmd.position.unwrap_or(0);
116 let role: role_entity::ActiveModel = role_entity::ActiveModel::builder()
117 .set_id(role_id)
118 .set_space_id(space_id)
119 .set_name(name.clone())
120 .set_position(position)
121 .set_color(color.clone())
122 .set_unicode_emoji(unicode_emoji.clone())
123 .set_icon_hash(icon_hash.clone())
124 .set_version(1)
125 .set_created_at(now)
126 .set_updated_at(Some(now))
127 .set_created_by(caller)
128 .into();
129 role.insert(&txn).await?;
127130
128 replace_permissions(&txn, role_id, &cmd.permissions).await?;
129 txn.commit().await?;
131 replace_permissions(&txn, role_id, &cmd.permissions).await?;
132 txn.commit().await?;
130133
131 let mut view = RoleView::from(role_entity::Model {
132 id: role_id,
133 space_id,
134 name,
135 position,
136 color,
137 unicode_emoji,
138 icon_hash,
139 version: 1,
140 created_at: now,
141 updated_at: Some(now),
142 created_by: caller,
143 });
144 view.permissions = cmd.permissions;
145 Ok(view)
134 let mut view = RoleView::from(role_entity::Model {
135 id: role_id,
136 space_id,
137 name,
138 position,
139 color,
140 unicode_emoji,
141 icon_hash,
142 version: 1,
143 created_at: now,
144 updated_at: Some(now),
145 created_by: caller,
146 });
147 view.permissions = cmd.permissions;
148 Ok(view)
149 }
150 .await;
151 metrics::record_outcome(metrics::FEATURE_ROLES_CREATE, &result);
152 result
146153}
147154
148155pub async fn get_role(
@@ -151,35 +158,41 @@
151158 space_id: Uuid,
152159 role_id: Uuid,
153160) -> SpaceResult<RoleView> {
154 let db = svc.db.reader();
155 let role = role_entity::Entity::find_by_id(role_id)
156 .one(&db)
157 .await?
158 .ok_or_else(|| SpaceError::NotFound(format!("role {role_id}")))?;
159 if role.space_id != space_id {
160 return Err(SpaceError::NotFound(format!("role {role_id}")));
161 }
162 let space = space_entity::Entity::find_by_id(space_id)
163 .one(&db)
164 .await?
165 .ok_or_else(|| SpaceError::NotFound(format!("space {space_id}")))?;
166 if !space.is_active {
167 return Err(SpaceError::NotFound(format!("role {role_id}")));
168 }
169 if !space.public {
170 let caller = caller.ok_or_else(|| SpaceError::NotFound(format!("role {role_id}")))?;
171 match authz::require_perm(svc, caller, space_id, authz::MANAGE_ROLES).await {
172 Ok(_) => {}
173 Err(SpaceError::Forbidden(_)) => {
174 return Err(SpaceError::NotFound(format!("role {role_id}")));
161 let _timer = metrics::Timer::start(metrics::FEATURE_ROLES_GET);
162 let result: SpaceResult<RoleView> = async {
163 let db = svc.db.reader();
164 let role = role_entity::Entity::find_by_id(role_id)
165 .one(&db)
166 .await?
167 .ok_or_else(|| SpaceError::NotFound(format!("role {role_id}")))?;
168 if role.space_id != space_id {
169 return Err(SpaceError::NotFound(format!("role {role_id}")));
170 }
171 let space = space_entity::Entity::find_by_id(space_id)
172 .one(&db)
173 .await?
174 .ok_or_else(|| SpaceError::NotFound(format!("space {space_id}")))?;
175 if !space.is_active {
176 return Err(SpaceError::NotFound(format!("role {role_id}")));
177 }
178 if !space.public {
179 let caller = caller.ok_or_else(|| SpaceError::NotFound(format!("role {role_id}")))?;
180 match authz::require_perm(svc, caller, space_id, authz::MANAGE_ROLES).await {
181 Ok(_) => {}
182 Err(SpaceError::Forbidden(_)) => {
183 return Err(SpaceError::NotFound(format!("role {role_id}")));
184 }
185 Err(e) => return Err(e),
175186 }
176 Err(e) => return Err(e),
177187 }
188 let perms = load_permissions(&db, role_id).await?;
189 let mut view = RoleView::from(role);
190 view.permissions = perms;
191 Ok(view)
178192 }
179 let perms = load_permissions(&db, role_id).await?;
180 let mut view = RoleView::from(role);
181 view.permissions = perms;
182 Ok(view)
193 .await;
194 metrics::record_outcome(metrics::FEATURE_ROLES_GET, &result);
195 result
183196}
184197
185198pub async fn update_role(
@@ -189,70 +202,76 @@
189202 role_id: Uuid,
190203 cmd: UpdateRoleCmd,
191204) -> SpaceResult<RoleView> {
192 owner_space(svc, caller, space_id).await?;
205 let _timer = metrics::Timer::start(metrics::FEATURE_ROLES_UPDATE);
206 let result: SpaceResult<RoleView> = async {
207 owner_space(svc, caller, space_id).await?;
193208
194 let db = svc.db.writer();
195 let txn = db.begin().await?;
209 let db = svc.db.writer();
210 let txn = db.begin().await?;
196211
197 let role = role_entity::Entity::find_by_id(role_id)
198 .one(&txn)
199 .await?
200 .ok_or_else(|| SpaceError::NotFound(format!("role {role_id}")))?;
201 if role.space_id != space_id {
202 return Err(SpaceError::NotFound(format!("role {role_id}")));
203 }
212 let role = role_entity::Entity::find_by_id(role_id)
213 .one(&txn)
214 .await?
215 .ok_or_else(|| SpaceError::NotFound(format!("role {role_id}")))?;
216 if role.space_id != space_id {
217 return Err(SpaceError::NotFound(format!("role {role_id}")));
218 }
204219
205 let mut active: role_entity::ActiveModel = role.into();
206 if let Some(name) = cmd.name {
207 let name = name.trim().to_string();
208 if name.is_empty() {
209 return Err(SpaceError::BadRequest("role name cannot be empty".to_string()));
220 let mut active: role_entity::ActiveModel = role.into();
221 if let Some(name) = cmd.name {
222 let name = name.trim().to_string();
223 if name.is_empty() {
224 return Err(SpaceError::BadRequest("role name cannot be empty".to_string()));
225 }
226 if name.chars().count() > svc.config.max_role_name_len {
227 return Err(SpaceError::BadRequest(format!(
228 "role name exceeds {} characters",
229 svc.config.max_role_name_len
230 )));
231 }
232 let dup = role_entity::Entity::find()
233 .filter(role_entity::Column::SpaceId.eq(space_id))
234 .filter(role_entity::Column::Name.eq(&name))
235 .filter(role_entity::Column::Id.ne(role_id))
236 .one(&txn)
237 .await?;
238 if dup.is_some() {
239 return Err(SpaceError::Conflict(format!(
240 "role with name {name} already exists in space {space_id}"
241 )));
242 }
243 active.name = Set(name);
244 }
245 if let Some(color) = cmd.color {
246 active.color = Set(color);
210247 }
211 if name.chars().count() > svc.config.max_role_name_len {
212 return Err(SpaceError::BadRequest(format!(
213 "role name exceeds {} characters",
214 svc.config.max_role_name_len
215 )));
248 if let Some(v) = cmd.unicode_emoji {
249 active.unicode_emoji = Set(v);
250 }
251 if let Some(v) = cmd.icon_hash {
252 active.icon_hash = Set(v);
216253 }
217 let dup = role_entity::Entity::find()
218 .filter(role_entity::Column::SpaceId.eq(space_id))
219 .filter(role_entity::Column::Name.eq(&name))
220 .filter(role_entity::Column::Id.ne(role_id))
221 .one(&txn)
222 .await?;
223 if dup.is_some() {
224 return Err(SpaceError::Conflict(format!(
225 "role with name {name} already exists in space {space_id}"
226 )));
254 if let Some(position) = cmd.position {
255 active.position = Set(position);
227256 }
228 active.name = Set(name);
229 }
230 if let Some(color) = cmd.color {
231 active.color = Set(color);
232 }
233 if let Some(v) = cmd.unicode_emoji {
234 active.unicode_emoji = Set(v);
235 }
236 if let Some(v) = cmd.icon_hash {
237 active.icon_hash = Set(v);
238 }
239 if let Some(position) = cmd.position {
240 active.position = Set(position);
241 }
242 active.version = Set(active.version.take().map_or(1, |v| v + 1));
243 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
244 let saved = active.update(&txn).await?;
257 active.version = Set(active.version.take().map_or(1, |v| v + 1));
258 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
259 let saved = active.update(&txn).await?;
245260
246 if let Some(perms) = cmd.permissions {
247 replace_permissions(&txn, role_id, &perms).await?;
248 }
261 if let Some(perms) = cmd.permissions {
262 replace_permissions(&txn, role_id, &perms).await?;
263 }
249264
250 txn.commit().await?;
265 txn.commit().await?;
251266
252 let perms = load_permissions(&svc.db.reader(), role_id).await?;
253 let mut view = RoleView::from(saved);
254 view.permissions = perms;
255 Ok(view)
267 let perms = load_permissions(&svc.db.reader(), role_id).await?;
268 let mut view = RoleView::from(saved);
269 view.permissions = perms;
270 Ok(view)
271 }
272 .await;
273 metrics::record_outcome(metrics::FEATURE_ROLES_UPDATE, &result);
274 result
256275}
257276
258277pub async fn delete_role(
@@ -261,21 +280,27 @@
261280 space_id: Uuid,
262281 role_id: Uuid,
263282) -> SpaceResult<()> {
264 owner_space(svc, caller, space_id).await?;
265 let db = svc.db.writer();
266 let txn = db.begin().await?;
267 role_exists(&txn, role_id).await?;
268 member_role_entity::Entity::delete_many()
269 .filter(member_role_entity::Column::RoleId.eq(role_id))
270 .exec(&txn)
271 .await?;
272 role_perm_entity::Entity::delete_many()
273 .filter(role_perm_entity::Column::RoleId.eq(role_id))
274 .exec(&txn)
275 .await?;
276 role_entity::Entity::delete_by_id(role_id).exec(&txn).await?;
277 txn.commit().await?;
278 Ok(())
283 let _timer = metrics::Timer::start(metrics::FEATURE_ROLES_DELETE);
284 let result: SpaceResult<()> = async {
285 owner_space(svc, caller, space_id).await?;
286 let db = svc.db.writer();
287 let txn = db.begin().await?;
288 role_exists(&txn, role_id).await?;
289 member_role_entity::Entity::delete_many()
290 .filter(member_role_entity::Column::RoleId.eq(role_id))
291 .exec(&txn)
292 .await?;
293 role_perm_entity::Entity::delete_many()
294 .filter(role_perm_entity::Column::RoleId.eq(role_id))
295 .exec(&txn)
296 .await?;
297 role_entity::Entity::delete_by_id(role_id).exec(&txn).await?;
298 txn.commit().await?;
299 Ok(())
300 }
301 .await;
302 metrics::record_outcome(metrics::FEATURE_ROLES_DELETE, &result);
303 result
279304}
280305
281306pub async fn list_roles(
@@ -285,32 +310,38 @@
285310 limit: u64,
286311 offset: u64,
287312) -> SpaceResult<RolePage> {
288 authz::require_perm(svc, caller, space_id, authz::MANAGE_ROLES).await?;
289 let db = svc.db.reader();
290 let limit = std::cmp::min(limit, svc.config.max_pagination);
291 let roles = role_entity::Entity::find()
292 .filter(role_entity::Column::SpaceId.eq(space_id))
293 .order_by_asc(role_entity::Column::Position)
294 .limit(Some(limit))
295 .offset(Some(offset))
296 .all(&db)
297 .await?;
298 let total = role_entity::Entity::find()
299 .filter(role_entity::Column::SpaceId.eq(space_id))
300 .count(&db)
301 .await?;
313 let _timer = metrics::Timer::start(metrics::FEATURE_ROLES_LIST);
314 let result: SpaceResult<RolePage> = async {
315 authz::require_perm(svc, caller, space_id, authz::MANAGE_ROLES).await?;
316 let db = svc.db.reader();
317 let limit = std::cmp::min(limit, svc.config.max_pagination);
318 let roles = role_entity::Entity::find()
319 .filter(role_entity::Column::SpaceId.eq(space_id))
320 .order_by_asc(role_entity::Column::Position)
321 .limit(Some(limit))
322 .offset(Some(offset))
323 .all(&db)
324 .await?;
325 let total = role_entity::Entity::find()
326 .filter(role_entity::Column::SpaceId.eq(space_id))
327 .count(&db)
328 .await?;
302329
303 let mut views = Vec::with_capacity(roles.len());
304 for role in roles {
305 let perms = load_permissions(&db, role.id).await?;
306 let mut view = RoleView::from(role);
307 view.permissions = perms;
308 views.push(view);
330 let mut views = Vec::with_capacity(roles.len());
331 for role in roles {
332 let perms = load_permissions(&db, role.id).await?;
333 let mut view = RoleView::from(role);
334 view.permissions = perms;
335 views.push(view);
336 }
337 Ok(RolePage {
338 items: views,
339 total: total as i64,
340 })
309341 }
310 Ok(RolePage {
311 items: views,
312 total: total as i64,
313 })
342 .await;
343 metrics::record_outcome(metrics::FEATURE_ROLES_LIST, &result);
344 result
314345}
315346
316347pub async fn assign_role(
@@ -320,19 +351,25 @@
320351 user_id: Uuid,
321352 role_id: Uuid,
322353) -> SpaceResult<()> {
323 owner_space(svc, caller, space_id).await?;
324 let db = svc.db.writer();
325 let txn = db.begin().await?;
326 role_exists(&txn, role_id).await?;
327 let mr: member_role_entity::ActiveModel = member_role_entity::ActiveModel::builder()
328 .set_space_id(space_id)
329 .set_user_id(user_id)
330 .set_role_id(role_id)
331 .set_created_at(OffsetDateTime::now_utc())
332 .into();
333 mr.insert(&txn).await?;
334 txn.commit().await?;
335 Ok(())
354 let _timer = metrics::Timer::start(metrics::FEATURE_ROLES_ASSIGN);
355 let result: SpaceResult<()> = async {
356 owner_space(svc, caller, space_id).await?;
357 let db = svc.db.writer();
358 let txn = db.begin().await?;
359 role_exists(&txn, role_id).await?;
360 let mr: member_role_entity::ActiveModel = member_role_entity::ActiveModel::builder()
361 .set_space_id(space_id)
362 .set_user_id(user_id)
363 .set_role_id(role_id)
364 .set_created_at(OffsetDateTime::now_utc())
365 .into();
366 mr.insert(&txn).await?;
367 txn.commit().await?;
368 Ok(())
369 }
370 .await;
371 metrics::record_outcome(metrics::FEATURE_ROLES_ASSIGN, &result);
372 result
336373}
337374
338375pub async fn unassign_role(
@@ -342,14 +379,20 @@
342379 user_id: Uuid,
343380 role_id: Uuid,
344381) -> SpaceResult<()> {
345 owner_space(svc, caller, _space_id).await?;
346 let db = svc.db.writer();
347 let txn = db.begin().await?;
348 member_role_entity::Entity::delete_many()
349 .filter(member_role_entity::Column::UserId.eq(user_id))
350 .filter(member_role_entity::Column::RoleId.eq(role_id))
351 .exec(&txn)
352 .await?;
353 txn.commit().await?;
354 Ok(())
382 let _timer = metrics::Timer::start(metrics::FEATURE_ROLES_UNASSIGN);
383 let result: SpaceResult<()> = async {
384 owner_space(svc, caller, _space_id).await?;
385 let db = svc.db.writer();
386 let txn = db.begin().await?;
387 member_role_entity::Entity::delete_many()
388 .filter(member_role_entity::Column::UserId.eq(user_id))
389 .filter(member_role_entity::Column::RoleId.eq(role_id))
390 .exec(&txn)
391 .await?;
392 txn.commit().await?;
393 Ok(())
394 }
395 .await;
396 metrics::record_outcome(metrics::FEATURE_ROLES_UNASSIGN, &result);
397 result
355398}
No newline at end of file
Modified lib/space/features/spaces/usecase.rs View lib/space/features/spaces/usecase.rs
@@ -6,6 +6,7 @@
66use crate::error::{SpaceError, SpaceResult};
77use crate::features::authz;
88use crate::features::SpaceService;
9use crate::metrics;
910
1011use super::{
1112 CreateSpaceCmd, SpaceAllowJoinLevel, SpacePage, SpaceView, UpdateSpaceCmd,
@@ -16,53 +17,59 @@
1617 caller: Uuid,
1718 cmd: CreateSpaceCmd,
1819) -> SpaceResult<SpaceView> {
19 let name = cmd.name.trim();
20 if name.is_empty() || name.len() > svc.config.max_space_name_len {
21 return Err(SpaceError::BadRequest(format!(
22 "invalid space name (length 1..={})",
23 svc.config.max_space_name_len
24 )));
25 }
26 if let Some(desc) = cmd.description.as_deref()
27 && desc.len() > svc.config.max_space_desc_len {
28 return Err(SpaceError::BadRequest("description too long".into()));
20 let _timer = metrics::Timer::start(metrics::FEATURE_SPACES_CREATE);
21 let result: SpaceResult<SpaceView> = async {
22 let name = cmd.name.trim();
23 if name.is_empty() || name.len() > svc.config.max_space_name_len {
24 return Err(SpaceError::BadRequest(format!(
25 "invalid space name (length 1..={})",
26 svc.config.max_space_name_len
27 )));
2928 }
29 if let Some(desc) = cmd.description.as_deref()
30 && desc.len() > svc.config.max_space_desc_len {
31 return Err(SpaceError::BadRequest("description too long".into()));
32 }
3033
31 let writer = svc.db.writer();
32 let now = OffsetDateTime::now_utc();
33 let id = Uuid::new_v4();
34 let active: space_entity::ActiveModel = space_entity::ActiveModel::builder()
35 .set_id(id)
36 .set_lower_name(name.to_lowercase())
37 .set_name(name.to_string())
38 .set_description(cmd.description)
39 .set_owner_id(caller)
40 .set_avatar_url(cmd.avatar_url)
41 .set_public(cmd.public)
42 .set_is_active(true)
43 .set_allow_join(cmd.allow_join)
44 .set_allow_invite(cmd.allow_invite)
45 .set_invite_sort_url_hash(None)
46 .set_member_count(0)
47 .set_banner_url(None)
48 .set_banner_width(None)
49 .set_banner_height(None)
50 .set_invite_banner_url(None)
51 .set_invite_banner_width(None)
52 .set_invite_banner_height(None)
53 .set_embed_splash_url(None)
54 .set_embed_splash_width(None)
55 .set_embed_splash_height(None)
56 .set_welcome_message(cmd.welcome_message)
57 .set_allow_join_level(space_entity::AllowJoinLevel::from(
58 cmd.allow_join_level.unwrap_or(SpaceAllowJoinLevel::None),
59 ))
60 .set_member_must_enable_2fa(cmd.member_must_enable_2fa)
61 .set_created_at(now)
62 .set_updated_at(now)
63 .into();
64 let saved = active.insert(&writer).await?;
65 Ok(saved.into())
34 let writer = svc.db.writer();
35 let now = OffsetDateTime::now_utc();
36 let id = Uuid::new_v4();
37 let active: space_entity::ActiveModel = space_entity::ActiveModel::builder()
38 .set_id(id)
39 .set_lower_name(name.to_lowercase())
40 .set_name(name.to_string())
41 .set_description(cmd.description)
42 .set_owner_id(caller)
43 .set_avatar_url(cmd.avatar_url)
44 .set_public(cmd.public)
45 .set_is_active(true)
46 .set_allow_join(cmd.allow_join)
47 .set_allow_invite(cmd.allow_invite)
48 .set_invite_sort_url_hash(None)
49 .set_member_count(0)
50 .set_banner_url(None)
51 .set_banner_width(None)
52 .set_banner_height(None)
53 .set_invite_banner_url(None)
54 .set_invite_banner_width(None)
55 .set_invite_banner_height(None)
56 .set_embed_splash_url(None)
57 .set_embed_splash_width(None)
58 .set_embed_splash_height(None)
59 .set_welcome_message(cmd.welcome_message)
60 .set_allow_join_level(space_entity::AllowJoinLevel::from(
61 cmd.allow_join_level.unwrap_or(SpaceAllowJoinLevel::None),
62 ))
63 .set_member_must_enable_2fa(cmd.member_must_enable_2fa)
64 .set_created_at(now)
65 .set_updated_at(now)
66 .into();
67 let saved = active.insert(&writer).await?;
68 Ok(saved.into())
69 }
70 .await;
71 metrics::record_outcome(metrics::FEATURE_SPACES_CREATE, &result);
72 result
6673}
6774
6875pub async fn get_space(
@@ -70,23 +77,29 @@
7077 caller: Option<Uuid>,
7178 id: Uuid,
7279) -> SpaceResult<SpaceView> {
73 let db = svc.db.reader();
74 let space = space_entity::Entity::find_by_id(id)
75 .one(&db)
76 .await?
77 .ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?;
78 if !space.is_active {
79 return Err(SpaceError::NotFound(format!("space {id}")));
80 }
81 if space.public {
82 return Ok(space.into());
83 }
84 let uid = caller.ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?;
85 match authz::effective_permissions(&db, id, uid).await {
86 Ok(_) => Ok(space.into()),
87 Err(SpaceError::Forbidden(_)) => Err(SpaceError::NotFound(format!("space {id}"))),
88 Err(e) => Err(e),
80 let _timer = metrics::Timer::start(metrics::FEATURE_SPACES_GET);
81 let result: SpaceResult<SpaceView> = async {
82 let db = svc.db.reader();
83 let space = space_entity::Entity::find_by_id(id)
84 .one(&db)
85 .await?
86 .ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?;
87 if !space.is_active {
88 return Err(SpaceError::NotFound(format!("space {id}")));
89 }
90 if space.public {
91 return Ok(space.into());
92 }
93 let uid = caller.ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?;
94 match authz::effective_permissions(&db, id, uid).await {
95 Ok(_) => Ok(space.into()),
96 Err(SpaceError::Forbidden(_)) => Err(SpaceError::NotFound(format!("space {id}"))),
97 Err(e) => Err(e),
98 }
8999 }
100 .await;
101 metrics::record_outcome(metrics::FEATURE_SPACES_GET, &result);
102 result
90103}
91104
92105pub async fn update_space(
@@ -95,80 +108,92 @@
95108 id: Uuid,
96109 cmd: UpdateSpaceCmd,
97110) -> SpaceResult<SpaceView> {
98 let writer = svc.db.writer();
99 let txn = writer.begin().await?;
111 let _timer = metrics::Timer::start(metrics::FEATURE_SPACES_UPDATE);
112 let result: SpaceResult<SpaceView> = async {
113 let writer = svc.db.writer();
114 let txn = writer.begin().await?;
100115
101 let space = space_entity::Entity::find_by_id(id)
102 .one(&txn)
103 .await?
104 .ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?;
105 if space.owner_id != caller {
106 return Err(SpaceError::Forbidden("only the owner can update the space".into()));
107 }
116 let space = space_entity::Entity::find_by_id(id)
117 .one(&txn)
118 .await?
119 .ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?;
120 if space.owner_id != caller {
121 return Err(SpaceError::Forbidden("only the owner can update the space".into()));
122 }
108123
109 let mut active: space_entity::ActiveModel = space.into();
110 if let Some(name) = cmd.name {
111 let name = name.trim();
112 if name.is_empty() || name.len() > svc.config.max_space_name_len {
113 return Err(SpaceError::BadRequest("invalid space name".into()));
124 let mut active: space_entity::ActiveModel = space.into();
125 if let Some(name) = cmd.name {
126 let name = name.trim();
127 if name.is_empty() || name.len() > svc.config.max_space_name_len {
128 return Err(SpaceError::BadRequest("invalid space name".into()));
129 }
130 active.name = Set(name.to_string());
131 active.lower_name = Set(name.to_lowercase());
114132 }
115 active.name = Set(name.to_string());
116 active.lower_name = Set(name.to_lowercase());
117 }
118 if let Some(desc) = cmd.description {
119 if desc.as_deref().is_some_and(|d| d.len() > svc.config.max_space_desc_len) {
120 return Err(SpaceError::BadRequest("description too long".into()));
133 if let Some(desc) = cmd.description {
134 if desc.as_deref().is_some_and(|d| d.len() > svc.config.max_space_desc_len) {
135 return Err(SpaceError::BadRequest("description too long".into()));
136 }
137 active.description = Set(desc);
121138 }
122 active.description = Set(desc);
123 }
124 if let Some(avatar) = cmd.avatar_url {
125 active.avatar_url = Set(avatar);
126 }
127 if let Some(public) = cmd.public {
128 active.public = Set(public);
129 }
130 if let Some(allow_join) = cmd.allow_join {
131 active.allow_join = Set(allow_join);
132 }
133 if let Some(allow_invite) = cmd.allow_invite {
134 active.allow_invite = Set(allow_invite);
135 }
136 if let Some(welcome) = cmd.welcome_message {
137 if welcome.as_deref().is_some_and(|w| w.len() > svc.config.max_space_desc_len) {
138 return Err(SpaceError::BadRequest("welcome message too long".into()));
139 if let Some(avatar) = cmd.avatar_url {
140 active.avatar_url = Set(avatar);
139141 }
140 active.welcome_message = Set(welcome);
141 }
142 if let Some(level) = cmd.allow_join_level {
143 active.allow_join_level = Set(level.into());
144 }
145 if let Some(twofa) = cmd.member_must_enable_2fa {
146 active.member_must_enable_2fa = Set(twofa);
142 if let Some(public) = cmd.public {
143 active.public = Set(public);
144 }
145 if let Some(allow_join) = cmd.allow_join {
146 active.allow_join = Set(allow_join);
147 }
148 if let Some(allow_invite) = cmd.allow_invite {
149 active.allow_invite = Set(allow_invite);
150 }
151 if let Some(welcome) = cmd.welcome_message {
152 if welcome.as_deref().is_some_and(|w| w.len() > svc.config.max_space_desc_len) {
153 return Err(SpaceError::BadRequest("welcome message too long".into()));
154 }
155 active.welcome_message = Set(welcome);
156 }
157 if let Some(level) = cmd.allow_join_level {
158 active.allow_join_level = Set(level.into());
159 }
160 if let Some(twofa) = cmd.member_must_enable_2fa {
161 active.member_must_enable_2fa = Set(twofa);
162 }
163 active.updated_at = Set(OffsetDateTime::now_utc());
164 let saved = active.update(&txn).await?;
165 txn.commit().await?;
166 Ok(saved.into())
147167 }
148 active.updated_at = Set(OffsetDateTime::now_utc());
149 let saved = active.update(&txn).await?;
150 txn.commit().await?;
151 Ok(saved.into())
168 .await;
169 metrics::record_outcome(metrics::FEATURE_SPACES_UPDATE, &result);
170 result
152171}
153172
154173pub async fn delete_space(svc: &SpaceService, caller: Uuid, id: Uuid) -> SpaceResult<()> {
155 let writer = svc.db.writer();
156 let txn = writer.begin().await?;
174 let _timer = metrics::Timer::start(metrics::FEATURE_SPACES_DELETE);
175 let result: SpaceResult<()> = async {
176 let writer = svc.db.writer();
177 let txn = writer.begin().await?;
157178
158 let space = space_entity::Entity::find_by_id(id)
159 .one(&txn)
160 .await?
161 .ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?;
162 if space.owner_id != caller {
163 return Err(SpaceError::Forbidden("only the owner can delete the space".into()));
164 }
179 let space = space_entity::Entity::find_by_id(id)
180 .one(&txn)
181 .await?
182 .ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?;
183 if space.owner_id != caller {
184 return Err(SpaceError::Forbidden("only the owner can delete the space".into()));
185 }
165186
166 let mut active: space_entity::ActiveModel = space.into();
167 active.is_active = Set(false);
168 active.updated_at = Set(OffsetDateTime::now_utc());
169 active.update(&txn).await?;
170 txn.commit().await?;
171 Ok(())
187 let mut active: space_entity::ActiveModel = space.into();
188 active.is_active = Set(false);
189 active.updated_at = Set(OffsetDateTime::now_utc());
190 active.update(&txn).await?;
191 txn.commit().await?;
192 Ok(())
193 }
194 .await;
195 metrics::record_outcome(metrics::FEATURE_SPACES_DELETE, &result);
196 result
172197}
173198
174199pub async fn list_spaces(
@@ -177,24 +202,30 @@
177202 limit: u64,
178203 offset: u64,
179204) -> SpaceResult<SpacePage> {
180 let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1) as u64;
181 let db = svc.db.reader();
205 let _timer = metrics::Timer::start(metrics::FEATURE_SPACES_LIST);
206 let result: SpaceResult<SpacePage> = async {
207 let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1) as u64;
208 let db = svc.db.reader();
182209
183 let (items, total) = futures::join!(
184 space_entity::Entity::find()
185 .filter(space_entity::Column::OwnerId.eq(caller))
186 .limit(Some(limit))
187 .offset(Some(offset))
188 .all(&db),
189 space_entity::Entity::find()
190 .filter(space_entity::Column::OwnerId.eq(caller))
191 .count(&db),
192 );
193 let items = items?;
194 let total = total? as i64;
210 let (items, total) = futures::join!(
211 space_entity::Entity::find()
212 .filter(space_entity::Column::OwnerId.eq(caller))
213 .limit(Some(limit))
214 .offset(Some(offset))
215 .all(&db),
216 space_entity::Entity::find()
217 .filter(space_entity::Column::OwnerId.eq(caller))
218 .count(&db),
219 );
220 let items = items?;
221 let total = total? as i64;
195222
196 Ok(SpacePage {
197 items: items.into_iter().map(Into::into).collect(),
198 total,
199 })
223 Ok(SpacePage {
224 items: items.into_iter().map(Into::into).collect(),
225 total,
226 })
227 }
228 .await;
229 metrics::record_outcome(metrics::FEATURE_SPACES_LIST, &result);
230 result
200231}
No newline at end of file