pinakes: import in parallel; various UI improvements
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I1eb47cd79cd4145c56af966f6756fe1d6a6a6964
This commit is contained in:
parent
278bcaa4b0
commit
116fe7b059
42 changed files with 4316 additions and 316 deletions
|
|
@ -5,6 +5,12 @@ use axum::http::{HeaderMap, StatusCode};
|
|||
use crate::dto::{LoginRequest, LoginResponse, UserInfoResponse};
|
||||
use crate::state::AppState;
|
||||
|
||||
/// Dummy password hash to use for timing-safe comparison when user doesn't exist.
|
||||
/// This is a valid argon2 hash that will always fail verification but takes
|
||||
/// similar time to verify as a real hash, preventing timing attacks that could
|
||||
/// reveal whether a username exists.
|
||||
const DUMMY_HASH: &str = "$argon2id$v=19$m=19456,t=2,p=1$VGltaW5nU2FmZUR1bW15$c2ltdWxhdGVkX2hhc2hfZm9yX3RpbWluZ19zYWZldHk";
|
||||
|
||||
pub async fn login(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<LoginRequest>,
|
||||
|
|
@ -25,27 +31,47 @@ pub async fn login(
|
|||
.iter()
|
||||
.find(|u| u.username == req.username);
|
||||
|
||||
let user = match user {
|
||||
Some(u) => u,
|
||||
None => {
|
||||
tracing::warn!(username = %req.username, "login failed: unknown user");
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
// Always perform password verification to prevent timing attacks.
|
||||
// If the user doesn't exist, we verify against a dummy hash to ensure
|
||||
// consistent response times regardless of whether the username exists.
|
||||
use argon2::password_hash::PasswordVerifier;
|
||||
|
||||
let (hash_to_verify, user_found) = match user {
|
||||
Some(u) => (&u.password_hash as &str, true),
|
||||
None => (DUMMY_HASH, false),
|
||||
};
|
||||
|
||||
// Verify password using argon2
|
||||
use argon2::password_hash::PasswordVerifier;
|
||||
let hash = &user.password_hash;
|
||||
let parsed_hash = argon2::password_hash::PasswordHash::new(hash)
|
||||
let parsed_hash = argon2::password_hash::PasswordHash::new(hash_to_verify)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let valid = argon2::Argon2::default()
|
||||
|
||||
let password_valid = argon2::Argon2::default()
|
||||
.verify_password(req.password.as_bytes(), &parsed_hash)
|
||||
.is_ok();
|
||||
if !valid {
|
||||
tracing::warn!(username = %req.username, "login failed: invalid password");
|
||||
|
||||
// Authentication fails if user wasn't found OR password was invalid
|
||||
if !user_found || !password_valid {
|
||||
// Log different messages for debugging but return same error
|
||||
if !user_found {
|
||||
tracing::warn!(username = %req.username, "login failed: unknown user");
|
||||
} else {
|
||||
tracing::warn!(username = %req.username, "login failed: invalid password");
|
||||
}
|
||||
|
||||
// Record failed login attempt in audit log
|
||||
let _ = pinakes_core::audit::record_action(
|
||||
&state.storage,
|
||||
None,
|
||||
pinakes_core::model::AuditAction::LoginFailed,
|
||||
Some(format!("username: {}", req.username)),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// At this point we know the user exists and password is valid
|
||||
let user = user.expect("user should exist at this point");
|
||||
|
||||
// Generate session token
|
||||
use rand::Rng;
|
||||
let token: String = rand::rng()
|
||||
|
|
@ -72,6 +98,15 @@ pub async fn login(
|
|||
|
||||
tracing::info!(username = %username, role = %role, "login successful");
|
||||
|
||||
// Record successful login in audit log
|
||||
let _ = pinakes_core::audit::record_action(
|
||||
&state.storage,
|
||||
None,
|
||||
pinakes_core::model::AuditAction::LoginSuccess,
|
||||
Some(format!("username: {}, role: {}", username, role)),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Json(LoginResponse {
|
||||
token,
|
||||
username,
|
||||
|
|
@ -81,8 +116,24 @@ pub async fn login(
|
|||
|
||||
pub async fn logout(State(state): State<AppState>, headers: HeaderMap) -> StatusCode {
|
||||
if let Some(token) = extract_bearer_token(&headers) {
|
||||
let sessions = state.sessions.read().await;
|
||||
let username = sessions.get(token).map(|s| s.username.clone());
|
||||
drop(sessions);
|
||||
|
||||
let mut sessions = state.sessions.write().await;
|
||||
sessions.remove(token);
|
||||
drop(sessions);
|
||||
|
||||
// Record logout in audit log
|
||||
if let Some(user) = username {
|
||||
let _ = pinakes_core::audit::record_action(
|
||||
&state.storage,
|
||||
None,
|
||||
pinakes_core::model::AuditAction::Logout,
|
||||
Some(format!("username: {}", user)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
StatusCode::OK
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue