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 AuthEvent {
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    OAuth { provider: String },
40}
41
42#[derive(Debug, Clone)]
43#[non_exhaustive]
44pub struct EventContext {
45    pub ip: Option<String>,
46    pub user_agent: Option<String>,
47    pub base_url: String,
48    pub occurred_at: DateTime<Utc>,
49}
50
51impl EventContext {
52    /// Constructor used by allowthem's route handlers. Integrators receive
53    /// `EventContext` values from the channel and should not construct them
54    /// directly.
55    pub fn new(
56        ip: Option<String>,
57        user_agent: Option<String>,
58        base_url: String,
59        occurred_at: DateTime<Utc>,
60    ) -> Self {
61        Self {
62            ip,
63            user_agent,
64            base_url,
65            occurred_at,
66        }
67    }
68}
69
70pub type AuthEventSender = mpsc::UnboundedSender<AuthEvent>;
71pub type AuthEventReceiver = mpsc::UnboundedReceiver<AuthEvent>;
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::types::{Email, UserId};
77
78    fn sample_user() -> User {
79        User {
80            id: UserId::new(),
81            email: Email::new("test@example.com".into()).unwrap(),
82            username: None,
83            password_hash: None,
84            email_verified: false,
85            is_active: true,
86            created_at: Utc::now(),
87            updated_at: Utc::now(),
88            custom_data: None,
89        }
90    }
91
92    #[test]
93    fn registered_event_constructs_and_clones() {
94        let event = AuthEvent::Registered(RegisteredEvent::new(
95            sample_user(),
96            RegistrationSource::Password,
97            EventContext::new(
98                Some("127.0.0.1".into()),
99                Some("test-agent".into()),
100                "http://test".into(),
101                Utc::now(),
102            ),
103        ));
104
105        let cloned = event.clone();
106        // Debug-format should work on the cloned value.
107        let _ = format!("{cloned:?}");
108    }
109
110    #[test]
111    fn oauth_source_carries_provider() {
112        let source = RegistrationSource::OAuth {
113            provider: "mock".into(),
114        };
115        let _ = format!("{source:?}");
116        let cloned = source.clone();
117        match cloned {
118            RegistrationSource::OAuth { provider } => assert_eq!(provider, "mock"),
119            _ => panic!("expected OAuth variant"),
120        }
121    }
122}