Implementing multi-language Admin UI
build / build-linux-amd64 (push) Successful in 2m2s

This commit is contained in:
2026-05-09 16:58:20 +02:00
parent a7b3e83f02
commit 1e961cdd92
14 changed files with 2989 additions and 487 deletions
+11 -8
View File
@@ -4,6 +4,7 @@
//! `/admin/*` and `/api/*`. The middleware in `api::middleware` already
//! accepts both `Authorization: Bearer …` and the cookie.
use crate::api::admin::i18n::{t, Lang};
use crate::api::auth::mint_token;
use crate::api::middleware::{sha256_token, SESSION_COOKIE};
use crate::api::state::AppState;
@@ -32,6 +33,7 @@ pub struct LoginForm {
pub async fn login(
Extension(state): Extension<Arc<AppState>>,
lang: Lang,
Form(form): Form<LoginForm>,
) -> Response {
// First leg: password verify. Same DB call paths as `/api/login` —
@@ -39,7 +41,7 @@ pub async fn login(
// diverge from the API's auth contract.
let user = match state.db.user_find_by_username(&form.username).await {
Ok(Some(u)) => u,
Ok(None) => return error_fragment("Bad credentials"),
Ok(None) => return error_fragment(t(lang, "login.bad_credentials")),
Err(e) => return error_fragment(&format!("internal: {}", e)),
};
let pw_ok = match verify_password(user.password_hash.clone(), form.password.clone()).await {
@@ -47,16 +49,16 @@ pub async fn login(
Err(e) => return error_fragment(&format!("internal: {}", e)),
};
if !pw_ok {
return error_fragment("Bad credentials");
return error_fragment(t(lang, "login.bad_credentials"));
}
if user.status == 0 {
return error_fragment("Account disabled");
return error_fragment(t(lang, "login.account_disabled"));
}
if !user.is_admin {
// Only admins can use the dashboard. Non-admin users still get
// tokens via `/api/login` for the desktop client; they just don't
// see the management surface.
return error_fragment("Admin access required");
return error_fragment(t(lang, "login.admin_required"));
}
// Optional second leg: TOTP. If the user has a secret enrolled and the
// form didn't carry a code, return a fragment that asks for one.
@@ -72,11 +74,12 @@ pub async fn login(
// leg: it watches for the special marker via HX-Trigger and
// reveals the #tfa-section.
let frag = format!(
r#"<span data-tfa-required="1" class="text-amber-300">Enter your 6-digit authenticator code.</span>
r#"<span data-tfa-required="1" class="text-amber-300">{msg}</span>
<script>
document.getElementById('tfa-section').classList.remove('hidden');
document.getElementById('tfaCode').focus();
</script>"#
</script>"#,
msg = t(lang, "login.totp_required"),
);
// We don't need a session yet — caller will resubmit with the
// same username/password plus the code. (No nonce involved on
@@ -88,10 +91,10 @@ pub async fn login(
// Verify the supplied code.
let ok = match crate::api::auth::verify_totp(&secret_b32, &form.tfa_code) {
Ok(b) => b,
Err(_) => return error_fragment("Internal TOTP error"),
Err(_) => return error_fragment(t(lang, "login.internal_totp")),
};
if !ok {
return error_fragment("Bad TOTP code");
return error_fragment(t(lang, "login.bad_totp"));
}
}