1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::sync::Arc;
11use tokio::sync::{broadcast, RwLock};
12use uuid::Uuid;
13
14use super::rbac::{Permission, Role, RoleAssignment};
15
16pub type TeamId = Uuid;
22
23pub type MemberId = Uuid;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct TeamWorkspace {
29 pub id: TeamId,
31 pub name: String,
33 pub description: Option<String>,
35 pub avatar_url: Option<String>,
37 pub owner_id: MemberId,
39 pub settings: TeamSettings,
41 pub members: Vec<TeamMember>,
43 pub shared_sessions: Vec<String>,
45 pub created_at: DateTime<Utc>,
47 pub updated_at: DateTime<Utc>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct TeamMember {
54 pub user_id: MemberId,
56 pub display_name: String,
58 pub email: String,
60 pub avatar_url: Option<String>,
62 pub role: Role,
64 pub custom_permissions: Option<Vec<Permission>>,
66 pub joined_at: DateTime<Utc>,
68 pub last_active: Option<DateTime<Utc>>,
70 pub status: MemberStatus,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum MemberStatus {
78 Active,
80 Invited,
82 Deactivated,
84 Left,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct TeamSettings {
91 pub default_session_visibility: SessionVisibility,
93 pub allow_external_sharing: bool,
95 pub default_member_role: Role,
97 pub require_join_approval: bool,
99 pub enable_realtime_collaboration: bool,
101 pub session_retention_days: u32,
103 pub max_members: u32,
105}
106
107impl Default for TeamSettings {
108 fn default() -> Self {
109 Self {
110 default_session_visibility: SessionVisibility::TeamOnly,
111 allow_external_sharing: false,
112 default_member_role: Role::Member,
113 require_join_approval: true,
114 enable_realtime_collaboration: true,
115 session_retention_days: 0,
116 max_members: 0,
117 }
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum SessionVisibility {
125 Private,
127 TeamOnly,
129 Public,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct TeamInvitation {
136 pub id: Uuid,
138 pub team_id: TeamId,
140 pub inviter_id: MemberId,
142 pub invitee_email: String,
144 pub role: Role,
146 pub message: Option<String>,
148 pub expires_at: DateTime<Utc>,
150 pub created_at: DateTime<Utc>,
152 pub status: InvitationStatus,
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case")]
159pub enum InvitationStatus {
160 Pending,
161 Accepted,
162 Declined,
163 Expired,
164 Cancelled,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct PresenceInfo {
170 pub user_id: MemberId,
172 pub display_name: String,
174 pub current_session: Option<String>,
176 pub cursor_position: Option<CursorPosition>,
178 pub status: PresenceStatus,
180 pub last_heartbeat: DateTime<Utc>,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct CursorPosition {
187 pub session_id: String,
189 pub message_index: usize,
191 pub offset: usize,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
197#[serde(rename_all = "snake_case")]
198pub enum PresenceStatus {
199 Online,
200 Away,
201 Busy,
202 Offline,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207#[serde(tag = "type", rename_all = "snake_case")]
208pub enum CollaborationEvent {
209 MemberJoined {
211 user_id: MemberId,
212 session_id: String,
213 },
214 MemberLeft {
216 user_id: MemberId,
217 session_id: String,
218 },
219 CursorMoved {
221 user_id: MemberId,
222 position: CursorPosition,
223 },
224 ContentChanged {
226 user_id: MemberId,
227 session_id: String,
228 change_type: ContentChangeType,
229 },
230 PresenceUpdated {
232 user_id: MemberId,
233 status: PresenceStatus,
234 },
235 SessionShared {
237 user_id: MemberId,
238 session_id: String,
239 },
240 CommentAdded {
242 user_id: MemberId,
243 session_id: String,
244 comment_id: String,
245 },
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
250#[serde(rename_all = "snake_case")]
251pub enum ContentChangeType {
252 MessageAdded,
253 MessageEdited,
254 MessageDeleted,
255 TagsUpdated,
256 TitleChanged,
257 Annotated,
258}
259
260pub struct TeamManager {
266 teams: Arc<RwLock<HashMap<TeamId, TeamWorkspace>>>,
268 user_teams: Arc<RwLock<HashMap<MemberId, Vec<TeamId>>>>,
270 presences: Arc<RwLock<HashMap<TeamId, Vec<PresenceInfo>>>>,
272 event_tx: broadcast::Sender<(TeamId, CollaborationEvent)>,
274 invitations: Arc<RwLock<HashMap<Uuid, TeamInvitation>>>,
276}
277
278impl TeamManager {
279 pub fn new() -> Self {
281 let (event_tx, _) = broadcast::channel(1000);
282 Self {
283 teams: Arc::new(RwLock::new(HashMap::new())),
284 user_teams: Arc::new(RwLock::new(HashMap::new())),
285 presences: Arc::new(RwLock::new(HashMap::new())),
286 event_tx,
287 invitations: Arc::new(RwLock::new(HashMap::new())),
288 }
289 }
290
291 pub async fn create_team(
293 &self,
294 name: String,
295 description: Option<String>,
296 owner_id: MemberId,
297 owner_name: String,
298 owner_email: String,
299 ) -> TeamWorkspace {
300 let team_id = Uuid::new_v4();
301 let now = Utc::now();
302
303 let owner_member = TeamMember {
304 user_id: owner_id,
305 display_name: owner_name,
306 email: owner_email,
307 avatar_url: None,
308 role: Role::Owner,
309 custom_permissions: None,
310 joined_at: now,
311 last_active: Some(now),
312 status: MemberStatus::Active,
313 };
314
315 let team = TeamWorkspace {
316 id: team_id,
317 name,
318 description,
319 avatar_url: None,
320 owner_id,
321 settings: TeamSettings::default(),
322 members: vec![owner_member],
323 shared_sessions: vec![],
324 created_at: now,
325 updated_at: now,
326 };
327
328 self.teams.write().await.insert(team_id, team.clone());
330
331 self.user_teams
333 .write()
334 .await
335 .entry(owner_id)
336 .or_default()
337 .push(team_id);
338
339 team
340 }
341
342 pub async fn get_team(&self, team_id: TeamId) -> Option<TeamWorkspace> {
344 self.teams.read().await.get(&team_id).cloned()
345 }
346
347 pub async fn get_user_teams(&self, user_id: MemberId) -> Vec<TeamWorkspace> {
349 let user_teams = self.user_teams.read().await;
350 let teams = self.teams.read().await;
351
352 user_teams
353 .get(&user_id)
354 .map(|team_ids| {
355 team_ids
356 .iter()
357 .filter_map(|id| teams.get(id).cloned())
358 .collect()
359 })
360 .unwrap_or_default()
361 }
362
363 pub async fn update_team_settings(
365 &self,
366 team_id: TeamId,
367 settings: TeamSettings,
368 ) -> Option<TeamWorkspace> {
369 let mut teams = self.teams.write().await;
370 if let Some(team) = teams.get_mut(&team_id) {
371 team.settings = settings;
372 team.updated_at = Utc::now();
373 Some(team.clone())
374 } else {
375 None
376 }
377 }
378
379 pub async fn invite_member(
381 &self,
382 team_id: TeamId,
383 inviter_id: MemberId,
384 invitee_email: String,
385 role: Role,
386 message: Option<String>,
387 ) -> Option<TeamInvitation> {
388 let teams = self.teams.read().await;
389 if !teams.contains_key(&team_id) {
390 return None;
391 }
392
393 let invitation = TeamInvitation {
394 id: Uuid::new_v4(),
395 team_id,
396 inviter_id,
397 invitee_email,
398 role,
399 message,
400 expires_at: Utc::now() + chrono::Duration::days(7),
401 created_at: Utc::now(),
402 status: InvitationStatus::Pending,
403 };
404
405 self.invitations
406 .write()
407 .await
408 .insert(invitation.id, invitation.clone());
409
410 Some(invitation)
411 }
412
413 pub async fn accept_invitation(
415 &self,
416 invitation_id: Uuid,
417 user_id: MemberId,
418 display_name: String,
419 email: String,
420 ) -> Result<TeamWorkspace, String> {
421 let mut invitations = self.invitations.write().await;
422 let invitation = invitations
423 .get_mut(&invitation_id)
424 .ok_or("Invitation not found")?;
425
426 if invitation.status != InvitationStatus::Pending {
427 return Err("Invitation is no longer pending".to_string());
428 }
429
430 if invitation.expires_at < Utc::now() {
431 invitation.status = InvitationStatus::Expired;
432 return Err("Invitation has expired".to_string());
433 }
434
435 invitation.status = InvitationStatus::Accepted;
436
437 let mut teams = self.teams.write().await;
439 let team = teams
440 .get_mut(&invitation.team_id)
441 .ok_or("Team not found")?;
442
443 let now = Utc::now();
444 let member = TeamMember {
445 user_id,
446 display_name,
447 email,
448 avatar_url: None,
449 role: invitation.role,
450 custom_permissions: None,
451 joined_at: now,
452 last_active: Some(now),
453 status: MemberStatus::Active,
454 };
455
456 team.members.push(member);
457 team.updated_at = now;
458
459 self.user_teams
461 .write()
462 .await
463 .entry(user_id)
464 .or_default()
465 .push(invitation.team_id);
466
467 Ok(team.clone())
468 }
469
470 pub async fn remove_member(
472 &self,
473 team_id: TeamId,
474 member_id: MemberId,
475 ) -> Result<(), String> {
476 let mut teams = self.teams.write().await;
477 let team = teams.get_mut(&team_id).ok_or("Team not found")?;
478
479 if team.owner_id == member_id {
481 return Err("Cannot remove team owner".to_string());
482 }
483
484 team.members.retain(|m| m.user_id != member_id);
485 team.updated_at = Utc::now();
486
487 if let Some(user_teams) = self.user_teams.write().await.get_mut(&member_id) {
489 user_teams.retain(|id| *id != team_id);
490 }
491
492 Ok(())
493 }
494
495 pub async fn update_member_role(
497 &self,
498 team_id: TeamId,
499 member_id: MemberId,
500 new_role: Role,
501 ) -> Result<(), String> {
502 let mut teams = self.teams.write().await;
503 let team = teams.get_mut(&team_id).ok_or("Team not found")?;
504
505 if team.owner_id == member_id && new_role != Role::Owner {
507 return Err("Cannot change owner's role".to_string());
508 }
509
510 if let Some(member) = team.members.iter_mut().find(|m| m.user_id == member_id) {
511 member.role = new_role;
512 team.updated_at = Utc::now();
513 Ok(())
514 } else {
515 Err("Member not found".to_string())
516 }
517 }
518
519 pub async fn share_session(
521 &self,
522 team_id: TeamId,
523 session_id: String,
524 sharer_id: MemberId,
525 ) -> Result<(), String> {
526 let mut teams = self.teams.write().await;
527 let team = teams.get_mut(&team_id).ok_or("Team not found")?;
528
529 if !team.shared_sessions.contains(&session_id) {
530 team.shared_sessions.push(session_id.clone());
531 team.updated_at = Utc::now();
532
533 let _ = self.event_tx.send((
535 team_id,
536 CollaborationEvent::SessionShared {
537 user_id: sharer_id,
538 session_id,
539 },
540 ));
541 }
542
543 Ok(())
544 }
545
546 pub async fn update_presence(&self, team_id: TeamId, presence: PresenceInfo) {
548 let mut presences = self.presences.write().await;
549 let team_presences = presences.entry(team_id).or_default();
550
551 if let Some(existing) = team_presences
553 .iter_mut()
554 .find(|p| p.user_id == presence.user_id)
555 {
556 *existing = presence.clone();
557 } else {
558 team_presences.push(presence.clone());
559 }
560
561 let _ = self.event_tx.send((
563 team_id,
564 CollaborationEvent::PresenceUpdated {
565 user_id: presence.user_id,
566 status: presence.status,
567 },
568 ));
569 }
570
571 pub async fn get_presences(&self, team_id: TeamId) -> Vec<PresenceInfo> {
573 self.presences
574 .read()
575 .await
576 .get(&team_id)
577 .cloned()
578 .unwrap_or_default()
579 }
580
581 pub fn subscribe(&self) -> broadcast::Receiver<(TeamId, CollaborationEvent)> {
583 self.event_tx.subscribe()
584 }
585
586 pub async fn broadcast_event(&self, team_id: TeamId, event: CollaborationEvent) {
588 let _ = self.event_tx.send((team_id, event));
589 }
590
591 pub async fn delete_team(&self, team_id: TeamId, requester_id: MemberId) -> Result<(), String> {
593 let teams = self.teams.read().await;
594 let team = teams.get(&team_id).ok_or("Team not found")?;
595
596 if team.owner_id != requester_id {
597 return Err("Only the owner can delete the team".to_string());
598 }
599
600 drop(teams);
601
602 self.teams.write().await.remove(&team_id);
604
605 let mut user_teams = self.user_teams.write().await;
607 for team_ids in user_teams.values_mut() {
608 team_ids.retain(|id| *id != team_id);
609 }
610
611 self.presences.write().await.remove(&team_id);
613
614 Ok(())
615 }
616}
617
618impl Default for TeamManager {
619 fn default() -> Self {
620 Self::new()
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[tokio::test]
629 async fn test_create_team() {
630 let manager = TeamManager::new();
631 let owner_id = Uuid::new_v4();
632
633 let team = manager
634 .create_team(
635 "Test Team".to_string(),
636 Some("A test team".to_string()),
637 owner_id,
638 "Owner".to_string(),
639 "owner@example.com".to_string(),
640 )
641 .await;
642
643 assert_eq!(team.name, "Test Team");
644 assert_eq!(team.owner_id, owner_id);
645 assert_eq!(team.members.len(), 1);
646 assert_eq!(team.members[0].role, Role::Owner);
647 }
648
649 #[tokio::test]
650 async fn test_invite_and_accept() {
651 let manager = TeamManager::new();
652 let owner_id = Uuid::new_v4();
653 let member_id = Uuid::new_v4();
654
655 let team = manager
656 .create_team(
657 "Test Team".to_string(),
658 None,
659 owner_id,
660 "Owner".to_string(),
661 "owner@example.com".to_string(),
662 )
663 .await;
664
665 let invitation = manager
666 .invite_member(
667 team.id,
668 owner_id,
669 "member@example.com".to_string(),
670 Role::Member,
671 None,
672 )
673 .await
674 .unwrap();
675
676 let updated_team = manager
677 .accept_invitation(
678 invitation.id,
679 member_id,
680 "Member".to_string(),
681 "member@example.com".to_string(),
682 )
683 .await
684 .unwrap();
685
686 assert_eq!(updated_team.members.len(), 2);
687 }
688}