acton_htmx/auth/
handlers.rs1use crate::auth::{CreateUser, EmailAddress, FlashMessage, Session, User, UserError};
21use crate::state::ActonHtmxState;
22use axum::{
23 extract::State,
24 http::StatusCode,
25 response::{Html, IntoResponse, Redirect, Response},
26 Form,
27};
28use axum_htmx::HxRequest;
29use serde::Deserialize;
30use validator::Validate;
31
32#[derive(Debug, Deserialize, Validate)]
34pub struct LoginForm {
35 #[validate(email)]
37 pub email: String,
38
39 #[validate(length(min = 8))]
41 pub password: String,
42}
43
44#[derive(Debug, Deserialize, Validate)]
46pub struct RegisterForm {
47 #[validate(email)]
49 pub email: String,
50
51 #[validate(length(min = 8))]
53 pub password: String,
54
55 #[validate(length(min = 8))]
57 pub password_confirm: String,
58}
59
60pub async fn login_form(
71 HxRequest(_is_htmx): HxRequest,
72) -> Response {
73 let html = r#"
74<!DOCTYPE html>
75<html>
76<head>
77 <title>Login</title>
78 <script src="https://unpkg.com/htmx.org@1.9.10"></script>
79</head>
80<body>
81 <h1>Login</h1>
82 <form hx-post="/login" hx-target="body">
83 <div>
84 <label for="email">Email:</label>
85 <input type="email" id="email" name="email" required />
86 </div>
87 <div>
88 <label for="password">Password:</label>
89 <input type="password" id="password" name="password" required />
90 </div>
91 <button type="submit">Login</button>
92 </form>
93 <p><a href="/register">Don't have an account? Register</a></p>
94</body>
95</html>
96 "#;
97
98 Html(html).into_response()
100}
101
102pub async fn login_post(
121 State(state): State<ActonHtmxState>,
122 mut session: Session,
123 Form(form): Form<LoginForm>,
124) -> Result<Response, AuthHandlerError> {
125 form.validate()
127 .map_err(|e| AuthHandlerError::ValidationFailed(e.to_string()))?;
128
129 let email = EmailAddress::parse(&form.email)
131 .map_err(|_| AuthHandlerError::InvalidCredentials)?;
132
133 let user = User::authenticate(&email, &form.password, state.database_pool())
135 .await
136 .map_err(|_| AuthHandlerError::InvalidCredentials)?;
137
138 session.set_user_id(Some(user.id));
140
141 session.add_flash(FlashMessage::success("Successfully logged in!"));
143
144 Ok(Redirect::to("/").into_response())
146}
147
148pub async fn register_form(
159 HxRequest(_is_htmx): HxRequest,
160) -> Response {
161 let html = r#"
162<!DOCTYPE html>
163<html>
164<head>
165 <title>Register</title>
166 <script src="https://unpkg.com/htmx.org@1.9.10"></script>
167</head>
168<body>
169 <h1>Register</h1>
170 <form hx-post="/register" hx-target="body">
171 <div>
172 <label for="email">Email:</label>
173 <input type="email" id="email" name="email" required />
174 </div>
175 <div>
176 <label for="password">Password:</label>
177 <input type="password" id="password" name="password" required minlength="8" />
178 </div>
179 <div>
180 <label for="password_confirm">Confirm Password:</label>
181 <input type="password" id="password_confirm" name="password_confirm" required minlength="8" />
182 </div>
183 <button type="submit">Register</button>
184 </form>
185 <p><a href="/login">Already have an account? Login</a></p>
186</body>
187</html>
188 "#;
189
190 Html(html).into_response()
191}
192
193pub async fn register_post(
213 State(state): State<ActonHtmxState>,
214 mut session: Session,
215 Form(form): Form<RegisterForm>,
216) -> Result<Response, AuthHandlerError> {
217 form.validate()
219 .map_err(|e| AuthHandlerError::ValidationFailed(e.to_string()))?;
220
221 let email = EmailAddress::parse(&form.email)
223 .map_err(|_| AuthHandlerError::InvalidEmail)?;
224
225 if form.password != form.password_confirm {
227 return Err(AuthHandlerError::PasswordMismatch);
228 }
229
230 let create_user = CreateUser {
232 email,
233 password: form.password,
234 };
235 let user = User::create(create_user, state.database_pool()).await?;
236
237 session.set_user_id(Some(user.id));
239
240 session.add_flash(FlashMessage::success("Account created successfully! Welcome!"));
242
243 Ok(Redirect::to("/").into_response())
245}
246
247pub async fn logout_post(
258 mut session: Session,
259) -> Response {
260 session.set_user_id(None);
262
263 session.add_flash(FlashMessage::info("You have been logged out."));
265
266 Redirect::to("/login").into_response()
268}
269
270#[derive(Debug)]
272pub enum AuthHandlerError {
273 ValidationFailed(String),
275
276 InvalidEmail,
278
279 PasswordMismatch,
281
282 InvalidCredentials,
284
285 UserError(UserError),
287
288 DatabaseNotConfigured,
290}
291
292impl From<UserError> for AuthHandlerError {
293 fn from(err: UserError) -> Self {
294 Self::UserError(err)
295 }
296}
297
298impl IntoResponse for AuthHandlerError {
299 fn into_response(self) -> Response {
300 let (status, message) = match self {
301 Self::ValidationFailed(msg) => (StatusCode::BAD_REQUEST, msg),
302 Self::InvalidEmail => (StatusCode::BAD_REQUEST, "Invalid email format".to_string()),
303 Self::PasswordMismatch => (
304 StatusCode::BAD_REQUEST,
305 "Passwords do not match".to_string(),
306 ),
307 Self::InvalidCredentials => (
308 StatusCode::UNAUTHORIZED,
309 "Invalid email or password".to_string(),
310 ),
311 Self::UserError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
312 Self::DatabaseNotConfigured => (
313 StatusCode::INTERNAL_SERVER_ERROR,
314 "Database not configured".to_string(),
315 ),
316 };
317
318 (status, message).into_response()
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_login_form_struct() {
328 let form = LoginForm {
329 email: "test@example.com".to_string(),
330 password: "password123".to_string(),
331 };
332 assert!(form.validate().is_ok());
333 }
334
335 #[test]
336 fn test_register_form_struct() {
337 let form = RegisterForm {
338 email: "test@example.com".to_string(),
339 password: "password123".to_string(),
340 password_confirm: "password123".to_string(),
341 };
342 assert!(form.validate().is_ok());
343 }
344
345 #[test]
346 fn test_invalid_email() {
347 let form = LoginForm {
348 email: "not-an-email".to_string(),
349 password: "password123".to_string(),
350 };
351 assert!(form.validate().is_err());
352 }
353
354 #[test]
355 fn test_short_password() {
356 let form = LoginForm {
357 email: "test@example.com".to_string(),
358 password: "short".to_string(),
359 };
360 assert!(form.validate().is_err());
361 }
362}