use axum::{
extract::{Form, Query, State},
http::{header::SET_COOKIE, HeaderMap, StatusCode},
response::{Html, IntoResponse, Redirect},
};
use serde::Deserialize;
use minijinja::context;
use sea_orm::*;
use tracing::error;
use crate::auth::{self, CreateUserRequest, LoginRequest};
use crate::entities::registration_tokens;
use crate::middleware::UserContext;
use crate::AppState;
#[derive(Debug, Deserialize)]
pub struct LoginForm {
pub username: String,
pub password: String,
}
#[derive(Debug, Deserialize)]
pub struct RegisterForm {
pub username: String,
pub email: String,
pub password: String,
pub confirm_password: String,
pub registration_token: String,
}
/// Show login page
pub async fn show_login(
State(app_state): State<AppState>,
) -> Result<Html<String>, StatusCode> {
let template = app_state.env.get_template("auth/login.html")
.map_err(|e| { error!(error = %e, "load auth/login.html failed"); StatusCode::INTERNAL_SERVER_ERROR })?;
let rendered = template
.render(context! {
title => "Login",
})
.map_err(|e| { error!(error = %e, "render auth/login.html failed"); StatusCode::INTERNAL_SERVER_ERROR })?;
Ok(Html(rendered))
}
#[derive(Debug, Deserialize)]
pub struct RegisterQuery {
pub token: Option<String>,
}
/// Show registration page. When `REGISTRATION_ENABLED=false` (default), the
/// page only renders if a `?token=` invite link is present.
pub async fn show_register(
State(app_state): State<AppState>,
Query(qs): Query<RegisterQuery>,
) -> Result<Html<String>, StatusCode> {
let invite_token = qs.token.as_deref().map(str::trim).filter(|s| !s.is_empty());
if invite_token.is_none() && !app_state.config.registration_enabled {
return Err(StatusCode::NOT_FOUND);
}
let template = app_state.env.get_template("auth/register.html")
.map_err(|e| { error!(error = %e, "load auth/register.html failed"); StatusCode::INTERNAL_SERVER_ERROR })?;
let rendered = template
.render(context! {
title => "Register",
invite_token => invite_token,
registration_open => app_state.config.registration_enabled,
})
.map_err(|e| { error!(error = %e, "render auth/register.html failed"); StatusCode::INTERNAL_SERVER_ERROR })?;
Ok(Html(rendered))
}
/// Show invite management page (logged-in users only).
pub async fn show_invites(
State(app_state): State<AppState>,
user_context: UserContext,
) -> Result<Html<String>, StatusCode> {
let mine = registration_tokens::Entity::find()
.filter(registration_tokens::Column::CreatedByUserId.eq(user_context.user.id))
.order_by_desc(registration_tokens::Column::CreatedAt)
.all(app_state.db.as_ref())
.await
.map_err(|e| { error!(error = %e, "load invites failed"); StatusCode::INTERNAL_SERVER_ERROR })?;
let template = app_state.env.get_template("auth/invites.html")
.map_err(|e| { error!(error = %e, "load auth/invites.html failed"); StatusCode::INTERNAL_SERVER_ERROR })?;
let rendered = template
.render(context! {
title => "Invites",
user => user_context.user,
invites => mine,
now => chrono::Utc::now(),
})
.map_err(|e| { error!(error = %e, "render auth/invites.html failed"); StatusCode::INTERNAL_SERVER_ERROR })?;
Ok(Html(rendered))
}
#[derive(Debug, Deserialize)]
pub struct CreateInviteForm {
pub email: Option<String>,
pub days: Option<i64>,
}
/// Create a new invite token for the current user. Returns redirect to /invites.
pub async fn handle_create_invite(
State(app_state): State<AppState>,
user_context: UserContext,
Form(form): Form<CreateInviteForm>,
) -> Result<Redirect, StatusCode> {
let email = form.email.map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
let days = form
.days
.filter(|d| *d > 0)
.unwrap_or(app_state.config.registration_token_duration_days);
auth::create_registration_token(
app_state.db.as_ref(),
Some(user_context.user.id),
email,
days,
)
.await
.map_err(|e| {
error!(error = %e, "create invite failed");
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(Redirect::to("/invites"))
}
/// Handle login form submission
pub async fn handle_login(
State(app_state): State<AppState>,
Form(form): Form<LoginForm>,
) -> Result<impl IntoResponse, StatusCode> {
let login_request = LoginRequest {
username: form.username,
password: form.password,
};
match auth::authenticate_user(&app_state.db, login_request).await {
Ok(Some(user)) => {
// Create session
match auth::create_session(&app_state.db, user.id, app_state.config.session_duration_hours).await {
Ok(session) => {
// Set cookie and redirect to dashboard
let mut headers = HeaderMap::new();
let cookie_value = format!(
"session_token={}; HttpOnly; SameSite=Strict; Path=/; Max-Age={}",
session.session_token,
app_state.config.session_duration_hours * 3600
);
headers.insert(SET_COOKIE, cookie_value.parse().unwrap());
Ok((headers, Redirect::to("/")))
}
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
Ok(None) => {
// Invalid credentials - redirect back to login with error
Err(StatusCode::UNAUTHORIZED)
}
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
/// Handle registration form submission
pub async fn handle_register(
State(app_state): State<AppState>,
Form(form): Form<RegisterForm>,
) -> Result<impl IntoResponse, StatusCode> {
// Validate password confirmation
if form.password != form.confirm_password {
return Err(StatusCode::BAD_REQUEST);
}
let create_request = CreateUserRequest {
username: form.username,
email: form.email,
password: form.password,
registration_token: form.registration_token,
};
match auth::create_user(&app_state.db, create_request).await {
Ok(user) => {
// Create session for the new user
match auth::create_session(&app_state.db, user.id, app_state.config.session_duration_hours).await {
Ok(session) => {
// Set cookie and redirect to dashboard
let mut headers = HeaderMap::new();
let cookie_value = format!(
"session_token={}; HttpOnly; SameSite=Strict; Path=/; Max-Age={}",
session.session_token,
app_state.config.session_duration_hours * 3600
);
headers.insert(SET_COOKIE, cookie_value.parse().unwrap());
Ok((headers, Redirect::to("/")))
}
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
Err(_) => {
// Registration failed - could be invalid token, duplicate user, etc.
Err(StatusCode::BAD_REQUEST)
}
}
}
/// Handle logout
pub async fn handle_logout(
State(app_state): State<AppState>,
user_context: Option<UserContext>,
) -> impl IntoResponse {
if let Some(context) = user_context {
// Delete the session from database
let _ = auth::delete_session(&app_state.db, &context.session.session_token).await;
}
// Clear cookie and redirect to login
let mut headers = HeaderMap::new();
let cookie_value = "session_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0";
headers.insert(SET_COOKIE, cookie_value.parse().unwrap());
(headers, Redirect::to("/login"))
}
/// Show user profile page
pub async fn show_profile(
State(app_state): State<AppState>,
user_context: UserContext,
) -> Result<Html<String>, StatusCode> {
let template = app_state.env.get_template("auth/profile.html")
.map_err(|e| { error!(error = %e, "load auth/profile.html failed"); StatusCode::INTERNAL_SERVER_ERROR })?;
let rendered = template
.render(context! {
title => "Profile",
user => user_context.user,
})
.map_err(|e| { error!(error = %e, "render auth/profile.html failed"); StatusCode::INTERNAL_SERVER_ERROR })?;
Ok(Html(rendered))
}
⭐
Log in to mark this repository.
8.5 KiB · text
History
1ce9641