fix(auth): reconcile role/ban from DB, revoke sessions, fix pin/comment bugs
H1: AuthUser now JOINs session→user and sources role/ban/event from the live DB row instead of trusting JWT claims. Bans take effect on the next request; demotion/promotion is immediate. Adds Session::find_auth_context and rejects tokens whose sub/event_id disagree with the session row. This also closes M2 (banned export) and M3 (banned edit/delete) globally — banned users can no longer pass the AuthUser extractor. Adds Session::delete_by_user_id and revokes all of a user's sessions on ban_user, set_role, and reset_user_pin so existing JWTs die immediately. ban_user also emits a user-banned SSE for forced client logout. H9: reset_user_pin used a non-existent column pin_failed_attempts (runtime sqlx, so it 500'd in prod). Corrected to failed_pin_attempts — the only account-recovery path now works. C4: LightboxModal posted comments to /comment (singular); backend only registers /comments. One-character route fix re-enables the comment feature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,10 +9,21 @@ use crate::auth::middleware::RequireHost;
|
||||
use crate::error::AppError;
|
||||
use crate::models::comment::Comment;
|
||||
use crate::models::event::Event;
|
||||
use crate::models::session::Session;
|
||||
use crate::models::upload::Upload;
|
||||
use crate::models::user::UserRole;
|
||||
use crate::state::{AppState, SseEvent};
|
||||
|
||||
/// Revoke all of a user's sessions (best-effort). A failure here is logged but
|
||||
/// never fails the surrounding admin action — the live-role/ban reconciliation
|
||||
/// in `AuthUser` is the authoritative gate; revocation is defense-in-depth.
|
||||
async fn revoke_sessions(pool: &sqlx::PgPool, user_id: Uuid, action: &str) {
|
||||
match Session::delete_by_user_id(pool, user_id).await {
|
||||
Ok(n) => tracing::info!(target_user_id = %user_id, revoked = n, action, "sessions revoked"),
|
||||
Err(e) => tracing::warn!(error = ?e, target_user_id = %user_id, action, "session revoke failed"),
|
||||
}
|
||||
}
|
||||
|
||||
// ── DTOs ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
@@ -120,6 +131,14 @@ pub async fn ban_user(
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
revoke_sessions(&state.pool, user_id, "ban_user").await;
|
||||
|
||||
// Tell the banned user's online devices to log out immediately.
|
||||
let _ = state.sse_tx.send(SseEvent::new(
|
||||
"user-banned",
|
||||
serde_json::json!({ "user_id": user_id }).to_string(),
|
||||
));
|
||||
|
||||
tracing::info!(
|
||||
actor_user_id = %auth.user_id,
|
||||
target_user_id = %user_id,
|
||||
@@ -201,6 +220,13 @@ pub async fn set_role(
|
||||
.bind(auth.event_id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
// Force a clean re-auth so the new role can't be cached client-side and so
|
||||
// any in-flight tokens carrying the old role are invalidated. The live-role
|
||||
// reconciliation in AuthUser already prevents privilege escalation, but
|
||||
// revoking is cheaper to reason about.
|
||||
revoke_sessions(&state.pool, user_id, "set_role").await;
|
||||
|
||||
tracing::info!(
|
||||
actor_user_id = %auth.user_id,
|
||||
target_user_id = %user_id,
|
||||
@@ -263,7 +289,7 @@ pub async fn reset_user_pin(
|
||||
sqlx::query(
|
||||
"UPDATE \"user\"
|
||||
SET recovery_pin_hash = $1,
|
||||
pin_failed_attempts = 0,
|
||||
failed_pin_attempts = 0,
|
||||
pin_locked_until = NULL
|
||||
WHERE id = $2",
|
||||
)
|
||||
@@ -272,6 +298,10 @@ pub async fn reset_user_pin(
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
// A PIN reset must invalidate existing sessions so a compromised/old token
|
||||
// can't outlive the reset.
|
||||
revoke_sessions(&state.pool, user_id, "reset_user_pin").await;
|
||||
|
||||
// Notify the *recipient* device(s) if they happen to be online so they can clear
|
||||
// their cached local PIN. They'll save the new one on the next /recover.
|
||||
let _ = state.sse_tx.send(SseEvent::new(
|
||||
|
||||
Reference in New Issue
Block a user