Skip to main content

allowthem_core/
events.rs

1//! Lifecycle events published by allowthem's route handlers.
2//!
3//! See `docs/superpowers/specs/2026-04-20-lifecycle-events-design.md` for the
4//! full contract. Summary: fire-and-forget, at-most-once, owned `'static` data,
5//! integrator owns recovery.
6
7use chrono::{DateTime, Utc};
8use tokio::sync::mpsc;
9
10use crate::types::User;
11
12#[derive(Debug, Clone)]
13#[non_exhaustive]
14pub enum LifecycleEvent {
15    Registered(RegisteredEvent),
16}
17
18#[derive(Debug, Clone)]
19#[non_exhaustive]
20pub struct RegisteredEvent {
21    pub user: User,
22    pub source: RegistrationSource,
23    pub ctx: EventContext,
24}
25
26impl RegisteredEvent {
27    /// Constructor used by allowthem's route handlers. Integrators receive
28    /// `RegisteredEvent` values from the channel and should not construct
29    /// them directly.
30    pub fn new(user: User, source: RegistrationSource, ctx: EventContext) -> Self {
31        Self { user, source, ctx }
32    }
33}
34
35#[derive(Debug, Clone)]
36#[non_exhaustive]
37pub enum RegistrationSource {
38    Password,
39    Invitation {
40        email: Option<String>,
41        metadata: Option<String>,
42    },
43    OAuth {
44        provider: String,
45    },
46}
47
48#[derive(Debug, Clone)]
49#[non_exhaustive]
50pub struct EventContext {
51    pub ip: Option<String>,
52    pub user_agent: Option<String>,
53    pub base_url: String,
54    pub occurred_at: DateTime<Utc>,
55}
56
57impl EventContext {
58    /// Constructor used by allowthem's route handlers. Integrators receive
59    /// `EventContext` values from the channel and should not construct them
60    /// directly.
61    pub fn new(
62        ip: Option<String>,
63        user_agent: Option<String>,
64        base_url: String,
65        occurred_at: DateTime<Utc>,
66    ) -> Self {
67        Self {
68            ip,
69            user_agent,
70            base_url,
71            occurred_at,
72        }
73    }
74}
75
76pub type LifecycleEventSender = mpsc::UnboundedSender<LifecycleEvent>;
77pub type LifecycleEventReceiver = mpsc::UnboundedReceiver<LifecycleEvent>;
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::types::{Email, UserId};
83
84    fn sample_user() -> User {
85        User {
86            id: UserId::new(),
87            email: Email::new("test@example.com".into()).unwrap(),
88            username: None,
89            password_hash: None,
90            email_verified: false,
91            is_active: true,
92            created_at: Utc::now(),
93            updated_at: Utc::now(),
94            custom_data: None,
95        }
96    }
97
98    #[test]
99    fn registered_event_constructs_and_clones() {
100        let event = LifecycleEvent::Registered(RegisteredEvent::new(
101            sample_user(),
102            RegistrationSource::Password,
103            EventContext::new(
104                Some("127.0.0.1".into()),
105                Some("test-agent".into()),
106                "http://test".into(),
107                Utc::now(),
108            ),
109        ));
110
111        let cloned = event.clone();
112        // Debug-format should work on the cloned value.
113        let _ = format!("{cloned:?}");
114    }
115
116    #[test]
117    fn oauth_source_carries_provider() {
118        let source = RegistrationSource::OAuth {
119            provider: "mock".into(),
120        };
121        let _ = format!("{source:?}");
122        let cloned = source.clone();
123        match cloned {
124            RegistrationSource::OAuth { provider } => assert_eq!(provider, "mock"),
125            _ => panic!("expected OAuth variant"),
126        }
127    }
128}