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 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}