use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use crate::ids::{InvitationId, PlatformApiKeyId, UserId, WorkspaceId}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum UserStatus { Active, Disabled, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum MembershipRole { Owner, Admin, Operator, Viewer, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum InvitationStatus { Pending, Accepted, Revoked, Expired, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum PlatformApiKeyStatus { Active, Revoked, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum PlatformApiKeyScope { Read, Write, Deploy, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct User { pub id: UserId, pub email: String, pub display_name: String, pub status: UserStatus, #[serde(with = "time::serde::rfc3339")] pub created_at: OffsetDateTime, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Membership { pub workspace_id: WorkspaceId, pub user_id: UserId, pub role: MembershipRole, #[serde(with = "time::serde::rfc3339")] pub created_at: OffsetDateTime, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct InvitationToken { pub id: InvitationId, pub workspace_id: WorkspaceId, pub email: String, pub role: MembershipRole, pub status: InvitationStatus, pub token_hash: String, #[serde(with = "time::serde::rfc3339")] pub expires_at: OffsetDateTime, #[serde(with = "time::serde::rfc3339")] pub created_at: OffsetDateTime, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct PlatformApiKey { pub id: PlatformApiKeyId, pub workspace_id: WorkspaceId, pub name: String, pub prefix: String, pub scopes: Vec, pub status: PlatformApiKeyStatus, #[serde(with = "time::serde::rfc3339")] pub created_at: OffsetDateTime, #[serde(with = "time::serde::rfc3339::option")] pub last_used_at: Option, } #[cfg(test)] mod tests { use serde_json::json; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use super::{PlatformApiKey, PlatformApiKeyScope, PlatformApiKeyStatus, User, UserStatus}; use crate::ids::{PlatformApiKeyId, UserId, WorkspaceId}; #[test] fn user_serializes_created_at_as_rfc3339() { let user = User { id: UserId::new("user_01"), email: "owner@example.com".to_owned(), display_name: "Owner".to_owned(), status: UserStatus::Active, created_at: OffsetDateTime::parse("2026-03-25T12:00:00Z", &Rfc3339).unwrap(), }; let value = serde_json::to_value(&user).unwrap(); assert_eq!(value["created_at"], json!("2026-03-25T12:00:00Z")); } #[test] fn user_deserializes_created_at_from_rfc3339() { let user: User = serde_json::from_value(json!({ "id": "user_01", "email": "owner@example.com", "display_name": "Owner", "status": "active", "created_at": "2026-03-25T12:00:00Z" })) .unwrap(); assert_eq!( user.created_at, OffsetDateTime::parse("2026-03-25T12:00:00Z", &Rfc3339).unwrap() ); } #[test] fn platform_api_key_serializes_timestamps_as_rfc3339() { let api_key = PlatformApiKey { id: PlatformApiKeyId::new("pk_01"), workspace_id: WorkspaceId::new("ws_01"), name: "Primary".to_owned(), prefix: "crk_live".to_owned(), scopes: vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write], status: PlatformApiKeyStatus::Active, created_at: OffsetDateTime::parse("2026-03-25T12:01:00Z", &Rfc3339).unwrap(), last_used_at: Some(OffsetDateTime::parse("2026-03-25T12:05:00Z", &Rfc3339).unwrap()), }; let value = serde_json::to_value(&api_key).unwrap(); assert_eq!(value["created_at"], json!("2026-03-25T12:01:00Z")); assert_eq!(value["last_used_at"], json!("2026-03-25T12:05:00Z")); } }