use axum::{ Form, extract::{Query, State}, http::{HeaderMap, HeaderValue, StatusCode, header}, response::{Html, IntoResponse, Response}, }; use tonic::Request; use crate::{AppState, auth::{LoginRequest, SetInitialPasswordRequest}, services::reject_cross_site, ui::Nav}; use super::{state::{InitialPasswordInput, LoginInput, LoginQuery}, ui}; pub(crate) async fn login_page( headers: HeaderMap, Query(query): Query, ) -> Html { Html(ui::render_page( Nav::new(&headers, "login"), query.permissions_changed, query.initial_password_set, )) } pub(crate) async fn initial_password_page(headers: HeaderMap) -> Html { Html(ui::render_initial_password_page(Nav::new(&headers, "login"))) } pub(crate) async fn set_initial_password( State(state): State, headers: HeaderMap, Form(input): Form, ) -> Response { if let Some(rejection) = reject_cross_site(&headers) { return rejection; } let username = input.username.trim(); if !matches!(username, "admin" | "superadmin") { return error( StatusCode::UNPROCESSABLE_ENTITY, "Only the bootstrap admin or superadmin account can be claimed here.", ); } let mut auth = state.auth; match auth .set_initial_password(tonic::Request::new(SetInitialPasswordRequest { username: username.to_string(), password: input.password, password_confirmation: input.password_confirmation, })) .await { Ok(_) => { let mut response = StatusCode::SEE_OTHER.into_response(); response.headers_mut().insert( header::LOCATION, HeaderValue::from_static("/login?initial_password_set=1"), ); response.headers_mut().insert( "hx-redirect", HeaderValue::from_static("/login?initial_password_set=1"), ); response } Err(status) => ( StatusCode::UNPROCESSABLE_ENTITY, Html(ui::render_initial_password_error(status.message())), ) .into_response(), } } pub(crate) async fn login( State(state): State, Form(input): Form, ) -> Response { if input.identifier.trim().is_empty() { return error(StatusCode::BAD_REQUEST, "Username or email is required"); } let mut client = state.auth; let login = match client .login(Request::new(LoginRequest { identifier: input.identifier, password: input.password, })) .await { Ok(response) => response.into_inner(), Err(status) => return error(StatusCode::UNAUTHORIZED, status.message()), }; let cookie = format!( "{}={}; Path=/; HttpOnly; SameSite=Strict; Max-Age={}", crate::ui::SESSION_COOKIE, login.access_token, login.expires_in, ); let Ok(cookie) = HeaderValue::try_from(cookie) else { return error( StatusCode::BAD_GATEWAY, "The server returned an invalid access token", ); }; let mut response = Html(String::new()).into_response(); response.headers_mut().insert(header::SET_COOKIE, cookie); let destination = if login .authorization .as_ref() .is_some_and(crate::authz::can_open_admin) { "/admin" } else { "/" }; response.headers_mut().insert( "hx-redirect", HeaderValue::from_static(destination), ); response } fn error(status: StatusCode, message: &str) -> Response { (status, Html(ui::render_error(message))).into_response() }