guts_auth/
organization.rs

1//! Organization types for multi-user repository ownership.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5
6/// Role within an organization.
7#[derive(
8    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
9)]
10#[serde(rename_all = "lowercase")]
11pub enum OrgRole {
12    /// Regular member with default access.
13    #[default]
14    Member,
15    /// Can manage teams and repositories.
16    Admin,
17    /// Full control of the organization.
18    Owner,
19}
20
21impl OrgRole {
22    /// Check if this role has at least the required role level.
23    pub fn has(&self, required: OrgRole) -> bool {
24        *self >= required
25    }
26
27    /// Parse from string.
28    pub fn parse(s: &str) -> Option<Self> {
29        match s.to_lowercase().as_str() {
30            "member" => Some(OrgRole::Member),
31            "admin" => Some(OrgRole::Admin),
32            "owner" => Some(OrgRole::Owner),
33            _ => None,
34        }
35    }
36}
37
38impl std::fmt::Display for OrgRole {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match self {
41            OrgRole::Member => write!(f, "member"),
42            OrgRole::Admin => write!(f, "admin"),
43            OrgRole::Owner => write!(f, "owner"),
44        }
45    }
46}
47
48/// A member of an organization.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct OrgMember {
51    /// User's public key (hex-encoded).
52    pub user: String,
53    /// Role within the organization.
54    pub role: OrgRole,
55    /// When the member was added (Unix timestamp).
56    pub added_at: u64,
57    /// Who added this member (public key hex).
58    pub added_by: String,
59}
60
61impl OrgMember {
62    /// Create a new organization member.
63    pub fn new(user: String, role: OrgRole, added_by: String) -> Self {
64        Self {
65            user,
66            role,
67            added_at: Self::now(),
68            added_by,
69        }
70    }
71
72    fn now() -> u64 {
73        std::time::SystemTime::now()
74            .duration_since(std::time::UNIX_EPOCH)
75            .unwrap_or_default()
76            .as_secs()
77    }
78}
79
80/// An organization for multi-user repository ownership.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct Organization {
83    /// Unique organization ID.
84    pub id: u64,
85    /// Unique organization name (URL-safe, e.g., "acme-corp").
86    pub name: String,
87    /// Display name (e.g., "Acme Corporation").
88    pub display_name: String,
89    /// Optional description.
90    pub description: Option<String>,
91    /// The creator's public key (hex).
92    pub created_by: String,
93    /// Organization members.
94    pub members: Vec<OrgMember>,
95    /// Team IDs belonging to this organization.
96    pub teams: HashSet<u64>,
97    /// Repository keys owned by this organization.
98    pub repos: HashSet<String>,
99    /// When the organization was created (Unix timestamp).
100    pub created_at: u64,
101    /// When the organization was last updated (Unix timestamp).
102    pub updated_at: u64,
103}
104
105impl Organization {
106    /// Create a new organization.
107    pub fn new(id: u64, name: String, display_name: String, created_by: String) -> Self {
108        let now = Self::now();
109        let founder = OrgMember {
110            user: created_by.clone(),
111            role: OrgRole::Owner,
112            added_at: now,
113            added_by: created_by.clone(),
114        };
115
116        Self {
117            id,
118            name,
119            display_name,
120            description: None,
121            created_by,
122            members: vec![founder],
123            teams: HashSet::new(),
124            repos: HashSet::new(),
125            created_at: now,
126            updated_at: now,
127        }
128    }
129
130    /// Set the organization description.
131    pub fn with_description(mut self, description: impl Into<String>) -> Self {
132        self.description = Some(description.into());
133        self.updated_at = Self::now();
134        self
135    }
136
137    /// Get a member by their public key.
138    pub fn get_member(&self, user: &str) -> Option<&OrgMember> {
139        self.members.iter().find(|m| m.user == user)
140    }
141
142    /// Check if a user is a member.
143    pub fn is_member(&self, user: &str) -> bool {
144        self.get_member(user).is_some()
145    }
146
147    /// Check if a user has at least the specified role.
148    pub fn has_role(&self, user: &str, required: OrgRole) -> bool {
149        self.get_member(user)
150            .map(|m| m.role.has(required))
151            .unwrap_or(false)
152    }
153
154    /// Check if a user is an owner.
155    pub fn is_owner(&self, user: &str) -> bool {
156        self.has_role(user, OrgRole::Owner)
157    }
158
159    /// Check if a user is an admin (or owner).
160    pub fn is_admin(&self, user: &str) -> bool {
161        self.has_role(user, OrgRole::Admin)
162    }
163
164    /// Add a member to the organization.
165    pub fn add_member(&mut self, member: OrgMember) -> bool {
166        if self.is_member(&member.user) {
167            return false;
168        }
169        self.members.push(member);
170        self.updated_at = Self::now();
171        true
172    }
173
174    /// Remove a member from the organization.
175    /// Returns an error string if this would remove the last owner.
176    pub fn remove_member(&mut self, user: &str) -> Result<bool, &'static str> {
177        // Check if this is an owner and they're the last one
178        if let Some(member) = self.get_member(user) {
179            if member.role == OrgRole::Owner {
180                let owner_count = self
181                    .members
182                    .iter()
183                    .filter(|m| m.role == OrgRole::Owner)
184                    .count();
185                if owner_count <= 1 {
186                    return Err("cannot remove last owner");
187                }
188            }
189        }
190
191        let before = self.members.len();
192        self.members.retain(|m| m.user != user);
193        let removed = self.members.len() < before;
194
195        if removed {
196            self.updated_at = Self::now();
197        }
198
199        Ok(removed)
200    }
201
202    /// Update a member's role.
203    /// Returns an error if demoting the last owner.
204    pub fn update_member_role(
205        &mut self,
206        user: &str,
207        new_role: OrgRole,
208    ) -> Result<bool, &'static str> {
209        // Check if demoting the last owner
210        if let Some(member) = self.get_member(user) {
211            if member.role == OrgRole::Owner && new_role != OrgRole::Owner {
212                let owner_count = self
213                    .members
214                    .iter()
215                    .filter(|m| m.role == OrgRole::Owner)
216                    .count();
217                if owner_count <= 1 {
218                    return Err("cannot demote last owner");
219                }
220            }
221        }
222
223        for member in &mut self.members {
224            if member.user == user {
225                member.role = new_role;
226                self.updated_at = Self::now();
227                return Ok(true);
228            }
229        }
230
231        Ok(false)
232    }
233
234    /// Add a team to this organization.
235    pub fn add_team(&mut self, team_id: u64) {
236        self.teams.insert(team_id);
237        self.updated_at = Self::now();
238    }
239
240    /// Remove a team from this organization.
241    pub fn remove_team(&mut self, team_id: u64) -> bool {
242        let removed = self.teams.remove(&team_id);
243        if removed {
244            self.updated_at = Self::now();
245        }
246        removed
247    }
248
249    /// Add a repository to this organization.
250    pub fn add_repo(&mut self, repo_key: String) {
251        self.repos.insert(repo_key);
252        self.updated_at = Self::now();
253    }
254
255    /// Remove a repository from this organization.
256    pub fn remove_repo(&mut self, repo_key: &str) -> bool {
257        let removed = self.repos.remove(repo_key);
258        if removed {
259            self.updated_at = Self::now();
260        }
261        removed
262    }
263
264    /// Count the number of owners.
265    pub fn owner_count(&self) -> usize {
266        self.members
267            .iter()
268            .filter(|m| m.role == OrgRole::Owner)
269            .count()
270    }
271
272    fn now() -> u64 {
273        std::time::SystemTime::now()
274            .duration_since(std::time::UNIX_EPOCH)
275            .unwrap_or_default()
276            .as_secs()
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_org_role_ordering() {
286        assert!(OrgRole::Member < OrgRole::Admin);
287        assert!(OrgRole::Admin < OrgRole::Owner);
288    }
289
290    #[test]
291    fn test_org_creation() {
292        let org = Organization::new(1, "acme".into(), "Acme Corp".into(), "abc123".into());
293
294        assert_eq!(org.id, 1);
295        assert_eq!(org.name, "acme");
296        assert_eq!(org.display_name, "Acme Corp");
297        assert_eq!(org.members.len(), 1);
298        assert!(org.is_owner("abc123"));
299    }
300
301    #[test]
302    fn test_add_remove_member() {
303        let mut org = Organization::new(1, "acme".into(), "Acme Corp".into(), "owner".into());
304
305        // Add a member
306        let member = OrgMember::new("user1".into(), OrgRole::Member, "owner".into());
307        assert!(org.add_member(member));
308        assert!(org.is_member("user1"));
309        assert!(!org.is_admin("user1"));
310
311        // Add admin
312        let admin = OrgMember::new("admin1".into(), OrgRole::Admin, "owner".into());
313        assert!(org.add_member(admin));
314        assert!(org.is_admin("admin1"));
315        assert!(!org.is_owner("admin1"));
316
317        // Remove member
318        assert!(org.remove_member("user1").unwrap());
319        assert!(!org.is_member("user1"));
320
321        // Cannot remove last owner
322        assert!(org.remove_member("owner").is_err());
323    }
324
325    #[test]
326    fn test_role_update() {
327        let mut org = Organization::new(1, "acme".into(), "Acme Corp".into(), "owner1".into());
328
329        // Add another owner
330        let owner2 = OrgMember::new("owner2".into(), OrgRole::Owner, "owner1".into());
331        org.add_member(owner2);
332
333        // Now we can demote owner1
334        assert!(org.update_member_role("owner1", OrgRole::Admin).unwrap());
335        assert!(org.is_admin("owner1"));
336        assert!(!org.is_owner("owner1"));
337
338        // But cannot demote the last owner
339        assert!(org.update_member_role("owner2", OrgRole::Member).is_err());
340    }
341}