use crate::auth::{CreateUser, EmailAddress, FlashMessage, Session, User, UserError};
use crate::state::ActonHtmxState;
use axum::{
extract::State,
http::StatusCode,
response::{Html, IntoResponse, Redirect, Response},
Form,
};
use axum_htmx::HxRequest;
use serde::Deserialize;
use validator::Validate;
#[derive(Debug, Deserialize, Validate)]
pub struct LoginForm {
#[validate(email)]
pub email: String,
#[validate(length(min = 8))]
pub password: String,
}
#[derive(Debug, Deserialize, Validate)]
pub struct RegisterForm {
#[validate(email)]
pub email: String,
#[validate(length(min = 8))]
pub password: String,
#[validate(length(min = 8))]
pub password_confirm: String,
}
pub async fn login_form(
HxRequest(_is_htmx): HxRequest,
) -> Response {
let html = r#"
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
<h1>Login</h1>
<form hx-post="/login" hx-target="body">
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required />
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required />
</div>
<button type="submit">Login</button>
</form>
<p><a href="/register">Don't have an account? Register</a></p>
</body>
</html>
"#;
Html(html).into_response()
}
pub async fn login_post(
State(state): State<ActonHtmxState>,
mut session: Session,
Form(form): Form<LoginForm>,
) -> Result<Response, AuthHandlerError> {
form.validate()
.map_err(|e| AuthHandlerError::ValidationFailed(e.to_string()))?;
let email = EmailAddress::parse(&form.email)
.map_err(|_| AuthHandlerError::InvalidCredentials)?;
let user = User::authenticate(&email, &form.password, state.database_pool())
.await
.map_err(|_| AuthHandlerError::InvalidCredentials)?;
session.set_user_id(Some(user.id));
session.add_flash(FlashMessage::success("Successfully logged in!"));
Ok(Redirect::to("/").into_response())
}
pub async fn register_form(
HxRequest(_is_htmx): HxRequest,
) -> Response {
let html = r#"
<!DOCTYPE html>
<html>
<head>
<title>Register</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
<h1>Register</h1>
<form hx-post="/register" hx-target="body">
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required />
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required minlength="8" />
</div>
<div>
<label for="password_confirm">Confirm Password:</label>
<input type="password" id="password_confirm" name="password_confirm" required minlength="8" />
</div>
<button type="submit">Register</button>
</form>
<p><a href="/login">Already have an account? Login</a></p>
</body>
</html>
"#;
Html(html).into_response()
}
pub async fn register_post(
State(state): State<ActonHtmxState>,
mut session: Session,
Form(form): Form<RegisterForm>,
) -> Result<Response, AuthHandlerError> {
form.validate()
.map_err(|e| AuthHandlerError::ValidationFailed(e.to_string()))?;
let email = EmailAddress::parse(&form.email)
.map_err(|_| AuthHandlerError::InvalidEmail)?;
if form.password != form.password_confirm {
return Err(AuthHandlerError::PasswordMismatch);
}
let create_user = CreateUser {
email,
password: form.password,
};
let user = User::create(create_user, state.database_pool()).await?;
session.set_user_id(Some(user.id));
session.add_flash(FlashMessage::success("Account created successfully! Welcome!"));
Ok(Redirect::to("/").into_response())
}
pub async fn logout_post(
mut session: Session,
) -> Response {
session.set_user_id(None);
session.add_flash(FlashMessage::info("You have been logged out."));
Redirect::to("/login").into_response()
}
#[derive(Debug)]
pub enum AuthHandlerError {
ValidationFailed(String),
InvalidEmail,
PasswordMismatch,
InvalidCredentials,
UserError(UserError),
DatabaseNotConfigured,
}
impl From<UserError> for AuthHandlerError {
fn from(err: UserError) -> Self {
Self::UserError(err)
}
}
impl IntoResponse for AuthHandlerError {
fn into_response(self) -> Response {
let (status, message) = match self {
Self::ValidationFailed(msg) => (StatusCode::BAD_REQUEST, msg),
Self::InvalidEmail => (StatusCode::BAD_REQUEST, "Invalid email format".to_string()),
Self::PasswordMismatch => (
StatusCode::BAD_REQUEST,
"Passwords do not match".to_string(),
),
Self::InvalidCredentials => (
StatusCode::UNAUTHORIZED,
"Invalid email or password".to_string(),
),
Self::UserError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
Self::DatabaseNotConfigured => (
StatusCode::INTERNAL_SERVER_ERROR,
"Database not configured".to_string(),
),
};
(status, message).into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_login_form_struct() {
let form = LoginForm {
email: "test@example.com".to_string(),
password: "password123".to_string(),
};
assert!(form.validate().is_ok());
}
#[test]
fn test_register_form_struct() {
let form = RegisterForm {
email: "test@example.com".to_string(),
password: "password123".to_string(),
password_confirm: "password123".to_string(),
};
assert!(form.validate().is_ok());
}
#[test]
fn test_invalid_email() {
let form = LoginForm {
email: "not-an-email".to_string(),
password: "password123".to_string(),
};
assert!(form.validate().is_err());
}
#[test]
fn test_short_password() {
let form = LoginForm {
email: "test@example.com".to_string(),
password: "short".to_string(),
};
assert!(form.validate().is_err());
}
}