126 lines
3.7 KiB
Rust
126 lines
3.7 KiB
Rust
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<LoginQuery>,
|
|
) -> Html<String> {
|
|
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<String> {
|
|
Html(ui::render_initial_password_page(Nav::new(&headers, "login")))
|
|
}
|
|
|
|
pub(crate) async fn set_initial_password(
|
|
State(state): State<AppState>,
|
|
headers: HeaderMap,
|
|
Form(input): Form<InitialPasswordInput>,
|
|
) -> 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<AppState>,
|
|
Form(input): Form<LoginInput>,
|
|
) -> 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()
|
|
}
|