diff --git a/.env.example b/.env.example index b60caea..bb6db27 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,6 @@ RATE_LIMIT_IMAGE_BURST=50 WEBP_QUALITY=85.0 # Method: 0 (fastest) to 6 (best quality), default 2 WEBP_METHOD=2 + +# Maximum concurrent sessions per user (default: 5, minimum: 1) +MAX_SESSIONS_PER_USER=5 diff --git a/src/api/auth.rs b/src/api/auth.rs index ddd435c..782eeae 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -188,12 +188,43 @@ pub async fn login(username: String, password: String) -> Result().ok()) + .unwrap_or(5) + .max(1); + + let session_count: i64 = client + .query_one( + "SELECT COUNT(*) FROM sessions WHERE user_id = $1 AND expires_at > NOW()", + &[&user_id], + ) + .await + .map_err(AppError::query)? + .get(0); + + if session_count >= max_sessions { + client + .execute( + "DELETE FROM sessions WHERE id IN ( + SELECT id FROM sessions + WHERE user_id = $1 AND expires_at > NOW() + ORDER BY created_at ASC + LIMIT 1 + )", + &[&user_id], + ) + .await + .map_err(AppError::query)?; + } + client .execute( - "INSERT INTO sessions (user_id, token, expires_at) VALUES ($1, $2, $3)", - &[&user_id, &token, &expires_at], + "INSERT INTO sessions (user_id, token_hash, user_agent, expires_at) VALUES ($1, $2, $3, $4)", + &[&user_id, &token_hash, &None::, &expires_at], ) .await .map_err(AppError::query)?; @@ -229,8 +260,9 @@ pub async fn logout() -> Result { } if let Some(t) = token { + let token_hash = session::hash_token(&t); client - .execute("DELETE FROM sessions WHERE token = $1", &[&t]) + .execute("DELETE FROM sessions WHERE token_hash = $1", &[&token_hash]) .await .map_err(AppError::query)?; } @@ -251,13 +283,14 @@ pub struct CurrentUserResponse { pub async fn get_user_by_token(token: &str) -> Result, ServerFnError> { let client = get_conn().await.map_err(AppError::db_conn)?; + let token_hash = session::hash_token(token); let row = client .query_opt( "SELECT u.id, u.username, u.email, u.password_hash, u.role, u.created_at FROM sessions s JOIN users u ON s.user_id = u.id - WHERE s.token = $1 AND s.expires_at > NOW()", - &[&token], + WHERE s.token_hash = $1 AND s.expires_at > NOW()", + &[&token_hash], ) .await .map_err(AppError::query)?;