This commit is contained in:
+11
-8
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user