everruns_core/
organization.rs1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::str::FromStr;
11use uuid::Uuid;
12
13pub const DEFAULT_ORG_ID: i64 = 1;
15
16pub const DEFAULT_ORG_PUBLIC_ID: &str = "org_00000000000000000000000000000001";
18
19pub const ANONYMOUS_USER_ID: Uuid = Uuid::from_bytes([
23 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
24]);
25
26pub const ANONYMOUS_USER_EMAIL: &str = "anonymous@local";
28
29pub const ANONYMOUS_USER_NAME: &str = "Anonymous";
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
35pub struct Organization {
36 pub public_id: String,
38 pub name: String,
40 pub created_at: DateTime<Utc>,
41 pub updated_at: DateTime<Utc>,
42}
43
44#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
47#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
48#[serde(rename_all = "lowercase")]
49pub enum OrgRole {
50 Member,
51 Admin,
52 #[default]
53 Owner,
54}
55
56impl OrgRole {
57 pub fn has_permission(self, required: OrgRole) -> bool {
59 self.level() >= required.level()
60 }
61
62 pub fn as_str(self) -> &'static str {
64 match self {
65 OrgRole::Member => "member",
66 OrgRole::Admin => "admin",
67 OrgRole::Owner => "owner",
68 }
69 }
70
71 fn level(self) -> u8 {
72 match self {
73 OrgRole::Member => 0,
74 OrgRole::Admin => 1,
75 OrgRole::Owner => 2,
76 }
77 }
78}
79
80impl fmt::Display for OrgRole {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 f.write_str(self.as_str())
83 }
84}
85
86impl FromStr for OrgRole {
87 type Err = String;
88
89 fn from_str(s: &str) -> Result<Self, Self::Err> {
90 match s {
91 "member" => Ok(OrgRole::Member),
92 "admin" => Ok(OrgRole::Admin),
93 "owner" => Ok(OrgRole::Owner),
94 _ => Err(format!("invalid org role: {s}")),
95 }
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
102pub struct OrgMembership {
103 #[serde(skip_serializing)]
105 pub org_id: i64,
106 pub public_id: String,
108 pub name: String,
110 #[serde(default)]
112 pub role: OrgRole,
113}
114
115pub fn org_public_id_from_internal(org_id: i64) -> String {
121 if org_id == DEFAULT_ORG_ID {
122 return DEFAULT_ORG_PUBLIC_ID.to_string();
123 }
124 format!("org_{:032x}", org_id)
125}
126
127pub fn org_internal_id_from_public(org_id: crate::typed_id::OrgId) -> i64 {
136 org_id.uuid().as_u128() as i64
137}
138
139pub fn generate_org_public_id() -> String {
142 let uuid = Uuid::new_v4();
143 format!("org_{}", uuid.simple())
144}
145
146pub fn validate_org_public_id(public_id: &str) -> bool {
149 if !public_id.starts_with("org_") {
150 return false;
151 }
152 let suffix = &public_id[4..];
153 suffix.len() == 32
154 && suffix
155 .chars()
156 .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn test_generate_org_public_id() {
165 let id = generate_org_public_id();
166 assert!(id.starts_with("org_"));
167 assert_eq!(id.len(), 36); assert!(validate_org_public_id(&id));
169 }
170
171 #[test]
172 fn test_validate_org_public_id() {
173 assert!(validate_org_public_id(
175 "org_00000000000000000000000000000001"
176 ));
177 assert!(validate_org_public_id(
178 "org_2f3c1b3e6a9d4c6f8a1d4e9c9b7f21a0"
179 ));
180
181 assert!(!validate_org_public_id(
183 "organization_12345678901234567890123456789012"
184 ));
185
186 assert!(!validate_org_public_id("org_123"));
188
189 assert!(!validate_org_public_id(
191 "org_123456789012345678901234567890123"
192 ));
193
194 assert!(!validate_org_public_id(
196 "org_2F3C1B3E6A9D4C6F8A1D4E9C9B7F21A0"
197 ));
198
199 assert!(!validate_org_public_id(
201 "org_ghijklmnopqrstuvwxyz1234567890"
202 ));
203 }
204
205 #[test]
206 fn test_default_org_public_id_valid() {
207 assert!(validate_org_public_id(DEFAULT_ORG_PUBLIC_ID));
208 }
209
210 #[test]
211 fn test_org_internal_id_round_trips() {
212 for internal in [DEFAULT_ORG_ID, 5, 42, 1_000_000] {
213 let public = org_public_id_from_internal(internal);
214 let org_id: crate::typed_id::OrgId = public.parse().expect("parse org id");
215 assert_eq!(org_internal_id_from_public(org_id), internal);
216 }
217 }
218
219 #[test]
220 fn test_org_role_hierarchy() {
221 assert!(OrgRole::Owner.has_permission(OrgRole::Owner));
222 assert!(OrgRole::Owner.has_permission(OrgRole::Admin));
223 assert!(OrgRole::Owner.has_permission(OrgRole::Member));
224
225 assert!(!OrgRole::Admin.has_permission(OrgRole::Owner));
226 assert!(OrgRole::Admin.has_permission(OrgRole::Admin));
227 assert!(OrgRole::Admin.has_permission(OrgRole::Member));
228
229 assert!(!OrgRole::Member.has_permission(OrgRole::Owner));
230 assert!(!OrgRole::Member.has_permission(OrgRole::Admin));
231 assert!(OrgRole::Member.has_permission(OrgRole::Member));
232 }
233
234 #[test]
235 fn test_org_role_str_roundtrip() {
236 for role in [OrgRole::Member, OrgRole::Admin, OrgRole::Owner] {
237 let s = role.as_str();
238 let parsed: OrgRole = s.parse().unwrap();
239 assert_eq!(parsed, role);
240 }
241 }
242
243 #[test]
244 fn test_org_role_default() {
245 assert_eq!(OrgRole::default(), OrgRole::Owner);
246 }
247}