Skip to content
Clone

feat(lib/issues): refresh services and share modules

ZhenYi committed
parent cb2a33b commit 5590e2e Browse files

17 files changed

+349 −73

File tree 17
Modified lib/issues/src/services/boards.rs View lib/issues/src/services/boards.rs
@@ -1,3 +1,5 @@
1use std::collections::HashMap;
2
13use models::dir::dirs::{self, DirKind};
24use models::issue::issue_item;
35use models::issue::issue_pin;
@@ -72,30 +74,65 @@
7274}
7375
7476impl IssueService {
75 async fn build_board_dto(&self, board: issues::Model) -> Result<BoardDto, IssueError> {
76 let open_count = issue_item::Entity::find()
77 .filter(issue_item::Column::CollectionId.eq(board.id))
78 .filter(issue_item::Column::ClosedAt.is_null())
79 .count(self.db().read())
80 .await? as i64;
81
82 let closed_count = issue_item::Entity::find()
83 .filter(issue_item::Column::CollectionId.eq(board.id))
84 .filter(issue_item::Column::ClosedAt.is_not_null())
85 .count(self.db().read())
86 .await? as i64;
87
88 Ok(BoardDto {
77 fn board_dto(board: issues::Model, nums_open_issue: i64, nums_closed_issue: i64) -> BoardDto {
78 BoardDto {
8979 id: board.id,
9080 dir_id: board.dir_id,
9181 name: board.name,
9282 icon: board.icon,
9383 description: board.description,
94 nums_open_issue: open_count,
95 nums_closed_issue: closed_count,
84 nums_open_issue,
85 nums_closed_issue,
9686 created_at: board.created_at,
9787 updated_at: board.updated_at,
98 })
88 }
89 }
90
91 async fn issue_counts_by_board(
92 &self,
93 board_ids: &[Uuid],
94 ) -> Result<(HashMap<Uuid, i64>, HashMap<Uuid, i64>), IssueError> {
95 let mut total_counts = HashMap::new();
96 let mut closed_counts = HashMap::new();
97 if board_ids.is_empty() {
98 return Ok((total_counts, closed_counts));
99 }
100
101 let total_rows: Vec<(Uuid, i64)> = issue_item::Entity::find()
102 .select_only()
103 .column(issue_item::Column::CollectionId)
104 .column_as(issue_item::Column::CollectionId.count(), "cnt")
105 .filter(issue_item::Column::CollectionId.is_in(board_ids.to_vec()))
106 .group_by(issue_item::Column::CollectionId)
107 .into_tuple()
108 .all(self.db().read())
109 .await?;
110 for (collection_id, cnt) in total_rows {
111 total_counts.insert(collection_id, cnt);
112 }
113
114 let closed_rows: Vec<(Uuid, i64)> = issue_item::Entity::find()
115 .select_only()
116 .column(issue_item::Column::CollectionId)
117 .column_as(issue_item::Column::CollectionId.count(), "cnt")
118 .filter(issue_item::Column::CollectionId.is_in(board_ids.to_vec()))
119 .filter(issue_item::Column::ClosedAt.is_not_null())
120 .group_by(issue_item::Column::CollectionId)
121 .into_tuple()
122 .all(self.db().read())
123 .await?;
124 for (collection_id, cnt) in closed_rows {
125 closed_counts.insert(collection_id, cnt);
126 }
127
128 Ok((total_counts, closed_counts))
129 }
130
131 async fn build_board_dto(&self, board: issues::Model) -> Result<BoardDto, IssueError> {
132 let (total_counts, closed_counts) = self.issue_counts_by_board(&[board.id]).await?;
133 let closed_count = closed_counts.get(&board.id).copied().unwrap_or(0);
134 let open_count = total_counts.get(&board.id).copied().unwrap_or(0) - closed_count;
135 Ok(Self::board_dto(board, open_count, closed_count))
99136 }
100137
101138 pub async fn create_board(
@@ -160,7 +197,7 @@
160197 let max_pagination = self.config().issues.max_pagination;
161198 let default_pagination = self.config().issues.default_pagination;
162199 let limit = q.limit.unwrap_or(default_pagination).clamp(1, max_pagination);
163 let offset = q.offset.unwrap_or(0);
200 let offset = q.offset.unwrap_or(0).clamp(0, 5000);
164201
165202 let dirs_in_space = dirs::Entity::find()
166203 .filter(dirs::Column::SpaceId.eq(Some(space.id)))
@@ -206,10 +243,17 @@
206243 .all(self.db().read())
207244 .await?;
208245
209 let mut items = Vec::with_capacity(rows.len());
210 for board in rows {
211 items.push(self.build_board_dto(board).await?);
212 }
246 let board_ids: Vec<Uuid> = rows.iter().map(|b| b.id).collect();
247 let (total_counts, closed_counts) = self.issue_counts_by_board(&board_ids).await?;
248
249 let items = rows
250 .into_iter()
251 .map(|board| {
252 let closed_count = closed_counts.get(&board.id).copied().unwrap_or(0);
253 let open_count = total_counts.get(&board.id).copied().unwrap_or(0) - closed_count;
254 Self::board_dto(board, open_count, closed_count)
255 })
256 .collect();
213257
214258 Ok(BoardPage { items, total })
215259 }
Modified lib/issues/src/services/comments.rs View lib/issues/src/services/comments.rs
@@ -1,6 +1,8 @@
11use models::issue::comment;
2use models::issue::issue_content_history;
23use models::issue::issue_member::{self, IssueMemberKind};
34use models::users::user_notifications::NotificationType;
5use sea_orm::sea_query::Expr;
46use sea_orm::{
57 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
68 QuerySelect, Set, TransactionTrait,
@@ -13,6 +15,7 @@
1315
1416use crate::errors::IssueError;
1517use crate::services::service::IssueService;
18use crate::share::authz::is_board_manager;
1619use crate::share::lookup::{lookup_board, lookup_issue, lookup_space_by_name, require_member};
1720
1821#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
@@ -282,7 +285,7 @@
282285 let max_pagination = self.config().issues.max_pagination;
283286 let default_pagination = self.config().issues.default_pagination;
284287 let limit = q.limit.unwrap_or(default_pagination).clamp(1, max_pagination);
285 let offset = q.offset.unwrap_or(0);
288 let offset = q.offset.unwrap_or(0).clamp(0, 5000);
286289
287290 let mut select = comment::Entity::find()
288291 .filter(comment::Column::IssueId.eq(Some(issue_id)));
@@ -375,7 +378,7 @@
375378 .await?
376379 .ok_or(IssueError::NotFound)?;
377380
378 if c.poster_id != caller {
381 if c.poster_id != caller && !is_board_manager(self, &space, caller).await? {
379382 return Err(IssueError::Forbidden);
380383 }
381384
@@ -384,6 +387,8 @@
384387 return Err(IssueError::BadRequest("comment content is empty or too long".to_string()));
385388 }
386389
390 let old_content = c.content.clone();
391 let content_changed: Option<String> = (content != old_content).then(|| content.to_string());
387392 let now = OffsetDateTime::now_utc();
388393 let mut am: comment::ActiveModel = c.into();
389394 am.content = Set(content.to_string());
@@ -394,8 +399,23 @@
394399 .db()
395400 .primary()
396401 .transaction::<_, comment::Model, IssueError>(move |txn| {
402 let content_change_clone = content_changed.clone();
397403 Box::pin(async move {
398404 let u = am.update(txn).await?;
405 if let Some(new_content) = content_change_clone {
406 issue_content_history::ActiveModel {
407 id: Set(Uuid::now_v7()),
408 issue_id: Set(issue_id),
409 poster_id: Set(caller),
410 comment_id: Set(Some(comment_id)),
411 content_before: Set(Some(old_content)),
412 content_after: Set(Some(new_content)),
413 edited_at: Set(now),
414 created_at: Set(now),
415 }
416 .insert(txn)
417 .await?;
418 }
399419 this.write_event(
400420 txn,
401421 issue_id,
@@ -445,7 +465,7 @@
445465 .await?
446466 .ok_or(IssueError::NotFound)?;
447467
448 if c.poster_id != caller {
468 if c.poster_id != caller && !is_board_manager(self, &space, caller).await? {
449469 return Err(IssueError::Forbidden);
450470 }
451471
@@ -455,6 +475,12 @@
455475 .primary()
456476 .transaction::<_, (), IssueError>(move |txn| {
457477 Box::pin(async move {
478 comment::Entity::update_many()
479 .filter(comment::Column::ParentId.eq(comment_id))
480 .col_expr(comment::Column::ParentId, Expr::value(None::<Uuid>))
481 .exec(txn)
482 .await?;
483
458484 comment::Entity::delete_by_id(comment_id).exec(txn).await?;
459485 this.write_event(
460486 txn,
Modified lib/issues/src/services/content_history.rs View lib/issues/src/services/content_history.rs
@@ -61,7 +61,7 @@
6161 let max_pagination = self.config().issues.max_pagination;
6262 let default_pagination = self.config().issues.default_pagination;
6363 let limit = q.limit.unwrap_or(default_pagination).clamp(1, max_pagination);
64 let offset = q.offset.unwrap_or(0);
64 let offset = q.offset.unwrap_or(0).clamp(0, 5000);
6565
6666 let mut select = issue_content_history::Entity::find()
6767 .filter(issue_content_history::Column::IssueId.eq(issue_id));
Modified lib/issues/src/services/dependencies.rs View lib/issues/src/services/dependencies.rs
@@ -10,6 +10,7 @@
1010
1111use crate::errors::IssueError;
1212use crate::services::service::IssueService;
13use crate::share::authz::require_board_manage;
1314use crate::share::lookup::{lookup_board, lookup_issue, lookup_space_by_name, require_member};
1415
1516#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
@@ -55,6 +56,7 @@
5556 ) -> Result<DependencyDto, IssueError> {
5657 let space = lookup_space_by_name(self, space_name).await?;
5758 require_member(self, &space, caller).await?;
59 require_board_manage(self, &space, caller).await?;
5860 let board = lookup_board(self, &space, board_id).await?;
5961 let _ = lookup_issue(self, &board, issue_id).await?;
6062
@@ -143,6 +145,7 @@
143145 ) -> Result<(), IssueError> {
144146 let space = lookup_space_by_name(self, space_name).await?;
145147 require_member(self, &space, caller).await?;
148 require_board_manage(self, &space, caller).await?;
146149 let board = lookup_board(self, &space, board_id).await?;
147150 let _ = lookup_issue(self, &board, issue_id).await?;
148151
Modified lib/issues/src/services/events.rs View lib/issues/src/services/events.rs
@@ -64,7 +64,7 @@
6464 let max_pagination = self.config().issues.max_pagination;
6565 let default_pagination = self.config().issues.default_pagination;
6666 let limit = q.limit.unwrap_or(default_pagination).clamp(1, max_pagination);
67 let offset = q.offset.unwrap_or(0);
67 let offset = q.offset.unwrap_or(0).clamp(0, 5000);
6868
6969 let select = issue_event_line::Entity::find()
7070 .filter(issue_event_line::Column::IssueId.eq(Some(issue_id)))
Modified lib/issues/src/services/issue_users.rs View lib/issues/src/services/issue_users.rs
@@ -66,7 +66,7 @@
6666 let max_pagination = self.config().issues.max_pagination;
6767 let default_pagination = self.config().issues.default_pagination;
6868 let limit = q.limit.unwrap_or(default_pagination).clamp(1, max_pagination);
69 let offset = q.offset.unwrap_or(0);
69 let offset = q.offset.unwrap_or(0).clamp(0, 5000);
7070
7171 let select = issue_user::Entity::find()
7272 .filter(issue_user::Column::IssueId.eq(issue_id))
Modified lib/issues/src/services/issues.rs View lib/issues/src/services/issues.rs
@@ -28,6 +28,7 @@
2828
2929use crate::errors::IssueError;
3030use crate::services::service::IssueService;
31use crate::share::authz::is_board_manager;
3132use crate::share::lookup::{lookup_board, lookup_issue, lookup_space_by_name, require_member};
3233
3334#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
@@ -392,7 +393,7 @@
392393 let max_pagination = self.config().issues.max_pagination;
393394 let default_pagination = self.config().issues.default_pagination;
394395 let limit = q.limit.unwrap_or(default_pagination).clamp(1, max_pagination);
395 let offset = q.offset.unwrap_or(0);
396 let offset = q.offset.unwrap_or(0).clamp(0, 5000);
396397
397398 let mut select = issue_item::Entity::find()
398399 .filter(issue_item::Column::CollectionId.eq(board.id));
@@ -513,7 +514,13 @@
513514 let board = lookup_board(self, &space, board_id).await?;
514515 let item = lookup_issue(self, &board, issue_id).await?;
515516
517 if item.author_id != caller && !is_board_manager(self, &space, caller).await? {
518 return Err(IssueError::Forbidden);
519 }
520
516521 let old_title = item.title.clone();
522 let old_description = item.description.clone();
523 let old_body = item.body.clone();
517524 let old_milestone = item.milestone_id;
518525
519526 let mut title_changed = None;
@@ -552,14 +559,22 @@
552559 }
553560
554561 let now = OffsetDateTime::now_utc();
562 let mut description_changed: Option<Option<String>> = None;
563 let mut body_changed: Option<String> = None;
555564 let mut am: issue_item::ActiveModel = item.into();
556565 if let Some(ref t) = title_changed {
557566 am.title = Set(t.clone());
558567 }
559568 if let Some(desc) = cmd.description {
569 if desc != old_description {
570 description_changed = Some(desc.clone());
571 }
560572 am.description = Set(desc);
561573 }
562574 if let Some(b) = cmd.body {
575 if b != old_body {
576 body_changed = Some(b.clone());
577 }
563578 am.body = Set(b);
564579 }
565580 if milestone_changed {
@@ -573,6 +588,8 @@
573588 .primary()
574589 .transaction::<_, issue_item::Model, IssueError>(move |txn| {
575590 let title_change_clone = title_changed.clone();
591 let desc_change_clone = description_changed.clone();
592 let body_change_clone = body_changed.clone();
576593 Box::pin(async move {
577594 let u = am.update(txn).await?;
578595
@@ -583,11 +600,54 @@
583600 caller,
584601 "title_updated",
585602 None,
586 Some(old_title),
587 Some(new_t),
603 Some(old_title.clone()),
604 Some(new_t.clone()),
588605 None,
589606 )
590607 .await?;
608
609 issue_content_history::ActiveModel {
610 id: Set(Uuid::now_v7()),
611 issue_id: Set(issue_id),
612 poster_id: Set(caller),
613 comment_id: Set(None),
614 content_before: Set(Some(old_title)),
615 content_after: Set(Some(new_t)),
616 edited_at: Set(now),
617 created_at: Set(now),
618 }
619 .insert(txn)
620 .await?;
621 }
622
623 if let Some(new_desc) = desc_change_clone {
624 issue_content_history::ActiveModel {
625 id: Set(Uuid::now_v7()),
626 issue_id: Set(issue_id),
627 poster_id: Set(caller),
628 comment_id: Set(None),
629 content_before: Set(old_description),
630 content_after: Set(new_desc),
631 edited_at: Set(now),
632 created_at: Set(now),
633 }
634 .insert(txn)
635 .await?;
636 }
637
638 if let Some(new_body) = body_change_clone {
639 issue_content_history::ActiveModel {
640 id: Set(Uuid::now_v7()),
641 issue_id: Set(issue_id),
642 poster_id: Set(caller),
643 comment_id: Set(None),
644 content_before: Set(Some(old_body)),
645 content_after: Set(Some(new_body)),
646 edited_at: Set(now),
647 created_at: Set(now),
648 }
649 .insert(txn)
650 .await?;
591651 }
592652
593653 if milestone_changed {
@@ -656,7 +716,11 @@
656716 let space = lookup_space_by_name(self, space_name).await?;
657717 require_member(self, &space, caller).await?;
658718 let board = lookup_board(self, &space, board_id).await?;
659 let _ = lookup_issue(self, &board, issue_id).await?;
719 let item = lookup_issue(self, &board, issue_id).await?;
720
721 if item.author_id != caller && !is_board_manager(self, &space, caller).await? {
722 return Err(IssueError::Forbidden);
723 }
660724
661725 self.db()
662726 .primary()
@@ -684,6 +748,10 @@
684748 let board = lookup_board(self, &space, board_id).await?;
685749 let item = lookup_issue(self, &board, issue_id).await?;
686750
751 if item.author_id != caller && !is_board_manager(self, &space, caller).await? {
752 return Err(IssueError::Forbidden);
753 }
754
687755 let is_currently_closed = item.closed_at.is_some();
688756 if is_currently_closed == closed {
689757 let user_map = self.load_user_briefs(&[item.author_id]).await?;
Modified lib/issues/src/services/labels.rs View lib/issues/src/services/labels.rs
@@ -11,6 +11,7 @@
1111
1212use crate::errors::IssueError;
1313use crate::services::service::IssueService;
14use crate::share::authz::require_board_manage;
1415use crate::share::lookup::{lookup_board, lookup_issue, lookup_space_by_name, require_member};
1516
1617#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
@@ -119,6 +120,7 @@
119120 ) -> Result<LabelDto, IssueError> {
120121 let space = lookup_space_by_name(self, space_name).await?;
121122 require_member(self, &space, caller).await?;
123 require_board_manage(self, &space, caller).await?;
122124 let board = lookup_board(self, &space, board_id).await?;
123125
124126 let name = cmd.name.trim();
@@ -173,7 +175,7 @@
173175 let max_pagination = self.config().issues.max_pagination;
174176 let default_pagination = self.config().issues.default_pagination;
175177 let limit = q.limit.unwrap_or(default_pagination).clamp(1, max_pagination);
176 let offset = q.offset.unwrap_or(0);
178 let offset = q.offset.unwrap_or(0).clamp(0, 5000);
177179
178180 let mut select = issue_label::Entity::find()
179181 .filter(issue_label::Column::DirId.eq(board.dir_id));
@@ -359,6 +361,7 @@
359361 ) -> Result<(), IssueError> {
360362 let space = lookup_space_by_name(self, space_name).await?;
361363 require_member(self, &space, caller).await?;
364 require_board_manage(self, &space, caller).await?;
362365 let board = lookup_board(self, &space, board_id).await?;
363366 let _ = lookup_issue(self, &board, issue_id).await?;
364367
@@ -425,6 +428,7 @@
425428 ) -> Result<(), IssueError> {
426429 let space = lookup_space_by_name(self, space_name).await?;
427430 require_member(self, &space, caller).await?;
431 require_board_manage(self, &space, caller).await?;
428432 let board = lookup_board(self, &space, board_id).await?;
429433 let _ = lookup_issue(self, &board, issue_id).await?;
430434
Modified lib/issues/src/services/members.rs View lib/issues/src/services/members.rs
@@ -1,5 +1,5 @@
11use models::issue::issue_member::{self, IssueMemberKind};
2use models::users::user;
2use models::space::space_members::{self, MemberStatus};
33use models::users::user_notifications::NotificationType;
44use sea_orm::{
55 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
@@ -13,6 +13,7 @@
1313
1414use crate::errors::IssueError;
1515use crate::services::service::IssueService;
16use crate::share::authz::require_board_manage;
1617use crate::share::lookup::{lookup_board, lookup_issue, lookup_space_by_name, require_member};
1718
1819#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
@@ -63,13 +64,21 @@
6364 ) -> Result<IssueMemberDto, IssueError> {
6465 let space = lookup_space_by_name(self, space_name).await?;
6566 require_member(self, &space, caller).await?;
67 require_board_manage(self, &space, caller).await?;
6668 let board = lookup_board(self, &space, board_id).await?;
6769 let item = lookup_issue(self, &board, issue_id).await?;
6870
69 let target_user = user::Entity::find_by_id(cmd.user_id)
70 .one(self.db().read())
71 .await?
72 .ok_or(IssueError::NotFound)?;
71 let target_is_member = if cmd.user_id == space.owner_id {
72 true
73 } else {
74 space_members::Entity::find_by_id((space.id, cmd.user_id))
75 .one(self.db().read())
76 .await?
77 .is_some_and(|m| m.status == MemberStatus::Active)
78 };
79 if !target_is_member {
80 return Err(IssueError::Forbidden);
81 }
7382
7483 let existing = issue_member::Entity::find()
7584 .filter(issue_member::Column::IssueId.eq(issue_id))
@@ -171,12 +180,13 @@
171180 }
172181 }
173182
174 let user_brief = UserBriefDto::from(target_user);
183 let user_map = self.load_user_briefs(&[cmd.user_id]).await?;
184 let user_brief = user_map.get(&cmd.user_id).cloned();
175185
176186 Ok(IssueMemberDto {
177187 id: inserted.id,
178188 issue_id: inserted.issue_id,
179 user: Some(user_brief),
189 user: user_brief,
180190 kind: inserted.kind,
181191 created_at: inserted.created_at,
182192 updated_at: inserted.updated_at,
@@ -199,7 +209,7 @@
199209 let max_pagination = self.config().issues.max_pagination;
200210 let default_pagination = self.config().issues.default_pagination;
201211 let limit = q.limit.unwrap_or(default_pagination).clamp(1, max_pagination);
202 let offset = q.offset.unwrap_or(0);
212 let offset = q.offset.unwrap_or(0).clamp(0, 5000);
203213
204214 let mut select = issue_member::Entity::find()
205215 .filter(issue_member::Column::IssueId.eq(issue_id));
@@ -248,6 +258,7 @@
248258 ) -> Result<(), IssueError> {
249259 let space = lookup_space_by_name(self, space_name).await?;
250260 require_member(self, &space, caller).await?;
261 require_board_manage(self, &space, caller).await?;
251262 let board = lookup_board(self, &space, board_id).await?;
252263 let _ = lookup_issue(self, &board, issue_id).await?;
253264
Modified lib/issues/src/services/milestones.rs View lib/issues/src/services/milestones.rs
@@ -1,3 +1,5 @@
1use std::collections::HashMap;
2
13use models::issue::issue_item;
24use models::issue::milestone;
35use sea_orm::sea_query::Expr;
@@ -12,6 +14,7 @@
1214
1315use crate::errors::IssueError;
1416use crate::services::service::IssueService;
17use crate::share::authz::require_board_manage;
1518use crate::share::lookup::{lookup_board, lookup_space_by_name, require_member};
1619
1720#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
@@ -71,33 +74,74 @@
7174}
7275
7376impl IssueService {
74 async fn build_milestone_dto(
75 &self,
76 m: milestone::Model,
77 ) -> Result<MilestoneDto, IssueError> {
78 let total_issues = issue_item::Entity::find()
79 .filter(issue_item::Column::MilestoneId.eq(Some(m.id)))
80 .count(self.db().read())
81 .await? as i64;
82
83 let closed_issues = issue_item::Entity::find()
84 .filter(issue_item::Column::MilestoneId.eq(Some(m.id)))
85 .filter(issue_item::Column::ClosedAt.is_not_null())
86 .count(self.db().read())
87 .await? as i64;
88
89 Ok(MilestoneDto {
77 fn milestone_dto(m: milestone::Model, nums_issues: i64, nums_closed_issues: i64) -> MilestoneDto {
78 MilestoneDto {
9079 id: m.id,
9180 board_id: m.collection_id,
9281 name: m.name,
9382 description: m.description,
9483 is_closed: m.is_closed,
9584 due_at: m.due_at,
96 nums_issues: total_issues,
97 nums_closed_issues: closed_issues,
85 nums_issues,
86 nums_closed_issues,
9887 created_at: m.created_at,
9988 updated_at: m.updated_at,
100 })
89 }
90 }
91
92 async fn issue_counts_by_milestone(
93 &self,
94 milestone_ids: &[Uuid],
95 ) -> Result<(HashMap<Uuid, i64>, HashMap<Uuid, i64>), IssueError> {
96 let mut total_counts = HashMap::new();
97 let mut closed_counts = HashMap::new();
98 if milestone_ids.is_empty() {
99 return Ok((total_counts, closed_counts));
100 }
101
102 let opt_ids: Vec<Option<Uuid>> = milestone_ids.iter().copied().map(Some).collect();
103 let total_rows: Vec<(Option<Uuid>, i64)> = issue_item::Entity::find()
104 .select_only()
105 .column(issue_item::Column::MilestoneId)
106 .column_as(issue_item::Column::MilestoneId.count(), "cnt")
107 .filter(issue_item::Column::MilestoneId.is_in(opt_ids.clone()))
108 .group_by(issue_item::Column::MilestoneId)
109 .into_tuple()
110 .all(self.db().read())
111 .await?;
112 for (milestone_id, cnt) in total_rows {
113 if let Some(mid) = milestone_id {
114 total_counts.insert(mid, cnt);
115 }
116 }
117
118 let closed_rows: Vec<(Option<Uuid>, i64)> = issue_item::Entity::find()
119 .select_only()
120 .column(issue_item::Column::MilestoneId)
121 .column_as(issue_item::Column::MilestoneId.count(), "cnt")
122 .filter(issue_item::Column::MilestoneId.is_in(opt_ids))
123 .filter(issue_item::Column::ClosedAt.is_not_null())
124 .group_by(issue_item::Column::MilestoneId)
125 .into_tuple()
126 .all(self.db().read())
127 .await?;
128 for (milestone_id, cnt) in closed_rows {
129 if let Some(mid) = milestone_id {
130 closed_counts.insert(mid, cnt);
131 }
132 }
133
134 Ok((total_counts, closed_counts))
135 }
136
137 async fn build_milestone_dto(
138 &self,
139 m: milestone::Model,
140 ) -> Result<MilestoneDto, IssueError> {
141 let (total_counts, closed_counts) = self.issue_counts_by_milestone(&[m.id]).await?;
142 let nums_issues = total_counts.get(&m.id).copied().unwrap_or(0);
143 let nums_closed_issues = closed_counts.get(&m.id).copied().unwrap_or(0);
144 Ok(Self::milestone_dto(m, nums_issues, nums_closed_issues))
101145 }
102146
103147 pub async fn create_milestone(
@@ -109,6 +153,7 @@
109153 ) -> Result<MilestoneDto, IssueError> {
110154 let space = lookup_space_by_name(self, space_name).await?;
111155 require_member(self, &space, caller).await?;
156 require_board_manage(self, &space, caller).await?;
112157 let board = lookup_board(self, &space, board_id).await?;
113158
114159 let name = cmd.name.trim();
@@ -165,7 +210,7 @@
165210 let max_pagination = self.config().issues.max_pagination;
166211 let default_pagination = self.config().issues.default_pagination;
167212 let limit = q.limit.unwrap_or(default_pagination).clamp(1, max_pagination);
168 let offset = q.offset.unwrap_or(0);
213 let offset = q.offset.unwrap_or(0).clamp(0, 5000);
169214
170215 let mut select = milestone::Entity::find()
171216 .filter(milestone::Column::CollectionId.eq(board.id));
@@ -190,10 +235,19 @@
190235 .all(self.db().read())
191236 .await?;
192237
193 let mut items = Vec::with_capacity(rows.len());
194 for m in rows {
195 items.push(self.build_milestone_dto(m).await?);
196 }
238 let milestone_ids: Vec<Uuid> = rows.iter().map(|m| m.id).collect();
239 let (total_counts, closed_counts) = self
240 .issue_counts_by_milestone(&milestone_ids)
241 .await?;
242
243 let items = rows
244 .into_iter()
245 .map(|m| {
246 let nums_issues = total_counts.get(&m.id).copied().unwrap_or(0);
247 let nums_closed_issues = closed_counts.get(&m.id).copied().unwrap_or(0);
248 Self::milestone_dto(m, nums_issues, nums_closed_issues)
249 })
250 .collect();
197251
198252 Ok(MilestonePage { items, total })
199253 }
@@ -228,6 +282,7 @@
228282 ) -> Result<MilestoneDto, IssueError> {
229283 let space = lookup_space_by_name(self, space_name).await?;
230284 require_member(self, &space, caller).await?;
285 require_board_manage(self, &space, caller).await?;
231286 let board = lookup_board(self, &space, board_id).await?;
232287
233288 let m = milestone::Entity::find_by_id(milestone_id)
@@ -282,6 +337,7 @@
282337 ) -> Result<(), IssueError> {
283338 let space = lookup_space_by_name(self, space_name).await?;
284339 require_member(self, &space, caller).await?;
340 require_board_manage(self, &space, caller).await?;
285341 let board = lookup_board(self, &space, board_id).await?;
286342
287343 let _ = milestone::Entity::find_by_id(milestone_id)
Modified lib/issues/src/services/pins.rs View lib/issues/src/services/pins.rs
@@ -9,6 +9,7 @@
99
1010use crate::errors::IssueError;
1111use crate::services::service::IssueService;
12use crate::share::authz::require_board_manage;
1213use crate::share::lookup::{lookup_board, lookup_issue, lookup_space_by_name, require_member};
1314
1415#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
@@ -55,6 +56,7 @@
5556 ) -> Result<PinDto, IssueError> {
5657 let space = lookup_space_by_name(self, space_name).await?;
5758 require_member(self, &space, caller).await?;
59 require_board_manage(self, &space, caller).await?;
5860 let board = lookup_board(self, &space, board_id).await?;
5961 let _ = lookup_issue(self, &board, cmd.issue_id).await?;
6062
@@ -117,6 +119,7 @@
117119 ) -> Result<(), IssueError> {
118120 let space = lookup_space_by_name(self, space_name).await?;
119121 require_member(self, &space, caller).await?;
122 require_board_manage(self, &space, caller).await?;
120123 let board = lookup_board(self, &space, board_id).await?;
121124
122125 let p = issue_pin::Entity::find_by_id(pin_id)
Modified lib/issues/src/services/reactions.rs View lib/issues/src/services/reactions.rs
@@ -2,10 +2,12 @@
22
33use models::issue::comment;
44use models::issue::issue_reactions;
5use models::space::space_join_rule::SpaceJoinTrigger;
56use sea_orm::{
67 ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set,
78};
89use serde::{Deserialize, Serialize};
10use space::services::join_rules::apply_space_join_rules;
911use space::services::spaces::UserBriefDto;
1012use time::OffsetDateTime;
1113use utoipa::{IntoParams, ToSchema};
@@ -107,6 +109,17 @@
107109 };
108110
109111 let inserted = row.insert(self.db().write()).await?;
112 if let Err(e) = apply_space_join_rules(
113 self.db().write(),
114 space.id,
115 caller,
116 SpaceJoinTrigger::IssueReaction,
117 &emoji,
118 )
119 .await
120 {
121 tracing::warn!(%e, "space join rule application failed");
122 }
110123 let user_map = self.load_user_briefs(&[caller]).await?;
111124 let user = user_map.get(&caller).cloned();
112125
@@ -165,6 +178,17 @@
165178 };
166179
167180 let inserted = row.insert(self.db().write()).await?;
181 if let Err(e) = apply_space_join_rules(
182 self.db().write(),
183 space.id,
184 caller,
185 SpaceJoinTrigger::IssueCommentReaction,
186 &emoji,
187 )
188 .await
189 {
190 tracing::warn!(%e, "space join rule application failed");
191 }
168192 let user_map = self.load_user_briefs(&[caller]).await?;
169193 let user = user_map.get(&caller).cloned();
170194
@@ -278,6 +302,21 @@
278302 .await?
279303 .ok_or(IssueError::NotFound)?;
280304
305 let allowed = match r.issue_id {
306 Some(iid) => iid == issue_id,
307 None => match r.comment_id {
308 Some(cid) => comment::Entity::find_by_id(cid)
309 .filter(comment::Column::IssueId.eq(Some(issue_id)))
310 .one(self.db().read())
311 .await?
312 .is_some(),
313 None => false,
314 },
315 };
316 if !allowed {
317 return Err(IssueError::NotFound);
318 }
319
281320 issue_reactions::Entity::delete_by_id(r.id)
282321 .exec(self.db().write())
283322 .await?;
Modified lib/issues/src/services/repo_relations.rs View lib/issues/src/services/repo_relations.rs
@@ -10,6 +10,7 @@
1010
1111use crate::errors::IssueError;
1212use crate::services::service::IssueService;
13use crate::share::authz::require_board_manage;
1314use crate::share::lookup::{lookup_board, lookup_issue, lookup_space_by_name, require_member};
1415
1516#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
@@ -55,6 +56,7 @@
5556 ) -> Result<RepoRelDto, IssueError> {
5657 let space = lookup_space_by_name(self, space_name).await?;
5758 require_member(self, &space, caller).await?;
59 require_board_manage(self, &space, caller).await?;
5860 let board = lookup_board(self, &space, board_id).await?;
5961 let _ = lookup_issue(self, &board, issue_id).await?;
6062
@@ -116,6 +118,7 @@
116118 ) -> Result<(), IssueError> {
117119 let space = lookup_space_by_name(self, space_name).await?;
118120 require_member(self, &space, caller).await?;
121 require_board_manage(self, &space, caller).await?;
119122 let board = lookup_board(self, &space, board_id).await?;
120123 let _ = lookup_issue(self, &board, issue_id).await?;
121124
Modified lib/issues/src/services/tracked_time.rs View lib/issues/src/services/tracked_time.rs
@@ -58,7 +58,7 @@
5858 let max_pagination = self.config().issues.max_pagination;
5959 let default_pagination = self.config().issues.default_pagination;
6060 let limit = q.limit.unwrap_or(default_pagination).clamp(1, max_pagination);
61 let offset = q.offset.unwrap_or(0);
61 let offset = q.offset.unwrap_or(0).clamp(0, 5000);
6262
6363 let mut select = tracked_time::Entity::find()
6464 .filter(tracked_time::Column::IssueId.eq(issue_id));
Modified lib/issues/src/share/authz.rs View lib/issues/src/share/authz.rs
@@ -10,12 +10,23 @@
1010 space: &spaces::Model,
1111 caller: Uuid,
1212) -> Result<(), IssueError> {
13 let perms = effective_permissions(service.db().read(), space, caller)
14 .await
15 .map_err(IssueError::from)?;
16 if space.owner_id == caller || perms.has(MANAGE_CHANNELS) {
13 if is_board_manager(service, space, caller).await? {
1714 Ok(())
1815 } else {
1916 Err(IssueError::Forbidden)
2017 }
2118}
19
20pub async fn is_board_manager(
21 service: &IssueService,
22 space: &spaces::Model,
23 caller: Uuid,
24) -> Result<bool, IssueError> {
25 if space.owner_id == caller {
26 return Ok(true);
27 }
28 let perms = effective_permissions(service.db().read(), space, caller)
29 .await
30 .map_err(IssueError::from)?;
31 Ok(perms.has(MANAGE_CHANNELS))
32}
Modified lib/issues/src/share/guard.rs View lib/issues/src/share/guard.rs
@@ -31,7 +31,11 @@
3131 let authorized = match outcome {
3232 Ok(Some((row, Some(current)))) => {
3333 !row.is_revoked
34 && row.user_id == uid
3435 && row.expires_at > OffsetDateTime::now_utc()
36 && current.is_active
37 && !current.prohibit_login
38 && !current.is_restricted
3539 && pwd_version_matches(&session, current.passwd_version)
3640 }
3741 Ok(_) => false,
Modified lib/issues/src/share/lookup.rs View lib/issues/src/share/lookup.rs
@@ -3,7 +3,7 @@
33use models::issue::issues;
44use models::space::spaces;
55use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
6use space::share::authz::effective_permissions;
6use space::share::authz::{effective_permissions, READ, VIEW_SPACE};
77use uuid::Uuid;
88
99use crate::errors::IssueError;
@@ -65,8 +65,12 @@
6565 space: &spaces::Model,
6666 caller: Uuid,
6767) -> Result<(), IssueError> {
68 effective_permissions(service.db().read(), space, caller)
68 let perms = effective_permissions(service.db().read(), space, caller)
6969 .await
7070 .map_err(IssueError::from)?;
71 Ok(())
71 if perms.has(VIEW_SPACE) || perms.has(READ) {
72 Ok(())
73 } else {
74 Err(IssueError::Forbidden)
75 }
7276}