1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
4use argon2::password_hash::{SaltString, rand_core::OsRng};
5
6use crate::core::{AuthPlugin, AuthRoute, AuthContext};
7use crate::types::{AuthRequest, AuthResponse, HttpMethod, CreateUser, User};
8use crate::error::{AuthError, AuthResult};
9
10pub struct EmailPasswordPlugin {
12 config: EmailPasswordConfig,
13}
14
15#[derive(Debug, Clone)]
16pub struct EmailPasswordConfig {
17 pub enable_signup: bool,
18 pub require_email_verification: bool,
19 pub password_min_length: usize,
20}
21
22#[derive(Debug, Deserialize)]
23struct SignUpRequest {
24 name: String,
25 email: String,
26 password: String,
27 #[serde(rename = "callbackURL")]
28 callback_url: Option<String>,
29}
30
31#[derive(Debug, Deserialize)]
32struct SignInRequest {
33 email: String,
34 password: String,
35 #[serde(rename = "callbackURL")]
36 callback_url: Option<String>,
37 #[serde(rename = "rememberMe")]
38 remember_me: Option<bool>,
39}
40
41#[derive(Debug, Serialize)]
42struct SignUpResponse {
43 token: Option<String>,
44 user: User,
45}
46
47#[derive(Debug, Serialize)]
48struct SignInResponse {
49 redirect: bool,
50 token: String,
51 url: Option<String>,
52 user: User,
53}
54
55impl EmailPasswordPlugin {
56 pub fn new() -> Self {
57 Self {
58 config: EmailPasswordConfig::default(),
59 }
60 }
61
62 pub fn with_config(config: EmailPasswordConfig) -> Self {
63 Self { config }
64 }
65
66 pub fn enable_signup(mut self, enable: bool) -> Self {
67 self.config.enable_signup = enable;
68 self
69 }
70
71 pub fn require_email_verification(mut self, require: bool) -> Self {
72 self.config.require_email_verification = require;
73 self
74 }
75
76 pub fn password_min_length(mut self, length: usize) -> Self {
77 self.config.password_min_length = length;
78 self
79 }
80
81 async fn handle_sign_up(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<AuthResponse> {
82 if !self.config.enable_signup {
83 return Ok(AuthResponse::json(403, &serde_json::json!({
84 "error": "Signup disabled",
85 "message": "User registration is not enabled"
86 }))?);
87 }
88
89 let signup_req: SignUpRequest = match req.body_as_json() {
90 Ok(req) => req,
91 Err(e) => {
92 return Ok(AuthResponse::json(400, &serde_json::json!({
93 "error": "Invalid request",
94 "message": format!("Invalid JSON: {}", e)
95 }))?);
96 }
97 };
98
99 if let Err(e) = self.validate_password(&signup_req.password, ctx) {
101 return Ok(AuthResponse::json(400, &serde_json::json!({
102 "error": "Invalid request",
103 "message": e.to_string()
104 }))?);
105 }
106
107 if let Some(_) = ctx.database.get_user_by_email(&signup_req.email).await? {
109 return Ok(AuthResponse::json(409, &serde_json::json!({
110 "error": "User exists",
111 "message": "A user with this email already exists"
112 }))?);
113 }
114
115 let password_hash = self.hash_password(&signup_req.password)?;
117
118 let mut metadata = std::collections::HashMap::new();
120 metadata.insert("password_hash".to_string(), serde_json::Value::String(password_hash));
121
122 let create_user = CreateUser::new()
123 .with_email(&signup_req.email)
124 .with_name(&signup_req.name);
125
126 let mut create_user = create_user;
127 create_user.metadata = Some(metadata);
128
129 let user = ctx.database.create_user(create_user).await?;
130
131 let session_manager = crate::core::SessionManager::new(ctx.config.clone(), ctx.database.clone());
133 let session = session_manager.create_session(&user, None, None).await?;
134
135 let response = SignUpResponse {
136 token: Some(session.token.clone()),
137 user,
138 };
139
140 let cookie_header = self.create_session_cookie(&session.token, ctx);
142
143 Ok(AuthResponse::json(200, &response)?
144 .with_header("Set-Cookie", cookie_header))
145 }
146
147 async fn handle_sign_in(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<AuthResponse> {
148 let signin_req: SignInRequest = match req.body_as_json() {
149 Ok(req) => req,
150 Err(e) => {
151 return Ok(AuthResponse::json(400, &serde_json::json!({
152 "error": "Invalid request",
153 "message": format!("Invalid JSON: {}", e)
154 }))?);
155 }
156 };
157
158 let user = match ctx.database.get_user_by_email(&signin_req.email).await? {
160 Some(user) => user,
161 None => {
162 return Ok(AuthResponse::json(401, &serde_json::json!({
163 "error": "Invalid credentials",
164 "message": "Email or password is incorrect"
165 }))?);
166 }
167 };
168
169 let stored_hash = match user.metadata.get("password_hash").and_then(|v| v.as_str()) {
171 Some(hash) => hash,
172 None => {
173 return Ok(AuthResponse::json(401, &serde_json::json!({
174 "error": "Invalid credentials",
175 "message": "Email or password is incorrect"
176 }))?);
177 }
178 };
179
180 if let Err(_) = self.verify_password(&signin_req.password, stored_hash) {
181 return Ok(AuthResponse::json(401, &serde_json::json!({
182 "error": "Invalid credentials",
183 "message": "Email or password is incorrect"
184 }))?);
185 }
186
187 let session_manager = crate::core::SessionManager::new(ctx.config.clone(), ctx.database.clone());
189 let session = session_manager.create_session(&user, None, None).await?;
190
191 let response = SignInResponse {
192 redirect: false,
193 token: session.token.clone(),
194 url: None,
195 user,
196 };
197
198 let cookie_header = self.create_session_cookie(&session.token, ctx);
200
201 Ok(AuthResponse::json(200, &response)?
202 .with_header("Set-Cookie", cookie_header))
203 }
204
205 fn validate_password(&self, password: &str, ctx: &AuthContext) -> AuthResult<()> {
206 if password.len() < ctx.config.password.min_length {
207 return Err(AuthError::InvalidRequest(format!(
208 "Password must be at least {} characters long",
209 ctx.config.password.min_length
210 )));
211 }
212
213 Ok(())
216 }
217
218 fn hash_password(&self, password: &str) -> AuthResult<String> {
219 let salt = SaltString::generate(&mut OsRng);
220 let argon2 = Argon2::default();
221
222 let password_hash = argon2.hash_password(password.as_bytes(), &salt)
223 .map_err(|e| AuthError::PasswordHash(format!("Failed to hash password: {}", e)))?;
224
225 Ok(password_hash.to_string())
226 }
227
228 fn create_session_cookie(&self, token: &str, ctx: &AuthContext) -> String {
229 let session_config = &ctx.config.session;
230 let secure = if session_config.cookie_secure { "; Secure" } else { "" };
231 let http_only = if session_config.cookie_http_only { "; HttpOnly" } else { "" };
232 let same_site = match session_config.cookie_same_site {
233 crate::core::config::SameSite::Strict => "; SameSite=Strict",
234 crate::core::config::SameSite::Lax => "; SameSite=Lax",
235 crate::core::config::SameSite::None => "; SameSite=None",
236 };
237
238 let expires = chrono::Utc::now() + session_config.expires_in;
240 let expires_str = expires.format("%a, %d %b %Y %H:%M:%S GMT");
241
242 format!("{}={}; Path=/; Expires={}{}{}{}",
243 session_config.cookie_name,
244 token,
245 expires_str,
246 secure,
247 http_only,
248 same_site)
249 }
250
251 fn verify_password(&self, password: &str, hash: &str) -> AuthResult<()> {
252 let parsed_hash = PasswordHash::new(hash)
253 .map_err(|e| AuthError::PasswordHash(format!("Invalid password hash: {}", e)))?;
254
255 let argon2 = Argon2::default();
256 argon2.verify_password(password.as_bytes(), &parsed_hash)
257 .map_err(|_| AuthError::InvalidCredentials)?;
258
259 Ok(())
260 }
261}
262
263impl Default for EmailPasswordConfig {
264 fn default() -> Self {
265 Self {
266 enable_signup: true,
267 require_email_verification: false,
268 password_min_length: 8,
269 }
270 }
271}
272
273#[async_trait]
274impl AuthPlugin for EmailPasswordPlugin {
275 fn name(&self) -> &'static str {
276 "email-password"
277 }
278
279 fn routes(&self) -> Vec<AuthRoute> {
280 let mut routes = vec![
281 AuthRoute::post("/sign-in/email", "sign_in_email"),
282 ];
283
284 if self.config.enable_signup {
285 routes.push(AuthRoute::post("/sign-up/email", "sign_up_email"));
286 }
287
288 routes
289 }
290
291 async fn on_request(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<Option<crate::types::AuthResponse>> {
292 match (req.method(), req.path()) {
293 (HttpMethod::Post, "/sign-up/email") if self.config.enable_signup => {
294 Ok(Some(self.handle_sign_up(req, ctx).await?))
295 },
296 (HttpMethod::Post, "/sign-in/email") => {
297 Ok(Some(self.handle_sign_in(req, ctx).await?))
298 },
299 _ => Ok(None),
300 }
301 }
302
303 async fn on_user_created(&self, user: &User, ctx: &AuthContext) -> AuthResult<()> {
304 if self.config.require_email_verification && !user.email_verified {
306 if let Some(email) = &user.email {
307 println!("📧 Email verification required for user: {}", email);
308 }
311 }
312
313 Ok(())
314 }
315}