pinakes-server: TLS support; session persistence and security polish

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If2c9c3e3af62bbf9f33a97be89ac40bc6a6a6964
This commit is contained in:
raf 2026-01-31 15:20:27 +03:00
commit 87a4482576
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
19 changed files with 1835 additions and 111 deletions

View file

@ -83,17 +83,21 @@ pub async fn login(
let role = user.role;
let username = user.username.clone();
// Store session
{
let mut sessions = state.sessions.write().await;
sessions.insert(
token.clone(),
crate::state::SessionInfo {
username: username.clone(),
role,
created_at: chrono::Utc::now(),
},
);
// Create session in database
let now = chrono::Utc::now();
let session_data = pinakes_core::storage::SessionData {
session_token: token.clone(),
user_id: None, // Could be set if we had user IDs
username: username.clone(),
role: role.to_string(),
created_at: now,
expires_at: now + chrono::Duration::hours(24), // 24 hour sessions
last_accessed: now,
};
if let Err(e) = state.storage.create_session(&session_data).await {
tracing::error!(error = %e, "failed to create session in database");
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
tracing::info!(username = %username, role = %role, "login successful");
@ -116,13 +120,17 @@ 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);
// Get username before deleting session
let username = match state.storage.get_session(token).await {
Ok(Some(session)) => Some(session.username),
_ => None,
};
let mut sessions = state.sessions.write().await;
sessions.remove(token);
drop(sessions);
// Delete session from database
if let Err(e) = state.storage.delete_session(token).await {
tracing::error!(error = %e, "failed to delete session from database");
return StatusCode::INTERNAL_SERVER_ERROR;
}
// Record logout in audit log
if let Some(user) = username {
@ -153,12 +161,16 @@ pub async fn me(
drop(config);
let token = extract_bearer_token(&headers).ok_or(StatusCode::UNAUTHORIZED)?;
let sessions = state.sessions.read().await;
let session = sessions.get(token).ok_or(StatusCode::UNAUTHORIZED)?;
let session = state
.storage
.get_session(token)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::UNAUTHORIZED)?;
Ok(Json(UserInfoResponse {
username: session.username.clone(),
role: session.role.to_string(),
role: session.role.clone(),
}))
}
@ -168,3 +180,89 @@ fn extract_bearer_token(headers: &HeaderMap) -> Option<&str> {
.and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
}
/// Revoke all sessions for the current user
pub async fn revoke_all_sessions(State(state): State<AppState>, headers: HeaderMap) -> StatusCode {
let token = match extract_bearer_token(&headers) {
Some(t) => t,
None => return StatusCode::UNAUTHORIZED,
};
// Get current session to find username
let session = match state.storage.get_session(token).await {
Ok(Some(s)) => s,
Ok(None) => return StatusCode::UNAUTHORIZED,
Err(e) => {
tracing::error!(error = %e, "failed to get session");
return StatusCode::INTERNAL_SERVER_ERROR;
}
};
let username = session.username.clone();
// Delete all sessions for this user
match state.storage.delete_user_sessions(&username).await {
Ok(count) => {
tracing::info!(username = %username, count = count, "revoked all user sessions");
// Record in audit log
let _ = pinakes_core::audit::record_action(
&state.storage,
None,
pinakes_core::model::AuditAction::Logout,
Some(format!("revoked all sessions for username: {}", username)),
)
.await;
StatusCode::OK
}
Err(e) => {
tracing::error!(error = %e, "failed to revoke sessions");
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
/// List all active sessions (admin only)
#[derive(serde::Serialize)]
pub struct SessionListResponse {
pub sessions: Vec<SessionInfo>,
}
#[derive(serde::Serialize)]
pub struct SessionInfo {
pub username: String,
pub role: String,
pub created_at: String,
pub last_accessed: String,
pub expires_at: String,
}
pub async fn list_active_sessions(
State(state): State<AppState>,
) -> Result<Json<SessionListResponse>, StatusCode> {
// Get all active sessions
let sessions = state
.storage
.list_active_sessions(None)
.await
.map_err(|e| {
tracing::error!(error = %e, "failed to list active sessions");
StatusCode::INTERNAL_SERVER_ERROR
})?;
let session_infos = sessions
.into_iter()
.map(|s| SessionInfo {
username: s.username,
role: s.role,
created_at: s.created_at.to_rfc3339(),
last_accessed: s.last_accessed.to_rfc3339(),
expires_at: s.expires_at.to_rfc3339(),
})
.collect();
Ok(Json(SessionListResponse {
sessions: session_infos,
}))
}