1use std::path::{Path, PathBuf};
6use std::sync::Mutex;
7
8use rusqlite::{Connection, OptionalExtension, params};
9
10use crate::models::group::{
11 Group, GroupId, GroupInvite, GroupMember, GroupMembership, GroupRole, normalize_email,
12};
13use crate::{Error, Result};
14
15use super::traits::GroupBackend;
16
17pub struct SqliteGroupBackend {
22 conn: Mutex<Connection>,
24}
25
26impl SqliteGroupBackend {
27 pub fn new(path: impl AsRef<Path>) -> Result<Self> {
37 let conn = Connection::open(path.as_ref()).map_err(|e| Error::OperationFailed {
38 operation: "open_group_database".to_string(),
39 cause: e.to_string(),
40 })?;
41
42 let backend = Self {
43 conn: Mutex::new(conn),
44 };
45 backend.initialize_schema()?;
46 Ok(backend)
47 }
48
49 pub fn in_memory() -> Result<Self> {
55 let conn = Connection::open_in_memory().map_err(|e| Error::OperationFailed {
56 operation: "open_group_database_memory".to_string(),
57 cause: e.to_string(),
58 })?;
59
60 let backend = Self {
61 conn: Mutex::new(conn),
62 };
63 backend.initialize_schema()?;
64 Ok(backend)
65 }
66
67 #[must_use]
71 pub fn default_org_path(org: &str) -> Option<PathBuf> {
72 directories::BaseDirs::new().map(|d| {
73 d.home_dir()
74 .join(".config")
75 .join("subcog")
76 .join("orgs")
77 .join(org)
78 .join("memories.db")
79 })
80 }
81
82 fn initialize_schema(&self) -> Result<()> {
84 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
85 operation: "lock_connection".to_string(),
86 cause: e.to_string(),
87 })?;
88
89 conn.execute_batch(
90 r"
91 -- Enable foreign keys first
92 PRAGMA foreign_keys = ON;
93
94 -- Groups table
95 CREATE TABLE IF NOT EXISTS groups (
96 id TEXT PRIMARY KEY,
97 org_id TEXT NOT NULL,
98 name TEXT NOT NULL,
99 description TEXT NOT NULL DEFAULT '',
100 created_at INTEGER NOT NULL,
101 updated_at INTEGER NOT NULL,
102 created_by TEXT NOT NULL,
103 UNIQUE(org_id, name)
104 );
105
106 CREATE INDEX IF NOT EXISTS idx_groups_org ON groups(org_id);
107
108 -- Group members table
109 CREATE TABLE IF NOT EXISTS group_members (
110 id TEXT PRIMARY KEY,
111 group_id TEXT NOT NULL,
112 email TEXT NOT NULL,
113 role TEXT NOT NULL,
114 joined_at INTEGER NOT NULL,
115 added_by TEXT NOT NULL,
116 UNIQUE(group_id, email),
117 FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE
118 );
119
120 CREATE INDEX IF NOT EXISTS idx_group_members_group ON group_members(group_id);
121 CREATE INDEX IF NOT EXISTS idx_group_members_email ON group_members(email);
122
123 -- Group invites table
124 CREATE TABLE IF NOT EXISTS group_invites (
125 id TEXT PRIMARY KEY,
126 group_id TEXT NOT NULL,
127 token_hash TEXT NOT NULL UNIQUE,
128 role TEXT NOT NULL,
129 created_by TEXT NOT NULL,
130 created_at INTEGER NOT NULL,
131 expires_at INTEGER NOT NULL,
132 max_uses INTEGER,
133 current_uses INTEGER NOT NULL DEFAULT 0,
134 revoked INTEGER NOT NULL DEFAULT 0,
135 FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE
136 );
137
138 CREATE INDEX IF NOT EXISTS idx_group_invites_group ON group_invites(group_id);
139 CREATE INDEX IF NOT EXISTS idx_group_invites_token_hash ON group_invites(token_hash);
140 CREATE INDEX IF NOT EXISTS idx_group_invites_expires ON group_invites(expires_at);
141 ",
142 )
143 .map_err(|e| Error::OperationFailed {
144 operation: "initialize_group_schema".to_string(),
145 cause: e.to_string(),
146 })?;
147
148 Ok(())
149 }
150
151 #[allow(clippy::cast_possible_wrap)]
153 fn now() -> i64 {
154 std::time::SystemTime::now()
155 .duration_since(std::time::UNIX_EPOCH)
156 .map(|d| d.as_secs() as i64)
157 .unwrap_or(0)
158 }
159
160 #[allow(clippy::cast_possible_wrap)]
162 const fn to_db_timestamp(ts: u64) -> i64 {
163 ts as i64
164 }
165
166 #[allow(clippy::cast_sign_loss)]
168 const fn from_db_timestamp(ts: i64) -> u64 {
169 ts as u64
170 }
171
172 fn parse_role(s: &str) -> GroupRole {
174 GroupRole::parse(s).unwrap_or(GroupRole::Read)
175 }
176}
177
178impl GroupBackend for SqliteGroupBackend {
179 fn create_group(
180 &self,
181 org_id: &str,
182 name: &str,
183 description: &str,
184 created_by: &str,
185 ) -> Result<Group> {
186 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
187 operation: "lock_connection".to_string(),
188 cause: e.to_string(),
189 })?;
190
191 let now = Self::now();
192 let group = Group {
193 id: GroupId::generate(),
194 org_id: org_id.to_string(),
195 name: name.to_string(),
196 description: description.to_string(),
197 created_at: Self::from_db_timestamp(now),
198 updated_at: Self::from_db_timestamp(now),
199 created_by: normalize_email(created_by),
200 };
201
202 conn.execute(
203 "INSERT INTO groups (id, org_id, name, description, created_at, updated_at, created_by)
204 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
205 params![
206 group.id.as_str(),
207 group.org_id,
208 group.name,
209 group.description,
210 now,
211 now,
212 group.created_by,
213 ],
214 )
215 .map_err(|e| {
216 if e.to_string().contains("UNIQUE constraint failed") {
217 Error::InvalidInput(format!(
218 "Group '{name}' already exists in organization '{org_id}'"
219 ))
220 } else {
221 Error::OperationFailed {
222 operation: "create_group".to_string(),
223 cause: e.to_string(),
224 }
225 }
226 })?;
227
228 Ok(group)
229 }
230
231 fn get_group(&self, group_id: &GroupId) -> Result<Option<Group>> {
232 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
233 operation: "lock_connection".to_string(),
234 cause: e.to_string(),
235 })?;
236
237 let mut stmt = conn
238 .prepare(
239 "SELECT id, org_id, name, description, created_at, updated_at, created_by
240 FROM groups WHERE id = ?1",
241 )
242 .map_err(|e| Error::OperationFailed {
243 operation: "prepare_get_group".to_string(),
244 cause: e.to_string(),
245 })?;
246
247 let result = stmt
248 .query_row(params![group_id.as_str()], |row| {
249 Ok(Group {
250 id: GroupId::new(row.get::<_, String>(0)?),
251 org_id: row.get(1)?,
252 name: row.get(2)?,
253 description: row.get(3)?,
254 created_at: Self::from_db_timestamp(row.get(4)?),
255 updated_at: Self::from_db_timestamp(row.get(5)?),
256 created_by: row.get(6)?,
257 })
258 })
259 .optional()
260 .map_err(|e| Error::OperationFailed {
261 operation: "get_group".to_string(),
262 cause: e.to_string(),
263 })?;
264
265 Ok(result)
266 }
267
268 fn get_group_by_name(&self, org_id: &str, name: &str) -> Result<Option<Group>> {
269 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
270 operation: "lock_connection".to_string(),
271 cause: e.to_string(),
272 })?;
273
274 let mut stmt = conn
275 .prepare(
276 "SELECT id, org_id, name, description, created_at, updated_at, created_by
277 FROM groups WHERE org_id = ?1 AND name = ?2",
278 )
279 .map_err(|e| Error::OperationFailed {
280 operation: "prepare_get_group_by_name".to_string(),
281 cause: e.to_string(),
282 })?;
283
284 let result = stmt
285 .query_row(params![org_id, name], |row| {
286 Ok(Group {
287 id: GroupId::new(row.get::<_, String>(0)?),
288 org_id: row.get(1)?,
289 name: row.get(2)?,
290 description: row.get(3)?,
291 created_at: Self::from_db_timestamp(row.get(4)?),
292 updated_at: Self::from_db_timestamp(row.get(5)?),
293 created_by: row.get(6)?,
294 })
295 })
296 .optional()
297 .map_err(|e| Error::OperationFailed {
298 operation: "get_group_by_name".to_string(),
299 cause: e.to_string(),
300 })?;
301
302 Ok(result)
303 }
304
305 fn list_groups(&self, org_id: &str) -> Result<Vec<Group>> {
306 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
307 operation: "lock_connection".to_string(),
308 cause: e.to_string(),
309 })?;
310
311 let mut stmt = conn
312 .prepare(
313 "SELECT id, org_id, name, description, created_at, updated_at, created_by
314 FROM groups WHERE org_id = ?1 ORDER BY name",
315 )
316 .map_err(|e| Error::OperationFailed {
317 operation: "prepare_list_groups".to_string(),
318 cause: e.to_string(),
319 })?;
320
321 let groups = stmt
322 .query_map(params![org_id], |row| {
323 Ok(Group {
324 id: GroupId::new(row.get::<_, String>(0)?),
325 org_id: row.get(1)?,
326 name: row.get(2)?,
327 description: row.get(3)?,
328 created_at: Self::from_db_timestamp(row.get(4)?),
329 updated_at: Self::from_db_timestamp(row.get(5)?),
330 created_by: row.get(6)?,
331 })
332 })
333 .map_err(|e| Error::OperationFailed {
334 operation: "list_groups".to_string(),
335 cause: e.to_string(),
336 })?
337 .collect::<std::result::Result<Vec<_>, _>>()
338 .map_err(|e| Error::OperationFailed {
339 operation: "collect_groups".to_string(),
340 cause: e.to_string(),
341 })?;
342
343 Ok(groups)
344 }
345
346 fn delete_group(&self, group_id: &GroupId) -> Result<bool> {
347 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
348 operation: "lock_connection".to_string(),
349 cause: e.to_string(),
350 })?;
351
352 let rows = conn
353 .execute(
354 "DELETE FROM groups WHERE id = ?1",
355 params![group_id.as_str()],
356 )
357 .map_err(|e| Error::OperationFailed {
358 operation: "delete_group".to_string(),
359 cause: e.to_string(),
360 })?;
361
362 Ok(rows > 0)
363 }
364
365 fn add_member(
366 &self,
367 group_id: &GroupId,
368 email: &str,
369 role: GroupRole,
370 added_by: &str,
371 ) -> Result<GroupMember> {
372 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
373 operation: "lock_connection".to_string(),
374 cause: e.to_string(),
375 })?;
376
377 let member = GroupMember::new(group_id.clone(), email, role, added_by);
378
379 conn.execute(
382 "INSERT INTO group_members (id, group_id, email, role, joined_at, added_by)
383 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
384 ON CONFLICT (group_id, email) DO UPDATE SET
385 role = excluded.role,
386 added_by = excluded.added_by",
387 params![
388 member.id,
389 member.group_id.as_str(),
390 member.email,
391 member.role.as_str(),
392 Self::to_db_timestamp(member.joined_at),
393 member.added_by,
394 ],
395 )
396 .map_err(|e| Error::OperationFailed {
397 operation: "add_member".to_string(),
398 cause: e.to_string(),
399 })?;
400
401 Ok(member)
402 }
403
404 fn get_member(&self, group_id: &GroupId, email: &str) -> Result<Option<GroupMember>> {
405 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
406 operation: "lock_connection".to_string(),
407 cause: e.to_string(),
408 })?;
409
410 let normalized_email = normalize_email(email);
411
412 let mut stmt = conn
413 .prepare(
414 "SELECT id, group_id, email, role, joined_at, added_by
415 FROM group_members WHERE group_id = ?1 AND email = ?2",
416 )
417 .map_err(|e| Error::OperationFailed {
418 operation: "prepare_get_member".to_string(),
419 cause: e.to_string(),
420 })?;
421
422 let result = stmt
423 .query_row(params![group_id.as_str(), normalized_email], |row| {
424 Ok(GroupMember {
425 id: row.get(0)?,
426 group_id: GroupId::new(row.get::<_, String>(1)?),
427 email: row.get(2)?,
428 role: Self::parse_role(&row.get::<_, String>(3)?),
429 joined_at: Self::from_db_timestamp(row.get(4)?),
430 added_by: row.get(5)?,
431 })
432 })
433 .optional()
434 .map_err(|e| Error::OperationFailed {
435 operation: "get_member".to_string(),
436 cause: e.to_string(),
437 })?;
438
439 Ok(result)
440 }
441
442 fn update_member_role(
443 &self,
444 group_id: &GroupId,
445 email: &str,
446 new_role: GroupRole,
447 ) -> Result<bool> {
448 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
449 operation: "lock_connection".to_string(),
450 cause: e.to_string(),
451 })?;
452
453 let normalized_email = normalize_email(email);
454
455 let rows = conn
456 .execute(
457 "UPDATE group_members SET role = ?1 WHERE group_id = ?2 AND email = ?3",
458 params![new_role.as_str(), group_id.as_str(), normalized_email],
459 )
460 .map_err(|e| Error::OperationFailed {
461 operation: "update_member_role".to_string(),
462 cause: e.to_string(),
463 })?;
464
465 Ok(rows > 0)
466 }
467
468 fn remove_member(&self, group_id: &GroupId, email: &str) -> Result<bool> {
469 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
470 operation: "lock_connection".to_string(),
471 cause: e.to_string(),
472 })?;
473
474 let normalized_email = normalize_email(email);
475
476 let rows = conn
477 .execute(
478 "DELETE FROM group_members WHERE group_id = ?1 AND email = ?2",
479 params![group_id.as_str(), normalized_email],
480 )
481 .map_err(|e| Error::OperationFailed {
482 operation: "remove_member".to_string(),
483 cause: e.to_string(),
484 })?;
485
486 Ok(rows > 0)
487 }
488
489 fn list_members(&self, group_id: &GroupId) -> Result<Vec<GroupMember>> {
490 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
491 operation: "lock_connection".to_string(),
492 cause: e.to_string(),
493 })?;
494
495 let mut stmt = conn
496 .prepare(
497 "SELECT id, group_id, email, role, joined_at, added_by
498 FROM group_members WHERE group_id = ?1 ORDER BY email",
499 )
500 .map_err(|e| Error::OperationFailed {
501 operation: "prepare_list_members".to_string(),
502 cause: e.to_string(),
503 })?;
504
505 let members = stmt
506 .query_map(params![group_id.as_str()], |row| {
507 Ok(GroupMember {
508 id: row.get(0)?,
509 group_id: GroupId::new(row.get::<_, String>(1)?),
510 email: row.get(2)?,
511 role: Self::parse_role(&row.get::<_, String>(3)?),
512 joined_at: Self::from_db_timestamp(row.get(4)?),
513 added_by: row.get(5)?,
514 })
515 })
516 .map_err(|e| Error::OperationFailed {
517 operation: "list_members".to_string(),
518 cause: e.to_string(),
519 })?
520 .collect::<std::result::Result<Vec<_>, _>>()
521 .map_err(|e| Error::OperationFailed {
522 operation: "collect_members".to_string(),
523 cause: e.to_string(),
524 })?;
525
526 Ok(members)
527 }
528
529 fn get_user_groups(&self, org_id: &str, email: &str) -> Result<Vec<GroupMembership>> {
530 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
531 operation: "lock_connection".to_string(),
532 cause: e.to_string(),
533 })?;
534
535 let normalized_email = normalize_email(email);
536
537 let mut stmt = conn
538 .prepare(
539 "SELECT g.id, g.name, g.org_id, gm.role
540 FROM groups g
541 JOIN group_members gm ON g.id = gm.group_id
542 WHERE g.org_id = ?1 AND gm.email = ?2
543 ORDER BY g.name",
544 )
545 .map_err(|e| Error::OperationFailed {
546 operation: "prepare_get_user_groups".to_string(),
547 cause: e.to_string(),
548 })?;
549
550 let memberships = stmt
551 .query_map(params![org_id, normalized_email], |row| {
552 Ok(GroupMembership {
553 group_id: GroupId::new(row.get::<_, String>(0)?),
554 group_name: row.get(1)?,
555 org_id: row.get(2)?,
556 role: Self::parse_role(&row.get::<_, String>(3)?),
557 })
558 })
559 .map_err(|e| Error::OperationFailed {
560 operation: "get_user_groups".to_string(),
561 cause: e.to_string(),
562 })?
563 .collect::<std::result::Result<Vec<_>, _>>()
564 .map_err(|e| Error::OperationFailed {
565 operation: "collect_user_groups".to_string(),
566 cause: e.to_string(),
567 })?;
568
569 Ok(memberships)
570 }
571
572 fn count_admins(&self, group_id: &GroupId) -> Result<u32> {
573 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
574 operation: "lock_connection".to_string(),
575 cause: e.to_string(),
576 })?;
577
578 let count: u32 = conn
579 .query_row(
580 "SELECT COUNT(*) FROM group_members WHERE group_id = ?1 AND role = 'admin'",
581 params![group_id.as_str()],
582 |row| row.get(0),
583 )
584 .map_err(|e| Error::OperationFailed {
585 operation: "count_admins".to_string(),
586 cause: e.to_string(),
587 })?;
588
589 Ok(count)
590 }
591
592 fn create_invite(
593 &self,
594 group_id: &GroupId,
595 role: GroupRole,
596 created_by: &str,
597 expires_in_secs: Option<u64>,
598 max_uses: Option<u32>,
599 ) -> Result<(GroupInvite, String)> {
600 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
601 operation: "lock_connection".to_string(),
602 cause: e.to_string(),
603 })?;
604
605 let (invite, token) = GroupInvite::new(
606 group_id.clone(),
607 role,
608 created_by,
609 expires_in_secs,
610 max_uses,
611 );
612
613 conn.execute(
614 "INSERT INTO group_invites
615 (id, group_id, token_hash, role, created_by, created_at, expires_at, max_uses, current_uses, revoked)
616 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
617 params![
618 invite.id,
619 invite.group_id.as_str(),
620 invite.token_hash,
621 invite.role.as_str(),
622 invite.created_by,
623 Self::to_db_timestamp(invite.created_at),
624 Self::to_db_timestamp(invite.expires_at),
625 invite.max_uses,
626 invite.current_uses,
627 i32::from(invite.revoked),
628 ],
629 )
630 .map_err(|e| Error::OperationFailed {
631 operation: "create_invite".to_string(),
632 cause: e.to_string(),
633 })?;
634
635 Ok((invite, token))
636 }
637
638 fn get_invite_by_token_hash(&self, token_hash: &str) -> Result<Option<GroupInvite>> {
639 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
640 operation: "lock_connection".to_string(),
641 cause: e.to_string(),
642 })?;
643
644 let mut stmt = conn
645 .prepare(
646 "SELECT id, group_id, token_hash, role, created_by, created_at, expires_at,
647 max_uses, current_uses, revoked
648 FROM group_invites WHERE token_hash = ?1",
649 )
650 .map_err(|e| Error::OperationFailed {
651 operation: "prepare_get_invite_by_token".to_string(),
652 cause: e.to_string(),
653 })?;
654
655 let result = stmt
656 .query_row(params![token_hash], |row| {
657 Ok(GroupInvite {
658 id: row.get(0)?,
659 group_id: GroupId::new(row.get::<_, String>(1)?),
660 token_hash: row.get(2)?,
661 role: Self::parse_role(&row.get::<_, String>(3)?),
662 created_by: row.get(4)?,
663 created_at: Self::from_db_timestamp(row.get(5)?),
664 expires_at: Self::from_db_timestamp(row.get(6)?),
665 max_uses: row.get(7)?,
666 current_uses: row.get(8)?,
667 revoked: row.get::<_, i32>(9)? != 0,
668 })
669 })
670 .optional()
671 .map_err(|e| Error::OperationFailed {
672 operation: "get_invite_by_token".to_string(),
673 cause: e.to_string(),
674 })?;
675
676 Ok(result)
677 }
678
679 fn get_invite(&self, invite_id: &str) -> Result<Option<GroupInvite>> {
680 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
681 operation: "lock_connection".to_string(),
682 cause: e.to_string(),
683 })?;
684
685 let mut stmt = conn
686 .prepare(
687 "SELECT id, group_id, token_hash, role, created_by, created_at, expires_at,
688 max_uses, current_uses, revoked
689 FROM group_invites WHERE id = ?1",
690 )
691 .map_err(|e| Error::OperationFailed {
692 operation: "prepare_get_invite".to_string(),
693 cause: e.to_string(),
694 })?;
695
696 let result = stmt
697 .query_row(params![invite_id], |row| {
698 Ok(GroupInvite {
699 id: row.get(0)?,
700 group_id: GroupId::new(row.get::<_, String>(1)?),
701 token_hash: row.get(2)?,
702 role: Self::parse_role(&row.get::<_, String>(3)?),
703 created_by: row.get(4)?,
704 created_at: Self::from_db_timestamp(row.get(5)?),
705 expires_at: Self::from_db_timestamp(row.get(6)?),
706 max_uses: row.get(7)?,
707 current_uses: row.get(8)?,
708 revoked: row.get::<_, i32>(9)? != 0,
709 })
710 })
711 .optional()
712 .map_err(|e| Error::OperationFailed {
713 operation: "get_invite".to_string(),
714 cause: e.to_string(),
715 })?;
716
717 Ok(result)
718 }
719
720 fn list_invites(&self, group_id: &GroupId, include_expired: bool) -> Result<Vec<GroupInvite>> {
721 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
722 operation: "lock_connection".to_string(),
723 cause: e.to_string(),
724 })?;
725
726 let query = if include_expired {
727 "SELECT id, group_id, token_hash, role, created_by, created_at, expires_at,
728 max_uses, current_uses, revoked
729 FROM group_invites WHERE group_id = ?1 ORDER BY created_at DESC"
730 } else {
731 "SELECT id, group_id, token_hash, role, created_by, created_at, expires_at,
732 max_uses, current_uses, revoked
733 FROM group_invites
734 WHERE group_id = ?1 AND revoked = 0 AND expires_at > ?2
735 ORDER BY created_at DESC"
736 };
737
738 let mut stmt = conn.prepare(query).map_err(|e| Error::OperationFailed {
739 operation: "prepare_list_invites".to_string(),
740 cause: e.to_string(),
741 })?;
742
743 let parse_invite = |row: &rusqlite::Row<'_>| -> rusqlite::Result<GroupInvite> {
745 Ok(GroupInvite {
746 id: row.get(0)?,
747 group_id: GroupId::new(row.get::<_, String>(1)?),
748 token_hash: row.get(2)?,
749 role: Self::parse_role(&row.get::<_, String>(3)?),
750 created_by: row.get(4)?,
751 created_at: Self::from_db_timestamp(row.get(5)?),
752 expires_at: Self::from_db_timestamp(row.get(6)?),
753 max_uses: row.get(7)?,
754 current_uses: row.get(8)?,
755 revoked: row.get::<_, i32>(9)? != 0,
756 })
757 };
758
759 let invites = if include_expired {
760 stmt.query_map(params![group_id.as_str()], parse_invite)
761 } else {
762 stmt.query_map(params![group_id.as_str(), Self::now()], parse_invite)
763 }
764 .map_err(|e| Error::OperationFailed {
765 operation: "list_invites".to_string(),
766 cause: e.to_string(),
767 })?
768 .collect::<std::result::Result<Vec<_>, _>>()
769 .map_err(|e| Error::OperationFailed {
770 operation: "collect_invites".to_string(),
771 cause: e.to_string(),
772 })?;
773
774 Ok(invites)
775 }
776
777 fn increment_invite_uses(&self, invite_id: &str) -> Result<()> {
778 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
779 operation: "lock_connection".to_string(),
780 cause: e.to_string(),
781 })?;
782
783 conn.execute(
784 "UPDATE group_invites SET current_uses = current_uses + 1 WHERE id = ?1",
785 params![invite_id],
786 )
787 .map_err(|e| Error::OperationFailed {
788 operation: "increment_invite_uses".to_string(),
789 cause: e.to_string(),
790 })?;
791
792 Ok(())
793 }
794
795 fn revoke_invite(&self, invite_id: &str) -> Result<bool> {
796 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
797 operation: "lock_connection".to_string(),
798 cause: e.to_string(),
799 })?;
800
801 let rows = conn
802 .execute(
803 "UPDATE group_invites SET revoked = 1 WHERE id = ?1",
804 params![invite_id],
805 )
806 .map_err(|e| Error::OperationFailed {
807 operation: "revoke_invite".to_string(),
808 cause: e.to_string(),
809 })?;
810
811 Ok(rows > 0)
812 }
813
814 fn cleanup_expired_invites(&self) -> Result<u64> {
815 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
816 operation: "lock_connection".to_string(),
817 cause: e.to_string(),
818 })?;
819
820 let rows = conn
821 .execute(
822 "DELETE FROM group_invites WHERE expires_at < ?1 OR revoked = 1",
823 params![Self::now()],
824 )
825 .map_err(|e| Error::OperationFailed {
826 operation: "cleanup_expired_invites".to_string(),
827 cause: e.to_string(),
828 })?;
829
830 Ok(rows as u64)
831 }
832}
833
834#[cfg(test)]
835mod tests {
836 use super::*;
837
838 fn create_test_backend() -> SqliteGroupBackend {
839 let dir = tempfile::TempDir::new().expect("Failed to create temp dir");
841 let path = dir.path().join("test_groups.db");
842 std::mem::forget(dir);
844 SqliteGroupBackend::new(&path).expect("Failed to create test backend")
845 }
846
847 #[test]
848 fn test_create_and_get_group() {
849 let backend = create_test_backend();
850
851 let group = backend
852 .create_group(
853 "acme-corp",
854 "research",
855 "Research team",
856 "admin@example.com",
857 )
858 .expect("Failed to create group");
859
860 assert_eq!(group.name, "research");
861 assert_eq!(group.org_id, "acme-corp");
862 assert_eq!(group.created_by, "admin@example.com");
863
864 let retrieved = backend
865 .get_group(&group.id)
866 .expect("Failed to get group")
867 .expect("Group not found");
868
869 assert_eq!(retrieved.id, group.id);
870 assert_eq!(retrieved.name, group.name);
871 }
872
873 #[test]
874 fn test_duplicate_group_name() {
875 let backend = create_test_backend();
876
877 backend
878 .create_group("acme-corp", "research", "", "admin@example.com")
879 .expect("Failed to create first group");
880
881 let result = backend.create_group("acme-corp", "research", "", "admin@example.com");
882
883 assert!(result.is_err());
884 }
885
886 #[test]
887 fn test_add_and_list_members() {
888 let backend = create_test_backend();
889
890 let group = backend
891 .create_group("acme-corp", "team", "", "admin@example.com")
892 .expect("Failed to create group");
893
894 backend
895 .add_member(
896 &group.id,
897 "alice@example.com",
898 GroupRole::Admin,
899 "admin@example.com",
900 )
901 .expect("Failed to add alice");
902 backend
903 .add_member(
904 &group.id,
905 "bob@example.com",
906 GroupRole::Write,
907 "admin@example.com",
908 )
909 .expect("Failed to add bob");
910
911 let members = backend
912 .list_members(&group.id)
913 .expect("Failed to list members");
914
915 assert_eq!(members.len(), 2);
916 assert!(members.iter().any(|m| m.email == "alice@example.com"));
917 assert!(members.iter().any(|m| m.email == "bob@example.com"));
918 }
919
920 #[test]
921 fn test_get_user_groups() {
922 let backend = create_test_backend();
923
924 let group1 = backend
925 .create_group("acme-corp", "team-a", "", "admin@example.com")
926 .expect("Failed to create group1");
927 let group2 = backend
928 .create_group("acme-corp", "team-b", "", "admin@example.com")
929 .expect("Failed to create group2");
930
931 backend
932 .add_member(
933 &group1.id,
934 "user@example.com",
935 GroupRole::Write,
936 "admin@example.com",
937 )
938 .expect("Failed to add user to group1");
939 backend
940 .add_member(
941 &group2.id,
942 "user@example.com",
943 GroupRole::Read,
944 "admin@example.com",
945 )
946 .expect("Failed to add user to group2");
947
948 let memberships = backend
949 .get_user_groups("acme-corp", "user@example.com")
950 .expect("Failed to get user groups");
951
952 assert_eq!(memberships.len(), 2);
953 }
954
955 #[test]
956 fn test_invite_workflow() {
957 let backend = create_test_backend();
958
959 let group = backend
960 .create_group("acme-corp", "team", "", "admin@example.com")
961 .expect("Failed to create group");
962
963 let (invite, token) = backend
964 .create_invite(
965 &group.id,
966 GroupRole::Write,
967 "admin@example.com",
968 None,
969 Some(1),
970 )
971 .expect("Failed to create invite");
972
973 assert!(invite.is_valid());
974
975 let token_hash = GroupInvite::hash_token(&token);
977 let retrieved = backend
978 .get_invite_by_token_hash(&token_hash)
979 .expect("Failed to get invite")
980 .expect("Invite not found");
981
982 assert_eq!(retrieved.id, invite.id);
983
984 backend
986 .increment_invite_uses(&invite.id)
987 .expect("Failed to increment uses");
988
989 let updated = backend
990 .get_invite(&invite.id)
991 .expect("Failed to get invite")
992 .expect("Invite not found");
993
994 assert_eq!(updated.current_uses, 1);
995 assert!(!updated.is_valid()); }
997
998 #[test]
999 fn test_count_admins() {
1000 let backend = create_test_backend();
1001
1002 let group = backend
1003 .create_group("acme-corp", "team", "", "admin@example.com")
1004 .expect("Failed to create group");
1005
1006 backend
1007 .add_member(&group.id, "admin1@example.com", GroupRole::Admin, "system")
1008 .expect("Failed to add admin1");
1009 backend
1010 .add_member(&group.id, "admin2@example.com", GroupRole::Admin, "system")
1011 .expect("Failed to add admin2");
1012 backend
1013 .add_member(&group.id, "user@example.com", GroupRole::Write, "system")
1014 .expect("Failed to add user");
1015
1016 let count = backend
1017 .count_admins(&group.id)
1018 .expect("Failed to count admins");
1019 assert_eq!(count, 2);
1020 }
1021
1022 #[test]
1023 fn test_delete_group_cascades() {
1024 let backend = create_test_backend();
1025
1026 let group = backend
1027 .create_group("acme-corp", "team", "", "admin@example.com")
1028 .expect("Failed to create group");
1029
1030 backend
1031 .add_member(
1032 &group.id,
1033 "user@example.com",
1034 GroupRole::Write,
1035 "admin@example.com",
1036 )
1037 .expect("Failed to add member");
1038 backend
1039 .create_invite(&group.id, GroupRole::Read, "admin@example.com", None, None)
1040 .expect("Failed to create invite");
1041
1042 let deleted = backend
1043 .delete_group(&group.id)
1044 .expect("Failed to delete group");
1045 assert!(deleted);
1046
1047 let members = backend
1048 .list_members(&group.id)
1049 .expect("Failed to list members");
1050 assert!(members.is_empty());
1051
1052 let invites = backend
1053 .list_invites(&group.id, true)
1054 .expect("Failed to list invites");
1055 assert!(invites.is_empty());
1056 }
1057}