diff --git a/devenv.lock b/devenv.lock index 3a86f3f..1a338c6 100644 --- a/devenv.lock +++ b/devenv.lock @@ -31,10 +31,31 @@ "type": "github" } }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1737465171, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "9364dc02281ce2d37a1f55b6e51f7c0f65a75f17", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, "gitignore": { "inputs": { "nixpkgs": [ - "pre-commit-hooks", + "git-hooks", "nixpkgs" ] }, @@ -66,32 +87,14 @@ "type": "github" } }, - "pre-commit-hooks": { - "inputs": { - "flake-compat": "flake-compat", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1735882644, - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "a5a961387e75ae44cc20f0a57ae463da5e959656", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "type": "github" - } - }, "root": { "inputs": { "devenv": "devenv", + "git-hooks": "git-hooks", "nixpkgs": "nixpkgs", - "pre-commit-hooks": "pre-commit-hooks" + "pre-commit-hooks": [ + "git-hooks" + ] } } }, diff --git a/server/src/auth.rs b/server/src/auth.rs index 4295958..320a9a5 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -11,21 +11,12 @@ use rand_chacha::ChaCha20Rng; use sqlx::PgPool; use tokio::task; -use crate::{ - database::model::Session, - model::User, -}; +use crate::{database::model::Session, model::User}; mod error; pub async fn get_user_from_header(pool: &PgPool, headers: &HeaderMap) -> Result { - let bearer_value = headers.get(header::AUTHORIZATION); - let bearer_value = bearer_value - .ok_or(AuthError::InvalidToken)? - .to_str() - .map_err(|_| AuthError::InvalidToken)?; - - let token = get_token_from_bearer(bearer_value)?; + let token = get_token_from_headers(&headers)?; let session = match Session::from_token(pool, &token).await { Ok(s) => s, @@ -44,7 +35,13 @@ pub async fn get_user_from_header(pool: &PgPool, headers: &HeaderMap) -> Result< Ok(db_user.into()) } -pub fn get_token_from_bearer(bearer: &str) -> Result { +pub fn get_token_from_headers(headers: &HeaderMap) -> Result { + let bearer = headers.get(header::AUTHORIZATION); + let bearer = bearer + .ok_or(AuthError::InvalidToken)? + .to_str() + .map_err(|_| AuthError::InvalidToken)?; + match bearer.strip_prefix("Bearer ") { Some(token) => Ok(token.to_string()), None => Err(AuthError::InvalidToken), diff --git a/server/src/database/model/session.rs b/server/src/database/model/session.rs index 2526b65..2176fc4 100644 --- a/server/src/database/model/session.rs +++ b/server/src/database/model/session.rs @@ -43,4 +43,54 @@ impl Session { Ok(session) } + + pub async fn remove_many( + session_ids: &[uuid::Uuid], + transaction: &mut sqlx::Transaction<'_, Postgres>, + ) -> Result<(), sqlx::Error> { + let deleted_count = sqlx::query_scalar!( + " + WITH deleted AS ( + DELETE FROM sessions + WHERE session_id = ANY($1) + RETURNING 1 + ) + SELECT COUNT(*) FROM deleted + ", + session_ids + ) + .fetch_one(&mut **transaction) + .await?; + + if !deleted_count.is_some_and(|c| c >= 1) { + return Err(sqlx::Error::RowNotFound); + } + + Ok(()) + } + + pub async fn remove_many_from_token( + transaction: &mut sqlx::Transaction<'_, Postgres>, + session_tokens: &[String], + ) -> Result<(), sqlx::Error> { + let deleted_count = sqlx::query_scalar!( + " + WITH deleted AS ( + DELETE FROM sessions + WHERE token = ANY($1) + RETURNING 1 + ) + SELECT COUNT(*) FROM deleted + ", + session_tokens + ) + .fetch_one(&mut **transaction) + .await?; + + if !deleted_count.is_some_and(|c| c >= 1) { + return Err(sqlx::Error::RowNotFound); + } + + Ok(()) + } } diff --git a/server/src/routes/auth.rs b/server/src/routes/auth.rs index cf825dd..158fa4f 100644 --- a/server/src/routes/auth.rs +++ b/server/src/routes/auth.rs @@ -1,8 +1,12 @@ use axum::http::HeaderMap; -use axum::{extract::State, routing::post, Json, Router}; +use axum::{ + extract::State, + routing::{get, post}, + Json, Router, +}; use serde::Deserialize; -use crate::auth::verify_password_hash; +use crate::auth::{get_token_from_headers, verify_password_hash}; use crate::auth::{get_user_from_header, AuthError}; use crate::database::model::user::UpdateUser; use crate::database::model::Member as DbMember; @@ -16,6 +20,7 @@ pub fn routes() -> Router { Router::new() .route("/auth/login", post(login)) .route("/auth/register", post(register)) + .route("/auth/logout", get(logout)) .route("/auth/change_password", post(change_password)) .route("/auth/change_email", post(change_email)) } @@ -88,6 +93,18 @@ pub async fn register( Ok(db_session.token) } +pub async fn logout(State(state): State, headers: HeaderMap) -> Result<(), crate::Error> { + let registration_token = get_token_from_headers(&headers)?; + + let mut transaction = state.pool.begin().await?; + + DbSession::remove_many_from_token(&mut transaction, &[registration_token]).await?; + + transaction.commit().await?; + + Ok(()) +} + #[derive(Debug, Deserialize)] pub struct ChangePasswordRequest { pub old_password: String,