Skip to main content

yauth/
plugin.rs

1use crate::state::YAuthState;
2use axum::Router;
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub enum AuthEvent {
8    UserRegistered {
9        user_id: Uuid,
10        email: String,
11    },
12    LoginSucceeded {
13        user_id: Uuid,
14        method: String,
15    },
16    LoginFailed {
17        email: String,
18        method: String,
19        reason: String,
20    },
21    SessionCreated {
22        user_id: Uuid,
23        session_id: Uuid,
24    },
25    Logout {
26        user_id: Uuid,
27        session_id: Uuid,
28    },
29    PasswordChanged {
30        user_id: Uuid,
31    },
32    EmailVerified {
33        user_id: Uuid,
34    },
35    MfaEnabled {
36        user_id: Uuid,
37        method: String,
38    },
39    MfaDisabled {
40        user_id: Uuid,
41        method: String,
42    },
43    UserBanned {
44        user_id: Uuid,
45    },
46    UserUnbanned {
47        user_id: Uuid,
48    },
49    MagicLinkSent {
50        email: String,
51    },
52    MagicLinkVerified {
53        user_id: Uuid,
54        is_new_user: bool,
55    },
56    #[cfg(feature = "account-lockout")]
57    AccountLocked {
58        user_id: Uuid,
59        email: String,
60        locked_until: Option<String>,
61    },
62    #[cfg(feature = "account-lockout")]
63    AccountUnlocked {
64        user_id: Uuid,
65        method: String,
66    },
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub enum EventResponse {
71    Continue,
72    RequireMfa {
73        user_id: Uuid,
74        pending_session_id: Uuid,
75    },
76    Block {
77        status: u16,
78        message: String,
79    },
80}
81
82pub struct PluginContext<'a> {
83    pub state: &'a YAuthState,
84}
85
86impl<'a> PluginContext<'a> {
87    pub fn new(state: &'a YAuthState) -> Self {
88        Self { state }
89    }
90}
91
92pub trait YAuthPlugin: Send + Sync + 'static {
93    fn name(&self) -> &'static str;
94    fn public_routes(&self, ctx: &PluginContext) -> Option<Router<YAuthState>>;
95    fn protected_routes(&self, ctx: &PluginContext) -> Option<Router<YAuthState>>;
96    fn on_event(&self, _event: &AuthEvent, _ctx: &PluginContext) -> EventResponse {
97        EventResponse::Continue
98    }
99    /// Declare the tables this plugin needs. Default: empty (no tables).
100    fn schema(&self) -> Vec<crate::schema::TableDef> {
101        Vec::new()
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn auth_event_serialization_roundtrip() {
111        let event = AuthEvent::LoginSucceeded {
112            user_id: Uuid::nil(),
113            method: "email".into(),
114        };
115        let json = serde_json::to_string(&event).unwrap();
116        let parsed: AuthEvent = serde_json::from_str(&json).unwrap();
117        match parsed {
118            AuthEvent::LoginSucceeded { user_id, method } => {
119                assert_eq!(user_id, Uuid::nil());
120                assert_eq!(method, "email");
121            }
122            _ => panic!("wrong variant"),
123        }
124    }
125
126    #[test]
127    fn event_response_continue_serializes() {
128        let resp = EventResponse::Continue;
129        let json = serde_json::to_string(&resp).unwrap();
130        assert!(json.contains("Continue"));
131    }
132
133    #[test]
134    fn event_response_require_mfa_serializes() {
135        let resp = EventResponse::RequireMfa {
136            user_id: Uuid::nil(),
137            pending_session_id: Uuid::nil(),
138        };
139        let json = serde_json::to_string(&resp).unwrap();
140        let parsed: EventResponse = serde_json::from_str(&json).unwrap();
141        match parsed {
142            EventResponse::RequireMfa { .. } => {}
143            _ => panic!("wrong variant"),
144        }
145    }
146
147    #[test]
148    fn event_response_block_serializes() {
149        let resp = EventResponse::Block {
150            status: 403,
151            message: "denied".into(),
152        };
153        let json = serde_json::to_string(&resp).unwrap();
154        assert!(json.contains("403"));
155        assert!(json.contains("denied"));
156    }
157
158    #[test]
159    fn all_event_variants_serialize() {
160        let events: Vec<AuthEvent> = vec![
161            AuthEvent::UserRegistered {
162                user_id: Uuid::nil(),
163                email: "a@b.c".into(),
164            },
165            AuthEvent::LoginSucceeded {
166                user_id: Uuid::nil(),
167                method: "email".into(),
168            },
169            AuthEvent::LoginFailed {
170                email: "a@b.c".into(),
171                method: "email".into(),
172                reason: "bad password".into(),
173            },
174            AuthEvent::SessionCreated {
175                user_id: Uuid::nil(),
176                session_id: Uuid::nil(),
177            },
178            AuthEvent::Logout {
179                user_id: Uuid::nil(),
180                session_id: Uuid::nil(),
181            },
182            AuthEvent::PasswordChanged {
183                user_id: Uuid::nil(),
184            },
185            AuthEvent::EmailVerified {
186                user_id: Uuid::nil(),
187            },
188            AuthEvent::MfaEnabled {
189                user_id: Uuid::nil(),
190                method: "totp".into(),
191            },
192            AuthEvent::MfaDisabled {
193                user_id: Uuid::nil(),
194                method: "totp".into(),
195            },
196            AuthEvent::UserBanned {
197                user_id: Uuid::nil(),
198            },
199            AuthEvent::UserUnbanned {
200                user_id: Uuid::nil(),
201            },
202            AuthEvent::MagicLinkSent {
203                email: "a@b.c".into(),
204            },
205            AuthEvent::MagicLinkVerified {
206                user_id: Uuid::nil(),
207                is_new_user: true,
208            },
209            #[cfg(feature = "account-lockout")]
210            AuthEvent::AccountLocked {
211                user_id: Uuid::nil(),
212                email: "a@b.c".into(),
213                locked_until: Some("2025-01-01T00:00:00Z".into()),
214            },
215            #[cfg(feature = "account-lockout")]
216            AuthEvent::AccountUnlocked {
217                user_id: Uuid::nil(),
218                method: "admin".into(),
219            },
220        ];
221        for event in &events {
222            let json = serde_json::to_string(event).unwrap();
223            let _: AuthEvent = serde_json::from_str(&json).unwrap();
224        }
225    }
226}