Skip to content
Clone

refactor(channel): tree-based channels with space-inherited permissions

- channel gains kind discriminator (Directory container / Object leaf);
  object channels are minted atomically with their resource (no dangling)
- repo/project/issue-collection/chat creation each mint their own object
  channel in the same transaction, under an optional parent directory
- permission inheritance: space_members.status == Active is the single
  access source for git and chat; admin authority = space owner/role
- remove channel_members and chat_members tables + all surface
  (space feature/handler/api/export, storagelayer member repo/mapper/mock)
- chat per-user state (read cursor + mute) folded into chat_reads
- gitauth permission reworked to space inheritance + collaborator
- project gains optional channel_id binding
- %apply whole-repo rustfmt + uncomment comment cleanup
zhenyi committed
parent b1b5ac8 commit 2138ab4 Browse files

300 files changed

+1400 −1492

This change is very large, so some diffs are not shown. Clone the repository to see the full change.

File tree 300
Modified chat/storagelayer/blob/media.rs View chat/storagelayer/blob/media.rs
@@ -56,10 +56,7 @@
5656 bytes: Bytes,
5757 ) -> ChatStorageResult<AttachmentPath>;
5858
59 async fn get_attachment_bytes(
60 &self,
61 path: &AttachmentPath,
62 ) -> ChatStorageResult<Bytes>;
59 async fn get_attachment_bytes(&self, path: &AttachmentPath) -> ChatStorageResult<Bytes>;
6360
6461 async fn delete_attachment(&self, path: &AttachmentPath) -> ChatStorageResult<()>;
6562
@@ -78,4 +75,4 @@
7875 ) -> ChatStorageResult<Url>;
7976}
8077
81pub type DynChatMediaStore = std::sync::Arc<dyn ChatMediaStore>;
No newline at end of file
78pub type DynChatMediaStore = std::sync::Arc<dyn ChatMediaStore>;
Modified chat/storagelayer/blob/object_store.rs View chat/storagelayer/blob/object_store.rs
@@ -70,18 +70,11 @@
7070 Ok(attachment)
7171 }
7272
73 async fn get_attachment_bytes(
74 &self,
75 path: &AttachmentPath,
76 ) -> ChatStorageResult<Bytes> {
73 async fn get_attachment_bytes(&self, path: &AttachmentPath) -> ChatStorageResult<Bytes> {
7774 use futures::TryStreamExt;
7875 let key = path.as_key();
7976 let op = self.object_path(&key);
80 let result = self
81 .inner
82 .get(&op)
83 .await
84 .map_err(ChatStorageError::from)?;
77 let result = self.inner.get(&op).await.map_err(ChatStorageError::from)?;
8578 let collected: Vec<Bytes> = result
8679 .into_stream()
8780 .try_collect()
@@ -102,10 +95,7 @@
10295 async fn delete_attachment(&self, path: &AttachmentPath) -> ChatStorageResult<()> {
10396 let key = path.as_key();
10497 let op = self.object_path(&key);
105 self.inner
106 .delete(&op)
107 .await
108 .map_err(ChatStorageError::from)
98 self.inner.delete(&op).await.map_err(ChatStorageError::from)
10999 }
110100
111101 async fn presign_upload(
@@ -136,4 +126,4 @@
136126 .await
137127 .map_err(ChatStorageError::from)
138128 }
139}
No newline at end of file
129}
Modified chat/storagelayer/mapper/attachment.rs View chat/storagelayer/mapper/attachment.rs
@@ -5,15 +5,14 @@
55use time::OffsetDateTime;
66use uuid::Uuid;
77
8use crate::Page;
89use crate::auth;
910use crate::error::{ChatStorageError, ChatStorageResult};
1011use crate::models::attachment::{Attachment, AttachmentOwner, NewAttachment};
11use crate::Page;
12use crate::repository::attachment::AttachmentRepository;
1312use crate::repository::ChatStorageCtx;
13use crate::repository::attachment::AttachmentRepository;
1414
1515use db::attachment::attachments as att_entity;
16
1716
1817pub struct SeaAttachmentRepository {
1918 db: Arc<db::conn::Db>,
@@ -32,7 +31,8 @@
3231 ctx: &ChatStorageCtx,
3332 input: NewAttachment,
3433 ) -> ChatStorageResult<Attachment> {
35 auth::require_chat_member_for_attachment_owner(&self.db, ctx, input.owner, input.owner_id).await?;
34 auth::require_chat_member_for_attachment_owner(&self.db, ctx, input.owner, input.owner_id)
35 .await?;
3636 let db = self.db.writer();
3737 let active = att_entity::ActiveModel {
3838 id: Set(Uuid::new_v4()),
@@ -61,7 +61,8 @@
6161 return Ok(None);
6262 };
6363 if let Some(chat_id) =
64 auth::chat_id_for_attachment_owner(&self.db, att.owner_type.into(), att.owner_id).await?
64 auth::chat_id_for_attachment_owner(&self.db, att.owner_type.into(), att.owner_id)
65 .await?
6566 {
6667 auth::require_member(&self.db, ctx, chat_id).await?;
6768 } else {
@@ -77,8 +78,7 @@
7778 owner_id: Uuid,
7879 page: Page,
7980 ) -> ChatStorageResult<Vec<Attachment>> {
80 if let Some(chat_id) =
81 auth::chat_id_for_attachment_owner(&self.db, owner, owner_id).await?
81 if let Some(chat_id) = auth::chat_id_for_attachment_owner(&self.db, owner, owner_id).await?
8282 {
8383 auth::require_member(&self.db, ctx, chat_id).await?;
8484 } else {
@@ -108,6 +108,7 @@
108108}
109109impl std::fmt::Debug for SeaAttachmentRepository {
110110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 f.debug_struct("SeaAttachmentRepository").finish_non_exhaustive()
111 f.debug_struct("SeaAttachmentRepository")
112 .finish_non_exhaustive()
112113 }
113}
No newline at end of file
114}
Modified chat/storagelayer/mapper/conversation.rs View chat/storagelayer/mapper/conversation.rs
@@ -1,20 +1,41 @@
11use std::sync::Arc;
22
33use async_trait::async_trait;
4use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Set};
4use sea_orm::{
5 ActiveModelTrait, EntityTrait, IntoActiveModel, PaginatorTrait, QueryFilter, QueryOrder,
6 QuerySelect, Set, TransactionTrait,
7};
8use space::error::SpaceError;
9use space::features::channels::usecase::ensure_channel_for_resource;
510use time::OffsetDateTime;
611use uuid::Uuid;
712
13use crate::Page;
814use crate::auth;
915use crate::error::{ChatStorageError, ChatStorageResult};
10use crate::models::conversation::{Conversation, ConversationKind, ConversationMember, NewConversation};
11use crate::Page;
12use crate::repository::conversation::ConversationRepository;
16use crate::models::conversation::{Conversation, ConversationKind, NewConversation};
1317use crate::repository::ChatStorageCtx;
18use crate::repository::conversation::ConversationRepository;
1419
15use db::chat::chat_members as member_entity;
1620use db::chat::chats as chat_entity;
1721
22impl From<SpaceError> for ChatStorageError {
23 fn from(e: SpaceError) -> Self {
24 match e {
25 SpaceError::BadRequest(s) | SpaceError::NotFound(s) => {
26 ChatStorageError::InvalidInput(s)
27 }
28 SpaceError::Unauthorized(s) | SpaceError::Forbidden(s) => {
29 ChatStorageError::PermissionDenied(s)
30 }
31 SpaceError::Conflict(s) => ChatStorageError::Conflict(s),
32 SpaceError::RateLimited(s) | SpaceError::Internal(s) => ChatStorageError::Internal(s),
33 SpaceError::Sqlx(e) => ChatStorageError::SeaOrm(e),
34 SpaceError::Uuid(e) => ChatStorageError::InvalidInput(e.to_string()),
35 }
36 }
37}
38
1839pub struct SeaConversationRepository {
1940 db: Arc<db::conn::Db>,
2041}
@@ -27,58 +48,87 @@
2748
2849#[async_trait]
2950impl ConversationRepository for SeaConversationRepository {
30 async fn create(&self, ctx: &ChatStorageCtx, input: NewConversation) -> ChatStorageResult<Conversation> {
51 async fn create(
52 &self,
53 ctx: &ChatStorageCtx,
54 input: NewConversation,
55 ) -> ChatStorageResult<Conversation> {
3156 let creator = auth::require_authenticated(ctx).await?;
32 let db = self.db.writer();
3357 let id = Uuid::new_v4();
3458 let now = OffsetDateTime::now_utc();
3559
36 let chat = chat_entity::ActiveModel {
37 id: Set(id),
38 channel_id: Set(input.channel_id),
39 kind: Set(input.kind.into()),
40 counterpart_user_id: Set(input.counterpart_user_id),
41 title: Set(input.title.clone()),
42 creator_user_id: Set(creator),
43 created_at: Set(now),
44 updated_at: Set(None),
45 };
46 chat_entity::Entity::insert(chat).exec(&db).await?;
60 let space_id = input.space_id;
61 let parent_dir = input.parent_dir;
62 let kind = input.kind;
63 let counterpart_user_id = input.counterpart_user_id;
64 let channel_name = input.title.as_deref().unwrap_or("chat").to_string();
65 let title = input.title;
66 let title_for_chat = title.clone();
4767
48 let mut initial: Vec<Uuid> = input.initial_members.clone();
49 if !initial.contains(&creator) {
50 initial.push(creator);
51 }
52 for user_id in initial {
53 let member = member_entity::ActiveModel {
54 chat_id: Set(id),
55 user_id: Set(user_id),
56 joined_at: Set(now),
57 last_read_at: Set(None),
58 muted: Set(false),
59 };
60 member_entity::Entity::insert(member).exec(&db).await?;
61 }
68 let channel_id = self
69 .db
70 .writer()
71 .transaction(|txn| {
72 Box::pin(async move {
73 let object_channel = ensure_channel_for_resource(
74 txn,
75 space_id,
76 parent_dir,
77 &channel_name,
78 creator,
79 )
80 .await?;
81 let chat = chat_entity::ActiveModel {
82 id: Set(id),
83 channel_id: Set(object_channel.id),
84 kind: Set(kind.into()),
85 counterpart_user_id: Set(counterpart_user_id),
86 title: Set(title_for_chat.clone()),
87 creator_user_id: Set(creator),
88 created_at: Set(now),
89 updated_at: Set(None),
90 };
91 chat_entity::Entity::insert(chat).exec(txn).await?;
92 Ok(object_channel.id)
93 })
94 })
95 .await
96 .map_err(|e| match e {
97 sea_orm::TransactionError::Connection(err) => ChatStorageError::SeaOrm(err),
98 sea_orm::TransactionError::Transaction(err) => err,
99 })?;
62100
63101 Ok(Conversation {
64102 id,
65 channel_id: input.channel_id,
66 kind: input.kind,
67 counterpart_user_id: input.counterpart_user_id,
68 title: input.title,
103 channel_id,
104 kind,
105 counterpart_user_id,
106 title,
69107 creator_user_id: creator,
70108 created_at: now,
71109 updated_at: None,
72110 })
73111 }
74112
75 async fn find_by_id(&self, ctx: &ChatStorageCtx, id: Uuid) -> ChatStorageResult<Option<Conversation>> {
113 async fn find_by_id(
114 &self,
115 ctx: &ChatStorageCtx,
116 id: Uuid,
117 ) -> ChatStorageResult<Option<Conversation>> {
76118 auth::require_member(&self.db, ctx, id).await?;
77119 let db = self.db.reader();
78 Ok(chat_entity::Entity::find_by_id(id).one(&db).await?.map(Into::into))
120 Ok(chat_entity::Entity::find_by_id(id)
121 .one(&db)
122 .await?
123 .map(Into::into))
79124 }
80125
81 async fn list_by_channel(&self, ctx: &ChatStorageCtx, channel_id: Uuid, page: Page) -> ChatStorageResult<Vec<Conversation>> {
126 async fn list_by_channel(
127 &self,
128 ctx: &ChatStorageCtx,
129 channel_id: Uuid,
130 page: Page,
131 ) -> ChatStorageResult<Vec<Conversation>> {
82132 auth::require_authenticated(ctx).await?;
83133 let db = self.db.reader();
84134 let rows = chat_entity::Entity::find()
@@ -90,7 +140,12 @@
90140 Ok(rows.into_iter().map(Into::into).collect())
91141 }
92142
93 async fn update_title(&self, ctx: &ChatStorageCtx, id: Uuid, title: Option<String>) -> ChatStorageResult<()> {
143 async fn update_title(
144 &self,
145 ctx: &ChatStorageCtx,
146 id: Uuid,
147 title: Option<String>,
148 ) -> ChatStorageResult<()> {
94149 auth::require_admin(&self.db, ctx, id).await?;
95150 let db = self.db.writer();
96151 let Some(chat) = chat_entity::Entity::find_by_id(id).one(&db).await? else {
@@ -103,7 +158,12 @@
103158 Ok(())
104159 }
105160
106 async fn update_kind(&self, ctx: &ChatStorageCtx, id: Uuid, kind: ConversationKind) -> ChatStorageResult<()> {
161 async fn update_kind(
162 &self,
163 ctx: &ChatStorageCtx,
164 id: Uuid,
165 kind: ConversationKind,
166 ) -> ChatStorageResult<()> {
107167 auth::require_admin(&self.db, ctx, id).await?;
108168 let db = self.db.writer();
109169 let Some(chat) = chat_entity::Entity::find_by_id(id).one(&db).await? else {
@@ -123,32 +183,17 @@
123183 if result.rows_affected == 0 {
124184 return Err(ChatStorageError::ConversationNotFound(id));
125185 }
126 member_entity::Entity::delete_many()
127 .filter(member_entity::COLUMN.chat_id.eq(id))
128 .exec(&db)
129 .await?;
130186 Ok(())
131187 }
132188
133189 async fn exists(&self, _ctx: &ChatStorageCtx, id: Uuid) -> ChatStorageResult<bool> {
134190 let db = self.db.reader();
135191 Ok(chat_entity::Entity::find_by_id(id).count(&db).await? > 0)
136 }
137
138 async fn list_members(&self, ctx: &ChatStorageCtx, chat_id: Uuid, page: Page) -> ChatStorageResult<Vec<ConversationMember>> {
139 auth::require_member(&self.db, ctx, chat_id).await?;
140 let db = self.db.reader();
141 let rows = member_entity::Entity::find()
142 .filter(member_entity::COLUMN.chat_id.eq(chat_id))
143 .order_by_asc(member_entity::COLUMN.joined_at)
144 .limit(page.clamped_limit())
145 .all(&db)
146 .await?;
147 Ok(rows.into_iter().map(Into::into).collect())
148192 }
149193}
150194impl std::fmt::Debug for SeaConversationRepository {
151195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 f.debug_struct("SeaConversationRepository").finish_non_exhaustive()
196 f.debug_struct("SeaConversationRepository")
197 .finish_non_exhaustive()
153198 }
154}
No newline at end of file
199}
Modified chat/storagelayer/mapper/mention.rs View chat/storagelayer/mapper/mention.rs
@@ -8,8 +8,8 @@
88use crate::auth;
99use crate::error::ChatStorageResult;
1010use crate::models::mention::{Mention, NewMention};
11use crate::repository::ChatStorageCtx;
1112use crate::repository::mention::MentionRepository;
12use crate::repository::ChatStorageCtx;
1313
1414use db::chat::chat_message_mentions as mention_entity;
1515
@@ -89,6 +89,7 @@
8989}
9090impl std::fmt::Debug for SeaMentionRepository {
9191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.debug_struct("SeaMentionRepository").finish_non_exhaustive()
92 f.debug_struct("SeaMentionRepository")
93 .finish_non_exhaustive()
9394 }
9495}
Modified chat/storagelayer/mapper/message.rs View chat/storagelayer/mapper/message.rs
@@ -10,13 +10,13 @@
1010use time::OffsetDateTime;
1111use uuid::Uuid;
1212
13use crate::Page;
1314use crate::error::{ChatStorageError, ChatStorageResult};
1415use crate::models::message::{
1516 ListDirection, Message, MessageFlags, MessagePage, MessageQuery, NewMessage,
1617};
17use crate::Page;
18use crate::repository::message::MessageRepository;
1918use crate::repository::ChatStorageCtx;
19use crate::repository::message::MessageRepository;
2020
2121use db::attachment::attachments as att_entity;
2222use db::chat::chat_messages as message_entity;
@@ -34,11 +34,7 @@
3434
3535#[async_trait]
3636impl MessageRepository for SeaMessageRepository {
37 async fn create(
38 &self,
39 ctx: &ChatStorageCtx,
40 input: NewMessage,
41 ) -> ChatStorageResult<Message> {
37 async fn create(&self, ctx: &ChatStorageCtx, input: NewMessage) -> ChatStorageResult<Message> {
4238 crate::auth::require_member(&self.db, ctx, input.chat_id).await?;
4339 crate::auth::require_self(ctx, input.sender_id)?;
4440 let db = self.db.writer();
@@ -143,7 +139,11 @@
143139 Ok(created)
144140 }
145141
146 async fn find_by_id(&self, ctx: &ChatStorageCtx, id: Uuid) -> ChatStorageResult<Option<Message>> {
142 async fn find_by_id(
143 &self,
144 ctx: &ChatStorageCtx,
145 id: Uuid,
146 ) -> ChatStorageResult<Option<Message>> {
147147 let chat_id = crate::auth::chat_id_for_message(&self.db, id).await?;
148148 crate::auth::require_member(&self.db, ctx, chat_id).await?;
149149 let db = self.db.reader();
@@ -240,7 +240,10 @@
240240 .all(&db)
241241 .await?;
242242 let items = models.into_iter().map(Into::into).collect();
243 Ok(MessagePage { items, next_cursor: None })
243 Ok(MessagePage {
244 items,
245 next_cursor: None,
246 })
244247 }
245248
246249 async fn edit(
@@ -262,8 +265,7 @@
262265 return Err(ChatStorageError::MessageDeleted(id));
263266 }
264267 if model.sender_id != user_id {
265 crate::auth::require_admin_in_txn(txn, user_id, model.chat_id)
266 .await?;
268 crate::auth::require_admin_in_txn(txn, user_id, model.chat_id).await?;
267269 }
268270 let flags = MessageFlags::from_bits(model.flags);
269271 let mut active = model.into_active_model();
@@ -276,9 +278,7 @@
276278 })
277279 .await
278280 .map_err(|e| match e {
279 sea_orm::TransactionError::Connection(err) => {
280 ChatStorageError::SeaOrm(err)
281 }
281 sea_orm::TransactionError::Connection(err) => ChatStorageError::SeaOrm(err),
282282 sea_orm::TransactionError::Transaction(err) => err,
283283 })
284284 }
@@ -297,8 +297,7 @@
297297 return Ok(());
298298 }
299299 if model.sender_id != user_id {
300 crate::auth::require_admin_in_txn(txn, user_id, model.chat_id)
301 .await?;
300 crate::auth::require_admin_in_txn(txn, user_id, model.chat_id).await?;
302301 }
303302 let flags = MessageFlags::from_bits(model.flags);
304303 let mut active = model.into_active_model();
@@ -354,6 +353,7 @@
354353}
355354impl std::fmt::Debug for SeaMessageRepository {
356355 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357 f.debug_struct("SeaMessageRepository").finish_non_exhaustive()
356 f.debug_struct("SeaMessageRepository")
357 .finish_non_exhaustive()
358358 }
359359}
Modified chat/storagelayer/mapper/pinned.rs View chat/storagelayer/mapper/pinned.rs
@@ -8,8 +8,8 @@
88use crate::auth;
99use crate::error::{ChatStorageError, ChatStorageResult};
1010use crate::models::pinned::{NewPinnedMessage, PinnedMessage};
11use crate::repository::ChatStorageCtx;
1112use crate::repository::pinned::PinnedMessageRepository;
12use crate::repository::ChatStorageCtx;
1313
1414use db::chat::chat_pinned_messages as pinned_entity;
1515
@@ -39,7 +39,11 @@
3939 auth::require_admin_in_txn(txn, caller, input.chat_id).await?;
4040 let existing = pinned_entity::Entity::find()
4141 .filter(pinned_entity::Entity::COLUMN.chat_id.eq(input.chat_id))
42 .filter(pinned_entity::Entity::COLUMN.message_id.eq(input.message_id))
42 .filter(
43 pinned_entity::Entity::COLUMN
44 .message_id
45 .eq(input.message_id),
46 )
4347 .one(txn)
4448 .await?;
4549 if existing.is_some() {
@@ -85,7 +89,9 @@
8589 let Some(model) = found else {
8690 return Err(ChatStorageError::PinnedMessageNotFound(message_id));
8791 };
88 let res = pinned_entity::Entity::delete_by_id(model.id).exec(txn).await?;
92 let res = pinned_entity::Entity::delete_by_id(model.id)
93 .exec(txn)
94 .await?;
8995 if res.rows_affected == 0 {
9096 return Err(ChatStorageError::PinnedMessageNotFound(model.id));
9197 }
@@ -116,6 +122,7 @@
116122}
117123impl std::fmt::Debug for SeaPinnedMessageRepository {
118124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 f.debug_struct("SeaPinnedMessageRepository").finish_non_exhaustive()
125 f.debug_struct("SeaPinnedMessageRepository")
126 .finish_non_exhaustive()
120127 }
121}
No newline at end of file
128}
Modified chat/storagelayer/mapper/reaction.rs View chat/storagelayer/mapper/reaction.rs
@@ -8,8 +8,8 @@
88use crate::auth;
99use crate::error::{ChatStorageError, ChatStorageResult};
1010use crate::models::reaction::{NewReaction, Reaction};
11use crate::repository::ChatStorageCtx;
1112use crate::repository::reaction::ReactionRepository;
12use crate::repository::ChatStorageCtx;
1313
1414use db::chat::chat_message_reactions as reaction_entity;
1515
@@ -35,7 +35,11 @@
3535 auth::require_self(ctx, input.user_id)?;
3636 let db = self.db.writer();
3737 let existing = reaction_entity::Entity::find()
38 .filter(reaction_entity::Entity::COLUMN.message_id.eq(input.message_id))
38 .filter(
39 reaction_entity::Entity::COLUMN
40 .message_id
41 .eq(input.message_id),
42 )
3943 .filter(reaction_entity::Entity::COLUMN.user_id.eq(input.user_id))
4044 .filter(reaction_entity::Entity::COLUMN.emoji.eq(&input.emoji))
4145 .one(&db)
@@ -95,6 +99,7 @@
9599}
96100impl std::fmt::Debug for SeaReactionRepository {
97101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 f.debug_struct("SeaReactionRepository").finish_non_exhaustive()
102 f.debug_struct("SeaReactionRepository")
103 .finish_non_exhaustive()
99104 }
100}
No newline at end of file
105}
Modified chat/storagelayer/mapper/read_receipt.rs View chat/storagelayer/mapper/read_receipt.rs
@@ -8,8 +8,8 @@
88use crate::auth;
99use crate::error::ChatStorageResult;
1010use crate::models::read_receipt::{NewReadReceipt, ReadReceipt};
11use crate::repository::ChatStorageCtx;
1112use crate::repository::read_receipt::ReadReceiptRepository;
12use crate::repository::ChatStorageCtx;
1313
1414use db::chat::chat_reads as read_entity;
1515
@@ -56,6 +56,7 @@
5656 user_id: Set(input.user_id),
5757 thread_id: Set(input.thread_id),
5858 last_read_message_id: Set(input.last_read_message_id),
59 muted: Set(false),
5960 updated_at: Set(now),
6061 };
6162 let model = read_entity::Entity::insert(active)
@@ -99,9 +100,47 @@
99100 .await?;
100101 Ok(model.map(Into::into))
101102 }
103
104 async fn set_muted(
105 &self,
106 ctx: &ChatStorageCtx,
107 chat_id: Uuid,
108 muted: bool,
109 ) -> ChatStorageResult<()> {
110 let _caller = auth::require_member(&self.db, ctx, chat_id).await?;
111 let user_id = ctx.require_user()?;
112 let db = self.db.writer();
113 let existing = read_entity::Entity::find()
114 .filter(read_entity::Entity::COLUMN.chat_id.eq(chat_id))
115 .filter(read_entity::Entity::COLUMN.user_id.eq(user_id))
116 .filter(read_entity::Entity::COLUMN.thread_id.is_null())
117 .one(&db)
118 .await?;
119 if let Some(model) = existing {
120 let mut active = model.into_active_model();
121 active.muted = Set(muted);
122 active.updated_at = Set(OffsetDateTime::now_utc());
123 active.update(&db).await?;
124 } else {
125 let active = read_entity::ActiveModel {
126 id: Set(Uuid::new_v4()),
127 chat_id: Set(chat_id),
128 user_id: Set(user_id),
129 thread_id: Set(None),
130 last_read_message_id: Set(None),
131 muted: Set(muted),
132 updated_at: Set(OffsetDateTime::now_utc()),
133 };
134 read_entity::Entity::insert(active)
135 .exec_with_returning(&db)
136 .await?;
137 }
138 Ok(())
139 }
102140}
103141impl std::fmt::Debug for SeaReadReceiptRepository {
104142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 f.debug_struct("SeaReadReceiptRepository").finish_non_exhaustive()
143 f.debug_struct("SeaReadReceiptRepository")
144 .finish_non_exhaustive()
106145 }
107146}
Modified chat/storagelayer/mapper/thread.rs View chat/storagelayer/mapper/thread.rs
@@ -8,8 +8,8 @@
88use crate::auth;
99use crate::error::{ChatStorageError, ChatStorageResult};
1010use crate::models::thread::Thread;
11use crate::repository::ChatStorageCtx;
1112use crate::repository::thread::ThreadRepository;
12use crate::repository::ChatStorageCtx;
1313
1414use db::chat::threads as thread_entity;
1515
@@ -41,7 +41,11 @@
4141 if let Some(model) = existing {
4242 thread_entity::Entity::update_many()
4343 .filter(thread_entity::Entity::COLUMN.chat_id.eq(chat_id))
44 .filter(thread_entity::Entity::COLUMN.root_message_id.eq(root_message_id))
44 .filter(
45 thread_entity::Entity::COLUMN
46 .root_message_id
47 .eq(root_message_id),
48 )
4549 .set(thread_entity::ActiveModel {
4650 reply_count: Set(model.reply_count + 1),
4751 last_reply_at: Set(Some(at)),
@@ -99,6 +103,7 @@
99103}
100104impl std::fmt::Debug for SeaThreadRepository {
101105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 f.debug_struct("SeaThreadRepository").finish_non_exhaustive()
106 f.debug_struct("SeaThreadRepository")
107 .finish_non_exhaustive()
103108 }
104109}
Modified chat/storagelayer/mock/in_memory.rs View chat/storagelayer/mock/in_memory.rs
@@ -9,11 +9,11 @@
99use url::Url;
1010use uuid::Uuid;
1111
12use crate::blob::ChatMediaStore;
1213use crate::blob::media::AttachmentPath;
13use crate::blob::ChatMediaStore;
1414use crate::error::{ChatStorageError, ChatStorageResult};
1515use crate::models::attachment::{Attachment, AttachmentOwner, NewAttachment};
16use crate::models::conversation::{Conversation, ConversationKind, ConversationMember, NewConversation};
16use crate::models::conversation::{Conversation, ConversationKind, NewConversation};
1717use crate::models::mention::{Mention, NewMention};
1818use crate::models::message::{
1919 ListDirection, Message, MessageFlags, MessagePage, MessageQuery, NewMessage, Page,
@@ -24,7 +24,6 @@
2424use crate::models::thread::Thread;
2525use crate::repository::attachment::{AttachmentRepository, DynAttachmentRepository};
2626use crate::repository::conversation::{ConversationRepository, DynConversationRepository};
27use crate::repository::member::{DynMemberRepository, MemberRepository};
2827use crate::repository::mention::{DynMentionRepository, MentionRepository};
2928use crate::repository::message::{DynMessageRepository, MessageRepository};
3029use crate::repository::pinned::{DynPinnedMessageRepository, PinnedMessageRepository};
@@ -39,7 +38,6 @@
3938 pub creators: RwLock<HashMap<Uuid, Uuid>>,
4039 pub messages: RwLock<HashMap<Uuid, Message>>,
4140 pub attachments: RwLock<HashMap<Uuid, Attachment>>,
42 pub members: RwLock<HashMap<(Uuid, Uuid), ConversationMember>>,
4341 pub reactions: RwLock<HashMap<(Uuid, Uuid, String), Reaction>>,
4442 pub mentions: RwLock<HashMap<Uuid, Mention>>,
4543 pub read_receipts: RwLock<HashMap<Uuid, ReadReceipt>>,
@@ -53,20 +51,15 @@
5351 ctx.require_user()
5452}
5553
56fn is_member(state: &SharedState, chat_id: Uuid, user_id: Uuid) -> bool {
57 state.members.read().unwrap().contains_key(&(chat_id, user_id))
58}
59
6054fn is_admin(state: &SharedState, chat_id: Uuid, user_id: Uuid) -> bool {
61 let is_creator = state
55 state
6256 .creators
6357 .read()
6458 .unwrap()
6559 .get(&chat_id)
6660 .copied()
6761 .map(|c| c == user_id)
68 .unwrap_or(false);
69 is_creator && is_member(state, chat_id, user_id)
62 .unwrap_or(false)
7063}
7164
7265fn chat_id_for_message(state: &SharedState, message_id: Uuid) -> Option<Uuid> {
@@ -86,17 +79,20 @@
8679 }
8780}
8881
89fn require_member(state: &SharedState, ctx: &ChatStorageCtx, chat_id: Uuid) -> ChatStorageResult<Uuid> {
90 let user_id = require_user(ctx)?;
91 if !is_member(state, chat_id, user_id) {
92 return Err(ChatStorageError::PermissionDenied(format!(
93 "user {user_id} is not a member of chat {chat_id}"
94 )));
95 }
96 Ok(user_id)
82fn require_member(
83 state: &SharedState,
84 ctx: &ChatStorageCtx,
85 _chat_id: Uuid,
86) -> ChatStorageResult<Uuid> {
87 let _ = state;
88 require_user(ctx)
9789}
9890
99fn require_admin(state: &SharedState, ctx: &ChatStorageCtx, chat_id: Uuid) -> ChatStorageResult<Uuid> {
91fn require_admin(
92 state: &SharedState,
93 ctx: &ChatStorageCtx,
94 chat_id: Uuid,
95) -> ChatStorageResult<Uuid> {
10096 let user_id = require_user(ctx)?;
10197 if !is_admin(state, chat_id, user_id) {
10298 return Err(ChatStorageError::PermissionDenied(format!(
@@ -181,7 +177,7 @@
181177 let now = OffsetDateTime::now_utc();
182178 let conv = Conversation {
183179 id: Uuid::new_v4(),
184 channel_id: input.channel_id,
180 channel_id: Uuid::new_v4(),
185181 kind: input.kind,
186182 counterpart_user_id: input.counterpart_user_id,
187183 title: input.title,
@@ -200,22 +196,6 @@
200196 .unwrap()
201197 .insert(conv.id, creator);
202198
203 let joined_at = now;
204 let mut members = self.state.members.write().unwrap();
205 let mut initial = input.initial_members.clone();
206 if !initial.contains(&creator) {
207 initial.push(creator);
208 }
209 for uid in initial {
210 let m = ConversationMember {
211 chat_id: conv.id,
212 user_id: uid,
213 joined_at,
214 last_read_at: None,
215 muted: false,
216 };
217 members.insert((conv.id, uid), m);
218 }
219199 Ok(conv)
220200 }
221201
@@ -280,43 +260,22 @@
280260
281261 async fn delete(&self, ctx: &ChatStorageCtx, id: Uuid) -> ChatStorageResult<()> {
282262 require_admin(&self.state, ctx, id)?;
283 let removed = self.state.conversations.write().unwrap().remove(&id).is_some();
263 let removed = self
264 .state
265 .conversations
266 .write()
267 .unwrap()
268 .remove(&id)
269 .is_some();
284270 if !removed {
285271 return Err(ChatStorageError::ConversationNotFound(id));
286272 }
287273 self.state.creators.write().unwrap().remove(&id);
288 let mut members = self.state.members.write().unwrap();
289 let keys: Vec<(Uuid, Uuid)> = members
290 .keys()
291 .filter(|(c, _)| *c == id)
292 .copied()
293 .collect();
294 for k in keys {
295 members.remove(&k);
296 }
297274 Ok(())
298275 }
299276
300277 async fn exists(&self, _ctx: &ChatStorageCtx, id: Uuid) -> ChatStorageResult<bool> {
301278 Ok(self.state.conversations.read().unwrap().contains_key(&id))
302 }
303
304 async fn list_members(
305 &self,
306 ctx: &ChatStorageCtx,
307 chat_id: Uuid,
308 page: Page,
309 ) -> ChatStorageResult<Vec<ConversationMember>> {
310 require_member(&self.state, ctx, chat_id)?;
311 let map = self.state.members.read().unwrap();
312 let mut out: Vec<ConversationMember> = map
313 .values()
314 .filter(|m| m.chat_id == chat_id)
315 .cloned()
316 .collect();
317 out.sort_by_key(|m| m.joined_at);
318 let limit = page.clamped_limit() as usize;
319 Ok(out.into_iter().take(limit).collect())
320279 }
321280}
322281
@@ -470,8 +429,8 @@
470429 }
471430
472431 async fn restore(&self, ctx: &ChatStorageCtx, id: Uuid) -> ChatStorageResult<()> {
473 let chat_id = chat_id_for_message(&self.state, id)
474 .ok_or(ChatStorageError::MessageNotFound(id))?;
432 let chat_id =
433 chat_id_for_message(&self.state, id).ok_or(ChatStorageError::MessageNotFound(id))?;
475434 require_admin(&self.state, ctx, chat_id)?;
476435 {
477436 let map = self.state.messages.read().unwrap();
@@ -516,14 +475,6 @@
516475 input: NewAttachment,
517476 ) -> ChatStorageResult<Attachment> {
518477 require_user(ctx)?;
519 if let Some(chat_id) =
520 chat_id_for_attachment_owner_in_state(&self.state, input.owner, input.owner_id)
521 && !is_member(&self.state, chat_id, require_user(ctx)?)
522 {
523 return Err(ChatStorageError::PermissionDenied(format!(
524 "user not a member of chat {chat_id}"
525 )));
526 }
527478 let att = Attachment {
528479 id: Uuid::new_v4(),
529480 owner: input.owner,
@@ -585,7 +536,13 @@
585536
586537 async fn delete(&self, ctx: &ChatStorageCtx, id: Uuid) -> ChatStorageResult<()> {
587538 require_uploader_or_admin(&self.state, ctx, id)?;
588 let removed = self.state.attachments.write().unwrap().remove(&id).is_some();
539 let removed = self
540 .state
541 .attachments
542 .write()
543 .unwrap()
544 .remove(&id)
545 .is_some();
589546 if !removed {
590547 return Err(ChatStorageError::AttachmentNotFound(id));
591548 }
@@ -605,127 +562,6 @@
605562}
606563
607564#[derive(Debug)]
608pub struct InMemoryMemberRepository {
609 state: SharedState,
610}
611
612impl InMemoryMemberRepository {
613 pub fn new(state: SharedState) -> Self {
614 Self { state }
615 }
616}
617
618#[async_trait]
619impl MemberRepository for InMemoryMemberRepository {
620 async fn add_member(
621 &self,
622 ctx: &ChatStorageCtx,
623 chat_id: Uuid,
624 user_id: Uuid,
625 ) -> ChatStorageResult<ConversationMember> {
626 require_admin(&self.state, ctx, chat_id)?;
627 let m = ConversationMember {
628 chat_id,
629 user_id,
630 joined_at: OffsetDateTime::now_utc(),
631 last_read_at: None,
632 muted: false,
633 };
634 self.state
635 .members
636 .write()
637 .unwrap()
638 .insert((chat_id, user_id), m.clone());
639 Ok(m)
640 }
641
642 async fn remove_member(
643 &self,
644 ctx: &ChatStorageCtx,
645 chat_id: Uuid,
646 user_id: Uuid,
647 ) -> ChatStorageResult<()> {
648 let caller = require_user(ctx)?;
649 if caller != user_id {
650 require_admin(&self.state, ctx, chat_id)?;
651 }
652 let removed = self
653 .state
654 .members
655 .write()
656 .unwrap()
657 .remove(&(chat_id, user_id))
658 .is_some();
659 if !removed {
660 return Err(ChatStorageError::MemberNotFound(chat_id, user_id));
661 }
662 Ok(())
663 }
664
665 async fn list_members(
666 &self,
667 ctx: &ChatStorageCtx,
668 chat_id: Uuid,
669 page: Page,
670 ) -> ChatStorageResult<Vec<ConversationMember>> {
671 require_member(&self.state, ctx, chat_id)?;
672 let map = self.state.members.read().unwrap();
673 let mut out: Vec<ConversationMember> = map
674 .values()
675 .filter(|m| m.chat_id == chat_id)
676 .cloned()
677 .collect();
678 out.sort_by_key(|m| m.joined_at);
679 let limit = page.clamped_limit() as usize;
680 Ok(out.into_iter().take(limit).collect())
681 }
682
683 async fn mark_read(
684 &self,
685 ctx: &ChatStorageCtx,
686 chat_id: Uuid,
687 at: OffsetDateTime,
688 ) -> ChatStorageResult<()> {
689 let user_id = require_member(&self.state, ctx, chat_id)?;
690 let mut map = self.state.members.write().unwrap();
691 let m = map
692 .get_mut(&(chat_id, user_id))
693 .ok_or(ChatStorageError::MemberNotFound(chat_id, user_id))?;
694 m.last_read_at = Some(at);
695 Ok(())
696 }
697
698 async fn set_muted(
699 &self,
700 ctx: &ChatStorageCtx,
701 chat_id: Uuid,
702 muted: bool,
703 ) -> ChatStorageResult<()> {
704 let user_id = require_member(&self.state, ctx, chat_id)?;
705 let mut map = self.state.members.write().unwrap();
706 let m = map
707 .get_mut(&(chat_id, user_id))
708 .ok_or(ChatStorageError::MemberNotFound(chat_id, user_id))?;
709 m.muted = muted;
710 Ok(())
711 }
712
713 async fn is_member(
714 &self,
715 _ctx: &ChatStorageCtx,
716 chat_id: Uuid,
717 user_id: Uuid,
718 ) -> ChatStorageResult<bool> {
719 Ok(self
720 .state
721 .members
722 .read()
723 .unwrap()
724 .contains_key(&(chat_id, user_id)))
725 }
726}
727
728#[derive(Debug)]
729565pub struct InMemoryReactionRepository {
730566 state: SharedState,
731567}
@@ -777,11 +613,7 @@
777613 let user_id = require_user(ctx)?;
778614 let chat_id = chat_id_for_message(&self.state, message_id)
779615 .ok_or(ChatStorageError::MessageNotFound(message_id))?;
780 if !is_member(&self.state, chat_id, user_id) {
781 return Err(ChatStorageError::PermissionDenied(format!(
782 "user {user_id} is not a member of chat {chat_id}"
783 )));
784 }
616 require_member(&self.state, ctx, chat_id)?;
785617 let removed = self
786618 .state
787619 .reactions
@@ -930,6 +762,7 @@
930762 user_id: input.user_id,
931763 thread_id: input.thread_id,
932764 last_read_message_id: input.last_read_message_id,
765 muted: false,
933766 updated_at: now,
934767 };
935768 map.insert(r.id, r.clone());
@@ -966,6 +799,39 @@
966799 .cloned();
967800 Ok(r)
968801 }
802
803 async fn set_muted(
804 &self,
805 ctx: &ChatStorageCtx,
806 chat_id: Uuid,
807 muted: bool,
808 ) -> ChatStorageResult<()> {
809 require_member(&self.state, ctx, chat_id)?;
810 let caller = require_user(ctx)?;
811 let mut map = self.state.read_receipts.write().unwrap();
812 let target = map
813 .values_mut()
814 .find(|r| r.chat_id == chat_id && r.user_id == caller && r.thread_id.is_none());
815 match target {
816 Some(r) => {
817 r.muted = muted;
818 r.updated_at = OffsetDateTime::now_utc();
819 }
820 None => {
821 let r = ReadReceipt {
822 id: Uuid::new_v4(),
823 chat_id,
824 user_id: caller,
825 thread_id: None,
826 last_read_message_id: None,
827 muted,
828 updated_at: OffsetDateTime::now_utc(),
829 };
830 map.insert(r.id, r);
831 }
832 }
833 Ok(())
834 }
969835}
970836
971837#[derive(Debug)]
@@ -1036,7 +902,11 @@
1036902 ) -> ChatStorageResult<Vec<PinnedMessage>> {
1037903 require_member(&self.state, ctx, chat_id)?;
1038904 let map = self.state.pinned.read().unwrap();
1039 Ok(map.values().filter(|p| p.chat_id == chat_id).cloned().collect())
905 Ok(map
906 .values()
907 .filter(|p| p.chat_id == chat_id)
908 .cloned()
909 .collect())
1040910 }
1041911}
1042912
@@ -1098,7 +968,11 @@
1098968 ) -> ChatStorageResult<Vec<Thread>> {
1099969 require_member(&self.state, ctx, chat_id)?;
1100970 let map = self.state.threads.read().unwrap();
1101 Ok(map.values().filter(|t| t.chat_id == chat_id).cloned().collect())
971 Ok(map
972 .values()
973 .filter(|t| t.chat_id == chat_id)
974 .cloned()
975 .collect())
1102976 }
1103977}
1104978
@@ -1184,9 +1058,6 @@
11841058 }
11851059 fn attachments(&self) -> DynAttachmentRepository {
11861060 Arc::new(InMemoryAttachmentRepository::new(self.state.clone()))
1187 }
1188 fn members(&self) -> DynMemberRepository {
1189 Arc::new(InMemoryMemberRepository::new(self.state.clone()))
11901061 }
11911062 fn reactions(&self) -> DynReactionRepository {
11921063 Arc::new(InMemoryReactionRepository::new(self.state.clone()))
@@ -1206,4 +1077,4 @@
12061077 fn blobs(&self) -> Arc<dyn ChatMediaStore> {
12071078 self.blobs.clone()
12081079 }
1209}
No newline at end of file
1080}
Modified chat/storagelayer/models/attachment.rs View chat/storagelayer/models/attachment.rs
@@ -63,4 +63,4 @@
6363 pub size: i64,
6464 pub url: String,
6565 pub uploaded_by: Option<Uuid>,
66}
No newline at end of file
66}
Modified chat/storagelayer/models/conversation.rs View chat/storagelayer/models/conversation.rs
@@ -23,19 +23,13 @@
2323}
2424
2525#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct ConversationMember {
27 pub chat_id: Uuid,
28 pub user_id: Uuid,
29 pub joined_at: OffsetDateTime,
30 pub last_read_at: Option<OffsetDateTime>,
31 pub muted: bool,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3526pub struct NewConversation {
36 pub channel_id: Uuid,
27 /// Space the chat lives in. Creating a chat atomically mints its own Object
28 /// channel (never a dangling object channel), mirroring repo/project/issue.
29 pub space_id: Uuid,
30 /// Optional Directory channel to host the chat's object channel under.
31 pub parent_dir: Option<Uuid>,
3732 pub kind: ConversationKind,
3833 pub counterpart_user_id: Option<Uuid>,
3934 pub title: Option<String>,
40 pub initial_members: Vec<Uuid>,
41}
No newline at end of file
35}
Modified chat/storagelayer/models/mention.rs View chat/storagelayer/models/mention.rs
@@ -22,4 +22,4 @@
2222 pub channel_id: Option<Uuid>,
2323 pub is_everyone: bool,
2424 pub is_here: bool,
25}
No newline at end of file
25}
Modified chat/storagelayer/models/message.rs View chat/storagelayer/models/message.rs
@@ -222,4 +222,4 @@
222222 pub fn clamped_limit(&self) -> u64 {
223223 self.limit.clamp(1, MAX_PAGE_LIMIT) as u64
224224 }
225}
No newline at end of file
225}
Modified chat/storagelayer/models/pinned.rs View chat/storagelayer/models/pinned.rs
@@ -16,4 +16,4 @@
1616 pub chat_id: Uuid,
1717 pub message_id: Uuid,
1818 pub pinned_by: Uuid,
19}
No newline at end of file
19}
Modified chat/storagelayer/models/reaction.rs View chat/storagelayer/models/reaction.rs
@@ -16,4 +16,4 @@
1616 pub message_id: Uuid,
1717 pub user_id: Uuid,
1818 pub emoji: String,
19}
No newline at end of file
19}
Modified chat/storagelayer/models/read_receipt.rs View chat/storagelayer/models/read_receipt.rs
@@ -9,6 +9,7 @@
99 pub user_id: Uuid,
1010 pub thread_id: Option<Uuid>,
1111 pub last_read_message_id: Option<Uuid>,
12 pub muted: bool,
1213 pub updated_at: OffsetDateTime,
1314}
1415
@@ -18,4 +19,4 @@
1819 pub user_id: Uuid,
1920 pub thread_id: Option<Uuid>,
2021 pub last_read_message_id: Option<Uuid>,
21}
No newline at end of file
22}
Modified chat/storagelayer/models/thread.rs View chat/storagelayer/models/thread.rs
@@ -9,4 +9,4 @@
99 pub reply_count: i32,
1010 pub last_reply_at: Option<OffsetDateTime>,
1111 pub last_reply_by: Option<Uuid>,
12}
No newline at end of file
12}
Modified chat/storagelayer/repository/attachment.rs View chat/storagelayer/repository/attachment.rs
@@ -33,4 +33,4 @@
3333 async fn delete(&self, ctx: &ChatStorageCtx, id: Uuid) -> ChatStorageResult<()>;
3434}
3535
36pub type DynAttachmentRepository = Arc<dyn AttachmentRepository>;
No newline at end of file
36pub type DynAttachmentRepository = Arc<dyn AttachmentRepository>;
Modified chat/storagelayer/repository/conversation.rs View chat/storagelayer/repository/conversation.rs
@@ -2,9 +2,7 @@
22use std::sync::Arc;
33
44use crate::error::ChatStorageResult;
5use crate::models::conversation::{
6 Conversation, ConversationKind, ConversationMember, NewConversation,
7};
5use crate::models::conversation::{Conversation, ConversationKind, NewConversation};
86use crate::models::message::Page;
97
108use super::ChatStorageCtx;
@@ -47,13 +45,6 @@
4745 async fn delete(&self, ctx: &ChatStorageCtx, id: uuid::Uuid) -> ChatStorageResult<()>;
4846
4947 async fn exists(&self, ctx: &ChatStorageCtx, id: uuid::Uuid) -> ChatStorageResult<bool>;
50
51 async fn list_members(
52 &self,
53 ctx: &ChatStorageCtx,
54 chat_id: uuid::Uuid,
55 page: Page,
56 ) -> ChatStorageResult<Vec<ConversationMember>>;
5748}
5849
59pub type DynConversationRepository = Arc<dyn ConversationRepository>;
No newline at end of file
50pub type DynConversationRepository = Arc<dyn ConversationRepository>;
Modified chat/storagelayer/repository/mention.rs View chat/storagelayer/repository/mention.rs
@@ -29,4 +29,4 @@
2929 ) -> ChatStorageResult<Vec<Mention>>;
3030}
3131
32pub type DynMentionRepository = Arc<dyn MentionRepository>;
No newline at end of file
32pub type DynMentionRepository = Arc<dyn MentionRepository>;
Modified chat/storagelayer/repository/message.rs View chat/storagelayer/repository/message.rs
@@ -9,11 +9,7 @@
99
1010#[async_trait]
1111pub trait MessageRepository: Send + Sync {
12 async fn create(
13 &self,
14 ctx: &ChatStorageCtx,
15 input: NewMessage,
16 ) -> ChatStorageResult<Message>;
12 async fn create(&self, ctx: &ChatStorageCtx, input: NewMessage) -> ChatStorageResult<Message>;
1713
1814 async fn find_by_id(
1915 &self,
@@ -50,4 +46,4 @@
5046 async fn exists(&self, ctx: &ChatStorageCtx, id: Uuid) -> ChatStorageResult<bool>;
5147}
5248
53pub type DynMessageRepository = Arc<dyn MessageRepository>;
No newline at end of file
49pub type DynMessageRepository = Arc<dyn MessageRepository>;
Modified chat/storagelayer/repository/pinned.rs View chat/storagelayer/repository/pinned.rs
@@ -29,4 +29,4 @@
2929 ) -> ChatStorageResult<Vec<PinnedMessage>>;
3030}
3131
32pub type DynPinnedMessageRepository = Arc<dyn PinnedMessageRepository>;
No newline at end of file
32pub type DynPinnedMessageRepository = Arc<dyn PinnedMessageRepository>;
Modified chat/storagelayer/repository/reaction.rs View chat/storagelayer/repository/reaction.rs
@@ -29,4 +29,4 @@
2929 ) -> ChatStorageResult<Vec<Reaction>>;
3030}
3131
32pub type DynReactionRepository = Arc<dyn ReactionRepository>;
No newline at end of file
32pub type DynReactionRepository = Arc<dyn ReactionRepository>;
Modified chat/storagelayer/repository/read_receipt.rs View chat/storagelayer/repository/read_receipt.rs
@@ -28,6 +28,14 @@
2828 user_id: Uuid,
2929 thread_id: Option<Uuid>,
3030 ) -> ChatStorageResult<Option<ReadReceipt>>;
31
32 /// Toggle the caller's per-chat mute flag (persisted on chat_reads.muted).
33 async fn set_muted(
34 &self,
35 ctx: &ChatStorageCtx,
36 chat_id: Uuid,
37 muted: bool,
38 ) -> ChatStorageResult<()>;
3139}
3240
33pub type DynReadReceiptRepository = Arc<dyn ReadReceiptRepository>;
No newline at end of file
41pub type DynReadReceiptRepository = Arc<dyn ReadReceiptRepository>;
Modified chat/storagelayer/repository/thread.rs View chat/storagelayer/repository/thread.rs
@@ -33,4 +33,4 @@
3333 ) -> ChatStorageResult<Vec<Thread>>;
3434}
3535
36pub type DynThreadRepository = Arc<dyn ThreadRepository>;
No newline at end of file
36pub type DynThreadRepository = Arc<dyn ThreadRepository>;
Modified chat/storagelayer/auth.rs View chat/storagelayer/auth.rs
@@ -1,4 +1,4 @@
1use sea_orm::{ConnectionTrait, DatabaseConnection, EntityTrait};
1use sea_orm::{ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter};
22use uuid::Uuid;
33
44use crate::error::{ChatStorageError, ChatStorageResult};
@@ -6,10 +6,14 @@
66use crate::repository::ChatStorageCtx;
77
88use db::attachment::attachments as att_entity;
9use db::chat::chat_members as member_entity;
9use db::channel::channels as channel_entity;
1010use db::chat::chat_messages as message_entity;
1111use db::chat::chats as chat_entity;
12use db::space::{space_member_roles, space_members, space_role_permissions, spaces};
1213
14/// Permission bit that elevates a space role to admin (mirrors gitauth::permission).
15pub const SPACE_ADMIN_PERMISSION: &str = "manage_members";
16
1317pub async fn require_authenticated(ctx: &ChatStorageCtx) -> ChatStorageResult<Uuid> {
1418 ctx.require_user()
1519}
@@ -24,81 +28,129 @@
2428 Ok(user_id)
2529}
2630
27pub async fn require_member(
28 db: &db::conn::Db,
29 ctx: &ChatStorageCtx,
31/// Resolve the space that owns a chat via chat -> channel -> space.
32async fn space_id_for_chat<C: ConnectionTrait>(
33 db: &C,
3034 chat_id: Uuid,
31) -> ChatStorageResult<Uuid> {
32 let user_id = require_authenticated(ctx).await?;
33 let member = member_entity::Entity::find_by_id((chat_id, user_id))
34 .one(&db.reader())
35) -> ChatStorageResult<Option<Uuid>> {
36 let chat = chat_entity::Entity::find_by_id(chat_id).one(db).await?;
37 let Some(chat) = chat else {
38 return Ok(None);
39 };
40 let Some(channel) = channel_entity::Entity::find_by_id(chat.channel_id)
41 .one(db)
42 .await?
43 else {
44 return Ok(None);
45 };
46 Ok(channel.space_id)
47}
48
49fn chat_not_in_space(chat_id: Uuid) -> ChatStorageError {
50 ChatStorageError::PermissionDenied(format!(
51 "chat {chat_id} is not bound to a space (channel or space missing)"
52 ))
53}
54
55async fn require_active_member<C: ConnectionTrait>(
56 db: &C,
57 space_id: Uuid,
58 user_id: Uuid,
59) -> ChatStorageResult<()> {
60 let member = space_members::Entity::find_by_id((space_id, user_id))
61 .one(db)
3562 .await?;
36 if member.is_none() {
63 let active = member
64 .map(|m| m.status == space_members::MemberStatus::Active)
65 .unwrap_or(false);
66 if !active {
3767 return Err(ChatStorageError::PermissionDenied(format!(
38 "user {user_id} is not a member of chat {chat_id}"
68 "user {user_id} is not an active member of space {space_id}"
3969 )));
4070 }
41 Ok(user_id)
71 Ok(())
4272}
4373
44pub async fn require_admin(
74/// Grants chat access iff the actor is an active member of the chat's space.
75///
76/// Channel/chat-level membership no longer exists: access to a chat inherits from
77/// space membership (space_members.status == Active).
78pub async fn require_member(
4579 db: &db::conn::Db,
4680 ctx: &ChatStorageCtx,
4781 chat_id: Uuid,
4882) -> ChatStorageResult<Uuid> {
4983 let user_id = require_authenticated(ctx).await?;
50 let chat = chat_entity::Entity::find_by_id(chat_id)
51 .one(&db.reader())
84 let space_id = space_id_for_chat(&db.reader(), chat_id)
5285 .await?
53 .ok_or(ChatStorageError::ConversationNotFound(chat_id))?;
54 if chat.creator_user_id != user_id {
55 return Err(ChatStorageError::PermissionDenied(format!(
56 "user {user_id} is not admin of chat {chat_id}"
57 )));
58 }
59 verify_creator_member(&db.reader(), chat_id, user_id).await?;
86 .ok_or_else(|| chat_not_in_space(chat_id))?;
87 require_active_member(&db.reader(), space_id, user_id).await?;
6088 Ok(user_id)
6189}
6290
63pub async fn require_admin_in_txn<C: ConnectionTrait>(
91async fn require_admin_in_space<C: ConnectionTrait>(
6492 txn: &C,
93 space_id: Uuid,
6594 user_id: Uuid,
66 chat_id: Uuid,
6795) -> ChatStorageResult<()> {
68 let chat = chat_entity::Entity::find_by_id(chat_id)
69 .one(txn)
96 let Some(space) = spaces::Entity::find_by_id(space_id).one(txn).await? else {
97 return Err(ChatStorageError::ConversationNotFound(space_id));
98 };
99 if space.owner_id == user_id {
100 return Ok(());
101 }
102 require_active_member(txn, space_id, user_id).await?;
103 let role_ids: Vec<Uuid> = space_member_roles::Entity::find()
104 .filter(space_member_roles::Column::SpaceId.eq(space_id))
105 .filter(space_member_roles::Column::UserId.eq(user_id))
106 .all(txn)
70107 .await?
71 .ok_or(ChatStorageError::ConversationNotFound(chat_id))?;
72 if chat.creator_user_id != user_id {
108 .into_iter()
109 .map(|m| m.role_id)
110 .collect();
111 if role_ids.is_empty() {
73112 return Err(ChatStorageError::PermissionDenied(format!(
74 "user {user_id} is not admin of chat {chat_id}"
113 "user {user_id} is not an admin of space {space_id}"
75114 )));
76115 }
77 verify_creator_member_in_txn(txn, chat_id, user_id).await
116 let is_admin = space_role_permissions::Entity::find()
117 .filter(space_role_permissions::Column::RoleId.is_in(role_ids))
118 .all(txn)
119 .await?
120 .iter()
121 .any(|p| p.permission == SPACE_ADMIN_PERMISSION);
122 if !is_admin {
123 return Err(ChatStorageError::PermissionDenied(format!(
124 "user {user_id} is not an admin of space {space_id}"
125 )));
126 }
127 Ok(())
78128}
79129
80async fn verify_creator_member(
81 db: &DatabaseConnection,
130/// Require the caller to be a space administrator (space owner, or a space role
131/// carrying the admin permission) for the chat's space.
132pub async fn require_admin(
133 db: &db::conn::Db,
134 ctx: &ChatStorageCtx,
82135 chat_id: Uuid,
83 user_id: Uuid,
84) -> ChatStorageResult<()> {
85 verify_creator_member_in_txn(db, chat_id, user_id).await
136) -> ChatStorageResult<Uuid> {
137 let user_id = require_authenticated(ctx).await?;
138 let space_id = space_id_for_chat(&db.reader(), chat_id)
139 .await?
140 .ok_or_else(|| chat_not_in_space(chat_id))?;
141 require_admin_in_space(&db.reader(), space_id, user_id).await?;
142 Ok(user_id)
86143}
87144
88async fn verify_creator_member_in_txn<C: ConnectionTrait>(
145pub async fn require_admin_in_txn<C: ConnectionTrait>(
89146 txn: &C,
147 user_id: Uuid,
90148 chat_id: Uuid,
91 user_id: Uuid,
92149) -> ChatStorageResult<()> {
93 let member = member_entity::Entity::find_by_id((chat_id, user_id))
94 .one(txn)
95 .await?;
96 if member.is_none() {
97 return Err(ChatStorageError::PermissionDenied(format!(
98 "creator/admin {user_id} is no longer a member of chat {chat_id}"
99 )));
100 }
101 Ok(())
150 let space_id = space_id_for_chat(txn, chat_id)
151 .await?
152 .ok_or_else(|| chat_not_in_space(chat_id))?;
153 require_admin_in_space(txn, space_id, user_id).await
102154}
103155
104156pub async fn require_sender_or_admin(
@@ -113,16 +165,8 @@
113165 .ok_or(ChatStorageError::MessageNotFound(message_id))?;
114166 if msg.sender_id == user_id {
115167 return Ok(user_id);
116 }
117 let chat = chat_entity::Entity::find_by_id(msg.chat_id)
118 .one(&db.reader())
119 .await?
120 .ok_or(ChatStorageError::ConversationNotFound(msg.chat_id))?;
121 if chat.creator_user_id != user_id {
122 return Err(ChatStorageError::PermissionDenied(format!(
123 "user {user_id} is neither sender nor admin of message {message_id}"
124 )));
125168 }
169 require_admin(db, ctx, msg.chat_id).await?;
126170 Ok(user_id)
127171}
128172
@@ -139,26 +183,13 @@
139183 if att.uploaded_by == Some(user_id) {
140184 return Ok(user_id);
141185 }
142 let owner_msg = message_entity::Entity::find_by_id(att.owner_id)
143 .one(&db.reader())
144 .await?;
145 let chat_id = match owner_msg {
146 Some(m) => m.chat_id,
147 None => {
148 return Err(ChatStorageError::PermissionDenied(format!(
149 "user {user_id} cannot delete attachment {attachment_id}: owner not found"
150 )));
151 }
152 };
153 let chat = chat_entity::Entity::find_by_id(chat_id)
186 let chat = message_entity::Entity::find_by_id(att.owner_id)
154187 .one(&db.reader())
155188 .await?
156 .ok_or(ChatStorageError::ConversationNotFound(chat_id))?;
157 if chat.creator_user_id != user_id {
158 return Err(ChatStorageError::PermissionDenied(format!(
159 "user {user_id} cannot delete attachment {attachment_id}"
160 )));
161 }
189 .ok_or(ChatStorageError::PermissionDenied(format!(
190 "user {user_id} cannot delete attachment {attachment_id}: owner not found"
191 )))?;
192 require_admin(db, ctx, chat.chat_id).await?;
162193 Ok(user_id)
163194}
164195
@@ -168,17 +199,12 @@
168199 owner_id: Uuid,
169200) -> ChatStorageResult<Option<Uuid>> {
170201 match owner {
171 AttachmentOwner::ChatMessage => {
172 let msg = message_entity::Entity::find_by_id(owner_id)
173 .one(&db.reader())
174 .await?
175 .ok_or(ChatStorageError::MessageNotFound(owner_id))?;
176 Ok(Some(msg.chat_id))
177 }
202 AttachmentOwner::ChatMessage => Ok(Some(chat_id_for_message(db, owner_id).await?)),
178203 _ => Ok(None),
179204 }
180205}
181206
207/// Resolve the owning chat for an attachment and grant space access to it.
182208pub async fn require_chat_member_for_attachment_owner(
183209 db: &db::conn::Db,
184210 ctx: &ChatStorageCtx,
@@ -187,26 +213,15 @@
187213) -> ChatStorageResult<Uuid> {
188214 let user_id = require_authenticated(ctx).await?;
189215 if let Some(chat_id) = chat_id_for_attachment_owner(db, owner, owner_id).await? {
190 let member = member_entity::Entity::find_by_id((chat_id, user_id))
191 .one(&db.reader())
192 .await?;
193 if member.is_none() {
194 return Err(ChatStorageError::PermissionDenied(format!(
195 "user {user_id} is not a member of chat {chat_id}"
196 )));
197 }
216 require_member(db, ctx, chat_id).await?;
198217 }
199218 Ok(user_id)
200219}
201220
202#[allow(dead_code)]
203pub async fn chat_id_for_message(
204 db: &db::conn::Db,
205 message_id: Uuid,
206) -> ChatStorageResult<Uuid> {
221pub async fn chat_id_for_message(db: &db::conn::Db, message_id: Uuid) -> ChatStorageResult<Uuid> {
207222 let msg = message_entity::Entity::find_by_id(message_id)
208223 .one(&db.reader())
209224 .await?
210225 .ok_or(ChatStorageError::MessageNotFound(message_id))?;
211226 Ok(msg.chat_id)
212}
No newline at end of file
227}
Modified chat/storagelayer/blob.rs View chat/storagelayer/blob.rs
@@ -2,4 +2,4 @@
22pub mod object_store;
33
44pub use media::{AttachmentPath, ChatMediaStore, DynChatMediaStore};
5pub use object_store::ObjectStoreChatMedia;
No newline at end of file
5pub use object_store::ObjectStoreChatMedia;
Modified chat/storagelayer/Cargo.toml View chat/storagelayer/Cargo.toml
@@ -18,6 +18,7 @@
1818queue.workspace = true
1919jwt.workspace = true
2020cache.workspace = true
21space.workspace = true
2122
2223tokio.workspace = true
2324async-trait.workspace = true
Modified chat/storagelayer/error.rs View chat/storagelayer/error.rs
@@ -55,4 +55,4 @@
5555 Internal(String),
5656}
5757
58pub type ChatStorageResult<T> = Result<T, ChatStorageError>;
No newline at end of file
58pub type ChatStorageResult<T> = Result<T, ChatStorageError>;
Modified chat/storagelayer/lib.rs View chat/storagelayer/lib.rs
@@ -20,7 +20,7 @@
2020pub use error::{ChatStorageError, ChatStorageResult};
2121pub use models::{
2222 attachment::{Attachment, AttachmentOwner, NewAttachment},
23 conversation::{Conversation, ConversationKind, ConversationMember, NewConversation},
23 conversation::{Conversation, ConversationKind, NewConversation},
2424 mention::{Mention, NewMention},
2525 message::{
2626 ListDirection, MAX_PAGE_LIMIT, Message, MessageFlags, MessagePage, MessageQuery,
@@ -32,17 +32,11 @@
3232 thread::Thread,
3333};
3434pub use repository::{
35 attachment::AttachmentRepository,
36 conversation::ConversationRepository,
37 member::MemberRepository,
38 mention::MentionRepository,
39 message::MessageRepository,
40 pinned::PinnedMessageRepository,
41 reaction::ReactionRepository,
42 read_receipt::ReadReceiptRepository,
43 thread::ThreadRepository,
4435 ChatStorage, ChatStorageCtx, ChatStorageFactory, DynChatStorage,
36 attachment::AttachmentRepository, conversation::ConversationRepository,
37 mention::MentionRepository, message::MessageRepository, pinned::PinnedMessageRepository,
38 reaction::ReactionRepository, read_receipt::ReadReceiptRepository, thread::ThreadRepository,
4539};
4640
4741#[cfg(feature = "mock")]
48pub use mock::in_memory::InMemoryChatStorage;
No newline at end of file
42pub use mock::in_memory::InMemoryChatStorage;
Modified chat/storagelayer/mapper.rs View chat/storagelayer/mapper.rs
@@ -1,6 +1,5 @@
11pub mod attachment;
22pub mod conversation;
3pub mod member;
43pub mod mention;
54pub mod message;
65pub mod pinned;
@@ -15,7 +14,6 @@
1514
1615use self::attachment::SeaAttachmentRepository;
1716use self::conversation::SeaConversationRepository;
18use self::member::SeaMemberRepository;
1917use self::mention::SeaMentionRepository;
2018use self::message::SeaMessageRepository;
2119use self::pinned::SeaPinnedMessageRepository;
@@ -45,10 +43,6 @@
4543
4644 fn attachments(&self) -> crate::repository::attachment::DynAttachmentRepository {
4745 Arc::new(SeaAttachmentRepository::new(self.db.clone()))
48 }
49
50 fn members(&self) -> crate::repository::member::DynMemberRepository {
51 Arc::new(SeaMemberRepository::new(self.db.clone()))
5246 }
5347
5448 fn reactions(&self) -> crate::repository::reaction::DynReactionRepository {
Modified chat/storagelayer/mapping.rs View chat/storagelayer/mapping.rs
@@ -5,7 +5,6 @@
55use db::chat::chat_message_edits as edit_entity;
66use db::chat::chat_message_mentions as mention_entity;
77use db::chat::chat_message_reactions as reaction_entity;
8use db::chat::chat_members as member_entity;
98use db::chat::chat_messages as message_entity;
109use db::chat::chat_pinned_messages as pinned_entity;
1110use db::chat::chat_reads as read_entity;
@@ -15,14 +14,13 @@
1514
1615use crate::error::{ChatStorageError, ChatStorageResult};
1716use crate::models::attachment::{Attachment, AttachmentOwner};
18use crate::models::conversation::{Conversation, ConversationKind, ConversationMember};
17use crate::models::conversation::{Conversation, ConversationKind};
1918use crate::models::mention::{Mention, NewMention};
2019use crate::models::message::{Message, NewMessage};
2120use crate::models::pinned::{NewPinnedMessage, PinnedMessage};
2221use crate::models::reaction::{NewReaction, Reaction};
2322use crate::models::read_receipt::{NewReadReceipt, ReadReceipt};
2423use crate::models::thread::Thread;
25
2624
2725impl From<chat_entity::ChatKind> for ConversationKind {
2826 fn from(kind: chat_entity::ChatKind) -> Self {
@@ -68,7 +66,6 @@
6866 }
6967}
7068
71
7269impl From<chat_entity::Model> for Conversation {
7370 fn from(m: chat_entity::Model) -> Self {
7471 Self {
@@ -100,33 +97,6 @@
10097 }
10198}
10299
103
104impl From<member_entity::Model> for ConversationMember {
105 fn from(m: member_entity::Model) -> Self {
106 Self {
107 chat_id: m.chat_id,
108 user_id: m.user_id,
109 joined_at: m.joined_at,
110 last_read_at: m.last_read_at,
111 muted: m.muted,
112 }
113 }
114}
115
116impl TryFrom<ConversationMember> for member_entity::ActiveModel {
117 type Error = ChatStorageError;
118 fn try_from(m: ConversationMember) -> ChatStorageResult<Self> {
119 Ok(Self {
120 chat_id: Set(m.chat_id),
121 user_id: Set(m.user_id),
122 joined_at: Set(m.joined_at),
123 last_read_at: Set(m.last_read_at),
124 muted: Set(m.muted),
125 })
126 }
127}
128
129
130100impl From<message_entity::Model> for Message {
131101 fn from(m: message_entity::Model) -> Self {
132102 Self {
@@ -165,7 +135,6 @@
165135 })
166136 }
167137}
168
169138
170139impl From<att_entity::Model> for Attachment {
171140 fn from(m: att_entity::Model) -> Self {
@@ -183,7 +152,6 @@
183152 }
184153}
185154
186
187155impl From<reaction_entity::Model> for Reaction {
188156 fn from(m: reaction_entity::Model) -> Self {
189157 Self {
@@ -208,7 +176,6 @@
208176 })
209177 }
210178}
211
212179
213180impl From<mention_entity::Model> for Mention {
214181 fn from(m: mention_entity::Model) -> Self {
@@ -241,7 +208,6 @@
241208 }
242209}
243210
244
245211impl From<pinned_entity::Model> for PinnedMessage {
246212 fn from(m: pinned_entity::Model) -> Self {
247213 Self {
@@ -266,7 +232,6 @@
266232 })
267233 }
268234}
269
270235
271236impl From<read_entity::Model> for ReadReceipt {
272237 fn from(m: read_entity::Model) -> Self {
@@ -276,6 +241,7 @@
276241 user_id: m.user_id,
277242 thread_id: m.thread_id,
278243 last_read_message_id: m.last_read_message_id,
244 muted: m.muted,
279245 updated_at: m.updated_at,
280246 }
281247 }
@@ -290,12 +256,12 @@
290256 user_id: Set(r.user_id),
291257 thread_id: Set(r.thread_id),
292258 last_read_message_id: Set(r.last_read_message_id),
259 muted: Set(false),
293260 updated_at: Set(OffsetDateTime::now_utc()),
294261 })
295262 }
296263}
297264
298
299265impl From<thread_entity::Model> for Thread {
300266 fn from(m: thread_entity::Model) -> Self {
301267 Self {
@@ -307,11 +273,16 @@
307273 }
308274 }
309275}
310
311276
312277#[allow(dead_code)]
313278pub(crate) fn message_edit_model_to_fields(
314279 m: edit_entity::Model,
315280) -> (Uuid, Uuid, Uuid, String, OffsetDateTime) {
316 (m.id, m.message_id, m.editor_id, m.previous_body, m.edited_at)
317}
No newline at end of file
281 (
282 m.id,
283 m.message_id,
284 m.editor_id,
285 m.previous_body,
286 m.edited_at,
287 )
288}
Modified chat/storagelayer/mock.rs View chat/storagelayer/mock.rs
@@ -1,2 +1,2 @@
11#[cfg(feature = "mock")]
2pub mod in_memory;
No newline at end of file
2pub mod in_memory;
Modified chat/storagelayer/models.rs View chat/storagelayer/models.rs
@@ -5,4 +5,4 @@
55pub mod pinned;
66pub mod reaction;
77pub mod read_receipt;
8pub mod thread;
No newline at end of file
8pub mod thread;
Modified chat/storagelayer/repository.rs View chat/storagelayer/repository.rs
@@ -4,7 +4,6 @@
44
55pub mod attachment;
66pub mod conversation;
7pub mod member;
87pub mod mention;
98pub mod message;
109pub mod pinned;
@@ -55,7 +54,6 @@
5554 fn conversations(&self) -> conversation::DynConversationRepository;
5655 fn messages(&self) -> message::DynMessageRepository;
5756 fn attachments(&self) -> attachment::DynAttachmentRepository;
58 fn members(&self) -> member::DynMemberRepository;
5957 fn reactions(&self) -> reaction::DynReactionRepository;
6058 fn mentions(&self) -> mention::DynMentionRepository;
6159 fn reads(&self) -> read_receipt::DynReadReceiptRepository;
@@ -81,4 +79,4 @@
8179 pub fn in_memory() -> DynChatStorage {
8280 crate::InMemoryChatStorage::new_shared()
8381 }
84}
No newline at end of file
82}
Modified dvcs/git/analysis/impls.rs View dvcs/git/analysis/impls.rs
@@ -1,12 +1,12 @@
11use crate::analysis::types::{CodeActivityStats, ContributorStat, LanguageStats, RefreshResult};
22
33pub trait AnalysisOps {
4 fn analysis_languages(&self) -> Result<LanguageStats, crate::Error>;
5 fn analysis_contributors(&self) -> Result<Vec<ContributorStat>, crate::Error>;
6 fn analysis_code_activity_stats(
4 fn analysis_languages(&self) -> Result<LanguageStats, crate::Error>;
5 fn analysis_contributors(&self) -> Result<Vec<ContributorStat>, crate::Error>;
6 fn analysis_code_activity_stats(
77 &self,
88 from: Option<i64>,
99 branch: Option<&str>,
1010 ) -> Result<CodeActivityStats, crate::Error>;
11 fn analysis_refresh(&self, commit_id: Option<&str>) -> Result<RefreshResult, crate::Error>;
11 fn analysis_refresh(&self, commit_id: Option<&str>) -> Result<RefreshResult, crate::Error>;
1212}
Modified dvcs/git/analysis/types.rs View dvcs/git/analysis/types.rs
@@ -2,12 +2,12 @@
22
33#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct LanguageStat {
5 pub language: String,
6 pub percentage: f32,
7 pub size: i64,
8 pub is_primary: bool,
9 pub commit_id: Option<String>,
10 pub color: Option<String>,
5 pub language: String,
6 pub percentage: f32,
7 pub size: i64,
8 pub is_primary: bool,
9 pub commit_id: Option<String>,
10 pub color: Option<String>,
1111}
1212
1313#[derive(Debug, Clone, Serialize, Deserialize)]
Modified dvcs/git/archive/impls.rs View dvcs/git/archive/impls.rs
@@ -1,7 +1,7 @@
11use crate::archive::types::{ArchiveOptions, ArchiveResult};
22
33pub trait ArchiveOps {
4 fn archive_archive(
4 fn archive_archive(
55 &self,
66 repo: &str,
77 rev: &str,
Modified dvcs/git/archive/types.rs View dvcs/git/archive/types.rs
@@ -18,5 +18,5 @@
1818pub struct ArchiveResult {
1919 pub format: ArchiveFormat,
2020 pub size: i64,
21 pub cache_key: String,
21 pub cache_key: String,
2222}
Modified dvcs/git/archive/usecase.rs View dvcs/git/archive/usecase.rs
@@ -113,9 +113,8 @@
113113 .root_dir(dir.join("storage").to_path_buf())
114114 .build(),
115115 );
116 let storage: std::sync::Arc<dyn storage::StorageRepository> = std::sync::Arc::new(
117 LocalStorage::from_config(&settings).expect("storage"),
118 );
116 let storage: std::sync::Arc<dyn storage::StorageRepository> =
117 std::sync::Arc::new(LocalStorage::from_config(&settings).expect("storage"));
119118 GitRepository::new(dir.join(name), oc, storage)
120119 }
121120
Modified dvcs/git/commit/impls.rs View dvcs/git/commit/impls.rs
@@ -9,7 +9,7 @@
99 fn commit_tag_commit(&self, tag: &str) -> Result<Commit, crate::Error>;
1010 fn commit_list(&self, limit: u64, skip: u64) -> Result<CommitList, crate::Error>;
1111 fn commit_parent_count(&self, id: &ObjectId) -> Result<usize, crate::Error>;
12 fn commit_write_merge(
12 fn commit_write_merge(
1313 &self,
1414 parents: Vec<ObjectId>,
1515 tree_id: ObjectId,
@@ -17,7 +17,7 @@
1717 author: &Signature,
1818 committer: &Signature,
1919 ) -> Result<ObjectId, crate::Error>;
20 fn commit_write_squash(
20 fn commit_write_squash(
2121 &self,
2222 parent: ObjectId,
2323 tree_id: ObjectId,
Modified dvcs/git/conflict/impls.rs View dvcs/git/conflict/impls.rs
@@ -1,19 +1,19 @@
11use crate::conflict::types::{ConflictDetail, MergeStatus};
22
33pub trait ConflictOps {
4 fn conflict_check(&self, base: &str, head: &str) -> Result<MergeStatus, crate::Error>;
5 fn conflict_conflicted_files(
4 fn conflict_check(&self, base: &str, head: &str) -> Result<MergeStatus, crate::Error>;
5 fn conflict_conflicted_files(
66 &self,
77 base: &str,
88 head: &str,
99 ) -> Result<Vec<String>, crate::Error>;
10 fn conflict_detail(
10 fn conflict_detail(
1111 &self,
1212 base: &str,
1313 head: &str,
1414 path: &str,
1515 ) -> Result<ConflictDetail, crate::Error>;
16 fn conflict_check_threeway(
16 fn conflict_check_threeway(
1717 &self,
1818 ours: &str,
1919 theirs: &str,
Modified dvcs/git/gc/impls.rs View dvcs/git/gc/impls.rs
@@ -1,9 +1,9 @@
11use crate::gc::types::{FsckResult, GcResult, PackStats, RepoStats};
22
33pub trait GcOps {
4 fn gc_stats(&self) -> Result<RepoStats, crate::Error>;
5 fn gc_gc(&self) -> Result<GcResult, crate::Error>;
6 fn gc_fsck(&self) -> Result<FsckResult, crate::Error>;
7 fn gc_count_objects(&self) -> Result<PackStats, crate::Error>;
8 fn gc_prune_orphans(&self) -> Result<GcResult, crate::Error>;
4 fn gc_stats(&self) -> Result<RepoStats, crate::Error>;
5 fn gc_gc(&self) -> Result<GcResult, crate::Error>;
6 fn gc_fsck(&self) -> Result<FsckResult, crate::Error>;
7 fn gc_count_objects(&self) -> Result<PackStats, crate::Error>;
8 fn gc_prune_orphans(&self) -> Result<GcResult, crate::Error>;
99}
Modified dvcs/git/hook/impls.rs View dvcs/git/hook/impls.rs
@@ -1,13 +1,13 @@
11use crate::hook::types::{HookEvent, HookEventKind, HookResult, ScriptOutput};
22
33pub trait HookOps {
4 fn hook_pre_receive(&self, event: &HookEvent) -> Result<HookResult, crate::Error>;
5 fn hook_post_receive(&self, event: &HookEvent) -> Result<HookResult, crate::Error>;
6 fn hook_emit(&self, event: &HookEvent) -> Result<(), crate::Error>;
7 fn hook_execute_script(
4 fn hook_pre_receive(&self, event: &HookEvent) -> Result<HookResult, crate::Error>;
5 fn hook_post_receive(&self, event: &HookEvent) -> Result<HookResult, crate::Error>;
6 fn hook_emit(&self, event: &HookEvent) -> Result<(), crate::Error>;
7 fn hook_execute_script(
88 &self,
99 script: &str,
1010 args: &[&str],
1111 ) -> Result<ScriptOutput, crate::Error>;
12 fn hook_supported(&self, kind: HookEventKind) -> bool;
12 fn hook_supported(&self, kind: HookEventKind) -> bool;
1313}
Modified dvcs/git/lfs/impls.rs View dvcs/git/lfs/impls.rs
@@ -3,13 +3,13 @@
33};
44
55pub trait LfsOps {
6 fn lfs_handle_batch(&self, req: &LfsBatchRequest) -> Result<LfsBatchResponse, crate::Error>;
7 fn lfs_upload(&self, obj: &LfsObject) -> Result<LfsUploadResult, crate::Error>;
8 fn lfs_download(&self, obj: &LfsObject) -> Result<String, crate::Error>;
9 fn lfs_verify(&self, obj: &LfsObject) -> Result<(), crate::Error>;
10 fn lfs_lock(&self, path: &str, owner: &str) -> Result<LfsLock, crate::Error>;
11 fn lfs_unlock(&self, path: &str, owner: &str) -> Result<LfsLock, crate::Error>;
12 fn lfs_list_locks(&self, cursor: Option<&str>, limit: u64)
6 fn lfs_handle_batch(&self, req: &LfsBatchRequest) -> Result<LfsBatchResponse, crate::Error>;
7 fn lfs_upload(&self, obj: &LfsObject) -> Result<LfsUploadResult, crate::Error>;
8 fn lfs_download(&self, obj: &LfsObject) -> Result<String, crate::Error>;
9 fn lfs_verify(&self, obj: &LfsObject) -> Result<(), crate::Error>;
10 fn lfs_lock(&self, path: &str, owner: &str) -> Result<LfsLock, crate::Error>;
11 fn lfs_unlock(&self, path: &str, owner: &str) -> Result<LfsLock, crate::Error>;
12 fn lfs_list_locks(&self, cursor: Option<&str>, limit: u64)
1313 -> Result<LfsLockList, crate::Error>;
14 fn lfs_cleanup_unreferenced(&self) -> Result<u64, crate::Error>;
14 fn lfs_cleanup_unreferenced(&self) -> Result<u64, crate::Error>;
1515}
Modified dvcs/git/lfs/usecase.rs View dvcs/git/lfs/usecase.rs
@@ -191,9 +191,8 @@
191191 .root_dir(dir.join("storage").to_path_buf())
192192 .build(),
193193 );
194 let storage: std::sync::Arc<dyn storage::StorageRepository> = std::sync::Arc::new(
195 LocalStorage::from_config(&settings).expect("storage"),
196 );
194 let storage: std::sync::Arc<dyn storage::StorageRepository> =
195 std::sync::Arc::new(LocalStorage::from_config(&settings).expect("storage"));
197196 GitRepository::new(dir.join("repo"), oc, storage)
198197 }
199198
@@ -258,4 +257,4 @@
258257
259258 let _ = std::fs::remove_dir_all(&tmp);
260259 }
261}
No newline at end of file
260}
Modified dvcs/git/log/impls.rs View dvcs/git/log/impls.rs
@@ -2,13 +2,13 @@
22use crate::log::types::{CommitGraph, LogList, LogOptions};
33
44pub trait LogOps {
5 fn log_list(&self, opts: &LogOptions) -> Result<LogList, crate::Error>;
6 fn log_commits_between(
5 fn log_list(&self, opts: &LogOptions) -> Result<LogList, crate::Error>;
6 fn log_commits_between(
77 &self,
88 after: &str,
99 before: &str,
1010 limit: u64,
1111 ) -> Result<LogList, crate::Error>;
12 fn log_graph(&self, opts: &LogOptions) -> Result<CommitGraph, crate::Error>;
13 fn log_last_commit(&self, branch_name: String) -> Result<Commit, crate::Error>;
12 fn log_graph(&self, opts: &LogOptions) -> Result<CommitGraph, crate::Error>;
13 fn log_last_commit(&self, branch_name: String) -> Result<Commit, crate::Error>;
1414}
Modified dvcs/git/log/types.rs View dvcs/git/log/types.rs
@@ -5,7 +5,7 @@
55#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct LogList {
77 pub commits: Vec<Commit>,
8 pub cursor: Option<String>,
8 pub cursor: Option<String>,
99 pub has_more: bool,
1010}
1111
Modified dvcs/git/object/cache.rs View dvcs/git/object/cache.rs
@@ -54,7 +54,6 @@
5454 Some(())
5555 }
5656
57
5857 pub fn get_sync<T: DeserializeOwned + Send + Sync>(&self, k: &str) -> Option<T> {
5958 let cache = self.cache.clone();
6059 let prefix = self.prefix.clone();
@@ -126,7 +125,7 @@
126125 });
127126 }
128127
129 pub fn set_sync_with_ttl<V: Serialize>(&self, k: &str, v: V, ttl: u64) {
128 pub fn set_sync_with_ttl<V: Serialize>(&self, k: &str, v: V, ttl: u64) {
130129 let cache = self.cache.clone();
131130 let prefix = self.prefix.clone();
132131 let key = format!("{}.{}", prefix, sanitize_key(k));
Modified dvcs/git/pack/impls.rs View dvcs/git/pack/impls.rs
@@ -1,9 +1,9 @@
11use crate::pack::types::{HotPackResult, IndexResult, IngestPackResult, PackStats, RepackResult};
22
33pub trait PackOps {
4 fn pack_ingest_pack(&self, data: &[u8]) -> Result<IngestPackResult, crate::Error>;
5 fn pack_index(&self, pack_id: &str) -> Result<IndexResult, crate::Error>;
6 fn pack_repack(&self) -> Result<RepackResult, crate::Error>;
7 fn pack_stats(&self) -> Result<PackStats, crate::Error>;
8 fn pack_cache_hot_pack(&self, pack_id: &str) -> Result<HotPackResult, crate::Error>;
4 fn pack_ingest_pack(&self, data: &[u8]) -> Result<IngestPackResult, crate::Error>;
5 fn pack_index(&self, pack_id: &str) -> Result<IndexResult, crate::Error>;
6 fn pack_repack(&self) -> Result<RepackResult, crate::Error>;
7 fn pack_stats(&self) -> Result<PackStats, crate::Error>;
8 fn pack_cache_hot_pack(&self, pack_id: &str) -> Result<HotPackResult, crate::Error>;
99}
Modified dvcs/git/patch/impls.rs View dvcs/git/patch/impls.rs
@@ -1,7 +1,7 @@
11use crate::patch::types::{ApplyResult, ParsedPatch, Patch};
22
33pub trait PatchOps {
4 fn patch_generate(&self, base: &str, head: &str) -> Result<Patch, crate::Error>;
5 fn patch_parse(&self, content: &str) -> Result<ParsedPatch, crate::Error>;
6 fn patch_apply(&self, patch: &str, base: &str) -> Result<ApplyResult, crate::Error>;
4 fn patch_generate(&self, base: &str, head: &str) -> Result<Patch, crate::Error>;
5 fn patch_parse(&self, content: &str) -> Result<ParsedPatch, crate::Error>;
6 fn patch_apply(&self, patch: &str, base: &str) -> Result<ApplyResult, crate::Error>;
77}
Modified dvcs/git/pipeline/impls.rs View dvcs/git/pipeline/impls.rs
@@ -3,17 +3,17 @@
33use crate::pipeline::types::{CommitStatus, Job, StatusList, TriggerResult};
44
55pub trait PipelineOps {
6 fn pipeline_trigger(
6 fn pipeline_trigger(
77 &self,
88 commit: &ObjectId,
99 branch: &str,
1010 ) -> Result<TriggerResult, crate::Error>;
11 fn pipeline_schedule(&self, commit: &ObjectId, job: &Job) -> Result<(), crate::Error>;
12 fn pipeline_set_commit_status(
11 fn pipeline_schedule(&self, commit: &ObjectId, job: &Job) -> Result<(), crate::Error>;
12 fn pipeline_set_commit_status(
1313 &self,
1414 commit: &ObjectId,
1515 status: &CommitStatus,
1616 ) -> Result<(), crate::Error>;
17 fn pipeline_list_statuses(&self, commit: &ObjectId) -> Result<StatusList, crate::Error>;
18 fn pipeline_cancel(&self, job_id: &str) -> Result<(), crate::Error>;
17 fn pipeline_list_statuses(&self, commit: &ObjectId) -> Result<StatusList, crate::Error>;
18 fn pipeline_cancel(&self, job_id: &str) -> Result<(), crate::Error>;
1919}
Modified dvcs/git/refs/impls.rs View dvcs/git/refs/impls.rs
@@ -4,21 +4,21 @@
44
55pub trait RefsOps {
66 fn refs_list(&self) -> Result<Vec<RefInfo>, crate::Error>;
7 fn refs_list_filtered(&self, pattern: &str) -> Result<Vec<RefInfo>, crate::Error>;
8 fn refs_get(&self, name: &str) -> Result<RefInfo, crate::Error>;
9 fn refs_resolve(&self, name: &str) -> Result<ObjectId, crate::Error>;
10 fn refs_update(
7 fn refs_list_filtered(&self, pattern: &str) -> Result<Vec<RefInfo>, crate::Error>;
8 fn refs_get(&self, name: &str) -> Result<RefInfo, crate::Error>;
9 fn refs_resolve(&self, name: &str) -> Result<ObjectId, crate::Error>;
10 fn refs_update(
1111 &self,
1212 name: &str,
1313 new: ObjectId,
1414 expected: Option<ObjectId>,
1515 ) -> Result<UpdateRefResult, crate::Error>;
16 fn refs_delete(&self, name: &str) -> Result<(), crate::Error>;
17 fn refs_validate_name(&self, name: &str) -> Result<(), crate::Error>;
18 fn refs_is_branch_protected(&self, branch: &str) -> Result<bool, crate::Error>;
19 fn refs_head(&self) -> Result<HeadInfo, crate::Error>;
20 fn refs_branches(&self, skip: u64, limit: u64) -> Result<Vec<BranchInfo>, crate::Error>;
21 fn refs_is_object_in_ref(&self, object: &ObjectId, refname: &str)
16 fn refs_delete(&self, name: &str) -> Result<(), crate::Error>;
17 fn refs_validate_name(&self, name: &str) -> Result<(), crate::Error>;
18 fn refs_is_branch_protected(&self, branch: &str) -> Result<bool, crate::Error>;
19 fn refs_head(&self) -> Result<HeadInfo, crate::Error>;
20 fn refs_branches(&self, skip: u64, limit: u64) -> Result<Vec<BranchInfo>, crate::Error>;
21 fn refs_is_object_in_ref(&self, object: &ObjectId, refname: &str)
2222 -> Result<bool, crate::Error>;
23 fn refs_classify(&self, name: &str) -> RefType;
23 fn refs_classify(&self, name: &str) -> RefType;
2424}
Modified dvcs/git/remote/impls.rs View dvcs/git/remote/impls.rs
@@ -1,21 +1,21 @@
11use crate::remote::types::{FetchResult, ImportResult, MirrorType, PushResult, RemoteConfig};
22
33pub trait RemoteOps {
4 fn remote_clone_import(
4 fn remote_clone_import(
55 &self,
66 src: &str,
77 config: &RemoteConfig,
88 ) -> Result<ImportResult, crate::Error>;
9 fn remote_fetch(&self, config: &RemoteConfig) -> Result<FetchResult, crate::Error>;
10 fn remote_push(
9 fn remote_fetch(&self, config: &RemoteConfig) -> Result<FetchResult, crate::Error>;
10 fn remote_push(
1111 &self,
1212 config: &RemoteConfig,
1313 refspec: Option<&str>,
1414 ) -> Result<PushResult, crate::Error>;
15 fn remote_set_mirror(
15 fn remote_set_mirror(
1616 &self,
1717 mirror_type: MirrorType,
1818 config: &RemoteConfig,
1919 ) -> Result<(), crate::Error>;
20 fn remote_remove_remote(&self, name: &str) -> Result<(), crate::Error>;
20 fn remote_remove_remote(&self, name: &str) -> Result<(), crate::Error>;
2121}
Modified dvcs/git/signature/impls.rs View dvcs/git/signature/impls.rs
@@ -3,12 +3,12 @@
33use crate::signature::types::{SignatureInfo, VerifyResult};
44
55pub trait SignatureOps {
6 fn signature_verify_commit(&self, id: &ObjectId) -> Result<VerifyResult, crate::Error>;
7 fn signature_verify_tag(&self, id: &ObjectId) -> Result<VerifyResult, crate::Error>;
8 fn signature_commit_signature(
6 fn signature_verify_commit(&self, id: &ObjectId) -> Result<VerifyResult, crate::Error>;
7 fn signature_verify_tag(&self, id: &ObjectId) -> Result<VerifyResult, crate::Error>;
8 fn signature_commit_signature(
99 &self,
1010 id: &ObjectId,
1111 ) -> Result<Option<SignatureInfo>, crate::Error>;
12 fn signature_tag_signature(&self, id: &ObjectId)
12 fn signature_tag_signature(&self, id: &ObjectId)
1313 -> Result<Option<SignatureInfo>, crate::Error>;
1414}
Modified dvcs/git/submodule/impls.rs View dvcs/git/submodule/impls.rs
@@ -3,7 +3,7 @@
33use crate::submodule::types::{LinkUrl, SubmoduleList};
44
55pub trait SubmoduleOps {
6 fn submodule_list(&self, tree_id: &ObjectId) -> Result<SubmoduleList, crate::Error>;
7 fn submodule_link_url(&self, path: &str, tree_id: &ObjectId) -> Result<LinkUrl, crate::Error>;
8 fn submodule_resolve(&self, path: &str, tree_id: &ObjectId) -> Result<ObjectId, crate::Error>;
6 fn submodule_list(&self, tree_id: &ObjectId) -> Result<SubmoduleList, crate::Error>;
7 fn submodule_link_url(&self, path: &str, tree_id: &ObjectId) -> Result<LinkUrl, crate::Error>;
8 fn submodule_resolve(&self, path: &str, tree_id: &ObjectId) -> Result<ObjectId, crate::Error>;
99}
Modified dvcs/git/tag/impls.rs View dvcs/git/tag/impls.rs
@@ -3,21 +3,21 @@
33use crate::tag::types::{CreateTagResult, Tag, TagList, TagType};
44
55pub trait TagOps {
6 fn tag_list(&self, page: u64, page_size: u64) -> Result<TagList, crate::Error>;
7 fn tag_get(&self, name: &str) -> Result<Tag, crate::Error>;
8 fn tag_resolve(&self, name: &str) -> Result<ObjectId, crate::Error>;
9 fn tag_create_lightweight(
6 fn tag_list(&self, page: u64, page_size: u64) -> Result<TagList, crate::Error>;
7 fn tag_get(&self, name: &str) -> Result<Tag, crate::Error>;
8 fn tag_resolve(&self, name: &str) -> Result<ObjectId, crate::Error>;
9 fn tag_create_lightweight(
1010 &self,
1111 name: &str,
1212 revision: &str,
1313 ) -> Result<CreateTagResult, crate::Error>;
14 fn tag_create_annotated(
14 fn tag_create_annotated(
1515 &self,
1616 name: &str,
1717 message: &str,
1818 revision: &str,
1919 ) -> Result<CreateTagResult, crate::Error>;
20 fn tag_delete(&self, name: &str) -> Result<(), crate::Error>;
21 fn tag_type(&self, name: &str) -> Result<TagType, crate::Error>;
22 fn tag_is_tag_protected(&self, name: &str) -> Result<bool, crate::Error>;
20 fn tag_delete(&self, name: &str) -> Result<(), crate::Error>;
21 fn tag_type(&self, name: &str) -> Result<TagType, crate::Error>;
22 fn tag_is_tag_protected(&self, name: &str) -> Result<bool, crate::Error>;
2323}
Modified dvcs/git/tag/types.rs View dvcs/git/tag/types.rs
@@ -13,7 +13,7 @@
1313pub struct Tag {
1414 pub name: String,
1515 pub id: gix::hash::ObjectId,
16 pub object: gix::hash::ObjectId,
16 pub object: gix::hash::ObjectId,
1717 pub tag_type: TagType,
1818 pub message: Option<String>,
1919 pub tagger: Option<Signature>,
Modified dvcs/git/tree/impls.rs View dvcs/git/tree/impls.rs
@@ -3,9 +3,9 @@
33use crate::tree::types::{EntryFollowResult, PathLastUpdated, PathResult, Tree, TreeEntry};
44
55pub trait TreeOps {
6 fn tree_read(&self, id: &ObjectId) -> Result<Tree, crate::Error>;
7 fn tree_resolve_path(&self, tree_id: &ObjectId, path: &str) -> Result<TreeEntry, crate::Error>;
8 fn tree_walk(&self, tree_id: &ObjectId) -> Result<Vec<PathResult>, crate::Error>;
9 fn tree_follow(&self, entry: &TreeEntry) -> Result<EntryFollowResult, crate::Error>;
10 fn tree_last_updated(&self, rev: &str) -> Result<Vec<PathLastUpdated>, crate::Error>;
6 fn tree_read(&self, id: &ObjectId) -> Result<Tree, crate::Error>;
7 fn tree_resolve_path(&self, tree_id: &ObjectId, path: &str) -> Result<TreeEntry, crate::Error>;
8 fn tree_walk(&self, tree_id: &ObjectId) -> Result<Vec<PathResult>, crate::Error>;
9 fn tree_follow(&self, entry: &TreeEntry) -> Result<EntryFollowResult, crate::Error>;
10 fn tree_last_updated(&self, rev: &str) -> Result<Vec<PathLastUpdated>, crate::Error>;
1111}
Modified dvcs/git/tree/types.rs View dvcs/git/tree/types.rs
@@ -70,6 +70,6 @@
7070pub struct PathLastUpdated {
7171 pub path: String,
7272 pub object_type: ObjectType,
73 pub last_updated: i64,
74 pub commit_id: gix::hash::ObjectId,
73 pub last_updated: i64,
74 pub commit_id: gix::hash::ObjectId,
7575}
Modified dvcs/git/tree/usecase.rs View dvcs/git/tree/usecase.rs
@@ -196,7 +196,7 @@
196196 std::collections::HashMap::new();
197197 let mut cur_hash: Option<gix::hash::ObjectId> = None;
198198 let mut cur_time: Option<i64> = None;
199 let mut stage = 0u8;
199 let mut stage = 0u8;
200200 for line in log.lines() {
201201 let line = line.trim();
202202 if line.is_empty() {
@@ -338,9 +338,8 @@
338338 .root_dir(dir.join("storage").to_path_buf())
339339 .build(),
340340 );
341 let storage: std::sync::Arc<dyn storage::StorageRepository> = std::sync::Arc::new(
342 LocalStorage::from_config(&settings).expect("storage"),
343 );
341 let storage: std::sync::Arc<dyn storage::StorageRepository> =
342 std::sync::Arc::new(LocalStorage::from_config(&settings).expect("storage"));
344343 GitRepository::new(dir.join(name), oc, storage)
345344 }
346345
Modified dvcs/git/policy.rs View dvcs/git/policy.rs
@@ -2,8 +2,8 @@
22
33/// on every ref/tag mutation to decide whether the operation is allowed.
44pub trait PolicyProvider: Send + Sync {
5 fn is_branch_protected(&self, branch: &str) -> bool;
6 fn is_tag_protected(&self, tag: &str) -> bool;
5 fn is_branch_protected(&self, branch: &str) -> bool;
6 fn is_tag_protected(&self, tag: &str) -> bool;
77}
88
99/// deletion of `main`/`master`/`develop` and `release/*` tags, while allowing
Modified dvcs/git/usecase_cache.rs View dvcs/git/usecase_cache.rs
@@ -10,14 +10,14 @@
1010pub const DERIVED_TTL: u64 = 60;
1111
1212impl GitRepository {
13 pub(crate) fn tip_id(&self) -> Result<ObjectId, Error> {
13 pub(crate) fn tip_id(&self) -> Result<ObjectId, Error> {
1414 let repo = self.repo()?;
1515 repo.head_id()
1616 .map(|id| id.detach())
1717 .map_err(|e| Error::Ref(e.to_string()))
1818 }
1919
20 pub(crate) fn cache_get_oid<T>(&self, kind: &str, id: &ObjectId) -> Option<T>
20 pub(crate) fn cache_get_oid<T>(&self, kind: &str, id: &ObjectId) -> Option<T>
2121 where
2222 T: DeserializeOwned + Send + Sync,
2323 {
@@ -25,12 +25,12 @@
2525 self.cache.object_pool_get_sync(&k)
2626 }
2727
28 pub(crate) fn cache_put_oid<T: Serialize>(&self, kind: &str, id: &ObjectId, v: &T) {
28 pub(crate) fn cache_put_oid<T: Serialize>(&self, kind: &str, id: &ObjectId, v: &T) {
2929 let k = format!("{kind}.{id}");
3030 self.cache.set_sync_with_ttl(&k, v, IMMUTABLE_TTL);
3131 }
3232
33 pub(crate) fn cache_get_rev<T>(
33 pub(crate) fn cache_get_rev<T>(
3434 &self,
3535 module: &str,
3636 func: &str,
@@ -44,7 +44,7 @@
4444 self.cache.get_sync(&k)
4545 }
4646
47 pub(crate) fn cache_put_rev<T: Serialize>(
47 pub(crate) fn cache_put_rev<T: Serialize>(
4848 &self,
4949 module: &str,
5050 func: &str,
Modified dvcs/git/validate.rs View dvcs/git/validate.rs
@@ -65,9 +65,9 @@
6565 }
6666}
6767
68pub const MAX_OBJECT_SIZE: u64 = 100 * 1024 * 1024;
68pub const MAX_OBJECT_SIZE: u64 = 100 * 1024 * 1024;
6969
70pub const MAX_BLOB_SIZE: u64 = 100 * 1024 * 1024;
70pub const MAX_BLOB_SIZE: u64 = 100 * 1024 * 1024;
7171
7272pub fn enforce_max_size(data: &[u8], max: u64, what: &str) -> Result<(), crate::Error> {
7373 if data.len() as u64 > max {
Modified dvcs/gitauth/config.rs View dvcs/gitauth/config.rs
@@ -1,6 +1,6 @@
11#[derive(Debug, Clone)]
22pub struct GitAuthConfig {
3 pub allow_anonymous_read: bool,
3 pub allow_anonymous_read: bool,
44}
55
66impl Default for GitAuthConfig {
Modified dvcs/gitauth/metrics.rs View dvcs/gitauth/metrics.rs
@@ -59,4 +59,4 @@
5959 Ok(_) => OUTCOME_SUCCESS,
6060 Err(_) => "error",
6161 }
62}
No newline at end of file
62}
Modified dvcs/gitauth/permission.rs View dvcs/gitauth/permission.rs
@@ -1,22 +1,13 @@
1use db::channel::channel_members;
21use db::conn::sea_orm::{ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter};
32use db::repo::repo_collaborations;
43use db::space::{space_member_roles, space_members, space_role_permissions, spaces};
5use repo::actor::{ActorContext, ChannelRole, RepoAccess, SpaceRole};
4use repo::actor::{ActorContext, RepoAccess, SpaceRole};
65use uuid::Uuid;
76
87use crate::error::{GitAuthError, GitAuthResult};
98use crate::resolve::ResolvedRepo;
109
1110pub const SPACE_ADMIN_PERMISSION: &str = "manage_members";
12
13pub fn channel_role_for(member: Option<&channel_members::Model>) -> Option<ChannelRole> {
14 member.map(|m| match m.role {
15 channel_members::ChannelMemberRole::Member => ChannelRole::Member,
16 channel_members::ChannelMemberRole::Admin => ChannelRole::Moderator,
17 channel_members::ChannelMemberRole::Owner => ChannelRole::Owner,
18 })
19}
2011
2112pub async fn space_role_for(
2213 db: &impl ConnectionTrait,
@@ -90,22 +81,19 @@
9081 return Err(GitAuthError::Unauthorized("authentication required".into()));
9182 }
9283
93 let (channel_role, space_role, collaborator) = match actor.user_id {
84 let (space_role, collaborator) = match actor.user_id {
9485 Some(user_id) => {
95 let member = channel_members::Entity::find_by_id((resolved.repo.channel_id, user_id))
96 .one(db)
97 .await?;
98 let channel_role = channel_role_for(member.as_ref());
9986 let space_role = if resolved.channel.space_id.is_some() {
10087 space_role_for(db, &resolved.space, user_id).await?
10188 } else {
10289 None
10390 };
10491 let collaborator = collaborator_access(db, resolved.repo.id, user_id).await?;
105 (channel_role, space_role, collaborator)
92 (space_role, collaborator)
10693 }
107 None => (None, None, None),
94 None => (None, None),
10895 };
96 let channel_role = None;
10997
11098 repo::permission::require(
11199 actor,
Modified dvcs/gitauth/service.rs View dvcs/gitauth/service.rs
@@ -17,7 +17,7 @@
1717#[derive(Clone)]
1818pub struct GitAuthService {
1919 pub db: Db,
20 pub kv: Option<Arc<RedisCache>>,
20 pub kv: Option<Arc<RedisCache>>,
2121 pub jwt: JwtState,
2222 pub config: GitAuthConfig,
2323}
@@ -37,7 +37,7 @@
3737 self
3838 }
3939
40 pub async fn resolve_repo(
40 pub async fn resolve_repo(
4141 &self,
4242 space_name: &str,
4343 repo_name: &str,
@@ -51,7 +51,7 @@
5151 result
5252 }
5353
54 pub async fn authenticate(
54 pub async fn authenticate(
5555 &self,
5656 username: &str,
5757 password: &str,
@@ -84,7 +84,7 @@
8484 result
8585 }
8686
87 pub async fn authorize(
87 pub async fn authorize(
8888 &self,
8989 space_name: &str,
9090 repo_name: &str,
@@ -153,7 +153,7 @@
153153#[cfg(test)]
154154mod tests {
155155 use super::*;
156 use db::channel::{channel_members, channels};
156 use db::channel::channels;
157157 use db::conn::sea_orm::{ActiveModelTrait, ConnectionTrait, DatabaseConnection, Set};
158158 use db::repo::{repo_collaborations, repos};
159159 use db::space::spaces;
@@ -299,23 +299,6 @@
299299 model.insert(db).await.expect("insert repo");
300300 }
301301
302 async fn insert_channel_member(
303 db: &impl ConnectionTrait,
304 channel_id: Uuid,
305 user_id: Uuid,
306 role: channel_members::ChannelMemberRole,
307 ) {
308 let model = channel_members::ActiveModel {
309 channel_id: Set(channel_id),
310 user_id: Set(user_id),
311 role: Set(role),
312 muted: Set(false),
313 joined_at: Set(now()),
314 ..Default::default()
315 };
316 model.insert(db).await.expect("insert channel member");
317 }
318
319302 async fn insert_collaborator(
320303 db: &impl ConnectionTrait,
321304 repo_id: Uuid,
@@ -492,37 +475,5 @@
492475 svc.authorize("acme", "widget", &actor, RepoAccess::Admin)
493476 .await
494477 .expect("space owner admin");
495 }
496
497 #[tokio::test]
498 async fn channel_role_mapping() {
499 let (svc, conn) = make_service().await;
500 let owner = Uuid::new_v4();
501 let member = Uuid::new_v4();
502 let space_id = Uuid::new_v4();
503 let channel_id = Uuid::new_v4();
504 let repo_id = Uuid::new_v4();
505 insert_user(&conn, owner, "owner", None).await;
506 insert_user(&conn, member, "carol", None).await;
507 insert_space(&conn, space_id, "acme", owner, true).await;
508 insert_channel(&conn, channel_id, space_id, "dev", owner).await;
509 insert_repo(&conn, repo_id, channel_id, "widget", false).await;
510 insert_channel_member(
511 &conn,
512 channel_id,
513 member,
514 channel_members::ChannelMemberRole::Admin,
515 )
516 .await;
517
518 let actor = actor_for_uid(member);
519 svc.authorize("acme", "widget", &actor, RepoAccess::Write)
520 .await
521 .expect("channel admin -> write");
522 let err = svc
523 .authorize("acme", "widget", &actor, RepoAccess::Admin)
524 .await
525 .expect_err("channel admin is not repo admin");
526 assert!(matches!(err, GitAuthError::Forbidden(_)));
527478 }
528479}
Modified dvcs/gitauth/testing.rs View dvcs/gitauth/testing.rs
@@ -14,7 +14,6 @@
1414 create_table::<db::user::users::Entity>(&db).await;
1515 create_table::<db::space::spaces::Entity>(&db).await;
1616 create_table::<db::channel::channels::Entity>(&db).await;
17 create_table::<db::channel::channel_members::Entity>(&db).await;
1817 create_table::<db::space::space_members::Entity>(&db).await;
1918 create_table::<db::space::space_roles::Entity>(&db).await;
2019 create_table::<db::space::space_member_roles::Entity>(&db).await;
Modified dvcs/gitlfs/error.rs View dvcs/gitlfs/error.rs
@@ -14,4 +14,4 @@
1414 Io(String),
1515 #[error("storage error: {0}")]
1616 Storage(#[from] storage::StorageError),
17}
No newline at end of file
17}
Modified dvcs/gitlfs/lib.rs View dvcs/gitlfs/lib.rs
@@ -5,4 +5,4 @@
55
66pub use error::LfsError;
77pub use oid::LfsOid;
8pub use store::{DefaultLfsStore, LfsObjectStore};
No newline at end of file
8pub use store::{DefaultLfsStore, LfsObjectStore};
Modified dvcs/gitlfs/metrics.rs View dvcs/gitlfs/metrics.rs
@@ -35,11 +35,10 @@
3535
3636pub(crate) fn record_op<T>(op: &'static str, result: &Result<T, LfsError>) {
3737 ensure_described();
38 counter!("gitlfs_store_ops_total", "op" => op, "outcome" => outcome_for(result))
39 .increment(1);
38 counter!("gitlfs_store_ops_total", "op" => op, "outcome" => outcome_for(result)).increment(1);
4039}
4140
4241pub(crate) fn record_bytes(op: &'static str, n: usize) {
4342 ensure_described();
4443 counter!("gitlfs_bytes_total", "op" => op).increment(n as u64);
45}
No newline at end of file
44}
Modified dvcs/gitlfs/oid.rs View dvcs/gitlfs/oid.rs
@@ -18,7 +18,7 @@
1818pub struct LfsOid(String);
1919
2020impl LfsOid {
21 pub fn new(hex: impl AsRef<str>) -> Result<Self, LfsError> {
21 pub fn new(hex: impl AsRef<str>) -> Result<Self, LfsError> {
2222 let hex = hex.as_ref();
2323 if hex.len() != HEX_CHARS || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
2424 return Err(LfsError::InvalidOid(hex.to_string()));
@@ -26,15 +26,15 @@
2626 Ok(Self(hex.to_ascii_lowercase()))
2727 }
2828
29 pub fn compute(data: &[u8]) -> Self {
29 pub fn compute(data: &[u8]) -> Self {
3030 Self(hex_encode(&Sha256::digest(data)))
3131 }
3232
33 pub fn from_hasher(hasher: Sha256) -> Self {
33 pub fn from_hasher(hasher: Sha256) -> Self {
3434 Self(hex_encode(&hasher.finalize()))
3535 }
3636
37 pub fn as_str(&self) -> &str {
37 pub fn as_str(&self) -> &str {
3838 &self.0
3939 }
4040}
@@ -88,4 +88,4 @@
8888 "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
8989 );
9090 }
91}
No newline at end of file
91}
Modified dvcs/gitlfs/store.rs View dvcs/gitlfs/store.rs
@@ -4,10 +4,10 @@
44
55use async_trait::async_trait;
66use bytes::Bytes;
7use futures::stream::BoxStream;
87use futures::StreamExt;
9use object_store::path::Path;
8use futures::stream::BoxStream;
109use object_store::PutPayload;
10use object_store::path::Path;
1111use sha2::{Digest, Sha256};
1212use storage::StorageRepository;
1313use url::Url;
@@ -18,21 +18,21 @@
1818
1919#[async_trait]
2020pub trait LfsObjectStore: Send + Sync {
21 async fn put_object(&self, oid: &LfsOid, data: Bytes) -> Result<(), LfsError>;
22 async fn get_object(&self, oid: &LfsOid) -> Result<Bytes, LfsError>;
23 async fn has_object(&self, oid: &LfsOid) -> Result<bool, LfsError>;
24 async fn object_size(&self, oid: &LfsOid) -> Result<u64, LfsError>;
25 async fn delete_object(&self, oid: &LfsOid) -> Result<(), LfsError>;
26 async fn list_oids(&self) -> Result<Vec<LfsOid>, LfsError>;
27 async fn cleanup(&self, keep: &HashSet<LfsOid>) -> Result<u64, LfsError>;
28 async fn presign_upload(&self, oid: &LfsOid, expires_in: Duration) -> Result<Url, LfsError>;
29 async fn presign_download(&self, oid: &LfsOid, expires_in: Duration) -> Result<Url, LfsError>;
30 async fn put_object_stream(
21 async fn put_object(&self, oid: &LfsOid, data: Bytes) -> Result<(), LfsError>;
22 async fn get_object(&self, oid: &LfsOid) -> Result<Bytes, LfsError>;
23 async fn has_object(&self, oid: &LfsOid) -> Result<bool, LfsError>;
24 async fn object_size(&self, oid: &LfsOid) -> Result<u64, LfsError>;
25 async fn delete_object(&self, oid: &LfsOid) -> Result<(), LfsError>;
26 async fn list_oids(&self) -> Result<Vec<LfsOid>, LfsError>;
27 async fn cleanup(&self, keep: &HashSet<LfsOid>) -> Result<u64, LfsError>;
28 async fn presign_upload(&self, oid: &LfsOid, expires_in: Duration) -> Result<Url, LfsError>;
29 async fn presign_download(&self, oid: &LfsOid, expires_in: Duration) -> Result<Url, LfsError>;
30 async fn put_object_stream(
3131 &self,
3232 oid: &LfsOid,
3333 stream: BoxStream<'static, Result<Bytes, LfsError>>,
3434 ) -> Result<(), LfsError>;
35 async fn get_object_stream(
35 async fn get_object_stream(
3636 &self,
3737 oid: &LfsOid,
3838 ) -> Result<BoxStream<'static, Result<Bytes, LfsError>>, LfsError>;
@@ -45,7 +45,7 @@
4545}
4646
4747impl DefaultLfsStore {
48 pub fn new(storage: Arc<dyn StorageRepository>, prefix: impl Into<String>) -> Self {
48 pub fn new(storage: Arc<dyn StorageRepository>, prefix: impl Into<String>) -> Self {
4949 Self {
5050 storage,
5151 prefix: prefix.into(),
@@ -91,8 +91,7 @@
9191
9292 async fn has_object(&self, oid: &LfsOid) -> Result<bool, LfsError> {
9393 let result: Result<bool, LfsError> =
94 async { Ok(self.storage.exists(&self.object_path(oid)).await?) }
95 .await;
94 async { Ok(self.storage.exists(&self.object_path(oid)).await?) }.await;
9695 metrics::record_op("has_object", &result);
9796 result
9897 }
@@ -202,7 +201,9 @@
202201 buf.extend_from_slice(&chunk);
203202 if buf.len() >= PART_SIZE {
204203 let part = std::mem::take(&mut buf);
205 if let Err(e) = upload.put_part(PutPayload::from_bytes(Bytes::from(part))).await
204 if let Err(e) = upload
205 .put_part(PutPayload::from_bytes(Bytes::from(part)))
206 .await
206207 {
207208 let _ = upload.abort().await;
208209 return Err(LfsError::Storage(e.into()));
@@ -221,7 +222,10 @@
221222 }
222223
223224 if !buf.is_empty() {
224 if let Err(e) = upload.put_part(PutPayload::from_bytes(Bytes::from(buf))).await {
225 if let Err(e) = upload
226 .put_part(PutPayload::from_bytes(Bytes::from(buf)))
227 .await
228 {
225229 let _ = upload.abort().await;
226230 return Err(LfsError::Storage(e.into()));
227231 }
@@ -236,7 +240,10 @@
236240 });
237241 }
238242
239 upload.complete().await.map_err(|e| LfsError::Storage(e.into()))?;
243 upload
244 .complete()
245 .await
246 .map_err(|e| LfsError::Storage(e.into()))?;
240247 Ok(())
241248 }
242249 .await;
@@ -249,14 +256,20 @@
249256 oid: &LfsOid,
250257 ) -> Result<BoxStream<'static, Result<Bytes, LfsError>>, LfsError> {
251258 let result: Result<BoxStream<'static, Result<Bytes, LfsError>>, LfsError> = async {
252 let res = self.storage.get(&self.object_path(oid)).await.map_err(|e| {
253 if matches!(&e, storage::StorageError::ObjectStore(object_store::Error::NotFound { .. }))
254 {
255 LfsError::NotFound(oid.as_str().to_string())
256 } else {
257 LfsError::Storage(e)
258 }
259 })?;
259 let res = self
260 .storage
261 .get(&self.object_path(oid))
262 .await
263 .map_err(|e| {
264 if matches!(
265 &e,
266 storage::StorageError::ObjectStore(object_store::Error::NotFound { .. })
267 ) {
268 LfsError::NotFound(oid.as_str().to_string())
269 } else {
270 LfsError::Storage(e)
271 }
272 })?;
260273 Ok(res
261274 .into_stream()
262275 .map(|r| r.map_err(|e| LfsError::Storage(storage::StorageError::from(e))))
@@ -359,10 +372,7 @@
359372 Ok(Bytes::from_static(b"hello ")),
360373 Ok(Bytes::from_static(b"streaming lfs")),
361374 ]);
362 store
363 .put_object_stream(&oid, stream.boxed())
364 .await
365 .unwrap();
375 store.put_object_stream(&oid, stream.boxed()).await.unwrap();
366376
367377 let mut got = store.get_object_stream(&oid).await.unwrap();
368378 let mut collected = Vec::new();
@@ -412,4 +422,4 @@
412422 let oid = LfsOid::new(&"0".repeat(64)).unwrap();
413423 assert!(store.get_object_stream(&oid).await.is_err());
414424 }
415}
No newline at end of file
425}
Modified dvcs/gitsmart/protocol/service.rs View dvcs/gitsmart/protocol/service.rs
@@ -26,7 +26,7 @@
2626 }
2727 }
2828
29 pub fn dashed_name(self) -> &'static str {
29 pub fn dashed_name(self) -> &'static str {
3030 match self {
3131 Self::UploadPack => "git-upload-pack",
3232 Self::ReceivePack => "git-receive-pack",
Modified dvcs/gitsmart/tests/smart.rs View dvcs/gitsmart/tests/smart.rs
@@ -3,7 +3,6 @@
33use std::sync::Arc;
44use std::time::Duration;
55
6use db::channel::channel_members;
76use db::channel::channels;
87use db::conn::Db;
98use db::conn::sea_orm::{
@@ -49,7 +48,6 @@
4948 create_table::<users::Entity>(&db).await;
5049 create_table::<spaces::Entity>(&db).await;
5150 create_table::<channels::Entity>(&db).await;
52 create_table::<channel_members::Entity>(&db).await;
5351 create_table::<db::space::space_members::Entity>(&db).await;
5452 create_table::<db::space::space_roles::Entity>(&db).await;
5553 create_table::<db::space::space_member_roles::Entity>(&db).await;
@@ -179,25 +177,6 @@
179177 .expect("insert repo");
180178}
181179
182async fn insert_channel_member(
183 db: &impl ConnectionTrait,
184 channel_id: Uuid,
185 user_id: Uuid,
186 role: channel_members::ChannelMemberRole,
187) {
188 channel_members::ActiveModel {
189 channel_id: Set(channel_id),
190 user_id: Set(user_id),
191 role: Set(role),
192 muted: Set(false),
193 joined_at: Set(now()),
194 ..Default::default()
195 }
196 .insert(db)
197 .await
198 .expect("insert channel member");
199}
200
201180async fn git(dir: &Path, args: &[&str]) -> String {
202181 let out = tokio::process::Command::new("git")
203182 .args(args)
@@ -257,13 +236,6 @@
257236 insert_space(&conn, space_id, "acme", alice, true).await;
258237 insert_channel(&conn, channel_id, space_id, "dev", alice).await;
259238 insert_repo(&conn, repo_id, channel_id, "widget", false).await;
260 insert_channel_member(
261 &conn,
262 channel_id,
263 alice,
264 channel_members::ChannelMemberRole::Owner,
265 )
266 .await;
267239
268240 let bare = git_root.join(format!("{}/{}.git", channel_id, repo_id));
269241 std::fs::create_dir_all(bare.parent().expect("bare parent")).expect("bare dirs");
@@ -516,7 +488,6 @@
516488 String::from_utf8_lossy(&body)
517489 );
518490}
519
520491
521492fn pseudo_random_bytes(len: usize, seed: u64) -> Vec<u8> {
522493 let mut out = Vec::with_capacity(len);
Modified dvcs/gitsmart/push.rs View dvcs/gitsmart/push.rs
@@ -10,15 +10,15 @@
1010
1111#[async_trait]
1212pub trait PushOperations: Send + Sync {
13 async fn acquire(&self, key: &str, member: &[u8], ttl_secs: u64) -> Result<bool, SmartError>;
13 async fn acquire(&self, key: &str, member: &[u8], ttl_secs: u64) -> Result<bool, SmartError>;
1414
15 async fn renew(&self, key: &str, token: &[u8], ttl_secs: u64) -> Result<bool, SmartError>;
15 async fn renew(&self, key: &str, token: &[u8], ttl_secs: u64) -> Result<bool, SmartError>;
1616
17 async fn release(&self, key: &str, token: &[u8]) -> Result<(), SmartError>;
17 async fn release(&self, key: &str, token: &[u8]) -> Result<(), SmartError>;
1818
19 async fn enqueue(&self, key: &str, member: &[u8]) -> Result<(), SmartError>;
19 async fn enqueue(&self, key: &str, member: &[u8]) -> Result<(), SmartError>;
2020
21 async fn dequeue(&self, key: &str, member: &[u8]) -> Result<(), SmartError>;
21 async fn dequeue(&self, key: &str, member: &[u8]) -> Result<(), SmartError>;
2222}
2323
2424pub struct RedisPushBackend {
@@ -106,7 +106,7 @@
106106 }
107107 }
108108
109 pub async fn run<T>(
109 pub async fn run<T>(
110110 &self,
111111 key: &str,
112112 f: impl std::future::Future<Output = SmartResult<T>>,
Modified dvcs/gitsmart/service.rs View dvcs/gitsmart/service.rs
@@ -44,22 +44,22 @@
4444 })
4545 }
4646
47 pub fn with_kv(mut self, kv: Option<Arc<RedisCache>>) -> Self {
47 pub fn with_kv(mut self, kv: Option<Arc<RedisCache>>) -> Self {
4848 self.kv = kv;
4949 self
5050 }
5151
52 pub fn with_queue(mut self, queue: Option<Arc<queue::Queue>>) -> Self {
52 pub fn with_queue(mut self, queue: Option<Arc<queue::Queue>>) -> Self {
5353 self.queue = queue;
5454 self
5555 }
5656
57 pub fn with_sync_settings(mut self, settings: gitsync::SyncSettings) -> Self {
57 pub fn with_sync_settings(mut self, settings: gitsync::SyncSettings) -> Self {
5858 self.sync_settings = settings;
5959 self
6060 }
6161
62 pub fn push_coordinator(&self) -> PushCoordinator {
62 pub fn push_coordinator(&self) -> PushCoordinator {
6363 let backend: Arc<dyn PushOperations> = match &self.kv {
6464 Some(kv) => Arc::new(RedisPushBackend { kv: kv.clone() }),
6565 None => Arc::new(NoopPushBackend),
Modified dvcs/gitssh/handler/auth.rs View dvcs/gitssh/handler/auth.rs
@@ -16,7 +16,7 @@
1616}
1717
1818impl GitSshHandler {
19 async fn find_user_by_public_key(
19 async fn find_user_by_public_key(
2020 &self,
2121 username: &str,
2222 public_key: &ssh_key::PublicKey,
Modified dvcs/gitssh/handler/channel.rs View dvcs/gitssh/handler/channel.rs
@@ -5,7 +5,7 @@
55use crate::handler::GitSshHandler;
66
77impl GitSshHandler {
8 pub async fn data_impl(&mut self, channel: ChannelId, data: &[u8]) -> Result<(), russh::Error> {
8 pub async fn data_impl(&mut self, channel: ChannelId, data: &[u8]) -> Result<(), russh::Error> {
99 let state = self.channels.entry(channel).or_default();
1010 state.buffer.extend_from_slice(data);
1111 Ok(())
@@ -15,7 +15,7 @@
1515 self.channels.remove(&channel);
1616 }
1717
18 pub async fn channel_eof_impl(
18 pub async fn channel_eof_impl(
1919 &mut self,
2020 channel: ChannelId,
2121 session: &mut Session,
Modified dvcs/gitssh/tests/ssh_roundtrip.rs View dvcs/gitssh/tests/ssh_roundtrip.rs
@@ -4,7 +4,6 @@
44use std::sync::Arc;
55use std::time::Duration;
66
7use db::channel::channel_members;
87use db::channel::channels;
98use db::conn::Db;
109use db::conn::sea_orm::{
@@ -52,7 +51,6 @@
5251 create_table::<users::Entity>(&db).await;
5352 create_table::<spaces::Entity>(&db).await;
5453 create_table::<channels::Entity>(&db).await;
55 create_table::<channel_members::Entity>(&db).await;
5654 create_table::<db::space::space_members::Entity>(&db).await;
5755 create_table::<db::space::space_roles::Entity>(&db).await;
5856 create_table::<db::space::space_member_roles::Entity>(&db).await;
@@ -172,25 +170,6 @@
172170 .expect("insert repo");
173171}
174172
175async fn insert_channel_member(
176 db: &impl ConnectionTrait,
177 channel_id: Uuid,
178 user_id: Uuid,
179 role: channel_members::ChannelMemberRole,
180) {
181 channel_members::ActiveModel {
182 channel_id: Set(channel_id),
183 user_id: Set(user_id),
184 role: Set(role),
185 muted: Set(false),
186 joined_at: Set(now()),
187 ..Default::default()
188 }
189 .insert(db)
190 .await
191 .expect("insert channel member");
192}
193
194173async fn insert_ssh_key(db: &impl ConnectionTrait, user_id: Uuid, content: &str) {
195174 let fingerprint = sha256_hex(content.as_bytes());
196175 let am: user_ssh_keys::ActiveModel = user_ssh_keys::ActiveModel::builder()
@@ -254,13 +233,6 @@
254233 insert_space(&conn, space_id, "acme", alice).await;
255234 insert_channel(&conn, channel_id, space_id, "dev", alice).await;
256235 insert_repo(&conn, repo_id, channel_id, "widget", false).await;
257 insert_channel_member(
258 &conn,
259 channel_id,
260 alice,
261 channel_members::ChannelMemberRole::Owner,
262 )
263 .await;
264236
265237 let mut rng = rand::rng();
266238 let client_key = russh::keys::PrivateKey::random(&mut rng, russh::keys::Algorithm::Ed25519)
Modified dvcs/gitsync/error.rs View dvcs/gitsync/error.rs
@@ -4,7 +4,7 @@
44
55#[derive(Debug, Error)]
66pub enum SyncError {
7 #[error("repository sync lock is held by another worker")]
7 #[error("repository sync lock is held by another worker")]
88 Locked,
99
1010 #[error("database error: {0}")]
@@ -38,7 +38,7 @@
3838pub type SyncResult<T> = Result<T, SyncError>;
3939
4040impl SyncError {
41 #[must_use]
41 #[must_use]
4242 pub fn is_locked(&self) -> bool {
4343 matches!(self, SyncError::Locked)
4444 }
Modified dvcs/gitsync/event.rs View dvcs/gitsync/event.rs
@@ -3,13 +3,13 @@
33
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
55pub struct PostReceiveRefUpdate {
6 pub name: String,
7 pub old_oid: String,
8 pub new_oid: String,
6 pub name: String,
7 pub old_oid: String,
8 pub new_oid: String,
99}
1010
1111impl PostReceiveRefUpdate {
12 #[must_use]
12 #[must_use]
1313 pub fn new(
1414 name: impl Into<String>,
1515 old_oid: impl Into<String>,
@@ -22,12 +22,12 @@
2222 }
2323 }
2424
25 #[must_use]
25 #[must_use]
2626 pub fn is_delete(&self) -> bool {
2727 self.is_zero(self.new_oid.as_str())
2828 }
2929
30 #[must_use]
30 #[must_use]
3131 pub fn is_create(&self) -> bool {
3232 self.is_zero(self.old_oid.as_str()) && !self.is_zero(self.new_oid.as_str())
3333 }
@@ -39,15 +39,15 @@
3939
4040#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4141pub struct PostReceiveTask {
42 pub id: Uuid,
43 pub repo_id: Uuid,
44 pub channel_id: Uuid,
45 pub actor_id: Option<Uuid>,
46 pub refs: Vec<PostReceiveRefUpdate>,
42 pub id: Uuid,
43 pub repo_id: Uuid,
44 pub channel_id: Uuid,
45 pub actor_id: Option<Uuid>,
46 pub refs: Vec<PostReceiveRefUpdate>,
4747}
4848
4949impl PostReceiveTask {
50 #[must_use]
50 #[must_use]
5151 pub fn new(
5252 repo_id: Uuid,
5353 channel_id: Uuid,
Modified dvcs/gitsync/lock.rs View dvcs/gitsync/lock.rs
@@ -15,24 +15,24 @@
1515}
1616
1717impl SyncLock {
18 #[must_use]
18 #[must_use]
1919 pub fn value(&self) -> &str {
2020 &self.value
2121 }
2222
23 #[must_use]
23 #[must_use]
2424 pub fn key(&self) -> &str {
2525 &self.key
2626 }
2727
28 pub async fn refresh(&self, ttl_secs: u64) -> SyncResult<bool> {
28 pub async fn refresh(&self, ttl_secs: u64) -> SyncResult<bool> {
2929 self.kv
3030 .refresh_lock(&self.key, self.value.as_bytes(), ttl_secs)
3131 .await
3232 .map_err(SyncError::Redis)
3333 }
3434
35 pub async fn release(&self) -> SyncResult<()> {
35 pub async fn release(&self) -> SyncResult<()> {
3636 self.kv
3737 .compare_and_delete(&self.key, self.value.as_bytes())
3838 .await
Modified dvcs/gitsync/settings.rs View dvcs/gitsync/settings.rs
@@ -2,15 +2,15 @@
22
33#[derive(Debug, Clone)]
44pub struct SyncSettings {
5 pub queue_topic: String,
6 pub dlq_topic: String,
7 pub group_id: String,
8 pub concurrency: usize,
9 pub lock_ttl_secs: u64,
10 pub max_commits_per_sync: usize,
11 pub max_lock_retries: u32,
12 pub language_timeout_secs: u64,
13 pub max_languages: usize,
5 pub queue_topic: String,
6 pub dlq_topic: String,
7 pub group_id: String,
8 pub concurrency: usize,
9 pub lock_ttl_secs: u64,
10 pub max_commits_per_sync: usize,
11 pub max_lock_retries: u32,
12 pub language_timeout_secs: u64,
13 pub max_languages: usize,
1414}
1515
1616impl Default for SyncSettings {
@@ -30,12 +30,12 @@
3030}
3131
3232impl SyncSettings {
33 #[must_use]
33 #[must_use]
3434 pub fn lock_ttl(&self) -> Duration {
3535 Duration::from_secs(self.lock_ttl_secs)
3636 }
3737
38 #[must_use]
38 #[must_use]
3939 pub fn language_timeout(&self) -> Duration {
4040 Duration::from_secs(self.language_timeout_secs)
4141 }
Modified dvcs/gitsync/sync.rs View dvcs/gitsync/sync.rs
@@ -26,7 +26,7 @@
2626}
2727
2828impl RepoSync {
29 #[must_use]
29 #[must_use]
3030 pub fn new(
3131 db: Db,
3232 config: SharedRepoConfig,
@@ -52,12 +52,12 @@
5252 }
5353 }
5454
55 #[must_use]
55 #[must_use]
5656 pub fn repo_id(&self) -> uuid::Uuid {
5757 self.repo.id
5858 }
5959
60 pub async fn sync(&self) -> SyncResult<()> {
60 pub async fn sync(&self) -> SyncResult<()> {
6161 let ttl = self.settings.lock_ttl_secs;
6262 let Some(lock) = lock::acquire_lock(&self.kv, self.repo.id, ttl).await? else {
6363 return Err(SyncError::Locked);
@@ -95,7 +95,7 @@
9595 result
9696 }
9797
98 pub async fn sync_work(&self) -> SyncResult<()> {
98 pub async fn sync_work(&self) -> SyncResult<()> {
9999 let new_commits = commits::collect_new_commits_for_sync(
100100 &self.git,
101101 &self.db,
@@ -145,7 +145,7 @@
145145 Ok((branch_count, synced_commits, tag_count))
146146 }
147147
148 pub fn set_default_branch(&self, name: &str) -> SyncResult<()> {
148 pub fn set_default_branch(&self, name: &str) -> SyncResult<()> {
149149 gitcli::run(
150150 &self.git.path,
151151 &["symbolic-ref", "HEAD", &format!("refs/heads/{name}")],
Modified dvcs/gitsync/webhook.rs View dvcs/gitsync/webhook.rs
@@ -4,7 +4,7 @@
44
55#[async_trait::async_trait]
66pub trait WebhookTx: Send + Sync {
7 async fn dispatch(&self, task: &PostReceiveTask) -> Result<(), Box<dyn Error + Send + Sync>>;
7 async fn dispatch(&self, task: &PostReceiveTask) -> Result<(), Box<dyn Error + Send + Sync>>;
88}
99
1010#[derive(Debug, Default, Clone)]
Modified dvcs/gitsync/worker.rs View dvcs/gitsync/worker.rs
@@ -35,7 +35,7 @@
3535}
3636
3737impl RunningWorker {
38 pub async fn shutdown(self) -> SyncResult<()> {
38 pub async fn shutdown(self) -> SyncResult<()> {
3939 for handle in self.handles {
4040 handle.shutdown().await.map_err(SyncError::Queue)?;
4141 }
@@ -44,7 +44,7 @@
4444}
4545
4646impl SyncWorker {
47 #[must_use]
47 #[must_use]
4848 #[allow(clippy::too_many_arguments)]
4949 pub fn new(
5050 db: Db,
@@ -70,13 +70,13 @@
7070 }
7171 }
7272
73 #[must_use]
73 #[must_use]
7474 pub fn with_concurrency(mut self, concurrency: usize) -> Self {
7575 self.concurrency = concurrency.max(1);
7676 self
7777 }
7878
79 pub async fn run(self) -> SyncResult<RunningWorker> {
79 pub async fn run(self) -> SyncResult<RunningWorker> {
8080 let Some(queue) = self.queue else {
8181 tracing::warn!("sync queue disabled; gitsync worker not started");
8282 return Ok(RunningWorker {
Modified lib/auth/features/agreement/impls.rs View lib/auth/features/agreement/impls.rs
@@ -1,7 +1,7 @@
11use uuid::Uuid;
22
3use super::usecase;
34use super::{AcceptAgreementCmd, AcceptAgreementResp};
4use super::usecase;
55use crate::config::AgreementDoc;
66use crate::error::AuthError;
77use crate::features::AuthService;
@@ -30,7 +30,7 @@
3030 .await
3131 }
3232
33 pub(crate) async fn record_agreement(
33 pub(crate) async fn record_agreement(
3434 &self,
3535 user_id: Uuid,
3636 key: &str,
Modified lib/auth/features/email_code/usecase.rs View lib/auth/features/email_code/usecase.rs
@@ -231,13 +231,19 @@
231231 let token_hash = sha256_hex(cmd.code.as_bytes());
232232 let user_id = match cmd.user_id {
233233 Some(uid) => uid,
234 None => EmailEntity::find()
235 .filter(EmailEntity::COLUMN.lower_email.eq(cmd.email.trim().to_ascii_lowercase()))
236 .filter(EmailEntity::COLUMN.deleted_at.is_null())
237 .one(db)
238 .await?
239 .ok_or_else(|| AuthError::BadRequest("verification code not found".into()))?
240 .user_id,
234 None => {
235 EmailEntity::find()
236 .filter(
237 EmailEntity::COLUMN
238 .lower_email
239 .eq(cmd.email.trim().to_ascii_lowercase()),
240 )
241 .filter(EmailEntity::COLUMN.deleted_at.is_null())
242 .one(db)
243 .await?
244 .ok_or_else(|| AuthError::BadRequest("verification code not found".into()))?
245 .user_id
246 }
241247 };
242248 let row = CodeEntity::find()
243249 .filter(CodeEntity::COLUMN.email.eq(cmd.email.clone()))
Modified lib/auth/features/login/types.rs View lib/auth/features/login/types.rs
@@ -62,7 +62,7 @@
6262
6363#[derive(Debug, Clone, Deserialize, ToSchema)]
6464pub struct LoginCmd {
65 pub identifier: String,
65 pub identifier: String,
6666 pub password: String,
6767 pub pow: crate::features::register::PowProof,
6868 #[serde(default)]
Modified lib/auth/features/login/usecase.rs View lib/auth/features/login/usecase.rs
@@ -2,9 +2,7 @@
22use jwt::rotation::{RotationContext, issue_login_pair};
33use time::OffsetDateTime;
44
5use db::conn::sea_orm::{
6 ActiveModelTrait, DatabaseConnection, EntityTrait, QueryFilter, Set,
7};
5use db::conn::sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, QueryFilter, Set};
86use db::user::user_emails::Entity as EmailEntity;
97use db::user::users::{ActiveModel as UserActiveModel, Entity as UserEntity};
108
@@ -17,7 +15,11 @@
1715
1816use super::types::{LoginCmd, LoginResp, UserPublic};
1917
20async fn verify_pow(pow: &PowService, proof: &PowProof, expected_cid: Option<&str>) -> Result<(), AuthError> {
18async fn verify_pow(
19 pow: &PowService,
20 proof: &PowProof,
21 expected_cid: Option<&str>,
22) -> Result<(), AuthError> {
2123 let result = pow
2224 .verify_solution_once(&proof.challenge, &proof.solution, expected_cid)
2325 .await?;
@@ -127,7 +129,7 @@
127129 let mut am: UserActiveModel = user.clone().into();
128130 am.last_login_at = Set(Some(OffsetDateTime::now_utc()));
129131 am.updated_at = Set(OffsetDateTime::now_utc());
130 let _ = am.update(&db).await;
132 let _ = am.update(&db).await;
131133
132134 Ok(build_resp(pair.into(), user))
133135 }
Modified lib/auth/features/logout/impls.rs View lib/auth/features/logout/impls.rs
@@ -1,5 +1,5 @@
1use super::usecase;
12use super::{LogoutCmd, LogoutResp};
2use super::usecase;
33use crate::error::AuthError;
44use crate::features::AuthService;
55
Modified lib/auth/features/logout/usecase.rs View lib/auth/features/logout/usecase.rs
@@ -71,13 +71,10 @@
7171 Ok(resp)
7272 }
7373 .await;
74 let revoked_session = matches!(
75 &result,
76 Ok(LogoutResp { revoked: true })
77 );
74 let revoked_session = matches!(&result, Ok(LogoutResp { revoked: true }));
7875 metrics::record_outcome(metrics::FEATURE_LOGOUT, &result);
7976 if revoked_session {
8077 metrics::record_jwt_session_revoked(JwtRevokeKind::Logout);
8178 }
8279 result
83}
No newline at end of file
80}
Modified lib/auth/features/oauth/types.rs View lib/auth/features/oauth/types.rs
@@ -43,13 +43,13 @@
4343 pub provider: OAuthProvider,
4444 #[serde(default)]
4545 pub app_id: Option<String>,
46 pub code_verifier: String,
46 pub code_verifier: String,
4747}
4848
4949#[derive(Debug, Clone, Serialize, ToSchema)]
5050pub struct AuthorizationUrlResp {
5151 pub url: String,
52 pub temp_key: TempKey,
52 pub temp_key: TempKey,
5353}
5454
5555#[derive(Debug, Clone, Deserialize, ToSchema)]
@@ -83,5 +83,5 @@
8383 pub expires_in: i64,
8484 #[schema(value_type = String)]
8585 pub user_id: Uuid,
86 pub auto_registered: bool,
86 pub auto_registered: bool,
8787}
Modified lib/auth/features/oauth/usecase.rs View lib/auth/features/oauth/usecase.rs
@@ -7,11 +7,9 @@
77use db::conn::sea_orm::{
88 ActiveModelTrait, DatabaseConnection, EntityTrait, QueryFilter, TransactionTrait,
99};
10use db::user::user_emails::{
11 ActiveModel as EmailActiveModel, Entity as EmailEntity,
12};
10use db::user::user_emails::{ActiveModel as EmailActiveModel, Entity as EmailEntity};
1311use db::user::user_oauth_provider::{
14 Column as ProviderColumn, ActiveModel as ProviderActiveModel, Entity as ProviderEntity,
12 ActiveModel as ProviderActiveModel, Column as ProviderColumn, Entity as ProviderEntity,
1513};
1614use db::user::users::{ActiveModel as UserActiveModel, Entity as UserEntity, ProfileVisibility};
1715
@@ -217,8 +215,16 @@
217215 db: &DatabaseConnection,
218216) -> Result<(db::user::users::Model, bool), AuthError> {
219217 let link = ProviderEntity::find()
220 .filter(ProviderEntity::COLUMN.provider.eq(identity.provider.as_str().to_string()))
221 .filter(ProviderEntity::COLUMN.provider_user_id.eq(identity.provider_user_id.clone()))
218 .filter(
219 ProviderEntity::COLUMN
220 .provider
221 .eq(identity.provider.as_str().to_string()),
222 )
223 .filter(
224 ProviderEntity::COLUMN
225 .provider_user_id
226 .eq(identity.provider_user_id.clone()),
227 )
222228 .find_also_related(UserEntity)
223229 .one(db)
224230 .await?;
@@ -246,10 +252,12 @@
246252 .set_provider_user_id(identity.provider_user_id.clone())
247253 .set_provider_username(identity.provider_username.clone())
248254 .set_access_token_hash(Some(sha256_hex(identity.access_token.as_bytes())))
249 .set_refresh_token_hash(identity
250 .refresh_token
251 .as_ref()
252 .map(|t| sha256_hex(t.as_bytes())))
255 .set_refresh_token_hash(
256 identity
257 .refresh_token
258 .as_ref()
259 .map(|t| sha256_hex(t.as_bytes())),
260 )
253261 .set_token_expires_at(identity.expires_at)
254262 .set_scopes(identity.scopes.clone())
255263 .set_created_at(now)
@@ -270,10 +278,16 @@
270278 }
271279 db::conn::sea_orm::TryInsertResult::Conflicted => {
272280 let winner = ProviderEntity::find()
273 .filter(ProviderEntity::COLUMN.provider
274 .eq(identity.provider.as_str().to_string()))
275 .filter(ProviderEntity::COLUMN.provider_user_id
276 .eq(identity.provider_user_id.clone()))
281 .filter(
282 ProviderEntity::COLUMN
283 .provider
284 .eq(identity.provider.as_str().to_string()),
285 )
286 .filter(
287 ProviderEntity::COLUMN
288 .provider_user_id
289 .eq(identity.provider_user_id.clone()),
290 )
277291 .find_also_related(UserEntity)
278292 .one(db)
279293 .await?;
@@ -363,10 +377,12 @@
363377 .set_provider_user_id(identity.provider_user_id.clone())
364378 .set_provider_username(identity.provider_username.clone())
365379 .set_access_token_hash(Some(sha256_hex(identity.access_token.as_bytes())))
366 .set_refresh_token_hash(identity
367 .refresh_token
368 .as_ref()
369 .map(|t| sha256_hex(t.as_bytes())))
380 .set_refresh_token_hash(
381 identity
382 .refresh_token
383 .as_ref()
384 .map(|t| sha256_hex(t.as_bytes())),
385 )
370386 .set_token_expires_at(identity.expires_at)
371387 .set_scopes(identity.scopes.clone())
372388 .set_created_at(now)
@@ -374,10 +390,7 @@
374390 .set_last_used_at(Some(now))
375391 .into();
376392 let inserted_provider = ProviderEntity::insert(provider_am)
377 .on_conflict_do_nothing_on([
378 ProviderColumn::Provider,
379 ProviderColumn::ProviderUserId,
380 ])
393 .on_conflict_do_nothing_on([ProviderColumn::Provider, ProviderColumn::ProviderUserId])
381394 .exec(&txn)
382395 .await?;
383396 match inserted_provider {
@@ -385,8 +398,16 @@
385398 db::conn::sea_orm::TryInsertResult::Conflicted => {
386399 txn.rollback().await?;
387400 let winner = ProviderEntity::find()
388 .filter(ProviderEntity::COLUMN.provider.eq(identity.provider.as_str().to_string()))
389 .filter(ProviderEntity::COLUMN.provider_user_id.eq(identity.provider_user_id.clone()))
401 .filter(
402 ProviderEntity::COLUMN
403 .provider
404 .eq(identity.provider.as_str().to_string()),
405 )
406 .filter(
407 ProviderEntity::COLUMN
408 .provider_user_id
409 .eq(identity.provider_user_id.clone()),
410 )
390411 .find_also_related(UserEntity)
391412 .one(db)
392413 .await?;
@@ -474,7 +495,11 @@
474495 let resp: TokenPairResponse = pair.into();
475496
476497 for doc in svc.config.agreements.iter() {
477 if svc.accept_agreement(user.uid, &doc.key, &doc.version).await.is_ok() {
498 if svc
499 .accept_agreement(user.uid, &doc.key, &doc.version)
500 .await
501 .is_ok()
502 {
478503 metrics::record_agreement_record(&doc.key);
479504 }
480505 }
Modified lib/auth/features/password_hash/impls.rs View lib/auth/features/password_hash/impls.rs
@@ -1,5 +1,5 @@
1use super::usecase::{hash_password, validate_password, verify_password};
12use super::{PasswordHashString, PasswordInput, ValidatePasswordRequest};
2use super::usecase::{hash_password, validate_password, verify_password};
33use crate::error::AuthError;
44use crate::features::AuthService;
55
Modified lib/auth/features/reclaim/impls.rs View lib/auth/features/reclaim/impls.rs
@@ -11,4 +11,4 @@
1111 ) -> Result<ReclaimResp, AuthError> {
1212 usecase::reclaim(cmd, self, expected_cid.as_deref()).await
1313 }
14}
No newline at end of file
14}
Modified lib/auth/features/reclaim/mod.rs View lib/auth/features/reclaim/mod.rs
@@ -1,3 +1,3 @@
11pub mod impls;
22pub mod types;
3pub mod usecase;
No newline at end of file
3pub mod usecase;
Modified lib/auth/features/reclaim/types.rs View lib/auth/features/reclaim/types.rs
@@ -14,4 +14,4 @@
1414pub struct ReclaimResp {
1515 pub sent: bool,
1616 pub temp_key: TempKey,
17}
No newline at end of file
17}
Modified lib/auth/features/reclaim/usecase.rs View lib/auth/features/reclaim/usecase.rs
@@ -106,8 +106,7 @@
106106 )
107107 .await?;
108108 temp_key = artifacts.temp_key;
109 if let Err(e) = send_reclaim_email(&svc.email, &cmd.email, &artifacts.code, ttl).await
110 {
109 if let Err(e) = send_reclaim_email(&svc.email, &cmd.email, &artifacts.code, ttl).await {
111110 tracing::warn!(?e, email = %cmd.email, "reclaim email send failed");
112111 } else {
113112 sent = true;
@@ -115,4 +114,4 @@
115114 }
116115
117116 Ok(ReclaimResp { sent, temp_key })
118}
No newline at end of file
117}
Modified lib/auth/features/refresh/impls.rs View lib/auth/features/refresh/impls.rs
@@ -1,5 +1,5 @@
1use super::usecase;
12use super::{RefreshCmd, RefreshResp};
2use super::usecase;
33use crate::error::AuthError;
44use crate::features::AuthService;
55
Modified lib/auth/features/register/types.rs View lib/auth/features/register/types.rs
@@ -18,11 +18,11 @@
1818
1919#[derive(Debug, Clone, Deserialize, ToSchema)]
2020pub struct RegisterCmd {
21 pub username: Option<String>,
21 pub username: Option<String>,
2222 pub email: String,
2323 pub password: String,
2424 pub pow: PowProof,
25 #[serde(default)]
25 #[serde(default)]
2626 pub accept_agreements: Vec<String>,
2727}
2828
@@ -31,7 +31,7 @@
3131 #[schema(value_type = String)]
3232 pub uid: Uuid,
3333 pub pending: bool,
34 pub temp_key: TempKey,
35 #[schema(value_type = String)]
34 pub temp_key: TempKey,
35 #[schema(value_type = String)]
3636 pub purpose: EmailVerificationPurpose,
3737}
Modified lib/auth/features/register/usecase.rs View lib/auth/features/register/usecase.rs
@@ -6,9 +6,7 @@
66};
77use db::user::user_email_verification_codes::EmailVerificationPurpose;
88use db::user::user_emails::{ActiveModel as EmailActiveModel, Entity as EmailEntity};
9use db::user::users::{
10 ActiveModel as UserActiveModel, Entity as UserEntity, ProfileVisibility,
11};
9use db::user::users::{ActiveModel as UserActiveModel, Entity as UserEntity, ProfileVisibility};
1210
1311use email::events::{EmailAddress, SendEmailRequest};
1412
Modified lib/auth/features/reset_password/impls.rs View lib/auth/features/reset_password/impls.rs
@@ -1,5 +1,5 @@
1use super::usecase;
12use super::{ResetPasswordCmd, ResetPasswordResp};
2use super::usecase;
33use crate::error::AuthError;
44use crate::features::AuthService;
55
Modified lib/auth/features/reset_password/usecase.rs View lib/auth/features/reset_password/usecase.rs
@@ -2,9 +2,7 @@
22use time::OffsetDateTime;
33use uuid::Uuid;
44
5use db::conn::sea_orm::{
6 ActiveModelTrait, EntityTrait, QueryFilter, Set, TransactionTrait,
7};
5use db::conn::sea_orm::{ActiveModelTrait, EntityTrait, QueryFilter, Set, TransactionTrait};
86use db::user::user_emails::Entity as EmailEntity;
97use db::user::user_password_reset_tokens::{
108 ActiveModel as TokenActiveModel, Entity as TokenEntity,
Modified lib/auth/features/mod.rs View lib/auth/features/mod.rs
@@ -17,9 +17,9 @@
1717pub mod logout;
1818pub mod oauth;
1919pub mod password_hash;
20pub mod reclaim;
2021pub mod refresh;
2122pub mod register;
22pub mod reclaim;
2323pub mod resend_code;
2424pub mod reset_password;
2525
@@ -76,7 +76,9 @@
7676 }
7777
7878 fn access_blacklist(&self) -> Option<Arc<rate_limit::AccessTokenBlacklist>> {
79 Some(Arc::new(rate_limit::AccessTokenBlacklist::new(self.kv.clone())))
79 Some(Arc::new(rate_limit::AccessTokenBlacklist::new(
80 self.kv.clone(),
81 )))
8082 }
8183}
8284
Modified lib/auth/handler/email/reclaim.rs View lib/auth/handler/email/reclaim.rs
@@ -25,8 +25,10 @@
2525 cid: Option<Extension<rate_limit::ClientId>>,
2626 cmd: AppRequest<ReclaimCmd>,
2727) -> Result<(CookieJar, AppResponseData<ReclaimResp>), AuthError> {
28 let resp = svc.reclaim_code(cmd.into_inner(), cid.map(|c| c.0 .0)).await?;
28 let resp = svc
29 .reclaim_code(cmd.into_inner(), cid.map(|c| c.0.0))
30 .await?;
2931 let cookie = cookies::temp_key_to_cookie(&svc.config, &resp.temp_key);
3032 let jar = jar.add(cookie);
3133 Ok((jar, AppResponseData::ok(resp)))
32}
No newline at end of file
34}
Modified lib/auth/handler/email/resend.rs View lib/auth/handler/email/resend.rs
@@ -26,7 +26,7 @@
2626 cmd: AppRequest<ResendCodeCmd>,
2727) -> Result<(CookieJar, AppResponseData<ResendCodeResp>), AuthError> {
2828 let resp = svc
29 .resend_verification_code(cmd.into_inner(), cid.map(|c| c.0 .0))
29 .resend_verification_code(cmd.into_inner(), cid.map(|c| c.0.0))
3030 .await?;
3131 let cookie = cookies::temp_key_to_cookie(&svc.config, &resp.temp_key);
3232 let jar = jar.add(cookie);
Modified lib/auth/handler/middleware/metrics.rs View lib/auth/handler/middleware/metrics.rs
@@ -44,4 +44,4 @@
4444 .record(start.elapsed().as_secs_f64());
4545
4646 resp
47}
No newline at end of file
47}
Modified lib/auth/handler/middleware/mod.rs View lib/auth/handler/middleware/mod.rs
@@ -1 +1 @@
1pub(crate) mod metrics;
No newline at end of file
1pub(crate) mod metrics;
Modified lib/auth/handler/oauth/callback.rs View lib/auth/handler/oauth/callback.rs
@@ -12,9 +12,9 @@
1212
1313#[derive(Debug, Deserialize, utoipa::IntoParams)]
1414pub(crate) struct CallbackQuery {
15 pub provider: OAuthProvider,
16 pub code: String,
17 pub state: String,
15 pub provider: OAuthProvider,
16 pub code: String,
17 pub state: String,
1818}
1919
2020#[utoipa::path(
Modified lib/auth/handler/password/forgot.rs View lib/auth/handler/password/forgot.rs
@@ -25,7 +25,9 @@
2525 cid: Option<Extension<rate_limit::ClientId>>,
2626 cmd: AppRequest<ForgotPasswordCmd>,
2727) -> Result<(CookieJar, AppResponseData<ForgotPasswordResp>), AuthError> {
28 let resp = svc.forgot_password(cmd.into_inner(), cid.map(|c| c.0 .0)).await?;
28 let resp = svc
29 .forgot_password(cmd.into_inner(), cid.map(|c| c.0.0))
30 .await?;
2931 let cookie = cookies::temp_key_to_cookie(&svc.config, &resp.temp_key);
3032 let jar = jar.add(cookie);
3133 Ok((jar, AppResponseData::ok(resp)))
Modified lib/auth/handler/login.rs View lib/auth/handler/login.rs
@@ -23,6 +23,6 @@
2323 cid: Option<Extension<rate_limit::ClientId>>,
2424 cmd: AppRequest<LoginCmd>,
2525) -> Result<AppResponseData<LoginResp>, AuthError> {
26 let resp = svc.login(cmd.into_inner(), cid.map(|c| c.0 .0)).await?;
26 let resp = svc.login(cmd.into_inner(), cid.map(|c| c.0.0)).await?;
2727 Ok(AppResponseData::ok(resp))
2828}
Modified lib/auth/handler/openapi.rs View lib/auth/handler/openapi.rs
@@ -2,21 +2,21 @@
22
33use crate::config::AgreementDoc;
44use crate::features::TempKey;
5use export::auth::agreement::{AcceptAgreementCmd, AcceptAgreementResp};
65use crate::features::email_code::types::{VerifyCodeCmd, VerifyCodeResult};
76use crate::features::forgot_password::types::{ForgotPasswordCmd, ForgotPasswordResp};
87use crate::features::login::types::{LoginCmd, LoginResp, UserMe, UserPublic};
9use export::auth::logout::{LogoutCmd, LogoutResp};
108use crate::features::oauth::types::{
119 AuthorizationUrlResp, AuthorizeCmd, CallbackCmd, OAuthLoginResp, OAuthProvider,
1210 OAuthProviderInfo,
1311};
14use export::auth::refresh::{RefreshCmd, RefreshResp};
12use crate::features::reclaim::types::{ReclaimCmd, ReclaimResp};
1513use crate::features::register::types::{PowProof, RegisterCmd, RegisterResp};
16use crate::features::reclaim::types::{ReclaimCmd, ReclaimResp};
1714use crate::features::resend_code::types::{ResendCodeCmd, ResendCodeResp};
18use export::auth::reset_password::{ResetPasswordCmd, ResetPasswordResp};
1915use crate::handler::pow::PowChallengeResp;
16use export::auth::agreement::{AcceptAgreementCmd, AcceptAgreementResp};
17use export::auth::logout::{LogoutCmd, LogoutResp};
18use export::auth::refresh::{RefreshCmd, RefreshResp};
19use export::auth::reset_password::{ResetPasswordCmd, ResetPasswordResp};
2020
2121#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
2222pub struct ErrorBody {
Modified lib/auth/handler/pow.rs View lib/auth/handler/pow.rs
@@ -7,9 +7,9 @@
77
88#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
99pub struct PowChallengeResp {
10 #[schema(value_type = String)]
10 #[schema(value_type = String)]
1111 pub challenge: Challenge,
12 pub ttl_seconds: u64,
12 pub ttl_seconds: u64,
1313}
1414
1515#[utoipa::path(
@@ -31,4 +31,4 @@
3131 challenge,
3232 ttl_seconds: ttl,
3333 }))
34}
No newline at end of file
34}
Modified lib/auth/handler/register.rs View lib/auth/handler/register.rs
@@ -26,7 +26,7 @@
2626 cid: Option<Extension<rate_limit::ClientId>>,
2727 cmd: AppRequest<RegisterCmd>,
2828) -> Result<(CookieJar, AppResponseData<RegisterResp>), AuthError> {
29 let resp = svc.register(cmd.into_inner(), cid.map(|c| c.0 .0)).await?;
29 let resp = svc.register(cmd.into_inner(), cid.map(|c| c.0.0)).await?;
3030 let cookie = cookies::temp_key_to_cookie(&svc.config, &resp.temp_key);
3131 let jar = jar.add(cookie);
3232 Ok((jar, AppResponseData::ok(resp)))
Modified lib/auth/handler/routes.rs View lib/auth/handler/routes.rs
@@ -12,11 +12,8 @@
1212 .route("/login", post(crate::handler::login::handler))
1313 .route("/logout", post(crate::handler::logout::handler))
1414 .route("/refresh", post(crate::handler::refresh::handler))
15 .route("/pow/challenge", get(crate::handler::pow::handler))
1516 .route(
16 "/pow/challenge",
17 get(crate::handler::pow::handler),
18 )
19 .route(
2017 "/email/verify",
2118 post(crate::handler::email::verify::handler),
2219 )
@@ -55,8 +52,10 @@
5552 )
5653 .route(
5754 "/me",
58 get(crate::handler::me::handler)
59 .layer(from_fn_with_state(svc.clone(), inject_jwt_ctx::<AuthService>)),
55 get(crate::handler::me::handler).layer(from_fn_with_state(
56 svc.clone(),
57 inject_jwt_ctx::<AuthService>,
58 )),
6059 )
6160 .layer(from_fn_with_state(svc.clone(), ratelimit::<AuthService>))
6261 .layer(axum::middleware::from_fn(
Modified lib/auth/pow/config.rs View lib/auth/pow/config.rs
@@ -3,14 +3,14 @@
33
44#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct PowSettings {
6 pub hmac_secret: String,
7 pub algorithm: String,
8 pub cost: u32,
9 pub key_length: usize,
10 pub key_prefix: String,
11 pub memory_cost: Option<u32>,
12 pub parallelism: Option<u32>,
13 pub challenge_ttl: Duration,
6 pub hmac_secret: String,
7 pub algorithm: String,
8 pub cost: u32,
9 pub key_length: usize,
10 pub key_prefix: String,
11 pub memory_cost: Option<u32>,
12 pub parallelism: Option<u32>,
13 pub challenge_ttl: Duration,
1414}
1515
1616impl Default for PowSettings {
Modified lib/auth/pow/error.rs View lib/auth/pow/error.rs
@@ -2,22 +2,22 @@
22
33#[derive(Debug, Error)]
44pub enum PowError {
5 #[error("pow hmac secret is not configured")]
5 #[error("pow hmac secret is not configured")]
66 MissingSecret,
7 #[error("unsupported pow algorithm: {0}")]
7 #[error("unsupported pow algorithm: {0}")]
88 UnsupportedAlgorithm(String),
9 #[error("pow challenge expired")]
9 #[error("pow challenge expired")]
1010 Expired,
11 #[error("invalid pow challenge signature")]
11 #[error("invalid pow challenge signature")]
1212 InvalidSignature,
13 #[error("invalid pow solution")]
13 #[error("invalid pow solution")]
1414 InvalidSolution,
15 #[error("pow challenge was not issued or already used")]
15 #[error("pow challenge was not issued or already used")]
1616 NotIssued,
17 #[error("pow challenge was issued for a different client")]
17 #[error("pow challenge was issued for a different client")]
1818 ClientMismatch,
19 #[error("pow store error: {0}")]
19 #[error("pow store error: {0}")]
2020 Storage(String),
21 #[error("pow error: {0}")]
21 #[error("pow error: {0}")]
2222 Altcha(#[from] altcha::Error),
2323}
Modified lib/auth/pow/mod.rs View lib/auth/pow/mod.rs
@@ -27,19 +27,16 @@
2727}
2828
2929impl PowService {
30 pub fn new(settings: PowSettings) -> Self {
31 Self {
32 settings,
33 kv: None,
34 }
30 pub fn new(settings: PowSettings) -> Self {
31 Self { settings, kv: None }
3532 }
3633
37 pub fn with_kv(mut self, kv: Arc<RedisCache>) -> Self {
34 pub fn with_kv(mut self, kv: Arc<RedisCache>) -> Self {
3835 self.kv = Some(kv);
3936 self
4037 }
4138
42 fn issued_key(challenge: &Challenge) -> String {
39 fn issued_key(challenge: &Challenge) -> String {
4340 let json = serde_json::to_string(challenge).unwrap_or_default();
4441 let digest = Sha256::digest(json.as_bytes());
4542 format!(
@@ -48,15 +45,15 @@
4845 )
4946 }
5047
51 pub fn settings(&self) -> &PowSettings {
48 pub fn settings(&self) -> &PowSettings {
5249 &self.settings
5350 }
5451
55 pub fn generate_challenge(&self) -> Result<Challenge, PowError> {
52 pub fn generate_challenge(&self) -> Result<Challenge, PowError> {
5653 self.generate_challenge_with(None)
5754 }
5855
59 pub fn generate_challenge_with(
56 pub fn generate_challenge_with(
6057 &self,
6158 data: Option<std::collections::BTreeMap<String, serde_json::Value>>,
6259 ) -> Result<Challenge, PowError> {
@@ -79,7 +76,7 @@
7976 create_challenge(options).map_err(PowError::from)
8077 }
8178
82 pub fn verify_solution(
79 pub fn verify_solution(
8380 &self,
8481 challenge: &Challenge,
8582 solution: &Solution,
@@ -90,7 +87,7 @@
9087 verify_solution(options).map_err(PowError::from)
9188 }
9289
93 pub fn verify(&self, challenge: &Challenge, solution: &Solution) -> Result<bool, PowError> {
90 pub fn verify(&self, challenge: &Challenge, solution: &Solution) -> Result<bool, PowError> {
9491 let result = self.verify_solution(challenge, solution)?;
9592 if result.verified {
9693 Ok(true)
@@ -103,24 +100,21 @@
103100 }
104101 }
105102
106 pub async fn issue_challenge_bound(
107 &self,
108 cid: &str,
109 ) -> Result<(Challenge, u64), PowError> {
110 let kv = self.kv.as_ref().ok_or_else(|| {
111 PowError::Storage("pow redis handle not attached".to_string())
112 })?;
103 pub async fn issue_challenge_bound(&self, cid: &str) -> Result<(Challenge, u64), PowError> {
104 let kv = self
105 .kv
106 .as_ref()
107 .ok_or_else(|| PowError::Storage("pow redis handle not attached".to_string()))?;
113108
114109 let mut data = BTreeMap::new();
115110 data.insert("cid".into(), serde_json::Value::String(cid.to_string()));
116 data.insert("nonce".into(), serde_json::Value::String(Uuid::new_v4().to_string()));
111 data.insert(
112 "nonce".into(),
113 serde_json::Value::String(Uuid::new_v4().to_string()),
114 );
117115 let challenge = self.generate_challenge_with(Some(data))?;
118116
119 let ttl = self
120 .settings
121 .challenge_ttl
122 .whole_seconds()
123 .max(1) as u64;
117 let ttl = self.settings.challenge_ttl.whole_seconds().max(1) as u64;
124118 let key = Self::issued_key(&challenge);
125119 kv.set_bytes(&key, b"1".to_vec(), ttl)
126120 .await
@@ -131,18 +125,19 @@
131125 Ok((challenge, ttl))
132126 }
133127
134 pub async fn verify_solution_once(
128 pub async fn verify_solution_once(
135129 &self,
136130 challenge: &Challenge,
137131 solution: &Solution,
138132 expected_cid: Option<&str>,
139133 ) -> Result<VerifySolutionResult, PowError> {
134 use crate::metrics::{PowVerifyOutcome, record_pow_verify};
140135 use std::time::Instant;
141 use crate::metrics::{PowVerifyOutcome, record_pow_verify};
142136
143 let kv = self.kv.as_ref().ok_or_else(|| {
144 PowError::Storage("pow redis handle not attached".to_string())
145 })?;
137 let kv = self
138 .kv
139 .as_ref()
140 .ok_or_else(|| PowError::Storage("pow redis handle not attached".to_string()))?;
146141
147142 let key = Self::issued_key(challenge);
148143 let consumed = kv
Modified lib/auth/metrics.rs View lib/auth/metrics.rs
@@ -4,7 +4,6 @@
44
55use crate::error::AuthError;
66
7
87pub const FEATURE_LOGIN: &str = "login";
98pub const FEATURE_REGISTER: &str = "register";
109pub const FEATURE_REFRESH: &str = "refresh";
@@ -23,7 +22,6 @@
2322pub const FEATURE_PASSWORD_HASH_VERIFY: &str = "password_hash_verify";
2423
2524pub const OUTCOME_SUCCESS: &str = "success";
26
2725
2826static DESCRIBE_ONCE: Once = Once::new();
2927
@@ -130,7 +128,6 @@
130128 );
131129}
132130
133
134131#[must_use = "timer is dropped silently if not bound"]
135132pub struct Timer {
136133 feature: &'static str,
@@ -187,7 +184,6 @@
187184 }
188185}
189186
190
191187pub fn record_pow_challenge_issued() {
192188 ensure_described();
193189 counter!("auth_pow_challenges_issued_total").increment(1);
@@ -511,4 +507,4 @@
511507pub fn record_jwt_denied_jti() {
512508 ensure_described();
513509 counter!("auth_jwt_denied_jti_total").increment(1);
514}
No newline at end of file
510}
Modified lib/cache/redis/mod.rs View lib/cache/redis/mod.rs
@@ -234,7 +234,7 @@
234234 result
235235 }
236236
237 pub async fn set_nx(
237 pub async fn set_nx(
238238 &self,
239239 key: &str,
240240 value: Vec<u8>,
@@ -259,7 +259,7 @@
259259 result
260260 }
261261
262 pub async fn compare_and_delete(&self, key: &str, expected: &[u8]) -> Result<bool, RedisError> {
262 pub async fn compare_and_delete(&self, key: &str, expected: &[u8]) -> Result<bool, RedisError> {
263263 let op = "compare_and_delete";
264264 let result: Result<bool, RedisError> = async {
265265 let full_key = self.prefixed(key);
@@ -276,7 +276,7 @@
276276 result
277277 }
278278
279 pub async fn refresh_lock(
279 pub async fn refresh_lock(
280280 &self,
281281 key: &str,
282282 value: &[u8],
@@ -299,7 +299,7 @@
299299 result
300300 }
301301
302 pub async fn rpush(&self, key: &str, value: Vec<u8>) -> Result<u64, RedisError> {
302 pub async fn rpush(&self, key: &str, value: Vec<u8>) -> Result<u64, RedisError> {
303303 let op = "rpush";
304304 let result: Result<u64, RedisError> = async {
305305 let full_key = self.prefixed(key);
@@ -312,7 +312,7 @@
312312 result
313313 }
314314
315 pub async fn lpush(&self, key: &str, value: Vec<u8>) -> Result<u64, RedisError> {
315 pub async fn lpush(&self, key: &str, value: Vec<u8>) -> Result<u64, RedisError> {
316316 let op = "lpush";
317317 let result: Result<u64, RedisError> = async {
318318 let full_key = self.prefixed(key);
@@ -325,7 +325,7 @@
325325 result
326326 }
327327
328 pub async fn lpop(&self, key: &str) -> Result<Option<Vec<u8>>, RedisError> {
328 pub async fn lpop(&self, key: &str) -> Result<Option<Vec<u8>>, RedisError> {
329329 let op = "lpop";
330330 let result: Result<Option<Vec<u8>>, RedisError> = async {
331331 let full_key = self.prefixed(key);
@@ -340,7 +340,7 @@
340340 result
341341 }
342342
343 pub async fn lrem(&self, key: &str, count: i64, member: &[u8]) -> Result<u64, RedisError> {
343 pub async fn lrem(&self, key: &str, count: i64, member: &[u8]) -> Result<u64, RedisError> {
344344 let op = "lrem";
345345 let result: Result<u64, RedisError> = async {
346346 let full_key = self.prefixed(key);
@@ -353,7 +353,7 @@
353353 result
354354 }
355355
356 pub async fn llen(&self, key: &str) -> Result<u64, RedisError> {
356 pub async fn llen(&self, key: &str) -> Result<u64, RedisError> {
357357 let op = "llen";
358358 let result: Result<u64, RedisError> = async {
359359 let full_key = self.prefixed(key);
@@ -366,7 +366,7 @@
366366 result
367367 }
368368
369 pub async fn getdel_bytes(&self, key: &str) -> Result<Option<Vec<u8>>, RedisError> {
369 pub async fn getdel_bytes(&self, key: &str) -> Result<Option<Vec<u8>>, RedisError> {
370370 let op = "getdel_bytes";
371371 let result: Result<Option<Vec<u8>>, RedisError> = async {
372372 let full_key = self.prefixed(key);
@@ -574,4 +574,4 @@
574574 RedisConn::Sentinel(c) => RedisConnClone::Sentinel(c.clone()),
575575 }
576576 }
577}
No newline at end of file
577}
Modified lib/cache/metrics.rs View lib/cache/metrics.rs
@@ -29,8 +29,11 @@
2929 "cache_lookup_total",
3030 "Cache lookups partitioned by outcome (hit_hybrid, hit_l3, miss). Hit rate = (hit_hybrid + hit_l3) / lookups."
3131 );
32 describe_counter!("cache_errors_total", "Cache errors at the hybrid or L3 layer.");
3332 describe_counter!(
33 "cache_errors_total",
34 "Cache errors at the hybrid or L3 layer."
35 );
36 describe_counter!(
3437 "cache_redis_ops_total",
3538 "Raw Redis operations partitioned by op and outcome (success, error)."
3639 );
@@ -54,4 +57,4 @@
5457 "outcome" => if ok { "success" } else { "error" },
5558 )
5659 .increment(1);
57}
No newline at end of file
60}
Modified lib/db/channel/channels.rs View lib/db/channel/channels.rs
@@ -1,7 +1,18 @@
11use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
23use time::OffsetDateTime;
34use uuid::Uuid;
45
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
10pub enum ChannelKind {
11 #[default]
12 Directory = 0,
13 Object = 1,
14}
15
516#[sea_orm::model]
617#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
718#[sea_orm(table_name = "channel")]
@@ -11,6 +22,8 @@
1122 #[sea_orm(indexed)]
1223 pub space_id: Option<Uuid>,
1324 pub name: String,
25 #[sea_orm(default_value = "0")]
26 pub kind: ChannelKind,
1427 #[sea_orm(indexed)]
1528 pub parent_id: Option<Uuid>,
1629 pub color: Option<String>,
Modified lib/db/channel/mod.rs View lib/db/channel/mod.rs
@@ -1,4 +1,3 @@
1pub mod channel_members;
21pub mod channel_overwrite_role;
32pub mod channel_overwrite_users;
43pub mod channels;
Modified lib/db/chat/chat_reads.rs View lib/db/chat/chat_reads.rs
@@ -12,6 +12,8 @@
1212 pub user_id: Uuid,
1313 pub thread_id: Option<Uuid>,
1414 pub last_read_message_id: Option<Uuid>,
15 #[sea_orm(default_value = "false")]
16 pub muted: bool,
1517 pub updated_at: OffsetDateTime,
1618 #[sea_orm(belongs_to, from = "chat_id", to = "id")]
1719 pub chat: BelongsTo<super::chats::Entity>,
Modified lib/db/chat/mod.rs View lib/db/chat/mod.rs
@@ -1,4 +1,3 @@
1pub mod chat_members;
21pub mod chat_message_edits;
32pub mod chat_message_mentions;
43pub mod chat_message_reactions;
Modified lib/db/issue/mod.rs View lib/db/issue/mod.rs
@@ -17,4 +17,4 @@
1717pub mod issues;
1818pub mod milestone;
1919pub mod stopwatch;
20pub mod tracked_time;
No newline at end of file
20pub mod tracked_time;
Modified lib/db/papers/mod.rs View lib/db/papers/mod.rs
@@ -4,4 +4,4 @@
44pub mod paper_categories;
55pub mod paper_folders;
66pub mod papers;
7pub mod user_paper_library;
No newline at end of file
7pub mod user_paper_library;
Modified lib/db/project/project.rs View lib/db/project/project.rs
@@ -11,6 +11,9 @@
1111pub struct Model {
1212 #[sea_orm(primary_key, auto_increment = false)]
1313 pub id: Uuid,
14 /// Optional object channel bound to this project once it enters the channel tree.
15 /// `None` while created without a parent dir.
16 pub channel_id: Option<Uuid>,
1417 pub space_id: Uuid,
1518 pub creator_id: Uuid,
1619 pub name: String,
@@ -30,6 +33,8 @@
3033 pub starred: i32,
3134 pub created_at: OffsetDateTime,
3235 pub updated_at: Option<OffsetDateTime>,
36 #[sea_orm(belongs_to, from = "channel_id", to = "id")]
37 pub channel: BelongsTo<Option<super::super::channel::channels::Entity>>,
3338 #[sea_orm(belongs_to, from = "space_id", to = "id")]
3439 pub space: BelongsTo<super::super::space::spaces::Entity>,
3540 #[sea_orm(belongs_to, relation_enum = "Creator", from = "creator_id", to = "uid")]
Modified lib/db/project/project_activities.rs View lib/db/project/project_activities.rs
@@ -1,5 +1,5 @@
1use sea_orm::JsonValue;
12use sea_orm::entity::prelude::*;
2use sea_orm::JsonValue;
33use time::OffsetDateTime;
44use uuid::Uuid;
55
Modified lib/db/project/project_documents.rs View lib/db/project/project_documents.rs
@@ -27,7 +27,12 @@
2727 pub parent: BelongsTo<Option<super::project_documents::Entity>>,
2828 #[sea_orm(belongs_to, relation_enum = "Creator", from = "creator_id", to = "uid")]
2929 pub creator: BelongsTo<super::super::user::users::Entity>,
30 #[sea_orm(belongs_to, relation_enum = "LastEditor", from = "last_editor_id", to = "uid")]
30 #[sea_orm(
31 belongs_to,
32 relation_enum = "LastEditor",
33 from = "last_editor_id",
34 to = "uid"
35 )]
3136 pub last_editor: BelongsTo<Option<super::super::user::users::Entity>>,
3237}
3338
Modified lib/db/project/project_health.rs View lib/db/project/project_health.rs
@@ -1,9 +1,7 @@
11use sea_orm::entity::prelude::*;
22use serde::{Deserialize, Serialize};
33
4#[derive(
5 Debug, Clone, Copy, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
6)]
4#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]
75#[sea_orm(rs_type = "i32", db_type = "SmallInteger")]
86pub enum ProjectHealth {
97 OnTrack = 0,
Modified lib/db/project/project_links.rs View lib/db/project/project_links.rs
@@ -3,9 +3,7 @@
33use time::OffsetDateTime;
44use uuid::Uuid;
55
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
6#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]
97#[sea_orm(rs_type = "i32", db_type = "SmallInteger")]
108pub enum ProjectLinkKind {
119 Related = 0,
Modified lib/db/project/project_views.rs View lib/db/project/project_views.rs
@@ -1,5 +1,5 @@
1use sea_orm::JsonValue;
12use sea_orm::entity::prelude::*;
2use sea_orm::JsonValue;
33use time::OffsetDateTime;
44use uuid::Uuid;
55
Modified lib/email/metrics.rs View lib/email/metrics.rs
@@ -10,7 +10,6 @@
1010pub const KIND_SENT: &str = "sent";
1111pub const KIND_FAILED: &str = "failed";
1212
13
1413static DESCRIBE_ONCE: Once = Once::new();
1514
1615pub fn ensure_described() {
@@ -44,7 +43,6 @@
4443 "Total SMTP transport sends partitioned by outcome"
4544 );
4645}
47
4846
4947fn outcome_for<T>(result: &Result<T, EmailError>) -> &'static str {
5048 match result {
@@ -128,4 +126,4 @@
128126 "outcome" => outcome_for(result)
129127 )
130128 .increment(1);
131}
No newline at end of file
129}
Modified lib/export/auth/mod.rs View lib/export/auth/mod.rs
@@ -2,4 +2,4 @@
22pub mod logout;
33pub mod password_hash;
44pub mod refresh;
5pub mod reset_password;
No newline at end of file
5pub mod reset_password;
Modified lib/export/issues/comment.rs View lib/export/issues/comment.rs
@@ -59,4 +59,4 @@
5959 pub parent_id: Option<Uuid>,
6060 pub limit: Option<u64>,
6161 pub offset: Option<u64>,
62}
No newline at end of file
62}
Modified lib/export/issues/issue.rs View lib/export/issues/issue.rs
@@ -94,4 +94,4 @@
9494 pub q: Option<String>,
9595 pub limit: Option<u64>,
9696 pub offset: Option<u64>,
97}
No newline at end of file
97}
Modified lib/export/issues/issue_member.rs View lib/export/issues/issue_member.rs
@@ -52,4 +52,4 @@
5252 pub kind: Option<String>,
5353 pub limit: Option<u64>,
5454 pub offset: Option<u64>,
55}
No newline at end of file
55}
Modified lib/export/issues/issue_reaction.rs View lib/export/issues/issue_reaction.rs
@@ -64,4 +64,4 @@
6464pub struct ListReactionQuery {
6565 pub limit: Option<u64>,
6666 pub offset: Option<u64>,
67}
No newline at end of file
67}
Modified lib/export/issues/issue_repo_rel.rs View lib/export/issues/issue_repo_rel.rs
@@ -31,4 +31,4 @@
3131#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
3232pub struct CreateRepoRelOption {
3333 pub repo_id: Uuid,
34}
No newline at end of file
34}
Modified lib/export/issues/issue_watch.rs View lib/export/issues/issue_watch.rs
@@ -38,4 +38,4 @@
3838#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
3939pub struct UpsertWatchOption {
4040 pub mode: String,
41}
No newline at end of file
41}
Modified lib/export/issues/issues.rs View lib/export/issues/issues.rs
@@ -49,7 +49,10 @@
4949
5050#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
5151pub struct CreateCollectionOption {
52 pub channel_id: Uuid,
52 /// Space the collection lives in. An object channel is minted under `parent_dir`
53 /// (or the space root when `None`) and bound to the collection.
54 pub space_id: Uuid,
55 pub parent_dir: Option<Uuid>,
5356 pub name: String,
5457 pub icon: Option<String>,
5558 pub description: Option<String>,
@@ -68,4 +71,4 @@
6871 pub q: Option<String>,
6972 pub limit: Option<u64>,
7073 pub offset: Option<u64>,
71}
No newline at end of file
74}
Modified lib/export/issues/label.rs View lib/export/issues/label.rs
@@ -74,4 +74,4 @@
7474 pub is_archived: Option<bool>,
7575 pub limit: Option<u64>,
7676 pub offset: Option<u64>,
77}
No newline at end of file
77}
Modified lib/export/issues/milestone.rs View lib/export/issues/milestone.rs
@@ -70,4 +70,4 @@
7070 pub q: Option<String>,
7171 pub limit: Option<u64>,
7272 pub offset: Option<u64>,
73}
No newline at end of file
73}
Modified lib/export/issues/mod.rs View lib/export/issues/mod.rs
@@ -14,4 +14,4 @@
1414pub mod issue_watch;
1515pub mod issues;
1616pub mod label;
17pub mod milestone;
No newline at end of file
17pub mod milestone;
Modified lib/export/project/activities.rs View lib/export/project/activities.rs
@@ -37,4 +37,4 @@
3737pub struct ActivityPage {
3838 pub total: u64,
3939 pub items: Vec<ActivityView>,
40}
No newline at end of file
40}
Modified lib/export/project/documents.rs View lib/export/project/documents.rs
@@ -41,4 +41,4 @@
4141pub struct DocumentPage {
4242 pub total: u64,
4343 pub items: Vec<DocumentView>,
44}
No newline at end of file
44}
Modified lib/export/project/favorites.rs View lib/export/project/favorites.rs
@@ -21,4 +21,4 @@
2121pub struct FavoritePage {
2222 pub total: u64,
2323 pub items: Vec<FavoriteView>,
24}
No newline at end of file
24}
Modified lib/export/project/lifecycle.rs View lib/export/project/lifecycle.rs
@@ -52,6 +52,8 @@
5252#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
5353pub struct CreateProjectCmd {
5454 pub space_id: Uuid,
55 /// Optional directory channel to host the project's object channel under.
56 pub parent_dir: Option<Uuid>,
5557 #[validate(length(min = 1, max = 100))]
5658 pub name: String,
5759 #[validate(length(max = 2000))]
Modified lib/export/project/mod.rs View lib/export/project/mod.rs
@@ -2,4 +2,4 @@
22pub mod documents;
33pub mod favorites;
44pub mod lifecycle;
5pub mod views;
No newline at end of file
5pub mod views;
Modified lib/export/project/views.rs View lib/export/project/views.rs
@@ -63,4 +63,4 @@
6363pub struct ViewPage {
6464 pub total: u64,
6565 pub items: Vec<ViewView>,
66}
No newline at end of file
66}
Modified lib/export/repo/lifecycle.rs View lib/export/repo/lifecycle.rs
@@ -223,7 +223,10 @@
223223
224224#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
225225pub struct CreateRepoOption {
226 pub channel_id: Uuid,
226 /// Space the repo lives in. An object channel is minted under `parent_dir` (or
227 /// the space root when `None`) and bound to the new repo.
228 pub space_id: Uuid,
229 pub parent_dir: Option<Uuid>,
227230 pub name: String,
228231 pub description: Option<String>,
229232 pub website: Option<String>,
Modified lib/export/repo/mod.rs View lib/export/repo/mod.rs
@@ -14,4 +14,4 @@
1414pub mod sync;
1515pub mod tag;
1616pub mod tag_protection;
17pub mod topic;
No newline at end of file
17pub mod topic;
Modified lib/export/space/bans.rs View lib/export/space/bans.rs
@@ -72,4 +72,4 @@
7272pub struct SpaceBanPage {
7373 pub items: Vec<SpaceBanView>,
7474 pub total: i64,
75}
No newline at end of file
75}
Modified lib/export/space/channels.rs View lib/export/space/channels.rs
@@ -1,4 +1,5 @@
11use db::channel::channels as channel_entity;
2use db::channel::channels::ChannelKind;
23use serde::{Deserialize, Serialize};
34use time::OffsetDateTime;
45use time::format_description::well_known::Rfc3339;
@@ -18,6 +19,7 @@
1819 pub id: Uuid,
1920 pub space_id: Option<Uuid>,
2021 pub name: String,
22 pub kind: String,
2123 pub parent_id: Option<Uuid>,
2224 pub color: Option<String>,
2325 pub position: i32,
@@ -32,12 +34,20 @@
3234 pub created_by: Uuid,
3335}
3436
37fn kind_str(k: ChannelKind) -> String {
38 match k {
39 ChannelKind::Directory => "directory".to_string(),
40 ChannelKind::Object => "object".to_string(),
41 }
42}
43
3544impl From<channel_entity::Model> for ChannelView {
3645 fn from(c: channel_entity::Model) -> Self {
3746 Self {
3847 id: c.id,
3948 space_id: c.space_id,
4049 name: c.name,
50 kind: kind_str(c.kind),
4151 parent_id: c.parent_id,
4252 color: c.color,
4353 position: c.position,
@@ -58,4 +68,4 @@
5868pub struct ChannelPage {
5969 pub items: Vec<ChannelView>,
6070 pub total: i64,
61}
No newline at end of file
71}
Modified lib/export/space/invites.rs View lib/export/space/invites.rs
@@ -63,4 +63,4 @@
6363pub struct SpaceInvitePage {
6464 pub items: Vec<SpaceInviteView>,
6565 pub total: i64,
66}
No newline at end of file
66}
Modified lib/export/space/members.rs View lib/export/space/members.rs
@@ -155,4 +155,4 @@
155155pub struct SpaceMemberPage {
156156 pub items: Vec<SpaceMemberView>,
157157 pub total: i64,
158}
No newline at end of file
158}
Modified lib/export/space/mod.rs View lib/export/space/mod.rs
@@ -1,8 +1,7 @@
11pub mod bans;
2pub mod channel_members;
32pub mod channels;
43pub mod invites;
54pub mod members;
65pub mod overwrites;
76pub mod roles;
8pub mod spaces;
No newline at end of file
7pub mod spaces;
Modified lib/export/space/overwrites.rs View lib/export/space/overwrites.rs
@@ -58,4 +58,4 @@
5858 pub user_id: Uuid,
5959 pub allow_mask: i64,
6060 pub deny_mask: i64,
61}
No newline at end of file
61}
Modified lib/export/space/roles.rs View lib/export/space/roles.rs
@@ -82,4 +82,4 @@
8282#[derive(Debug, Clone, Deserialize, ToSchema)]
8383pub struct UnassignRoleCmd {
8484 pub role_id: Uuid,
85}
No newline at end of file
85}
Modified lib/export/space/spaces.rs View lib/export/space/spaces.rs
@@ -148,4 +148,4 @@
148148pub struct SpacePage {
149149 pub items: Vec<SpaceView>,
150150 pub total: i64,
151}
No newline at end of file
151}
Modified lib/export/user/deletion.rs View lib/export/user/deletion.rs
@@ -56,9 +56,15 @@
5656 user_id: m.user_id,
5757 reason: m.reason,
5858 status: m.status.into(),
59 scheduled_for: m.scheduled_for.map(|t| t.format(&Rfc3339).unwrap_or_default()),
60 completed_at: m.completed_at.map(|t| t.format(&Rfc3339).unwrap_or_default()),
61 cancelled_at: m.cancelled_at.map(|t| t.format(&Rfc3339).unwrap_or_default()),
59 scheduled_for: m
60 .scheduled_for
61 .map(|t| t.format(&Rfc3339).unwrap_or_default()),
62 completed_at: m
63 .completed_at
64 .map(|t| t.format(&Rfc3339).unwrap_or_default()),
65 cancelled_at: m
66 .cancelled_at
67 .map(|t| t.format(&Rfc3339).unwrap_or_default()),
6268 created_at: m.created_at.format(&Rfc3339).unwrap_or_default(),
6369 updated_at: m.updated_at.format(&Rfc3339).unwrap_or_default(),
6470 }
@@ -68,4 +74,4 @@
6874#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
6975pub struct RequestDeletionCmd {
7076 pub reason: Option<String>,
71}
No newline at end of file
77}
Modified lib/export/user/devices.rs View lib/export/user/devices.rs
@@ -38,4 +38,4 @@
3838pub struct UpdateDeviceCmd {
3939 pub device_name: Option<Option<String>>,
4040 pub is_trusted: Option<bool>,
41}
No newline at end of file
41}
Modified lib/export/user/emails.rs View lib/export/user/emails.rs
@@ -12,7 +12,9 @@
1212 Cancelled,
1313}
1414
15impl From<db::user::user_email_change_tickets::EmailChangeTicketStatus> for EmailChangeTicketStatus {
15impl From<db::user::user_email_change_tickets::EmailChangeTicketStatus>
16 for EmailChangeTicketStatus
17{
1618 fn from(s: db::user::user_email_change_tickets::EmailChangeTicketStatus) -> Self {
1719 use db::user::user_email_change_tickets::EmailChangeTicketStatus as Db;
1820 match s {
@@ -24,7 +26,9 @@
2426 }
2527}
2628
27impl From<EmailChangeTicketStatus> for db::user::user_email_change_tickets::EmailChangeTicketStatus {
29impl From<EmailChangeTicketStatus>
30 for db::user::user_email_change_tickets::EmailChangeTicketStatus
31{
2832 fn from(s: EmailChangeTicketStatus) -> Self {
2933 use db::user::user_email_change_tickets::EmailChangeTicketStatus as Db;
3034 match s {
@@ -54,7 +58,9 @@
5458 email: m.email,
5559 is_activated: m.is_activated,
5660 is_primary: m.is_primary,
57 activated_at: m.activated_at.map(|t| t.format(&Rfc3339).unwrap_or_default()),
61 activated_at: m
62 .activated_at
63 .map(|t| t.format(&Rfc3339).unwrap_or_default()),
5864 created_at: m.created_at.format(&Rfc3339).unwrap_or_default(),
5965 updated_at: m.updated_at.format(&Rfc3339).unwrap_or_default(),
6066 }
@@ -102,4 +108,4 @@
102108#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
103109pub struct ConfirmEmailChangeCmd {
104110 pub ticket_id: Uuid,
105}
No newline at end of file
111}
Modified lib/export/user/mod.rs View lib/export/user/mod.rs
@@ -5,4 +5,4 @@
55pub mod preferences;
66pub mod security;
77pub mod sessions;
8pub mod suspensions;
No newline at end of file
8pub mod suspensions;
Modified lib/export/user/notifications.rs View lib/export/user/notifications.rs
@@ -249,4 +249,4 @@
249249 created_at: m.created_at.format(&Rfc3339).unwrap_or_default(),
250250 }
251251 }
252}
No newline at end of file
252}
Modified lib/export/user/preferences.rs View lib/export/user/preferences.rs
@@ -91,4 +91,4 @@
9191 pub allow_dms_from: Option<Audience>,
9292 pub allow_mentions_from: Option<Audience>,
9393 pub show_online_status: Option<bool>,
94}
No newline at end of file
94}
Modified lib/export/user/security.rs View lib/export/user/security.rs
@@ -175,7 +175,9 @@
175175 name: m.name,
176176 key_type: m.key_type.into(),
177177 fingerprint: m.fingerprint,
178 last_used_at: m.last_used_at.map(|t| t.format(&Rfc3339).unwrap_or_default()),
178 last_used_at: m
179 .last_used_at
180 .map(|t| t.format(&Rfc3339).unwrap_or_default()),
179181 created_at: m.created_at.format(&Rfc3339).unwrap_or_default(),
180182 }
181183 }
@@ -216,4 +218,4 @@
216218 pub auth_key: String,
217219 pub platform: PushPlatform,
218220 pub user_agent: Option<String>,
219}
No newline at end of file
221}
Modified lib/export/user/sessions.rs View lib/export/user/sessions.rs
@@ -43,7 +43,9 @@
4343 is_revoked: m.is_revoked,
4444 created_at: m.created_at.format(&Rfc3339).unwrap_or_default(),
4545 expires_at: m.expires_at.format(&Rfc3339).unwrap_or_default(),
46 replaced_at: m.replaced_at.map(|t| t.format(&Rfc3339).unwrap_or_default()),
46 replaced_at: m
47 .replaced_at
48 .map(|t| t.format(&Rfc3339).unwrap_or_default()),
4749 }
4850 }
4951}
@@ -54,4 +56,4 @@
5456 pub app_id: Option<String>,
5557 #[serde(default)]
5658 pub include_revoked: bool,
57}
No newline at end of file
59}
Modified lib/export/user/suspensions.rs View lib/export/user/suspensions.rs
@@ -69,4 +69,4 @@
6969#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
7070pub struct RevokeSuspensionCmd {
7171 pub reason: Option<String>,
72}
No newline at end of file
72}
Modified lib/export/data.rs View lib/export/data.rs
@@ -1,7 +1,7 @@
1use axum::Json;
12use axum::extract::{FromRequest, Request};
23use axum::http::StatusCode;
34use axum::response::{IntoResponse, Response};
4use axum::Json;
55use serde::de::DeserializeOwned;
66use serde::{Deserialize, Serialize};
77use std::fmt;
@@ -150,8 +150,8 @@
150150
151151impl<T: Serialize> IntoResponse for AppResponseData<T> {
152152 fn into_response(self) -> Response {
153 let status = StatusCode::from_u16(self.code as u16)
154 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
153 let status =
154 StatusCode::from_u16(self.code as u16).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
155155 (status, Json(self)).into_response()
156156 }
157157}
@@ -188,13 +188,11 @@
188188 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
189189 let Json(payload) = Json::<AppRequestData<T>>::from_request(req, state)
190190 .await
191 .map_err(|e| {
192 AppResponseData::<()>::err(400, format!("invalid request body: {}", e))
193 })?;
191 .map_err(|e| AppResponseData::<()>::err(400, format!("invalid request body: {}", e)))?;
194192
195193 match payload.data {
196194 Some(data) => Ok(AppRequest(data)),
197195 None => Err(AppResponseData::<()>::err(400, "missing request data")),
198196 }
199197 }
200}
No newline at end of file
198}
Modified lib/export/lib.rs View lib/export/lib.rs
@@ -1,9 +1,9 @@
11pub mod data;
22pub use data::*;
3pub mod openapi;
3pub mod auth;
44pub mod issues;
5pub mod openapi;
56pub mod project;
7pub mod repo;
68pub mod space;
79pub mod user;
8pub mod auth;
9pub mod repo;
Modified lib/export/openapi.rs View lib/export/openapi.rs
@@ -39,4 +39,4 @@
3939 $( __acc.merge(<$rest as ::utoipa::OpenApi>::openapi()); )*
4040 __acc
4141 }};
42}
No newline at end of file
42}
Modified lib/issues/features/comment/impls.rs View lib/issues/features/comment/impls.rs
@@ -3,10 +3,8 @@
33use crate::error::IssueResult;
44use crate::features::IssueService;
55
6use super::{
7 CommentPage, CommentView, CreateCommentOption, ListCommentQuery, UpdateCommentOption,
8};
96use super::usecase;
7use super::{CommentPage, CommentView, CreateCommentOption, ListCommentQuery, UpdateCommentOption};
108
119impl IssueService {
1210 pub async fn create_comment(
@@ -59,4 +57,4 @@
5957 ) -> IssueResult<()> {
6058 usecase::delete_comment(self, actor, channel_id, issue_id, comment_id).await
6159 }
62}
No newline at end of file
60}
Modified lib/issues/features/comment/mod.rs View lib/issues/features/comment/mod.rs
@@ -1,4 +1,4 @@
11pub mod impls;
22pub mod usecase;
33
4pub use export::issues::comment::*;
No newline at end of file
4pub use export::issues::comment::*;
Modified lib/issues/features/issue/impls.rs View lib/issues/features/issue/impls.rs
@@ -3,10 +3,10 @@
33use crate::error::IssueResult;
44use crate::features::IssueService;
55
6use super::usecase;
67use super::{
78 CloseIssueOption, CreateIssueOption, IssuePage, IssueView, ListIssueQuery, UpdateIssueOption,
89};
9use super::usecase;
1010
1111impl IssueService {
1212 pub async fn create_issue(
@@ -64,4 +64,4 @@
6464 ) -> IssueResult<()> {
6565 usecase::delete_issue(self, actor, channel_id, issue_id).await
6666 }
67}
No newline at end of file
67}
Modified lib/issues/features/issue/mod.rs View lib/issues/features/issue/mod.rs
@@ -1,4 +1,4 @@
11pub mod impls;
22pub mod usecase;
33
4pub use export::issues::issue::*;
No newline at end of file
4pub use export::issues::issue::*;
Modified lib/issues/features/issue_attachment/impls.rs View lib/issues/features/issue_attachment/impls.rs
@@ -4,8 +4,8 @@
44use crate::error::IssueResult;
55use crate::features::IssueService;
66
7use super::usecase;
78use super::{AttachmentPage, AttachmentView, ListAttachmentQuery, UploadAttachmentInput};
8use super::usecase;
99
1010impl IssueService {
1111 pub async fn upload_attachment(
Modified lib/issues/features/issue_content_history/impls.rs View lib/issues/features/issue_content_history/impls.rs
@@ -3,8 +3,8 @@
33use crate::error::IssueResult;
44use crate::features::IssueService;
55
6use super::usecase;
67use super::{ContentHistoryPage, ListContentHistoryQuery};
7use super::usecase;
88
99impl IssueService {
1010 pub async fn list_content_histories(
Modified lib/issues/features/issue_dependency/impls.rs View lib/issues/features/issue_dependency/impls.rs
@@ -3,8 +3,8 @@
33use crate::error::IssueResult;
44use crate::features::IssueService;
55
6use super::usecase;
67use super::{CreateDependencyOption, DependencyView};
7use super::usecase;
88
99impl IssueService {
1010 pub async fn create_dependency(
Modified lib/issues/features/issue_event/impls.rs View lib/issues/features/issue_event/impls.rs
@@ -3,8 +3,8 @@
33use crate::error::IssueResult;
44use crate::features::IssueService;
55
6use super::usecase;
67use super::{EventPage, ListEventQuery};
7use super::usecase;
88
99impl IssueService {
1010 pub async fn list_events(
Modified lib/issues/features/issue_member/impls.rs View lib/issues/features/issue_member/impls.rs
@@ -3,8 +3,8 @@
33use crate::error::IssueResult;
44use crate::features::IssueService;
55
6use super::usecase;
67use super::{CreateMemberOption, ListMemberQuery, MemberPage, MemberView};
7use super::usecase;
88
99impl IssueService {
1010 pub async fn create_member(
@@ -36,4 +36,4 @@
3636 ) -> IssueResult<()> {
3737 usecase::delete_member(self, actor, channel_id, issue_id, member_id).await
3838 }
39}
No newline at end of file
39}
Modified lib/issues/features/issue_member/mod.rs View lib/issues/features/issue_member/mod.rs
@@ -1,4 +1,4 @@
11pub mod impls;
22pub mod usecase;
33
4pub use export::issues::issue_member::*;
No newline at end of file
4pub use export::issues::issue_member::*;
Modified lib/issues/features/issue_pin/impls.rs View lib/issues/features/issue_pin/impls.rs
@@ -3,8 +3,8 @@
33use crate::error::IssueResult;
44use crate::features::IssueService;
55
6use super::usecase;
67use super::{CreatePinOption, PinView};
7use super::usecase;
88
99impl IssueService {
1010 pub async fn create_pin(
Modified lib/issues/features/issue_reaction/impls.rs View lib/issues/features/issue_reaction/impls.rs
@@ -3,8 +3,8 @@
33use crate::error::IssueResult;
44use crate::features::IssueService;
55
6use super::usecase;
67use super::{CreateReactionOption, ReactionAggView};
7use super::usecase;
88
99impl IssueService {
1010 pub async fn create_issue_reaction(
@@ -28,11 +28,7 @@
2828 usecase::create_comment_reaction(self, actor, channel_id, issue_id, comment_id, input).await
2929 }
3030
31 pub async fn delete_reaction(
32 &self,
33 actor: Uuid,
34 reaction_id: Uuid,
35 ) -> IssueResult<()> {
31 pub async fn delete_reaction(&self, actor: Uuid, reaction_id: Uuid) -> IssueResult<()> {
3632 usecase::delete_reaction(self, actor, reaction_id).await
3733 }
3834
@@ -54,4 +50,4 @@
5450 ) -> IssueResult<Vec<ReactionAggView>> {
5551 usecase::list_reactions_for_comment(self, actor, channel_id, issue_id, comment_id).await
5652 }
57}
No newline at end of file
53}
Modified lib/issues/features/issue_reaction/mod.rs View lib/issues/features/issue_reaction/mod.rs
@@ -1,4 +1,4 @@
11pub mod impls;
22pub mod usecase;
33
4pub use export::issues::issue_reaction::*;
No newline at end of file
4pub use export::issues::issue_reaction::*;
Modified lib/issues/features/issue_repo_rel/impls.rs View lib/issues/features/issue_repo_rel/impls.rs
@@ -3,8 +3,8 @@
33use crate::error::IssueResult;
44use crate::features::IssueService;
55
6use super::usecase;
67use super::{CreateRepoRelOption, RepoRelView};
7use super::usecase;
88
99impl IssueService {
1010 pub async fn create_repo_rel(
Modified lib/issues/features/issue_repo_rel/mod.rs View lib/issues/features/issue_repo_rel/mod.rs
@@ -1,4 +1,4 @@
11pub mod impls;
22pub mod usecase;
33
4pub use export::issues::issue_repo_rel::*;
No newline at end of file
4pub use export::issues::issue_repo_rel::*;
Modified lib/issues/features/issue_tracked_time/impls.rs View lib/issues/features/issue_tracked_time/impls.rs
@@ -3,8 +3,8 @@
33use crate::error::IssueResult;
44use crate::features::IssueService;
55
6use super::usecase;
67use super::{ListTrackedTimeQuery, TrackedTimePage};
7use super::usecase;
88
99impl IssueService {
1010 pub async fn list_tracked_times(
Modified lib/issues/features/issue_user/impls.rs View lib/issues/features/issue_user/impls.rs
@@ -3,8 +3,8 @@
33use crate::error::IssueResult;
44use crate::features::IssueService;
55
6use super::usecase;
67use super::{IssueUserView, UpdateIssueUserOption};
7use super::usecase;
88
99impl IssueService {
1010 pub async fn list_issue_users(
Modified lib/issues/features/issue_watch/impls.rs View lib/issues/features/issue_watch/impls.rs
@@ -3,8 +3,8 @@
33use crate::error::IssueResult;
44use crate::features::IssueService;
55
6use super::usecase;
67use super::{UpsertWatchOption, WatchView};
7use super::usecase;
88
99impl IssueService {
1010 pub async fn get_my_watch(
@@ -36,4 +36,4 @@
3636 ) -> IssueResult<()> {
3737 usecase::delete_watch(self, actor, channel_id, issue_id).await
3838 }
39}
No newline at end of file
39}
Modified lib/issues/features/issue_watch/mod.rs View lib/issues/features/issue_watch/mod.rs
@@ -1,4 +1,4 @@
11pub mod impls;
22pub mod usecase;
33
4pub use export::issues::issue_watch::*;
No newline at end of file
4pub use export::issues::issue_watch::*;
Modified lib/issues/features/issues/impls.rs View lib/issues/features/issues/impls.rs
@@ -3,11 +3,11 @@
33use crate::error::IssueResult;
44use crate::features::IssueService;
55
6use super::usecase;
67use super::{
78 CollectionPage, CollectionView, CreateCollectionOption, ListCollectionQuery,
89 UpdateCollectionOption,
910};
10use super::usecase;
1111
1212impl IssueService {
1313 pub async fn create_collection(
@@ -54,17 +54,11 @@
5454 usecase::delete_collection(self, actor, channel_id, collection_id).await
5555 }
5656
57 pub async fn ensure_default_collection(
58 &self,
59 channel_id: Uuid,
60 ) -> IssueResult<CollectionView> {
57 pub async fn ensure_default_collection(&self, channel_id: Uuid) -> IssueResult<CollectionView> {
6158 usecase::ensure_default_collection(self, channel_id).await
6259 }
6360
64 pub async fn load_default_collection(
65 &self,
66 channel_id: Uuid,
67 ) -> IssueResult<CollectionView> {
61 pub async fn load_default_collection(&self, channel_id: Uuid) -> IssueResult<CollectionView> {
6862 usecase::load_default_collection(self, channel_id).await
6963 }
70}
No newline at end of file
64}
Modified lib/issues/features/issues/mod.rs View lib/issues/features/issues/mod.rs
@@ -1,4 +1,4 @@
11pub mod impls;
22pub mod usecase;
33
4pub use export::issues::issues::*;
No newline at end of file
4pub use export::issues::issues::*;
Modified lib/issues/features/label/impls.rs View lib/issues/features/label/impls.rs
@@ -3,8 +3,8 @@
33use crate::error::IssueResult;
44use crate::features::IssueService;
55
6use super::usecase;
67use super::{CreateLabelOption, LabelPage, LabelView, ListLabelQuery, UpdateLabelOption};
7use super::usecase;
88
99impl IssueService {
1010 pub async fn create_label(
@@ -81,4 +81,4 @@
8181 ) -> IssueResult<Vec<LabelView>> {
8282 usecase::list_labels_for_issue(self, actor, channel_id, issue_id).await
8383 }
84}
No newline at end of file
84}
Modified lib/issues/features/label/mod.rs View lib/issues/features/label/mod.rs
@@ -1,4 +1,4 @@
11pub mod impls;
22pub mod usecase;
33
4pub use export::issues::label::*;
No newline at end of file
4pub use export::issues::label::*;
Modified lib/issues/features/milestone/impls.rs View lib/issues/features/milestone/impls.rs
@@ -3,10 +3,10 @@
33use crate::error::IssueResult;
44use crate::features::IssueService;
55
6use super::usecase;
67use super::{
78 CreateMilestoneOption, ListMilestoneQuery, MilestonePage, MilestoneView, UpdateMilestoneOption,
89};
9use super::usecase;
1010
1111impl IssueService {
1212 pub async fn create_milestone(
@@ -54,4 +54,4 @@
5454 ) -> IssueResult<()> {
5555 usecase::delete_milestone(self, actor, channel_id, milestone_id).await
5656 }
57}
No newline at end of file
57}
Modified lib/issues/features/milestone/mod.rs View lib/issues/features/milestone/mod.rs
@@ -1,4 +1,4 @@
11pub mod impls;
22pub mod usecase;
33
4pub use export::issues::milestone::*;
No newline at end of file
4pub use export::issues::milestone::*;
Modified lib/issues/features/mod.rs View lib/issues/features/mod.rs
@@ -16,4 +16,4 @@
1616pub mod issue_watch;
1717pub mod issues;
1818pub mod label;
19pub mod milestone;
No newline at end of file
19pub mod milestone;
Modified lib/issues/handler/comment/create.rs View lib/issues/handler/comment/create.rs
@@ -25,4 +25,4 @@
2525 .create_comment(claims.sub, channel_id, issue_id, input.into_inner())
2626 .await?;
2727 Ok(AppResponseData::ok(view).with_code(201))
28}
No newline at end of file
28}
Modified lib/issues/handler/comment/delete.rs View lib/issues/handler/comment/delete.rs
@@ -21,4 +21,4 @@
2121 svc.delete_comment(claims.sub, channel_id, issue_id, comment_id)
2222 .await?;
2323 Ok(AppResponseData::<NoData>::empty())
24}
No newline at end of file
24}
Modified lib/issues/handler/comment/get.rs View lib/issues/handler/comment/get.rs
@@ -23,4 +23,4 @@
2323 .get_comment(claims.sub, channel_id, issue_id, comment_id)
2424 .await?;
2525 Ok(AppResponseData::ok(view))
26}
No newline at end of file
26}
Modified lib/issues/handler/comment/list.rs View lib/issues/handler/comment/list.rs
@@ -21,9 +21,11 @@
2121 Path((channel_id, issue_id)): Path<(Uuid, Uuid)>,
2222 Query(q): Query<ListCommentQuery>,
2323) -> IssueResult<AppResponseData<serde_json::Value>> {
24 let page = svc.list_comments(claims.sub, channel_id, issue_id, q).await?;
24 let page = svc
25 .list_comments(claims.sub, channel_id, issue_id, q)
26 .await?;
2527 Ok(AppResponseData::ok(serde_json::json!({
2628 "items": page.items,
2729 "total": page.total,
2830 })))
29}
No newline at end of file
31}
Modified lib/issues/handler/comment/mod.rs View lib/issues/handler/comment/mod.rs
@@ -2,4 +2,4 @@
22pub(crate) mod delete;
33pub(crate) mod get;
44pub(crate) mod list;
5pub(crate) mod update;
No newline at end of file
5pub(crate) mod update;
Modified lib/issues/handler/comment/update.rs View lib/issues/handler/comment/update.rs
@@ -22,7 +22,13 @@
2222 input: AppRequest<UpdateCommentOption>,
2323) -> IssueResult<AppResponseData<CommentView>> {
2424 let view = svc
25 .update_comment(claims.sub, channel_id, issue_id, comment_id, input.into_inner())
25 .update_comment(
26 claims.sub,
27 channel_id,
28 issue_id,
29 comment_id,
30 input.into_inner(),
31 )
2632 .await?;
2733 Ok(AppResponseData::ok(view))
28}
No newline at end of file
34}
Modified lib/issues/handler/issue/close.rs View lib/issues/handler/issue/close.rs
@@ -25,4 +25,4 @@
2525 .close_issue(claims.sub, channel_id, issue_id, input.into_inner())
2626 .await?;
2727 Ok(AppResponseData::ok(view))
28}
No newline at end of file
28}
Modified lib/issues/handler/issue/create.rs View lib/issues/handler/issue/create.rs
@@ -21,6 +21,8 @@
2121 Path(channel_id): Path<Uuid>,
2222 input: AppRequest<CreateIssueOption>,
2323) -> IssueResult<AppResponseData<IssueView>> {
24 let view = svc.create_issue(claims.sub, channel_id, input.into_inner()).await?;
24 let view = svc
25 .create_issue(claims.sub, channel_id, input.into_inner())
26 .await?;
2527 Ok(AppResponseData::ok(view).with_code(201))
26}
No newline at end of file
28}
Modified lib/issues/handler/issue/delete.rs View lib/issues/handler/issue/delete.rs
@@ -20,4 +20,4 @@
2020) -> IssueResult<AppResponseData<NoData>> {
2121 svc.delete_issue(claims.sub, channel_id, issue_id).await?;
2222 Ok(AppResponseData::<NoData>::empty())
23}
No newline at end of file
23}
Modified lib/issues/handler/issue/get.rs View lib/issues/handler/issue/get.rs
@@ -21,4 +21,4 @@
2121) -> IssueResult<AppResponseData<IssueView>> {
2222 let view = svc.get_issue(claims.sub, channel_id, issue_id).await?;
2323 Ok(AppResponseData::ok(view))
24}
No newline at end of file
24}
Modified lib/issues/handler/issue/mod.rs View lib/issues/handler/issue/mod.rs
@@ -2,4 +2,4 @@
22pub(crate) mod create;
33pub(crate) mod delete;
44pub(crate) mod get;
5pub(crate) mod update;
No newline at end of file
5pub(crate) mod update;
Modified lib/issues/handler/issue/update.rs View lib/issues/handler/issue/update.rs
@@ -25,4 +25,4 @@
2525 .update_issue(claims.sub, channel_id, issue_id, input.into_inner())
2626 .await?;
2727 Ok(AppResponseData::ok(view))
28}
No newline at end of file
28}
Modified lib/issues/handler/issue_attachment/delete.rs View lib/issues/handler/issue_attachment/delete.rs
@@ -24,4 +24,4 @@
2424 svc.delete_attachment(claims.sub, channel_id, owner_type, owner_id, att_id)
2525 .await?;
2626 Ok(AppResponseData::<NoData>::empty())
27}
No newline at end of file
27}
Modified lib/issues/handler/issue_attachment/list.rs View lib/issues/handler/issue_attachment/list.rs
@@ -30,4 +30,4 @@
3030 "items": page.items,
3131 "total": page.total,
3232 })))
33}
No newline at end of file
33}
Modified lib/issues/handler/issue_attachment/mod.rs View lib/issues/handler/issue_attachment/mod.rs
@@ -1,3 +1,3 @@
11pub(crate) mod delete;
22pub(crate) mod list;
3pub(crate) mod upload;
No newline at end of file
3pub(crate) mod upload;
Modified lib/issues/handler/issue_attachment/upload.rs View lib/issues/handler/issue_attachment/upload.rs
@@ -5,7 +5,9 @@
55
66use crate::error::{IssueError, IssueResult};
77use crate::features::IssueService;
8use crate::features::issue_attachment::{AttachmentView, UploadAttachmentInput, owner_type_from_str};
8use crate::features::issue_attachment::{
9 AttachmentView, UploadAttachmentInput, owner_type_from_str,
10};
911
1012#[utoipa::path(
1113 post,
@@ -24,7 +26,13 @@
2426 let owner_type = owner_type_from_str(&owner_type)
2527 .ok_or_else(|| IssueError::BadRequest(format!("invalid owner_type {owner_type}")))?;
2628 let view = svc
27 .upload_attachment(claims.sub, channel_id, owner_type, owner_id, input.into_inner())
29 .upload_attachment(
30 claims.sub,
31 channel_id,
32 owner_type,
33 owner_id,
34 input.into_inner(),
35 )
2836 .await?;
2937 Ok(AppResponseData::ok(view).with_code(201))
30}
No newline at end of file
38}
Modified lib/issues/handler/issue_content_history/list.rs View lib/issues/handler/issue_content_history/list.rs
@@ -28,4 +28,4 @@
2828 "items": page.items,
2929 "total": page.total,
3030 })))
31}
No newline at end of file
31}
Modified lib/issues/handler/issue_content_history/mod.rs View lib/issues/handler/issue_content_history/mod.rs
@@ -1 +1 @@
1pub(crate) mod list;
No newline at end of file
1pub(crate) mod list;
Modified lib/issues/handler/issue_dependency/create.rs View lib/issues/handler/issue_dependency/create.rs
@@ -25,4 +25,4 @@
2525 .create_dependency(claims.sub, channel_id, issue_id, input.into_inner())
2626 .await?;
2727 Ok(AppResponseData::ok(view).with_code(201))
28}
No newline at end of file
28}
Modified lib/issues/handler/issue_dependency/delete.rs View lib/issues/handler/issue_dependency/delete.rs
@@ -21,4 +21,4 @@
2121 svc.delete_dependency(claims.sub, channel_id, issue_id, dep_id)
2222 .await?;
2323 Ok(AppResponseData::<NoData>::empty())
24}
No newline at end of file
24}
Modified lib/issues/handler/issue_dependency/list.rs View lib/issues/handler/issue_dependency/list.rs
@@ -23,4 +23,4 @@
2323 .list_dependencies(claims.sub, channel_id, issue_id)
2424 .await?;
2525 Ok(AppResponseData::ok(views))
26}
No newline at end of file
26}
Modified lib/issues/handler/issue_dependency/mod.rs View lib/issues/handler/issue_dependency/mod.rs
@@ -1,3 +1,3 @@
11pub(crate) mod create;
22pub(crate) mod delete;
3pub(crate) mod list;
No newline at end of file
3pub(crate) mod list;
Modified lib/issues/handler/issue_event/list.rs View lib/issues/handler/issue_event/list.rs
@@ -21,11 +21,9 @@
2121 Path((channel_id, issue_id)): Path<(Uuid, Uuid)>,
2222 Query(q): Query<ListEventQuery>,
2323) -> IssueResult<AppResponseData<serde_json::Value>> {
24 let page = svc
25 .list_events(claims.sub, channel_id, issue_id, q)
26 .await?;
24 let page = svc.list_events(claims.sub, channel_id, issue_id, q).await?;
2725 Ok(AppResponseData::ok(serde_json::json!({
2826 "items": page.items,
2927 "total": page.total,
3028 })))
31}
No newline at end of file
29}
Modified lib/issues/handler/issue_event/mod.rs View lib/issues/handler/issue_event/mod.rs
@@ -1 +1 @@
1pub(crate) mod list;
No newline at end of file
1pub(crate) mod list;
Modified lib/issues/handler/issue_member/create.rs View lib/issues/handler/issue_member/create.rs
@@ -25,4 +25,4 @@
2525 .create_member(claims.sub, channel_id, issue_id, input.into_inner())
2626 .await?;
2727 Ok(AppResponseData::ok(view).with_code(201))
28}
No newline at end of file
28}
Modified lib/issues/handler/issue_member/delete.rs View lib/issues/handler/issue_member/delete.rs
@@ -21,4 +21,4 @@
2121 svc.delete_member(claims.sub, channel_id, issue_id, member_id)
2222 .await?;
2323 Ok(AppResponseData::<NoData>::empty())
24}
No newline at end of file
24}
Modified lib/issues/handler/issue_member/list.rs View lib/issues/handler/issue_member/list.rs
@@ -28,4 +28,4 @@
2828 "items": page.items,
2929 "total": page.total,
3030 })))
31}
No newline at end of file
31}
Modified lib/issues/handler/issue_member/mod.rs View lib/issues/handler/issue_member/mod.rs
@@ -1,3 +1,3 @@
11pub(crate) mod create;
22pub(crate) mod delete;
3pub(crate) mod list;
No newline at end of file
3pub(crate) mod list;
Modified lib/issues/handler/issue_pin/create.rs View lib/issues/handler/issue_pin/create.rs
@@ -26,4 +26,4 @@
2626 .create_pin(claims.sub, channel_id, collection.id, input.into_inner())
2727 .await?;
2828 Ok(AppResponseData::ok(view).with_code(201))
29}
No newline at end of file
29}
Modified lib/issues/handler/issue_pin/delete.rs View lib/issues/handler/issue_pin/delete.rs
@@ -22,4 +22,4 @@
2222 svc.delete_pin(claims.sub, channel_id, collection.id, pin_id)
2323 .await?;
2424 Ok(AppResponseData::<NoData>::empty())
25}
No newline at end of file
25}
Modified lib/issues/handler/issue_pin/list.rs View lib/issues/handler/issue_pin/list.rs
@@ -22,4 +22,4 @@
2222 let collection = svc.ensure_default_collection(channel_id).await?;
2323 let views = svc.list_pins(claims.sub, channel_id, collection.id).await?;
2424 Ok(AppResponseData::ok(views))
25}
No newline at end of file
25}
Modified lib/issues/handler/mod.rs View lib/issues/handler/mod.rs
@@ -19,4 +19,4 @@
1919pub mod openapi;
2020pub mod routes;
2121pub use openapi::IssuesApiDoc;
22pub use routes::router;
No newline at end of file
22pub use routes::router;
Modified lib/issues/handler/openapi.rs View lib/issues/handler/openapi.rs
@@ -186,4 +186,4 @@
186186
187187pub fn build_openapi() -> utoipa::openapi::OpenApi {
188188 <IssuesApiDoc as utoipa::OpenApi>::openapi()
189}
No newline at end of file
189}
Modified lib/issues/handler/routes.rs View lib/issues/handler/routes.rs
@@ -12,7 +12,10 @@
1212 .route("/issues", get(super::issues::list::handler))
1313 .route("/issues/:channel_id", get(super::issues::get::handler))
1414 .route("/issues/:channel_id", patch(super::issues::update::handler))
15 .route("/issues/:channel_id", delete(super::issues::delete::handler))
15 .route(
16 "/issues/:channel_id",
17 delete(super::issues::delete::handler),
18 )
1619 .route("/issues/:channel_id", post(super::issue::create::handler))
1720 .route(
1821 "/issues/:channel_id/:issue_id",
@@ -64,8 +67,7 @@
6467 )
6568 .route(
6669 "/issues/:channel_id/:issue_id/reactions",
67 post(super::issue_reaction::create::handler)
68 .get(super::issue_reaction::list::handler),
70 post(super::issue_reaction::create::handler).get(super::issue_reaction::list::handler),
6971 )
7072 .route(
7173 "/issues/:channel_id/:issue_id/comments/:comment_id/reactions",
@@ -162,4 +164,4 @@
162164 crate::handler::middleware::metrics::metrics_layer,
163165 ))
164166 .with_state(svc)
165}
No newline at end of file
167}
Modified lib/issues/Cargo.toml View lib/issues/Cargo.toml
@@ -18,6 +18,7 @@
1818cache.workspace = true
1919jwt.workspace = true
2020rate_limit.workspace = true
21space.workspace = true
2122storage.workspace = true
2223
2324tokio.workspace = true
Modified lib/issues/config.rs View lib/issues/config.rs
@@ -104,4 +104,4 @@
104104 }
105105}
106106
107pub type SharedIssueConfig = Arc<IssueConfig>;
No newline at end of file
107pub type SharedIssueConfig = Arc<IssueConfig>;
Modified lib/issues/error.rs View lib/issues/error.rs
@@ -58,9 +58,9 @@
5858 IssueError::Conflict(_) => StatusCode::CONFLICT,
5959 IssueError::Unprocessable(_) => StatusCode::UNPROCESSABLE_ENTITY,
6060 IssueError::RateLimited(_) => StatusCode::TOO_MANY_REQUESTS,
61 IssueError::Internal(_)
62 | IssueError::Sqlx(_)
63 | IssueError::Uuid(_) => StatusCode::INTERNAL_SERVER_ERROR,
61 IssueError::Internal(_) | IssueError::Sqlx(_) | IssueError::Uuid(_) => {
62 StatusCode::INTERNAL_SERVER_ERROR
63 }
6464 }
6565 }
6666
@@ -77,7 +77,7 @@
7777 )
7878 }
7979
80 pub fn error_kind(&self) -> &'static str {
80 pub fn error_kind(&self) -> &'static str {
8181 match self {
8282 IssueError::BadRequest(_) => "bad_request",
8383 IssueError::Unauthorized(_) => "unauthorized",
@@ -104,4 +104,4 @@
104104 };
105105 AppResponseData::<NoData>::err_plain(status.as_u16() as i32, msg).into_response()
106106 }
107}
No newline at end of file
107}
Modified lib/issues/lib.rs View lib/issues/lib.rs
@@ -8,5 +8,5 @@
88pub use config::{IssueConfig, SharedIssueConfig};
99pub use error::{IssueError, IssueResult};
1010pub use features::IssueService;
11pub use handler::IssuesApiDoc;
1112pub use handler::router as issues_router;
12pub use handler::IssuesApiDoc;
No newline at end of file
Modified lib/issues/metrics.rs View lib/issues/metrics.rs
@@ -4,7 +4,6 @@
44
55use crate::error::IssueError;
66
7
87pub const FEATURE_ISSUES_CREATE: &str = "issues_create";
98pub const FEATURE_ISSUES_GET: &str = "issues_get";
109pub const FEATURE_ISSUES_UPDATE: &str = "issues_update";
@@ -78,10 +77,8 @@
7877pub const FEATURE_MILESTONES_DELETE: &str = "milestones_delete";
7978pub const FEATURE_MILESTONES_LIST: &str = "milestones_list";
8079
81
8280pub const OUTCOME_SUCCESS: &str = "success";
8381
84
8582static DESCRIBE_ONCE: std::sync::Once = std::sync::Once::new();
8683
8784pub fn ensure_described() {
@@ -116,7 +113,6 @@
116113 "Object storage operations initiated by the issues service"
117114 );
118115}
119
120116
121117pub struct Timer {
122118 pub feature: &'static str,
@@ -144,7 +140,6 @@
144140 }
145141}
146142
147
148143pub fn record_outcome<T>(feature: &'static str, result: &Result<T, IssueError>) {
149144 match result {
150145 Ok(_) => {
@@ -172,7 +167,6 @@
172167 }
173168 }
174169}
175
176170
177171#[derive(Debug, Clone, Copy)]
178172pub enum IssueCreateOutcome {
@@ -205,7 +199,6 @@
205199 )
206200 .increment(1);
207201}
208
209202
210203#[derive(Debug, Clone, Copy)]
211204pub enum IssueReactionOutcome {
Modified lib/issues/service.rs View lib/issues/service.rs
@@ -52,4 +52,4 @@
5252 fn rate_limit_settings(&self) -> Arc<rate_limit::RateLimitConfig> {
5353 Arc::new(rate_limit::RateLimitConfig::default())
5454 }
55}
No newline at end of file
55}
Modified lib/jwt/claims.rs View lib/jwt/claims.rs
@@ -27,7 +27,7 @@
2727 pub jti: Uuid,
2828 pub scope: String,
2929 pub kid: String,
30 pub pwd_version: i64,
30 pub pwd_version: i64,
3131}
3232
3333impl AccessClaims {
@@ -72,7 +72,7 @@
7272 pub aud: String,
7373 pub kid: String,
7474 pub parent_id: Option<Uuid>,
75 pub pwd_version: i64,
75 pub pwd_version: i64,
7676}
7777
7878impl RefreshClaims {
Modified lib/jwt/config.rs View lib/jwt/config.rs
@@ -3,14 +3,14 @@
33
44#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct JwtSettings {
6 pub issuer: String,
7 pub audience: String,
8 pub access_ttl: Duration,
9 pub refresh_ttl: Duration,
10 pub grace_window: Duration,
11 pub clock_skew_seconds: i64,
12 pub default_grace_duration: Duration,
13 pub default_key_pair_algorithm: String,
6 pub issuer: String,
7 pub audience: String,
8 pub access_ttl: Duration,
9 pub refresh_ttl: Duration,
10 pub grace_window: Duration,
11 pub clock_skew_seconds: i64,
12 pub default_grace_duration: Duration,
13 pub default_key_pair_algorithm: String,
1414}
1515
1616impl Default for JwtSettings {
Modified lib/jwt/handlers.rs View lib/jwt/handlers.rs
@@ -32,7 +32,6 @@
3232 }
3333}
3434
35
3635impl IntoResponse for JwtError {
3736 fn into_response(self) -> Response {
3837 let status = match &self {
@@ -58,7 +57,6 @@
5857 .into_response()
5958 }
6059}
61
6260
6361#[derive(Debug, Deserialize)]
6462pub struct LogoutRequest {
@@ -83,7 +81,6 @@
8381 }
8482 }
8583}
86
8784
8885pub async fn logout_handler(
8986 State(state): State<JwtState>,
Modified lib/jwt/metrics.rs View lib/jwt/metrics.rs
@@ -23,19 +23,10 @@
2323 "jwt_decode_total",
2424 "JWT tokens decoded, by kind and outcome."
2525 );
26 describe_counter!("jwt_issue_total", "Login token pair issuance by outcome.");
27 describe_counter!("jwt_rotate_total", "Refresh token rotations by outcome.");
28 describe_counter!("jwt_validate_total", "Access token validations by outcome.");
2629 describe_counter!(
27 "jwt_issue_total",
28 "Login token pair issuance by outcome."
29 );
30 describe_counter!(
31 "jwt_rotate_total",
32 "Refresh token rotations by outcome."
33 );
34 describe_counter!(
35 "jwt_validate_total",
36 "Access token validations by outcome."
37 );
38 describe_counter!(
3930 "jwt_session_ops_total",
4031 "Session persistence operations by op and outcome."
4132 );
@@ -52,9 +43,9 @@
5243 JwtError::InvalidAudience => "invalid_audience",
5344 JwtError::InvalidIssuer => "invalid_issuer",
5445 JwtError::NotYetValid => "not_yet_valid",
55 JwtError::SessionRevoked
56 | JwtError::SessionReplaced
57 | JwtError::SessionNotFound => "session",
46 JwtError::SessionRevoked | JwtError::SessionReplaced | JwtError::SessionNotFound => {
47 "session"
48 }
5849 _ => "other",
5950 },
6051 }
@@ -105,4 +96,4 @@
10596 "outcome" => outcome_for(result),
10697 )
10798 .increment(1);
108}
No newline at end of file
99}
Modified lib/jwt/middleware.rs View lib/jwt/middleware.rs
@@ -12,17 +12,13 @@
1212 fn jwt_state(&self) -> &JwtState;
1313
1414 /// Optional access-token deny-list. When present, the JWT extractors
15 /// password change, suspension). Defaults to `None` (no deny-check).
15 /// password change, suspension). Defaults to `None` (no deny-check).
1616 fn access_blacklist(&self) -> Option<Arc<AccessTokenBlacklist>> {
1717 None
1818 }
1919}
2020
21pub async fn inject_jwt_ctx<S>(
22 State(svc): State<S>,
23 mut req: Request,
24 next: Next,
25) -> Response
21pub async fn inject_jwt_ctx<S>(State(svc): State<S>, mut req: Request, next: Next) -> Response
2622where
2723 S: HasJwtState + Send + Sync + Clone + 'static,
2824{
@@ -32,4 +28,4 @@
3228 req.extensions_mut().insert(jwt.db.clone());
3329 req.extensions_mut().insert(svc.access_blacklist());
3430 next.run(req).await
35}
No newline at end of file
31}
Modified lib/jwt/session.rs View lib/jwt/session.rs
@@ -1,4 +1,6 @@
1use sea_orm::{ActiveModelTrait, ConnectionTrait, DatabaseConnection, EntityTrait, QueryFilter, Set};
1use sea_orm::{
2 ActiveModelTrait, ConnectionTrait, DatabaseConnection, EntityTrait, QueryFilter, Set,
3};
24use time::OffsetDateTime;
35use uuid::Uuid;
46
Modified lib/jwt/signer.rs View lib/jwt/signer.rs
@@ -51,8 +51,9 @@
5151 validation.set_audience(&[audience.to_string()]);
5252 validation.leeway = clock_skew_seconds as u64;
5353 validation.validate_exp = true;
54 let result: Result<AccessClaims, JwtError> =
55 decode::<AccessClaims>(token, key, &validation).map(|data| data.claims).map_err(JwtError::from);
54 let result: Result<AccessClaims, JwtError> = decode::<AccessClaims>(token, key, &validation)
55 .map(|data| data.claims)
56 .map_err(JwtError::from);
5657 metrics::record_decode(metrics::TOKEN_KIND_ACCESS, &result);
5758 result
5859}
@@ -70,8 +71,9 @@
7071 validation.set_audience(&[audience.to_string()]);
7172 validation.leeway = clock_skew_seconds as u64;
7273 validation.validate_exp = true;
73 let result: Result<RefreshClaims, JwtError> =
74 decode::<RefreshClaims>(token, key, &validation).map(|data| data.claims).map_err(JwtError::from);
74 let result: Result<RefreshClaims, JwtError> = decode::<RefreshClaims>(token, key, &validation)
75 .map(|data| data.claims)
76 .map_err(JwtError::from);
7577 metrics::record_decode(metrics::TOKEN_KIND_REFRESH, &result);
7678 result
7779}
Modified lib/jwt/tests.rs View lib/jwt/tests.rs
@@ -75,7 +75,7 @@
7575fn expired_token_rejected() {
7676 let (enc, dec) = ed25519_key();
7777 let mut claims = access_claims("kid-1");
78 claims.exp = 0;
78 claims.exp = 0;
7979 let token = encode_access_token(&claims, &enc, jsonwebtoken::Algorithm::EdDSA).unwrap();
8080 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA);
8181 validation.set_issuer(&["test-issuer".to_string()]);
Modified lib/obs/lib.rs View lib/obs/lib.rs
@@ -0,0 +1 @@
1
Modified lib/project/features/authz.rs View lib/project/features/authz.rs
@@ -6,8 +6,7 @@
66use crate::error::{ProjectError, ProjectResult};
77use crate::permission::{
88 CREATE_ISSUE_LINK, DELETE_PROJECT, EDIT_PROJECT, MANAGE_ACTIVITIES, MANAGE_DOCUMENTS,
9 MANAGE_LINKS, MANAGE_MEMBERS, MANAGE_VIEWS, Permissions, STAR, TRANSITION_STATE,
10 VIEW_PROJECT,
9 MANAGE_LINKS, MANAGE_MEMBERS, MANAGE_VIEWS, Permissions, STAR, TRANSITION_STATE, VIEW_PROJECT,
1110};
1211use crate::service::ProjectService;
1312
@@ -18,8 +17,7 @@
1817 project_id: Uuid,
1918) -> ProjectResult<Permissions> {
2019 let reader = svc.db.reader();
21 if let Err(e) =
22 space::features::authz::effective_permissions(&reader, space_id, user_id).await
20 if let Err(e) = space::features::authz::effective_permissions(&reader, space_id, user_id).await
2321 {
2422 return Err(ProjectError::Forbidden(format!(
2523 "space membership check failed: {e}"
@@ -37,8 +35,7 @@
3735 project_id: Uuid,
3836) -> ProjectResult<Permissions> {
3937 let reader = svc.db.reader();
40 if let Err(e) =
41 space::features::authz::effective_permissions(&reader, space_id, user_id).await
38 if let Err(e) = space::features::authz::effective_permissions(&reader, space_id, user_id).await
4239 {
4340 return Err(ProjectError::Forbidden(format!(
4441 "space membership check failed: {e}"
Modified lib/project/graphql/mod.rs View lib/project/graphql/mod.rs
@@ -9,4 +9,4 @@
99
1010pub fn build_schema() -> ProjectSchema {
1111 Schema::build(QueryRoot, MutationRoot, EmptySubscription).finish()
12}
No newline at end of file
12}
Modified lib/project/graphql/resolvers.rs View lib/project/graphql/resolvers.rs
@@ -1,7 +1,7 @@
11use async_graphql::{Context, Object, Result as GqlResult};
22use db::conn::sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
33
4use crate::graphql::types::{parse_id, ProjectGql, ProjectMemberGql};
4use crate::graphql::types::{ProjectGql, ProjectMemberGql, parse_id};
55use crate::service::ProjectService;
66
77pub struct QueryRoot;
@@ -65,4 +65,4 @@
6565 async fn star_project(&self, _project_id: String) -> GqlResult<bool> {
6666 Ok(false)
6767 }
68}
No newline at end of file
68}
Modified lib/project/graphql/types.rs View lib/project/graphql/types.rs
@@ -71,8 +71,7 @@
7171}
7272
7373pub fn parse_id(id: &str) -> async_graphql::Result<Uuid> {
74 Uuid::parse_str(id)
75 .map_err(|e| async_graphql::Error::new(format!("invalid id `{id}`: {e}")))
74 Uuid::parse_str(id).map_err(|e| async_graphql::Error::new(format!("invalid id `{id}`: {e}")))
7675}
7776
7877fn odt_to_string(v: OffsetDateTime) -> String {
@@ -82,4 +81,4 @@
8281
8382fn dt_to_string(v: time::Date) -> String {
8483 v.to_string()
85}
No newline at end of file
84}
Modified lib/project/handler/mod.rs View lib/project/handler/mod.rs
@@ -12,4 +12,4 @@
1212pub mod routes;
1313
1414pub use openapi::ProjectApiDoc;
15pub use routes::router;
No newline at end of file
15pub use routes::router;
Modified lib/project/handler/openapi.rs View lib/project/handler/openapi.rs
@@ -13,4 +13,4 @@
1313 paths(),
1414 components(schemas())
1515)]
16pub struct ProjectApiDoc;
No newline at end of file
16pub struct ProjectApiDoc;
Modified lib/project/handler/query.rs View lib/project/handler/query.rs
@@ -40,4 +40,4 @@
4040 pub space_id: Uuid,
4141 pub project_id: Uuid,
4242 pub favorite_id: Uuid,
43}
No newline at end of file
43}
Modified lib/project/handler/routes.rs View lib/project/handler/routes.rs
@@ -81,4 +81,4 @@
8181 crate::handler::middleware::metrics::metrics_layer,
8282 ))
8383 .with_state(svc)
84}
No newline at end of file
84}
Modified lib/project/error.rs View lib/project/error.rs
@@ -86,7 +86,7 @@
8686 )
8787 }
8888
89 pub fn error_kind(&self) -> &'static str {
89 pub fn error_kind(&self) -> &'static str {
9090 match self {
9191 ProjectError::BadRequest(_) => "bad_request",
9292 ProjectError::Unauthorized(_) => "unauthorized",
Modified lib/project/lib.rs View lib/project/lib.rs
@@ -15,6 +15,6 @@
1515pub use error::{ProjectError, ProjectResult};
1616pub use features::ProjectService;
1717pub use graphql::ProjectSchema;
18pub use handler::{router as project_router, ProjectApiDoc};
18pub use handler::{ProjectApiDoc, router as project_router};
1919pub use permission::Permissions;
2020pub use service::ProjectService as Service;
Modified lib/project/metrics.rs View lib/project/metrics.rs
@@ -4,7 +4,6 @@
44
55use crate::error::ProjectError;
66
7
87pub const FEATURE_PROJECTS_CREATE: &str = "projects_create";
98pub const FEATURE_PROJECTS_GET: &str = "projects_get";
109pub const FEATURE_PROJECTS_UPDATE: &str = "projects_update";
@@ -38,11 +37,9 @@
3837pub const FEATURE_PROJECT_FAVORITES_LIST: &str = "project_favorites_list";
3938
4039pub const FEATURE_PROJECT_ACTIVITIES_LIST: &str = "project_activities_list";
41
4240
4341pub const OUTCOME_SUCCESS: &str = "success";
4442
45
4643static DESCRIBE_ONCE: std::sync::Once = std::sync::Once::new();
4744
4845pub fn ensure_described() {
@@ -78,7 +75,6 @@
7875 );
7976}
8077
81
8278pub struct Timer {
8379 pub feature: &'static str,
8480 pub start: Instant,
@@ -104,7 +100,6 @@
104100 .record(elapsed);
105101 }
106102}
107
108103
109104pub fn record_outcome<T>(feature: &'static str, result: &Result<T, ProjectError>) {
110105 match result {
@@ -133,7 +128,6 @@
133128 }
134129 }
135130}
136
137131
138132#[derive(Debug, Clone, Copy)]
139133pub enum ProjectCreateOutcome {
Modified lib/project/service.rs View lib/project/service.rs
@@ -40,7 +40,9 @@
4040 }
4141
4242 fn access_blacklist(&self) -> Option<Arc<rate_limit::AccessTokenBlacklist>> {
43 Some(Arc::new(rate_limit::AccessTokenBlacklist::new(self.kv.clone())))
43 Some(Arc::new(rate_limit::AccessTokenBlacklist::new(
44 self.kv.clone(),
45 )))
4446 }
4547}
4648
Modified lib/queue/metrics.rs View lib/queue/metrics.rs
@@ -13,8 +13,11 @@
1313}
1414
1515fn describe_all() {
16 describe_counter!("queue_publish_total", "Messages published to Kafka partitioned by outcome.");
1716 describe_counter!(
17 "queue_publish_total",
18 "Messages published to Kafka partitioned by outcome."
19 );
20 describe_counter!(
1821 "queue_message_received_total",
1922 "Messages received from Kafka by subscribers."
2023 );
@@ -73,4 +76,4 @@
7376pub(crate) fn record_message_dropped() {
7477 ensure_described();
7578 counter!("queue_message_dropped_total").increment(1);
76}
No newline at end of file
79}
Modified lib/rate_limit/blacklist.rs View lib/rate_limit/blacklist.rs
@@ -1,7 +1,7 @@
11use std::sync::Arc;
22
3use cache::RedisCache;
34use cache::redis::RedisError;
4use cache::RedisCache;
55use uuid::Uuid;
66
77use crate::metrics;
@@ -39,11 +39,7 @@
3939 result
4040 }
4141
42 pub async fn is_revoked(
43 &self,
44 jti: Uuid,
45 sub: Uuid,
46 ) -> Result<bool, RedisError> {
42 pub async fn is_revoked(&self, jti: Uuid, sub: Uuid) -> Result<bool, RedisError> {
4743 let result: Result<bool, RedisError> = async {
4844 let jti_key = format!("auth:deny:jti:{}", jti.simple());
4945 let user_key = format!("auth:deny:user:{}", sub.simple());
@@ -57,4 +53,4 @@
5753 metrics::record_blacklist_op("is_revoked", &result);
5854 result
5955 }
60}
No newline at end of file
56}
Modified lib/rate_limit/config.rs View lib/rate_limit/config.rs
@@ -2,10 +2,10 @@
22
33#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct RateLimitConfig {
5 pub default_limit: u64,
6 pub window_secs: i64,
7 pub cookie_name: String,
8 pub cookie_secure: bool,
5 pub default_limit: u64,
6 pub window_secs: i64,
7 pub cookie_name: String,
8 pub cookie_secure: bool,
99}
1010
1111impl Default for RateLimitConfig {
@@ -27,4 +27,4 @@
2727 ..Self::default()
2828 }
2929 }
30}
No newline at end of file
30}
Modified lib/rate_limit/lib.rs View lib/rate_limit/lib.rs
@@ -5,4 +5,4 @@
55
66pub use blacklist::AccessTokenBlacklist;
77pub use config::RateLimitConfig;
8pub use middleware::{ratelimit, ClientId, RateLimitProvider};
No newline at end of file
8pub use middleware::{ClientId, RateLimitProvider, ratelimit};
Modified lib/rate_limit/metrics.rs View lib/rate_limit/metrics.rs
@@ -31,6 +31,10 @@
3131
3232pub(crate) fn record_blacklist_op<T>(op: &'static str, result: &Result<T, RedisError>) {
3333 ensure_described();
34 let outcome = if result.is_ok() { OUTCOME_SUCCESS } else { "error" };
34 let outcome = if result.is_ok() {
35 OUTCOME_SUCCESS
36 } else {
37 "error"
38 };
3539 counter!("rate_limit_blacklist_ops_total", "op" => op, "outcome" => outcome).increment(1);
36}
No newline at end of file
40}
Modified lib/rate_limit/middleware.rs View lib/rate_limit/middleware.rs
@@ -1,11 +1,11 @@
11use std::sync::Arc;
22
3use axum::Json;
34use axum::extract::{Request, State};
45use axum::http::header::{HeaderValue, SET_COOKIE};
56use axum::http::{HeaderMap, StatusCode};
67use axum::middleware::Next;
78use axum::response::{IntoResponse, Response};
8use axum::Json;
99use axum_extra::extract::cookie::{Cookie, SameSite};
1010use cache::RedisCache;
1111use serde_json::json;
@@ -62,7 +62,9 @@
6262 let window_start = now - settings.window_secs;
6363
6464 let existing = read_cookie(request.headers(), &settings.cookie_name);
65 let rl_id = existing.clone().unwrap_or_else(|| Uuid::new_v4().to_string());
65 let rl_id = existing
66 .clone()
67 .unwrap_or_else(|| Uuid::new_v4().to_string());
6668
6769 let api = request.uri().path();
6870 let key = format!("rate:{}:{}", api, rl_id);
@@ -79,12 +81,17 @@
7981 let count = kv.zcard(&key).await.unwrap_or(0);
8082
8183 let reset = now + settings.window_secs;
82 let remaining = count.saturating_sub(settings.default_limit).saturating_sub(1);
84 let remaining = count
85 .saturating_sub(settings.default_limit)
86 .saturating_sub(1);
8387
8488 let mut response: Response;
8589 if count > settings.default_limit {
8690 metrics::record_request(metrics::OUTCOME_THROTTLED);
87 response = (StatusCode::TOO_MANY_REQUESTS, Json(json!({ "error": "rate limit exceeded" })))
91 response = (
92 StatusCode::TOO_MANY_REQUESTS,
93 Json(json!({ "error": "rate limit exceeded" })),
94 )
8895 .into_response();
8996 } else {
9097 metrics::record_request(metrics::OUTCOME_ALLOWED);
@@ -108,8 +115,13 @@
108115 headers.insert(RETRY_AFTER, v);
109116 }
110117 if existing.is_none() {
111 write_cookie(response.headers_mut(), &settings.cookie_name, &rl_id, settings.cookie_secure);
118 write_cookie(
119 response.headers_mut(),
120 &settings.cookie_name,
121 &rl_id,
122 settings.cookie_secure,
123 );
112124 }
113125
114126 response
115}
No newline at end of file
127}
Modified lib/repo/Cargo.toml View lib/repo/Cargo.toml
@@ -20,6 +20,7 @@
2020cache.workspace = true
2121jwt.workspace = true
2222rate_limit.workspace = true
23space.workspace = true
2324gix.workspace = true
2425
2526tokio.workspace = true
Modified lib/repo/error.rs View lib/repo/error.rs
@@ -80,7 +80,7 @@
8080 )
8181 }
8282
83 pub fn error_kind(&self) -> &'static str {
83 pub fn error_kind(&self) -> &'static str {
8484 match self {
8585 RepoError::BadRequest(_) => "bad_request",
8686 RepoError::Unauthorized(_) => "unauthorized",
Modified lib/repo/metrics.rs View lib/repo/metrics.rs
@@ -4,7 +4,6 @@
44
55use crate::error::RepoError;
66
7
87pub const FEATURE_ARCHIVES_GET: &str = "archives_get";
98
109pub const FEATURE_BRANCH_PROTECTIONS_LIST: &str = "branch_protections_list";
@@ -12,7 +11,8 @@
1211pub const FEATURE_BRANCH_PROTECTIONS_GET: &str = "branch_protections_get";
1312pub const FEATURE_BRANCH_PROTECTIONS_EDIT: &str = "branch_protections_edit";
1413pub const FEATURE_BRANCH_PROTECTIONS_DELETE: &str = "branch_protections_delete";
15pub const FEATURE_BRANCH_PROTECTIONS_UPDATE_PRIORITIES: &str = "branch_protections_update_priorities";
14pub const FEATURE_BRANCH_PROTECTIONS_UPDATE_PRIORITIES: &str =
15 "branch_protections_update_priorities";
1616
1717pub const FEATURE_BRANCHES_LIST: &str = "branches_list";
1818pub const FEATURE_BRANCHES_GET: &str = "branches_get";
@@ -149,9 +149,7 @@
149149pub const FEATURE_REPOS_FORK: &str = "repos_fork";
150150pub const FEATURE_REPOS_GENERATE: &str = "repos_generate";
151151
152
153152pub const OUTCOME_SUCCESS: &str = "success";
154
155153
156154static DESCRIBE_ONCE: std::sync::Once = std::sync::Once::new();
157155
@@ -187,7 +185,6 @@
187185 "Object storage operations initiated by the repo service"
188186 );
189187}
190
191188
192189pub struct Timer {
193190 pub feature: &'static str,
@@ -215,7 +212,6 @@
215212 }
216213}
217214
218
219215pub fn record_outcome<T>(feature: &'static str, result: &Result<T, RepoError>) {
220216 match result {
221217 Ok(_) => {
@@ -243,7 +239,6 @@
243239 }
244240 }
245241}
246
247242
248243#[derive(Debug, Clone, Copy)]
249244pub enum RepoCreateOutcome {
@@ -275,7 +270,6 @@
275270 .increment(1);
276271}
277272
278
279273#[derive(Debug, Clone, Copy)]
280274pub enum BranchCreateOutcome {
281275 Success,
@@ -306,7 +300,6 @@
306300 .increment(1);
307301}
308302
309
310303#[derive(Debug, Clone, Copy)]
311304pub enum PullRequestMergeOutcome {
312305 Success,
@@ -335,4 +328,4 @@
335328 "outcome" => outcome.as_label()
336329 )
337330 .increment(1);
338}
No newline at end of file
331}
Modified lib/repo/permission.rs View lib/repo/permission.rs
@@ -1,5 +1,3 @@
1use uuid::Uuid;
2
31use crate::actor::{ActorContext, ChannelRole, RepoAccess, SpaceRole, TokenScope};
42
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -56,6 +54,12 @@
5654 if let Some(mode) = collaborator_mode {
5755 return mode;
5856 }
57 if matches!(
58 space_role,
59 Some(SpaceRole::Member | SpaceRole::Admin | SpaceRole::Owner)
60 ) {
61 return RepoAccess::Read;
62 }
5963 if is_public {
6064 return RepoAccess::Read;
6165 }
@@ -104,3 +108,99 @@
104108 }
105109 Err(PermissionError::Insufficient)
106110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::actor::ActorContext;
116
117 fn member_actor(space_role: SpaceRole) -> ActorContext {
118 ActorContext {
119 user_id: Some(uuid::Uuid::new_v4()),
120 scopes: Default::default(),
121 is_site_admin: false,
122 channel_role: None,
123 space_role: Some(space_role),
124 }
125 }
126
127 fn stranger_actor() -> ActorContext {
128 ActorContext {
129 user_id: Some(uuid::Uuid::new_v4()),
130 scopes: Default::default(),
131 is_site_admin: false,
132 channel_role: None,
133 space_role: None,
134 }
135 }
136
137 #[test]
138 fn space_member_reads_private_repo_without_collaborator() {
139 let actor = member_actor(SpaceRole::Member);
140 assert!(
141 require(
142 &actor,
143 RepoAccess::Read,
144 None,
145 Some(SpaceRole::Member),
146 None,
147 false
148 )
149 .is_ok()
150 );
151 assert!(matches!(
152 require(
153 &actor,
154 RepoAccess::Write,
155 None,
156 Some(SpaceRole::Member),
157 None,
158 false
159 ),
160 Err(PermissionError::Insufficient)
161 ));
162 }
163
164 #[test]
165 fn space_owner_admin_on_private_repo() {
166 let actor = member_actor(SpaceRole::Owner);
167 assert!(
168 require(
169 &actor,
170 RepoAccess::Admin,
171 None,
172 Some(SpaceRole::Owner),
173 None,
174 false
175 )
176 .is_ok()
177 );
178 }
179
180 #[test]
181 fn non_member_stranger_denied_private_repo() {
182 let actor = stranger_actor();
183 assert!(matches!(
184 require(&actor, RepoAccess::Read, None, None, None, false),
185 Err(PermissionError::Insufficient)
186 ));
187 let anon = ActorContext::anonymous();
188 assert!(require(&anon, RepoAccess::Read, None, None, None, true).is_ok());
189 }
190
191 #[test]
192 fn collaborator_gives_explicit_write() {
193 let actor = stranger_actor();
194 assert!(
195 require(
196 &actor,
197 RepoAccess::Write,
198 None,
199 None,
200 Some(RepoAccess::Write),
201 false
202 )
203 .is_ok()
204 );
205 }
206}
Modified lib/space/features/authz.rs View lib/space/features/authz.rs
@@ -70,7 +70,9 @@
7070 }
7171
7272 let now = OffsetDateTime::now_utc();
73 let ban = ban_entity::Entity::find_by_id((space_id, user_id)).one(db).await?;
73 let ban = ban_entity::Entity::find_by_id((space_id, user_id))
74 .one(db)
75 .await?;
7476 if let Some(b) = ban
7577 && b.expires_at.is_none_or(|exp| exp > now)
7678 {
@@ -79,7 +81,9 @@
7981 ));
8082 }
8183
82 let member = member_entity::Entity::find_by_id((space_id, user_id)).one(db).await?;
84 let member = member_entity::Entity::find_by_id((space_id, user_id))
85 .one(db)
86 .await?;
8387 let active = member
8488 .map(|m| m.status == member_entity::MemberStatus::Active)
8589 .unwrap_or(false);
@@ -121,4 +125,4 @@
121125 )));
122126 }
123127 Ok(space)
124}
No newline at end of file
128}
Modified lib/space/features/mod.rs View lib/space/features/mod.rs
@@ -8,7 +8,6 @@
88
99pub mod authz;
1010pub mod bans;
11pub mod channel_members;
1211pub mod channels;
1312pub mod invites;
1413pub mod members;
@@ -46,7 +45,9 @@
4645 }
4746
4847 fn access_blacklist(&self) -> Option<Arc<rate_limit::AccessTokenBlacklist>> {
49 Some(Arc::new(rate_limit::AccessTokenBlacklist::new(self.kv.clone())))
48 Some(Arc::new(rate_limit::AccessTokenBlacklist::new(
49 self.kv.clone(),
50 )))
5051 }
5152}
5253
@@ -58,4 +59,4 @@
5859 fn rate_limit_settings(&self) -> Arc<rate_limit::RateLimitConfig> {
5960 Arc::new(rate_limit::RateLimitConfig::default())
6061 }
61}
No newline at end of file
62}
Modified lib/space/handler/mod.rs View lib/space/handler/mod.rs
@@ -1,5 +1,4 @@
11pub(crate) mod bans;
2pub(crate) mod channel_members;
32pub(crate) mod channels;
43pub(crate) mod invites;
54pub(crate) mod members;
@@ -12,4 +11,4 @@
1211pub(crate) mod spaces;
1312
1413pub use openapi::SpaceApiDoc;
15pub use routes::router;
No newline at end of file
14pub use routes::router;
Modified lib/space/handler/openapi.rs View lib/space/handler/openapi.rs
@@ -1,16 +1,8 @@
11use utoipa::OpenApi;
22
3use export::space::bans::{
4 CreateBanCmd, SpaceBanPage, SpaceBanView, UpdateBanCmd,
5};
6use export::space::channel_members::{
7 AddChannelMemberCmd, ChannelMemberPage, ChannelMemberRole, ChannelMemberView,
8 UpdateChannelMemberCmd,
9};
3use export::space::bans::{CreateBanCmd, SpaceBanPage, SpaceBanView, UpdateBanCmd};
104use export::space::channels::{ChannelPage, ChannelView};
11use export::space::invites::{
12 AcceptInviteCmd, CreateInviteCmd, SpaceInvitePage, SpaceInviteView,
13};
5use export::space::invites::{AcceptInviteCmd, CreateInviteCmd, SpaceInvitePage, SpaceInviteView};
146use export::space::members::{
157 AddMemberCmd, MessageNotifications, SpaceMemberPage, SpaceMemberStatus, SpaceMemberView,
168 UpdateMemberCmd,
@@ -64,11 +56,6 @@
6456 crate::handler::bans::unban::handler,
6557 crate::handler::channels::get::handler,
6658 crate::handler::channels::list::handler,
67 crate::handler::channel_members::add::handler,
68 crate::handler::channel_members::get::handler,
69 crate::handler::channel_members::list::handler,
70 crate::handler::channel_members::update::handler,
71 crate::handler::channel_members::remove::handler,
7259 crate::handler::overwrites::set_role::handler,
7360 crate::handler::overwrites::remove_role::handler,
7461 crate::handler::overwrites::list_role::handler,
@@ -84,9 +71,7 @@
8471 SpaceInviteView, CreateInviteCmd, AcceptInviteCmd, SpaceInvitePage,
8572 SpaceBanView, CreateBanCmd, UpdateBanCmd, SpaceBanPage,
8673 ChannelView, ChannelPage,
87 ChannelMemberView, AddChannelMemberCmd, UpdateChannelMemberCmd, ChannelMemberPage,
88 ChannelMemberRole,
8974 RoleOverwriteView, SetRoleOverwriteCmd, UserOverwriteView, SetUserOverwriteCmd,
9075 ))
9176)]
92pub struct SpaceApiDoc;
No newline at end of file
77pub struct SpaceApiDoc;
Modified lib/space/handler/query.rs View lib/space/handler/query.rs
@@ -5,4 +5,4 @@
55pub(crate) struct SpaceListQuery {
66 pub limit: Option<u64>,
77 pub offset: Option<u64>,
8}
No newline at end of file
8}
Modified lib/space/handler/routes.rs View lib/space/handler/routes.rs
@@ -9,10 +9,7 @@
99pub fn router(svc: SpaceService) -> Router {
1010 let public = Router::new()
1111 .route("/spaces/:sid", get(super::spaces::get::handler))
12 .route(
13 "/spaces/:sid/roles/:rid",
14 get(super::roles::get::handler),
15 )
12 .route("/spaces/:sid/roles/:rid", get(super::roles::get::handler))
1613 .layer(from_fn_with_state(
1714 svc.clone(),
1815 inject_jwt_ctx::<SpaceService>,
@@ -59,8 +56,11 @@
5956 "/spaces/:sid/invites/:iid",
6057 delete(super::invites::revoke::handler),
6158 )
62 .route("/spaces/invites/accept", post(super::invites::accept::handler))
6359 .route(
60 "/spaces/invites/accept",
61 post(super::invites::accept::handler),
62 )
63 .route(
6464 "/spaces/:sid/bans",
6565 post(super::bans::ban::handler).get(super::bans::list::handler),
6666 )
@@ -69,26 +69,13 @@
6969 get(super::bans::get::handler)
7070 .patch(super::bans::update::handler)
7171 .delete(super::bans::unban::handler),
72 )
73 .route(
74 "/spaces/:sid/channels",
75 get(super::channels::list::handler),
7672 )
73 .route("/spaces/:sid/channels", get(super::channels::list::handler))
7774 .route(
7875 "/spaces/:sid/channels/:cid",
7976 get(super::channels::get::handler),
8077 )
8178 .route(
82 "/spaces/:sid/channels/:cid/members",
83 post(super::channel_members::add::handler).get(super::channel_members::list::handler),
84 )
85 .route(
86 "/spaces/:sid/channels/:cid/members/:uid",
87 get(super::channel_members::get::handler)
88 .patch(super::channel_members::update::handler)
89 .delete(super::channel_members::remove::handler),
90 )
91 .route(
9279 "/spaces/:sid/channels/:cid/overwrites/roles",
9380 post(super::overwrites::set_role::handler).get(super::overwrites::list_role::handler),
9481 )
@@ -117,4 +104,4 @@
117104 crate::handler::middleware::metrics::metrics_layer,
118105 ))
119106 .with_state(svc)
120}
No newline at end of file
107}
Modified lib/space/config.rs View lib/space/config.rs
@@ -81,4 +81,4 @@
8181}
8282
8383#[allow(dead_code)]
84pub type SharedSpaceConfig = Arc<SpaceConfig>;
No newline at end of file
84pub type SharedSpaceConfig = Arc<SpaceConfig>;
Modified lib/space/error.rs View lib/space/error.rs
@@ -73,7 +73,7 @@
7373 )
7474 }
7575
76 pub fn error_kind(&self) -> &'static str {
76 pub fn error_kind(&self) -> &'static str {
7777 match self {
7878 SpaceError::BadRequest(_) => "bad_request",
7979 SpaceError::Unauthorized(_) => "unauthorized",
@@ -99,4 +99,4 @@
9999 };
100100 AppResponseData::<NoData>::err_plain(status.as_u16() as i32, msg).into_response()
101101 }
102}
No newline at end of file
102}
Modified lib/space/lib.rs View lib/space/lib.rs
@@ -7,5 +7,5 @@
77pub use config::{SharedSpaceConfig, SpaceConfig};
88pub use error::{SpaceError, SpaceResult};
99pub use features::SpaceService;
10pub use handler::SpaceApiDoc;
1011pub use handler::router as space_router;
11pub use handler::SpaceApiDoc;
No newline at end of file
Modified lib/space/metrics.rs View lib/space/metrics.rs
@@ -4,7 +4,6 @@
44
55use crate::error::SpaceError;
66
7
87pub const FEATURE_SPACES_CREATE: &str = "spaces_create";
98pub const FEATURE_SPACES_GET: &str = "spaces_get";
109pub const FEATURE_SPACES_UPDATE: &str = "spaces_update";
@@ -40,23 +39,15 @@
4039pub const FEATURE_CHANNELS_LIST: &str = "channels_list";
4140pub const FEATURE_CHANNELS_ENSURE: &str = "channels_ensure";
4241
43pub const FEATURE_CHANNEL_MEMBERS_ADD: &str = "channel_members_add";
44pub const FEATURE_CHANNEL_MEMBERS_GET: &str = "channel_members_get";
45pub const FEATURE_CHANNEL_MEMBERS_UPDATE: &str = "channel_members_update";
46pub const FEATURE_CHANNEL_MEMBERS_REMOVE: &str = "channel_members_remove";
47pub const FEATURE_CHANNEL_MEMBERS_LIST: &str = "channel_members_list";
48
4942pub const FEATURE_OVERWRITES_SET_ROLE: &str = "overwrites_set_role";
5043pub const FEATURE_OVERWRITES_REMOVE_ROLE: &str = "overwrites_remove_role";
5144pub const FEATURE_OVERWRITES_LIST_ROLE: &str = "overwrites_list_role";
5245pub const FEATURE_OVERWRITES_SET_USER: &str = "overwrites_set_user";
5346pub const FEATURE_OVERWRITES_REMOVE_USER: &str = "overwrites_remove_user";
5447pub const FEATURE_OVERWRITES_LIST_USER: &str = "overwrites_list_user";
55
5648
5749pub const OUTCOME_SUCCESS: &str = "success";
5850
59
6051static DESCRIBE_ONCE: std::sync::Once = std::sync::Once::new();
6152
6253pub fn ensure_described() {
@@ -92,7 +83,6 @@
9283 );
9384}
9485
95
9686pub struct Timer {
9787 pub feature: &'static str,
9888 pub start: Instant,
@@ -118,7 +108,6 @@
118108 .record(elapsed);
119109 }
120110}
121
122111
123112pub fn record_outcome<T>(feature: &'static str, result: &Result<T, SpaceError>) {
124113 match result {
@@ -147,7 +136,6 @@
147136 }
148137 }
149138}
150
151139
152140#[derive(Debug, Clone, Copy)]
153141pub enum InviteAcceptOutcome {
Modified lib/storage/local.rs View lib/storage/local.rs
@@ -45,10 +45,8 @@
4545 async fn put(&self, path: &Path, data: Bytes) -> Result<PutResult, StorageError> {
4646 let _timer = metrics::OpTimer::start();
4747 let len = data.len();
48 let result: Result<PutResult, StorageError> = async {
49 Ok(self.store.put(path, data.into()).await?)
50 }
51 .await;
48 let result: Result<PutResult, StorageError> =
49 async { Ok(self.store.put(path, data.into()).await?) }.await;
5250 metrics::record_put_bytes(len);
5351 metrics::record_op("put", &result);
5452 result
@@ -56,10 +54,8 @@
5654
5755 async fn get(&self, path: &Path) -> Result<GetResult, StorageError> {
5856 let _timer = metrics::OpTimer::start();
59 let result: Result<GetResult, StorageError> = async {
60 Ok(self.store.get(path).await?)
61 }
62 .await;
57 let result: Result<GetResult, StorageError> =
58 async { Ok(self.store.get(path).await?) }.await;
6359 metrics::record_op("get", &result);
6460 result
6561 }
@@ -71,19 +67,15 @@
7167 }
7268
7369 async fn head(&self, path: &Path) -> Result<ObjectMeta, StorageError> {
74 let result: Result<ObjectMeta, StorageError> = async {
75 Ok(self.store.head(path).await?)
76 }
77 .await;
70 let result: Result<ObjectMeta, StorageError> =
71 async { Ok(self.store.head(path).await?) }.await;
7872 metrics::record_op("head", &result);
7973 result
8074 }
8175
8276 async fn exists(&self, path: &Path) -> Result<bool, StorageError> {
83 let result: Result<bool, StorageError> = async {
84 Ok(self.store.head(path).await.is_ok())
85 }
86 .await;
77 let result: Result<bool, StorageError> =
78 async { Ok(self.store.head(path).await.is_ok()) }.await;
8779 metrics::record_op("exists", &result);
8880 result
8981 }
@@ -99,10 +91,7 @@
9991 }
10092
10193 async fn copy(&self, from: &Path, to: &Path) -> Result<(), StorageError> {
102 let result: Result<(), StorageError> = async {
103 Ok(self.store.copy(from, to).await?)
104 }
105 .await;
94 let result: Result<(), StorageError> = async { Ok(self.store.copy(from, to).await?) }.await;
10695 metrics::record_op("copy", &result);
10796 result
10897 }
@@ -225,7 +214,9 @@
225214 .await
226215 .unwrap();
227216 upload
228 .put_part(PutPayload::from_bytes(Bytes::from_static(b"from multipart")))
217 .put_part(PutPayload::from_bytes(Bytes::from_static(
218 b"from multipart",
219 )))
229220 .await
230221 .unwrap();
231222 upload.complete().await.unwrap();
Modified lib/storage/metrics.rs View lib/storage/metrics.rs
@@ -18,7 +18,10 @@
1818 "storage_ops_total",
1919 "Storage operations partitioned by op and outcome (success, error)."
2020 );
21 describe_counter!("storage_put_bytes_total", "Total bytes written via storage put.");
21 describe_counter!(
22 "storage_put_bytes_total",
23 "Total bytes written via storage put."
24 );
2225 describe_histogram!(
2326 "storage_op_duration_seconds",
2427 Unit::Seconds,
@@ -61,4 +64,4 @@
6164pub(crate) fn record_put_bytes(n: usize) {
6265 ensure_described();
6366 counter!("storage_put_bytes_total").increment(n as u64);
64}
No newline at end of file
67}
Modified lib/storage/s3.rs View lib/storage/s3.rs
@@ -72,10 +72,8 @@
7272 async fn put(&self, path: &Path, data: Bytes) -> Result<PutResult, StorageError> {
7373 let _timer = metrics::OpTimer::start();
7474 let len = data.len();
75 let result: Result<PutResult, StorageError> = async {
76 Ok(self.store.put(path, data.into()).await?)
77 }
78 .await;
75 let result: Result<PutResult, StorageError> =
76 async { Ok(self.store.put(path, data.into()).await?) }.await;
7977 metrics::record_put_bytes(len);
8078 metrics::record_op("put", &result);
8179 result
@@ -83,10 +81,8 @@
8381
8482 async fn get(&self, path: &Path) -> Result<GetResult, StorageError> {
8583 let _timer = metrics::OpTimer::start();
86 let result: Result<GetResult, StorageError> = async {
87 Ok(self.store.get(path).await?)
88 }
89 .await;
84 let result: Result<GetResult, StorageError> =
85 async { Ok(self.store.get(path).await?) }.await;
9086 metrics::record_op("get", &result);
9187 result
9288 }
@@ -98,19 +94,15 @@
9894 }
9995
10096 async fn head(&self, path: &Path) -> Result<ObjectMeta, StorageError> {
101 let result: Result<ObjectMeta, StorageError> = async {
102 Ok(self.store.head(path).await?)
103 }
104 .await;
97 let result: Result<ObjectMeta, StorageError> =
98 async { Ok(self.store.head(path).await?) }.await;
10599 metrics::record_op("head", &result);
106100 result
107101 }
108102
109103 async fn exists(&self, path: &Path) -> Result<bool, StorageError> {
110 let result: Result<bool, StorageError> = async {
111 Ok(self.store.head(path).await.is_ok())
112 }
113 .await;
104 let result: Result<bool, StorageError> =
105 async { Ok(self.store.head(path).await.is_ok()) }.await;
114106 metrics::record_op("exists", &result);
115107 result
116108 }
@@ -126,10 +118,7 @@
126118 }
127119
128120 async fn copy(&self, from: &Path, to: &Path) -> Result<(), StorageError> {
129 let result: Result<(), StorageError> = async {
130 Ok(self.store.copy(from, to).await?)
131 }
132 .await;
121 let result: Result<(), StorageError> = async { Ok(self.store.copy(from, to).await?) }.await;
133122 metrics::record_op("copy", &result);
134123 result
135124 }
@@ -154,10 +143,8 @@
154143 path: &Path,
155144 expires_in: Duration,
156145 ) -> Result<Url, StorageError> {
157 let result: Result<Url, StorageError> = async {
158 Ok(self.store.signed_url(Method::PUT, path, expires_in).await?)
159 }
160 .await;
146 let result: Result<Url, StorageError> =
147 async { Ok(self.store.signed_url(Method::PUT, path, expires_in).await?) }.await;
161148 metrics::record_op("presigned_upload", &result);
162149 result
163150 }
@@ -167,10 +154,8 @@
167154 path: &Path,
168155 expires_in: Duration,
169156 ) -> Result<Url, StorageError> {
170 let result: Result<Url, StorageError> = async {
171 Ok(self.store.signed_url(Method::GET, path, expires_in).await?)
172 }
173 .await;
157 let result: Result<Url, StorageError> =
158 async { Ok(self.store.signed_url(Method::GET, path, expires_in).await?) }.await;
174159 metrics::record_op("presigned_download", &result);
175160 result
176161 }
Modified lib/user/features/mod.rs View lib/user/features/mod.rs
@@ -56,7 +56,9 @@
5656 }
5757
5858 fn access_blacklist(&self) -> Option<Arc<rate_limit::AccessTokenBlacklist>> {
59 Some(Arc::new(rate_limit::AccessTokenBlacklist::new(self.kv.clone())))
59 Some(Arc::new(rate_limit::AccessTokenBlacklist::new(
60 self.kv.clone(),
61 )))
6062 }
6163}
6264
@@ -68,4 +70,4 @@
6870 fn rate_limit_settings(&self) -> Arc<rate_limit::RateLimitConfig> {
6971 Arc::new(rate_limit::RateLimitConfig::default())
7072 }
71}
No newline at end of file
73}
Modified lib/user/handler/mod.rs View lib/user/handler/mod.rs
@@ -14,4 +14,4 @@
1414pub(crate) mod suspensions;
1515
1616pub use openapi::UserApiDoc;
17pub use routes::router;
No newline at end of file
17pub use routes::router;
Modified lib/user/handler/openapi.rs View lib/user/handler/openapi.rs
@@ -1,38 +1,32 @@
11use utoipa::OpenApi;
22
3use crate::config::ProfileVisibility;
34use crate::features::avatar::types::{AvatarFormat, AvatarView, UploadAvatarCmd};
4use export::user::deletion::{
5 DeletionRequestView, DeletionStatus, RequestDeletionCmd,
5use crate::features::profile::types::{
6 MemeView, ProfileView, UpdateProfileCmd, UpdateProfileFields, UpsertMemeCmd, UserMe, UserPublic,
7};
8use crate::features::social::types::{
9 BlockedSummary, FollowerSummary, ListBlocksQuery, ListFollowersQuery, ListFollowingQuery,
610};
11use export::user::deletion::{DeletionRequestView, DeletionStatus, RequestDeletionCmd};
712use export::user::devices::{DeviceView, UpdateDeviceCmd};
813use export::user::emails::{
914 AddEmailCmd, AddEmailResp, ConfirmEmailChangeCmd, EmailChangeTicketStatus,
1015 EmailChangeTicketView, EmailView, SetPrimaryEmailResp,
1116};
1217use export::user::notifications::{
13 ListNotificationsQuery, MarkReadCmd, NotificationType, NotificationView,
14 RecentMentionView, ReportStatus, ReportView, SaveMessageCmd, SavedMessageView,
15 ScheduledMessageStatus, ScheduledMessageView, SubmitReportCmd, UnreadCountResp,
16 UpdateReportCmd,
18 ListNotificationsQuery, MarkReadCmd, NotificationType, NotificationView, RecentMentionView,
19 ReportStatus, ReportView, SaveMessageCmd, SavedMessageView, ScheduledMessageStatus,
20 ScheduledMessageView, SubmitReportCmd, UnreadCountResp, UpdateReportCmd,
1721};
1822use export::user::preferences::{Audience, UpdatePreferencesCmd, UserPreferences};
19use crate::features::profile::types::{
20 MemeView, ProfileView, UpdateProfileCmd, UpdateProfileFields, UserMe,
21 UserPublic, UpsertMemeCmd,
22};
23use crate::config::ProfileVisibility;
2423use export::user::security::{
2524 AccessKeyScope, AccessKeyView, AddSshKeyCmd, CreateAccessKeyCmd, CreateAccessKeyResp,
2625 PushPlatform, PushSubscriptionView, RecoveryCodeView, RegenerateRecoveryResp, SshKeyType,
2726 SshKeyView, UpsertPushCmd,
2827};
2928use export::user::sessions::{ListSessionsQuery, SessionKind, SessionView};
30use crate::features::social::types::{
31 BlockedSummary, FollowerSummary, ListBlocksQuery, ListFollowersQuery, ListFollowingQuery,
32};
33use export::user::suspensions::{
34 RevokeSuspensionCmd, SuspendCmd, SuspensionType, SuspensionView,
35};
29use export::user::suspensions::{RevokeSuspensionCmd, SuspendCmd, SuspensionType, SuspensionView};
3630
3731#[derive(OpenApi)]
3832#[openapi(
@@ -135,4 +129,4 @@
135129 ScheduledMessageView, ScheduledMessageStatus,
136130 ))
137131)]
138pub struct UserApiDoc;
No newline at end of file
132pub struct UserApiDoc;
Modified lib/user/handler/routes.rs View lib/user/handler/routes.rs
@@ -25,8 +25,11 @@
2525
2626 let me_protected = Router::new()
2727 .route("/me", get(super::profile::me::handler))
28 .route("/me/profile", patch(super::profile::update_profile::handler))
2928 .route(
29 "/me/profile",
30 patch(super::profile::update_profile::handler),
31 )
32 .route(
3033 "/me/avatar",
3134 get(super::avatar::current::handler)
3235 .put(super::avatar::upload::handler)
@@ -44,17 +47,16 @@
4447 "/me/memes/:id",
4548 delete(super::profile::memes::delete_handler),
4649 )
47 .route(
48 "/me/devices",
49 get(super::devices::list_devices::handler),
50 )
50 .route("/me/devices", get(super::devices::list_devices::handler))
5151 .route(
5252 "/me/devices/:id",
53 patch(super::devices::update_device::handler).delete(super::devices::delete_device::handler),
53 patch(super::devices::update_device::handler)
54 .delete(super::devices::delete_device::handler),
5455 )
5556 .route(
5657 "/me/sessions",
57 get(super::sessions::list_sessions::handler).delete(super::sessions::revoke_sessions::handler),
58 get(super::sessions::list_sessions::handler)
59 .delete(super::sessions::revoke_sessions::handler),
5860 )
5961 .route(
6062 "/me/sessions/:id",
@@ -84,10 +86,7 @@
8486 "/me/email-change/:ticket_id",
8587 delete(super::emails::cancel_change::handler),
8688 )
87 .route(
88 "/me/blocks",
89 get(super::social::list_blocks::handler),
90 )
89 .route("/me/blocks", get(super::social::list_blocks::handler))
9190 .route(
9291 "/me/blocks/:uid",
9392 post(super::social::block::handler).delete(super::social::unblock::handler),
@@ -123,7 +122,8 @@
123122 )
124123 .route(
125124 "/me/ssh-keys",
126 get(super::security::ssh_keys::list_keys::handler).post(super::security::ssh_keys::add_key::handler),
125 get(super::security::ssh_keys::list_keys::handler)
126 .post(super::security::ssh_keys::add_key::handler),
127127 )
128128 .route(
129129 "/me/ssh-keys/:id",
@@ -221,7 +221,10 @@
221221 inject_jwt_ctx::<UserService>,
222222 ));
223223
224 let public_reports = Router::new().route("/reports", post(super::notifications::reports::submit::handler));
224 let public_reports = Router::new().route(
225 "/reports",
226 post(super::notifications::reports::submit::handler),
227 );
225228
226229 Router::new()
227230 .merge(public)
@@ -235,4 +238,4 @@
235238 crate::handler::middleware::metrics::metrics_layer,
236239 ))
237240 .with_state(svc)
238}
No newline at end of file
241}
Modified lib/user/config.rs View lib/user/config.rs
@@ -155,4 +155,4 @@
155155}
156156
157157#[allow(dead_code)]
158pub type SharedUserConfig = Arc<UserConfig>;
No newline at end of file
158pub type SharedUserConfig = Arc<UserConfig>;
Modified lib/user/error.rs View lib/user/error.rs
@@ -82,7 +82,7 @@
8282 )
8383 }
8484
85 pub fn error_kind(&self) -> &'static str {
85 pub fn error_kind(&self) -> &'static str {
8686 match self {
8787 UserError::BadRequest(_) => "bad_request",
8888 UserError::Unauthorized(_) => "unauthorized",
@@ -110,4 +110,4 @@
110110 };
111111 AppResponseData::<NoData>::err_plain(status.as_u16() as i32, msg).into_response()
112112 }
113}
No newline at end of file
113}
Modified lib/user/lib.rs View lib/user/lib.rs
@@ -7,5 +7,5 @@
77pub use config::{SharedUserConfig, UserConfig};
88pub use error::{UserError, UserResult};
99pub use features::UserService;
10pub use handler::UserApiDoc;
1011pub use handler::router as user_router;
11pub use handler::UserApiDoc;
No newline at end of file
Modified lib/user/metrics.rs View lib/user/metrics.rs
@@ -4,7 +4,6 @@
44
55use crate::error::UserError;
66
7
87pub const FEATURE_PROFILE_ME: &str = "profile_me";
98pub const FEATURE_PROFILE_UPDATE: &str = "profile_update";
109pub const FEATURE_PROFILE_UPDATE_ADMIN: &str = "profile_update_admin";
@@ -82,11 +81,9 @@
8281pub const FEATURE_PREFERENCES_GET: &str = "preferences_get";
8382pub const FEATURE_PREFERENCES_UPDATE: &str = "preferences_update";
8483
85
8684pub const OUTCOME_SUCCESS: &str = "success";
8785pub const OUTCOME_ERROR: &str = "error";
8886
89
9087static DESCRIBE_ONCE: std::sync::Once = std::sync::Once::new();
9188
9289pub fn ensure_described() {
@@ -121,7 +118,6 @@
121118 "Object storage operations initiated by the user service"
122119 );
123120}
124
125121
126122pub struct Timer {
127123 pub feature: &'static str,
@@ -157,7 +153,6 @@
157153 }
158154}
159155
160
161156pub fn record_outcome<T>(feature: &'static str, result: &Result<T, UserError>) {
162157 match result {
163158 Ok(_) => {
@@ -185,7 +180,6 @@
185180 }
186181 }
187182}
188
189183
190184#[derive(Debug, Clone, Copy)]
191185pub enum AvatarFormat {
@@ -229,7 +223,6 @@
229223 )
230224 .increment(1);
231225}
232
233226
234227#[derive(Debug, Clone, Copy)]
235228pub enum SocialFollowOutcome {
@@ -285,7 +278,6 @@
285278 .increment(1);
286279}
287280
288
289281#[derive(Debug, Clone, Copy)]
290282pub enum EmailOp {
291283 Add,
@@ -346,7 +338,6 @@
346338 )
347339 .increment(1);
348340}
349
350341
351342#[derive(Debug, Clone, Copy)]
352343pub enum SecurityResourceOp {
@@ -390,7 +381,11 @@
390381 }
391382}
392383
393pub fn record_security_operation(resource: SecurityResource, op: SecurityResourceOp, outcome: &'static str) {
384pub fn record_security_operation(
385 resource: SecurityResource,
386 op: SecurityResourceOp,
387 outcome: &'static str,
388) {
394389 counter!(
395390 "user_usecase_invocations_total",
396391 "feature" => format!("{}_{}", resource.as_label(), op.as_label()),
@@ -399,7 +394,6 @@
399394 .increment(1);
400395}
401396
402
403397#[derive(Debug, Clone, Copy)]
404398pub enum NotificationOp {
405399 List,
@@ -450,7 +444,11 @@
450444 }
451445}
452446
453pub fn record_notification_operation(kind: NotificationKind, op: NotificationOp, outcome: &'static str) {
447pub fn record_notification_operation(
448 kind: NotificationKind,
449 op: NotificationOp,
450 outcome: &'static str,
451) {
454452 counter!(
455453 "user_usecase_invocations_total",
456454 "feature" => format!("{}_{}", kind.as_label(), op.as_label()),
@@ -483,7 +481,6 @@
483481 .increment(1);
484482}
485483
486
487484#[derive(Debug, Clone, Copy)]
488485pub enum SuspensionOp {
489486 Create,
@@ -535,7 +532,6 @@
535532 .increment(1);
536533}
537534
538
539535#[derive(Debug, Clone, Copy)]
540536pub enum DeletionOp {
541537 Request,
@@ -586,7 +582,6 @@
586582 )
587583 .increment(1);
588584}
589
590585
591586#[derive(Debug, Clone, Copy)]
592587pub enum PreferencesOp {
@@ -611,7 +606,6 @@
611606 )
612607 .increment(1);
613608}
614
615609
616610pub fn record_storage_operation(op: &'static str, kind: &'static str, outcome: &'static str) {
617611 counter!(
Modified lib/webapi/openapi.rs View lib/webapi/openapi.rs
@@ -1,8 +1,8 @@
1use crate::{health, router::API_PREFIX};
12use axum::http::{Response, StatusCode};
3use once_cell::sync::Lazy;
24use utoipa::{Modify, OpenApi};
35use utoipa_scalar::{Scalar, Servable};
4use once_cell::sync::Lazy;
5use crate::{health, router::API_PREFIX};
66
77#[derive(OpenApi)]
88#[openapi(
@@ -32,10 +32,7 @@
3232 doc
3333}
3434
35
36static DOCS_HTML: Lazy<String> = Lazy::new(|| {
37 Scalar::with_url("/api-docs", openapi()).to_html()
38});
35static DOCS_HTML: Lazy<String> = Lazy::new(|| Scalar::with_url("/api-docs", openapi()).to_html());
3936
4037pub async fn docs() -> Response<String> {
4138 Response::builder()
@@ -43,4 +40,4 @@
4340 .header("Content-Type", "text/html")
4441 .body(DOCS_HTML.clone())
4542 .expect("Failed to build response")
46}
No newline at end of file
43}
Modified lib/webapi/services.rs View lib/webapi/services.rs
@@ -32,7 +32,7 @@
3232 Queue(#[from] queue::QueueError),
3333 #[error("storage: {0}")]
3434 Storage(#[from] storage::StorageError),
35 #[error("email service is required (auth depends on it); enable email in config")]
35 #[error("email service is required (auth depends on it); enable email in config")]
3636 EmailRequired,
3737}
3838
@@ -54,7 +54,7 @@
5454}
5555
5656impl AppServices {
57 pub async fn build(cfg: &AppConfig) -> WebapiResult<Self> {
57 pub async fn build(cfg: &AppConfig) -> WebapiResult<Self> {
5858 tracing::info!("building service stack");
5959
6060 let db = Db::from_config(&cfg.db).await?;
Modified lib/webhook/dispatcher.rs View lib/webhook/dispatcher.rs
@@ -25,12 +25,12 @@
2525}
2626
2727impl WebhookDispatcher {
28 #[must_use]
28 #[must_use]
2929 pub fn new(db: Db, http: reqwest::Client, timeout: Duration) -> Self {
3030 Self { db, http, timeout }
3131 }
3232
33 #[must_use]
33 #[must_use]
3434 pub fn with_defaults(db: Db) -> Self {
3535 Self::new(db, reqwest::Client::new(), DEFAULT_WEBHOOK_TIMEOUT)
3636 }
Modified Cargo.lock View Cargo.lock
@@ -3838,6 +3838,7 @@
38383838 "sea-orm",
38393839 "serde",
38403840 "serde_json",
3841 "space",
38413842 "storage",
38423843 "thiserror 2.0.19",
38433844 "time",
@@ -5571,6 +5572,7 @@
55715572 "sea-orm",
55725573 "serde",
55735574 "serde_json",
5575 "space",
55745576 "storage",
55755577 "thiserror 2.0.19",
55765578 "time",
@@ -6853,6 +6855,7 @@
68536855 "queue",
68546856 "sea-orm",
68556857 "serde",
6858 "space",
68566859 "storage",
68576860 "thiserror 2.0.19",
68586861 "time",