1use std::sync::Arc;
47
48use crate::models::group::{Group, GroupId, GroupInvite, GroupMember, GroupMembership, GroupRole};
49use crate::storage::group::GroupBackend;
50use crate::{Error, Result};
51
52pub struct GroupService {
57 backend: Arc<dyn GroupBackend>,
58}
59
60impl GroupService {
61 #[must_use]
63 pub fn new(backend: Arc<dyn GroupBackend>) -> Self {
64 Self { backend }
65 }
66
67 pub fn try_default() -> crate::Result<Self> {
75 use crate::services::PathManager;
76 use crate::storage::group::SqliteGroupBackend;
77
78 let user_dir = crate::storage::get_user_data_dir()?;
79 let paths = PathManager::for_user(&user_dir);
80 let db_path = paths.index_path().join("groups.db");
81 let backend = Arc::new(SqliteGroupBackend::new(&db_path)?);
82 Ok(Self::new(backend))
83 }
84
85 pub fn create_group(
106 &self,
107 org_id: &str,
108 name: &str,
109 description: &str,
110 creator_email: &str,
111 ) -> Result<Group> {
112 if name.is_empty() {
114 return Err(Error::InvalidInput(
115 "Group name cannot be empty".to_string(),
116 ));
117 }
118 if creator_email.is_empty() {
119 return Err(Error::InvalidInput(
120 "Creator email cannot be empty".to_string(),
121 ));
122 }
123
124 let group = self
126 .backend
127 .create_group(org_id, name, description, creator_email)?;
128
129 self.backend
131 .add_member(&group.id, creator_email, GroupRole::Admin, creator_email)?;
132
133 tracing::info!(
134 org_id = %org_id,
135 group_id = %group.id.as_str(),
136 group_name = %name,
137 creator = %creator_email,
138 "Group created"
139 );
140
141 Ok(group)
142 }
143
144 pub fn get_group(&self, group_id: &GroupId) -> Result<Option<Group>> {
150 self.backend.get_group(group_id)
151 }
152
153 pub fn get_group_by_name(&self, org_id: &str, name: &str) -> Result<Option<Group>> {
159 self.backend.get_group_by_name(org_id, name)
160 }
161
162 pub fn list_groups(&self, org_id: &str) -> Result<Vec<Group>> {
168 self.backend.list_groups(org_id)
169 }
170
171 pub fn delete_group(&self, group_id: &GroupId, requester_email: &str) -> Result<bool> {
186 self.require_admin(group_id, requester_email)?;
188
189 let deleted = self.backend.delete_group(group_id)?;
190
191 if deleted {
192 tracing::info!(
193 group_id = %group_id.as_str(),
194 requester = %requester_email,
195 "Group deleted"
196 );
197 }
198
199 Ok(deleted)
200 }
201
202 pub fn add_member(
225 &self,
226 group_id: &GroupId,
227 email: &str,
228 role: GroupRole,
229 requester_email: &str,
230 ) -> Result<GroupMember> {
231 self.require_admin(group_id, requester_email)?;
233
234 let member = self
235 .backend
236 .add_member(group_id, email, role, requester_email)?;
237
238 tracing::info!(
239 group_id = %group_id.as_str(),
240 member_email = %email,
241 role = %role.as_str(),
242 added_by = %requester_email,
243 "Member added to group"
244 );
245
246 Ok(member)
247 }
248
249 pub fn get_member(&self, group_id: &GroupId, email: &str) -> Result<Option<GroupMember>> {
255 self.backend.get_member(group_id, email)
256 }
257
258 pub fn update_member_role(
276 &self,
277 group_id: &GroupId,
278 email: &str,
279 new_role: GroupRole,
280 requester_email: &str,
281 ) -> Result<bool> {
282 self.require_admin(group_id, requester_email)?;
284
285 if let Some(member) = self.backend.get_member(group_id, email)?
287 && member.role == GroupRole::Admin
288 && new_role != GroupRole::Admin
289 {
290 let admin_count = self.backend.count_admins(group_id)?;
292 if admin_count <= 1 {
293 return Err(Error::InvalidInput(
294 "Cannot demote the last admin. Promote another member first.".to_string(),
295 ));
296 }
297 }
298
299 let updated = self.backend.update_member_role(group_id, email, new_role)?;
300
301 if updated {
302 tracing::info!(
303 group_id = %group_id.as_str(),
304 member_email = %email,
305 new_role = %new_role.as_str(),
306 updated_by = %requester_email,
307 "Member role updated"
308 );
309 }
310
311 Ok(updated)
312 }
313
314 pub fn remove_member(
331 &self,
332 group_id: &GroupId,
333 email: &str,
334 requester_email: &str,
335 ) -> Result<bool> {
336 self.require_admin(group_id, requester_email)?;
338
339 if let Some(member) = self.backend.get_member(group_id, email)?
341 && member.role == GroupRole::Admin
342 {
343 let admin_count = self.backend.count_admins(group_id)?;
345 if admin_count <= 1 {
346 return Err(Error::InvalidInput(
347 "Cannot remove the last admin. Promote another member first.".to_string(),
348 ));
349 }
350 }
351
352 let removed = self.backend.remove_member(group_id, email)?;
353
354 if removed {
355 tracing::info!(
356 group_id = %group_id.as_str(),
357 member_email = %email,
358 removed_by = %requester_email,
359 "Member removed from group"
360 );
361 }
362
363 Ok(removed)
364 }
365
366 pub fn list_members(&self, group_id: &GroupId) -> Result<Vec<GroupMember>> {
372 self.backend.list_members(group_id)
373 }
374
375 pub fn get_user_groups(&self, org_id: &str, email: &str) -> Result<Vec<GroupMembership>> {
381 self.backend.get_user_groups(org_id, email)
382 }
383
384 pub fn leave_group(&self, group_id: &GroupId, email: &str) -> Result<bool> {
399 if let Some(member) = self.backend.get_member(group_id, email)?
401 && member.role == GroupRole::Admin
402 {
403 let admin_count = self.backend.count_admins(group_id)?;
404 if admin_count <= 1 {
405 return Err(Error::InvalidInput(
406 "Cannot leave as the last admin. Promote another member first.".to_string(),
407 ));
408 }
409 }
410
411 let left = self.backend.remove_member(group_id, email)?;
412
413 if left {
414 tracing::info!(
415 group_id = %group_id.as_str(),
416 member_email = %email,
417 "Member left group"
418 );
419 }
420
421 Ok(left)
422 }
423
424 pub fn create_invite(
451 &self,
452 group_id: &GroupId,
453 role: GroupRole,
454 creator_email: &str,
455 expires_in_secs: Option<u64>,
456 max_uses: Option<u32>,
457 ) -> Result<(GroupInvite, String)> {
458 self.require_admin(group_id, creator_email)?;
460
461 let (invite, token) =
462 self.backend
463 .create_invite(group_id, role, creator_email, expires_in_secs, max_uses)?;
464
465 tracing::info!(
466 group_id = %group_id.as_str(),
467 invite_id = %invite.id,
468 role = %role.as_str(),
469 created_by = %creator_email,
470 expires_in_secs = ?expires_in_secs,
471 max_uses = ?max_uses,
472 "Group invite created"
473 );
474
475 Ok((invite, token))
476 }
477
478 pub fn join_via_invite(&self, token: &str, email: &str) -> Result<GroupMember> {
497 let token_hash = GroupInvite::hash_token(token);
499 let invite = self
500 .backend
501 .get_invite_by_token_hash(&token_hash)?
502 .ok_or_else(|| Error::InvalidInput("Invalid invite token".to_string()))?;
503
504 if !invite.is_valid() {
506 return Err(Error::InvalidInput(
507 "Invite is expired or has reached its usage limit".to_string(),
508 ));
509 }
510
511 if let Some(existing) = self.backend.get_member(&invite.group_id, email)? {
513 return Err(Error::InvalidInput(format!(
514 "Already a member of this group with role '{}'",
515 existing.role.as_str()
516 )));
517 }
518
519 let member = self.backend.add_member(
521 &invite.group_id,
522 email,
523 invite.role,
524 &invite.created_by, )?;
526
527 self.backend.increment_invite_uses(&invite.id)?;
529
530 tracing::info!(
531 group_id = %invite.group_id.as_str(),
532 invite_id = %invite.id,
533 member_email = %email,
534 role = %invite.role.as_str(),
535 "Member joined via invite"
536 );
537
538 Ok(member)
539 }
540
541 pub fn list_invites(
552 &self,
553 group_id: &GroupId,
554 include_expired: bool,
555 ) -> Result<Vec<GroupInvite>> {
556 self.backend.list_invites(group_id, include_expired)
557 }
558
559 pub fn revoke_invite(&self, invite_id: &str, requester_email: &str) -> Result<bool> {
574 let invite = self
576 .backend
577 .get_invite(invite_id)?
578 .ok_or_else(|| Error::InvalidInput("Invite not found".to_string()))?;
579
580 self.require_admin(&invite.group_id, requester_email)?;
582
583 let revoked = self.backend.revoke_invite(invite_id)?;
584
585 if revoked {
586 tracing::info!(
587 invite_id = %invite_id,
588 group_id = %invite.group_id.as_str(),
589 revoked_by = %requester_email,
590 "Group invite revoked"
591 );
592 }
593
594 Ok(revoked)
595 }
596
597 pub fn cleanup_expired_invites(&self) -> Result<u64> {
607 self.backend.cleanup_expired_invites()
608 }
609
610 fn require_admin(&self, group_id: &GroupId, email: &str) -> Result<()> {
616 let member = self.backend.get_member(group_id, email)?.ok_or_else(|| {
617 Error::Unauthorized(format!("User '{email}' is not a member of this group"))
618 })?;
619
620 if member.role != GroupRole::Admin {
621 return Err(Error::Unauthorized(
622 "Admin role required for this operation".to_string(),
623 ));
624 }
625
626 Ok(())
627 }
628
629 pub fn require_role(&self, group_id: &GroupId, email: &str, min_role: GroupRole) -> Result<()> {
639 let member = self.backend.get_member(group_id, email)?.ok_or_else(|| {
640 Error::Unauthorized(format!("User '{email}' is not a member of this group"))
641 })?;
642
643 let has_permission = match min_role {
645 GroupRole::Admin => member.role.can_manage(),
646 GroupRole::Write => member.role.can_write(),
647 GroupRole::Read => member.role.can_read(),
648 };
649
650 if !has_permission {
651 return Err(Error::Unauthorized(format!(
652 "Role '{}' or higher required, but user has '{}'",
653 min_role.as_str(),
654 member.role.as_str()
655 )));
656 }
657
658 Ok(())
659 }
660
661 pub fn is_member(&self, group_id: &GroupId, email: &str) -> Result<bool> {
667 Ok(self.backend.get_member(group_id, email)?.is_some())
668 }
669
670 pub fn get_user_role(&self, group_id: &GroupId, email: &str) -> Result<Option<GroupRole>> {
676 Ok(self.backend.get_member(group_id, email)?.map(|m| m.role))
677 }
678}
679
680#[cfg(test)]
681mod tests {
682 use super::*;
683 use crate::storage::group::GroupStorageFactory;
684
685 fn create_test_service() -> GroupService {
686 let backend = GroupStorageFactory::create_in_memory().expect("Failed to create backend");
687 GroupService::new(backend)
688 }
689
690 #[test]
691 fn test_create_group_adds_creator_as_admin() {
692 let service = create_test_service();
693
694 let group = service
695 .create_group("test-org", "engineering", "Eng team", "alice@example.com")
696 .expect("Failed to create group");
697
698 let member = service
700 .get_member(&group.id, "alice@example.com")
701 .expect("Failed to get member")
702 .expect("Member not found");
703
704 assert_eq!(member.role, GroupRole::Admin);
705 }
706
707 #[test]
708 fn test_add_member_requires_admin() {
709 let service = create_test_service();
710
711 let group = service
713 .create_group("test-org", "engineering", "", "alice@example.com")
714 .expect("Failed to create group");
715
716 service
718 .add_member(
719 &group.id,
720 "bob@example.com",
721 GroupRole::Write,
722 "alice@example.com",
723 )
724 .expect("Failed to add member");
725
726 let result = service.add_member(
728 &group.id,
729 "charlie@example.com",
730 GroupRole::Read,
731 "bob@example.com",
732 );
733 assert!(result.is_err());
734 assert!(result.unwrap_err().to_string().contains("Admin role"));
735 }
736
737 #[test]
738 fn test_cannot_remove_last_admin() {
739 let service = create_test_service();
740
741 let group = service
742 .create_group("test-org", "engineering", "", "alice@example.com")
743 .expect("Failed to create group");
744
745 let result = service.remove_member(&group.id, "alice@example.com", "alice@example.com");
747 assert!(result.is_err());
748 assert!(result.unwrap_err().to_string().contains("last admin"));
749 }
750
751 #[test]
752 fn test_cannot_demote_last_admin() {
753 let service = create_test_service();
754
755 let group = service
756 .create_group("test-org", "engineering", "", "alice@example.com")
757 .expect("Failed to create group");
758
759 let result = service.update_member_role(
761 &group.id,
762 "alice@example.com",
763 GroupRole::Write,
764 "alice@example.com",
765 );
766 assert!(result.is_err());
767 assert!(result.unwrap_err().to_string().contains("last admin"));
768 }
769
770 #[test]
771 fn test_invite_workflow() {
772 let service = create_test_service();
773
774 let group = service
775 .create_group("test-org", "engineering", "", "alice@example.com")
776 .expect("Failed to create group");
777
778 let (invite, token) = service
780 .create_invite(
781 &group.id,
782 GroupRole::Write,
783 "alice@example.com",
784 Some(3600),
785 Some(5),
786 )
787 .expect("Failed to create invite");
788
789 assert_eq!(invite.role, GroupRole::Write);
790 assert!(!token.is_empty());
791
792 let member = service
794 .join_via_invite(&token, "bob@example.com")
795 .expect("Failed to join");
796
797 assert_eq!(member.role, GroupRole::Write);
798
799 let result = service.join_via_invite(&token, "bob@example.com");
801 assert!(result.is_err());
802 assert!(result.unwrap_err().to_string().contains("Already a member"));
803 }
804
805 #[test]
806 fn test_leave_group() {
807 let service = create_test_service();
808
809 let group = service
810 .create_group("test-org", "engineering", "", "alice@example.com")
811 .expect("Failed to create group");
812
813 service
815 .add_member(
816 &group.id,
817 "bob@example.com",
818 GroupRole::Write,
819 "alice@example.com",
820 )
821 .expect("Failed to add member");
822
823 assert!(
825 service
826 .leave_group(&group.id, "bob@example.com")
827 .expect("Failed to leave")
828 );
829
830 assert!(
832 !service
833 .is_member(&group.id, "bob@example.com")
834 .expect("Failed to check membership")
835 );
836 }
837
838 #[test]
839 fn test_last_admin_cannot_leave() {
840 let service = create_test_service();
841
842 let group = service
843 .create_group("test-org", "engineering", "", "alice@example.com")
844 .expect("Failed to create group");
845
846 let result = service.leave_group(&group.id, "alice@example.com");
848 assert!(result.is_err());
849 assert!(result.unwrap_err().to_string().contains("last admin"));
850 }
851
852 #[test]
853 fn test_get_user_groups() {
854 let service = create_test_service();
855
856 let group1 = service
858 .create_group("test-org", "engineering", "", "alice@example.com")
859 .expect("Failed to create group 1");
860 let _group2 = service
861 .create_group("test-org", "design", "", "alice@example.com")
862 .expect("Failed to create group 2");
863
864 service
866 .add_member(
867 &group1.id,
868 "bob@example.com",
869 GroupRole::Write,
870 "alice@example.com",
871 )
872 .expect("Failed to add member");
873
874 let alice_groups = service
876 .get_user_groups("test-org", "alice@example.com")
877 .expect("Failed to get groups");
878 assert_eq!(alice_groups.len(), 2);
879
880 let bob_groups = service
882 .get_user_groups("test-org", "bob@example.com")
883 .expect("Failed to get groups");
884 assert_eq!(bob_groups.len(), 1);
885 assert_eq!(bob_groups[0].group_id, group1.id);
886 }
887
888 #[test]
889 fn test_require_role() {
890 let service = create_test_service();
891
892 let group = service
893 .create_group("test-org", "engineering", "", "alice@example.com")
894 .expect("Failed to create group");
895
896 service
898 .add_member(
899 &group.id,
900 "bob@example.com",
901 GroupRole::Write,
902 "alice@example.com",
903 )
904 .expect("Failed to add member");
905
906 service
908 .add_member(
909 &group.id,
910 "charlie@example.com",
911 GroupRole::Read,
912 "alice@example.com",
913 )
914 .expect("Failed to add member");
915
916 assert!(
918 service
919 .require_role(&group.id, "alice@example.com", GroupRole::Admin)
920 .is_ok()
921 );
922 assert!(
923 service
924 .require_role(&group.id, "alice@example.com", GroupRole::Write)
925 .is_ok()
926 );
927 assert!(
928 service
929 .require_role(&group.id, "alice@example.com", GroupRole::Read)
930 .is_ok()
931 );
932
933 assert!(
935 service
936 .require_role(&group.id, "bob@example.com", GroupRole::Admin)
937 .is_err()
938 );
939 assert!(
940 service
941 .require_role(&group.id, "bob@example.com", GroupRole::Write)
942 .is_ok()
943 );
944 assert!(
945 service
946 .require_role(&group.id, "bob@example.com", GroupRole::Read)
947 .is_ok()
948 );
949
950 assert!(
952 service
953 .require_role(&group.id, "charlie@example.com", GroupRole::Admin)
954 .is_err()
955 );
956 assert!(
957 service
958 .require_role(&group.id, "charlie@example.com", GroupRole::Write)
959 .is_err()
960 );
961 assert!(
962 service
963 .require_role(&group.id, "charlie@example.com", GroupRole::Read)
964 .is_ok()
965 );
966 }
967}