8.5 KiB · text History 1ce9641
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))
}