use axum::{
extract::{Form, Query, State},
http::{header::SET_COOKIE, HeaderMap, StatusCode},
response::{Html, IntoResponse, Redirect},
};
use minijinja::context;
use sea_orm::*;
use serde::Deserialize;
use tracing::error;
use super::render;
use crate::auth::{self, CreateUserRequest, LoginRequest};
use crate::entities::registration_tokens;
use crate::middleware::UserContext;
use crate::AppState;
/// Build the `Set-Cookie` value for a fresh session.
fn session_cookie(token: &str, duration_hours: i64) -> String {
format!(
"session_token={token}; HttpOnly; SameSite=Strict; Path=/; Max-Age={}",
duration_hours * 3600
)
}
#[derive(Debug, Deserialize)]
pub struct LoginForm {
pub username: String,
pub password: String,
}
pub async fn show_login(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
render(&state, "auth/login.html", context! { title => "Login" })
}
#[derive(Debug, Deserialize)]
pub struct RegisterQuery {
pub token: Option<String>,
}
/// Show the registration page. When public registration is off (the default),
/// the page 404s unless an invite `?token=` is present.
pub async fn show_register(
State(state): State<AppState>,
Query(qs): Query<RegisterQuery>,
) -> Result<Html<String>, StatusCode> {
let invite = qs.token.as_deref().map(str::trim).filter(|s| !s.is_empty());
if invite.is_none() && !state.config.registration_enabled {
return Err(StatusCode::NOT_FOUND);
}
render(
&state,
"auth/register.html",
context! {
title => "Register",
invite_token => invite,
registration_open => state.config.registration_enabled,
},
)
}
#[derive(Debug, Deserialize)]
pub struct RegisterForm {
pub username: String,
pub email: String,
pub password: String,
pub confirm_password: String,
pub registration_token: String,
}
pub async fn handle_register(
State(state): State<AppState>,
Form(form): Form<RegisterForm>,
) -> Result<impl IntoResponse, StatusCode> {
if form.password != form.confirm_password {
return Err(StatusCode::BAD_REQUEST);
}
let user = auth::create_user(
&state.db,
CreateUserRequest {
username: form.username,
email: form.email,
password: form.password,
registration_token: form.registration_token,
},
)
.await
.map_err(|e| {
error!(error = %e, "registration failed");
StatusCode::BAD_REQUEST
})?;
let session = auth::create_session(&state.db, user.id, state.config.session_duration_hours)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let mut headers = HeaderMap::new();
headers.insert(
SET_COOKIE,
session_cookie(&session.session_token, state.config.session_duration_hours)
.parse()
.unwrap(),
);
Ok((headers, Redirect::to("/")))
}
pub async fn handle_login(
State(state): State<AppState>,
Form(form): Form<LoginForm>,
) -> Result<impl IntoResponse, StatusCode> {
let user = auth::authenticate_user(
&state.db,
LoginRequest { username: form.username, password: form.password },
)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::UNAUTHORIZED)?;
let session = auth::create_session(&state.db, user.id, state.config.session_duration_hours)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let mut headers = HeaderMap::new();
headers.insert(
SET_COOKIE,
session_cookie(&session.session_token, state.config.session_duration_hours)
.parse()
.unwrap(),
);
Ok((headers, Redirect::to("/")))
}
pub async fn handle_logout(
State(state): State<AppState>,
user: Option<UserContext>,
) -> impl IntoResponse {
if let Some(ctx) = user {
let _ = auth::delete_session(&state.db, &ctx.session.session_token).await;
}
let mut headers = HeaderMap::new();
headers.insert(
SET_COOKIE,
"session_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0".parse().unwrap(),
);
(headers, Redirect::to("/login"))
}
pub async fn show_profile(
State(state): State<AppState>,
user: UserContext,
) -> Result<Html<String>, StatusCode> {
render(&state, "auth/profile.html", context! { title => "Profile", user => user.user })
}
/// List and create invite links (signed-in users only).
pub async fn show_invites(
State(state): State<AppState>,
user: UserContext,
) -> Result<Html<String>, StatusCode> {
let invites = registration_tokens::Entity::find()
.filter(registration_tokens::Column::CreatedByUserId.eq(user.user.id))
.order_by_desc(registration_tokens::Column::CreatedAt)
.all(state.db.as_ref())
.await
.map_err(|e| {
error!(error = %e, "load invites failed");
StatusCode::INTERNAL_SERVER_ERROR
})?;
render(
&state,
"auth/invites.html",
context! {
title => "Invites",
user => user.user,
invites => invites,
now => chrono::Utc::now(),
},
)
}
#[derive(Debug, Deserialize)]
pub struct CreateInviteForm {
pub email: Option<String>,
pub days: Option<i64>,
}
pub async fn handle_create_invite(
State(state): State<AppState>,
user: 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(state.config.registration_token_duration_days);
auth::create_registration_token(state.db.as_ref(), Some(user.user.id), email, days)
.await
.map_err(|e| {
error!(error = %e, "create invite failed");
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(Redirect::to("/invites"))
}
neiam
/bismuth
MOTHER-FUCKING-BISMUTH
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
5.9 KiB · text
History
487bb12