pinakes-server: wire backup, session refresh, webhooks, and rate limit config

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If2855d44cc700c0f65a5f5ac850ee3866a6a6964
This commit is contained in:
raf 2026-03-08 00:42:14 +03:00
commit 52f0b5defc
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
8 changed files with 257 additions and 105 deletions

View file

@ -57,10 +57,10 @@ pub async fn login(
// 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 {
if user_found {
tracing::warn!(username = %req.username, "login failed: invalid password");
} else {
tracing::warn!(username = %req.username, "login failed: unknown user");
}
// Record failed login attempt in audit log
@ -103,7 +103,8 @@ pub async fn login(
username: username.clone(),
role: role.to_string(),
created_at: now,
expires_at: now + chrono::Duration::hours(24), // 24 hour sessions
expires_at: now
+ chrono::Duration::hours(config.accounts.session_expiry_hours as i64),
last_accessed: now,
};
@ -119,7 +120,7 @@ pub async fn login(
&state.storage,
None,
pinakes_core::model::AuditAction::LoginSuccess,
Some(format!("username: {}, role: {}", username, role)),
Some(format!("username: {username}, role: {role}")),
)
.await
{
@ -151,17 +152,16 @@ pub async fn logout(
}
// Record logout in audit log
if let Some(user) = username {
if let Err(e) = pinakes_core::audit::record_action(
if let Some(user) = username
&& let Err(e) = pinakes_core::audit::record_action(
&state.storage,
None,
pinakes_core::model::AuditAction::Logout,
Some(format!("username: {}", user)),
Some(format!("username: {user}")),
)
.await
{
tracing::warn!(error = %e, "failed to record logout audit");
}
{
tracing::warn!(error = %e, "failed to record logout audit");
}
}
StatusCode::OK
@ -191,7 +191,7 @@ pub async fn me(
Ok(Json(UserInfoResponse {
username: session.username.clone(),
role: session.role.clone(),
role: session.role,
}))
}
@ -202,6 +202,35 @@ fn extract_bearer_token(headers: &HeaderMap) -> Option<&str> {
.and_then(|s| s.strip_prefix("Bearer "))
}
/// Refresh the current session, extending its expiry by the configured
/// duration.
pub async fn refresh(
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Json<serde_json::Value>, StatusCode> {
let token = extract_bearer_token(&headers).ok_or(StatusCode::UNAUTHORIZED)?;
let config = state.config.read().await;
let expiry_hours = config.accounts.session_expiry_hours as i64;
drop(config);
let new_expires_at =
chrono::Utc::now() + chrono::Duration::hours(expiry_hours);
match state.storage.extend_session(token, new_expires_at).await {
Ok(Some(expires)) => {
Ok(Json(serde_json::json!({
"expires_at": expires.to_rfc3339()
})))
},
Ok(None) => Err(StatusCode::UNAUTHORIZED),
Err(e) => {
tracing::error!(error = %e, "failed to extend session");
Err(StatusCode::INTERNAL_SERVER_ERROR)
},
}
}
/// Revoke all sessions for the current user
pub async fn revoke_all_sessions(
State(state): State<AppState>,
@ -234,7 +263,7 @@ pub async fn revoke_all_sessions(
&state.storage,
None,
pinakes_core::model::AuditAction::Logout,
Some(format!("revoked all sessions for username: {}", username)),
Some(format!("revoked all sessions for username: {username}")),
)
.await
{