From 1a6c60ff0397bd28a0224088b0ba163e02ce2901 Mon Sep 17 00:00:00 2001 From: 409 <409dev@protonmail.com> Date: Fri, 18 Jul 2025 12:11:29 +0200 Subject: [PATCH] fetch auth session data from token --- .../domain/warren/models/auth_session/mod.rs | 4 + .../warren/models/auth_session/requests.rs | 47 +++++++++++ .../src/lib/domain/warren/ports/metrics.rs | 3 + backend/src/lib/domain/warren/ports/mod.rs | 9 +- .../src/lib/domain/warren/ports/notifier.rs | 5 ++ .../src/lib/domain/warren/ports/repository.rs | 9 +- backend/src/lib/domain/warren/service/auth.rs | 19 ++++- backend/src/lib/inbound/http/errors.rs | 15 ++++ .../http/handlers/auth/fetch_session.rs | 82 +++++++++++++++++++ .../lib/inbound/http/handlers/auth/login.rs | 1 + .../src/lib/inbound/http/handlers/auth/mod.rs | 8 +- .../src/lib/outbound/metrics_debug_logger.rs | 7 ++ .../src/lib/outbound/notifier_debug_logger.rs | 8 ++ backend/src/lib/outbound/postgres.rs | 73 ++++++++++++++++- frontend/components/AppSidebar.vue | 3 +- frontend/components/SidebarUser.vue | 22 +++-- frontend/lib/api/auth/getSession.ts | 42 ++++++++++ frontend/lib/api/auth/login.ts | 21 ++++- frontend/types/auth.ts | 10 +++ 19 files changed, 368 insertions(+), 20 deletions(-) create mode 100644 backend/src/lib/inbound/http/handlers/auth/fetch_session.rs create mode 100644 frontend/lib/api/auth/getSession.ts diff --git a/backend/src/lib/domain/warren/models/auth_session/mod.rs b/backend/src/lib/domain/warren/models/auth_session/mod.rs index ec85adf..3912873 100644 --- a/backend/src/lib/domain/warren/models/auth_session/mod.rs +++ b/backend/src/lib/domain/warren/models/auth_session/mod.rs @@ -35,6 +35,10 @@ impl AuthSession { pub fn user_id(&self) -> &Uuid { &self.user_id } + + pub fn expires_at(&self) -> &NaiveDateTime { + &self.expires_at + } } /// A valid auth session id diff --git a/backend/src/lib/domain/warren/models/auth_session/requests.rs b/backend/src/lib/domain/warren/models/auth_session/requests.rs index 6e35242..ab286d4 100644 --- a/backend/src/lib/domain/warren/models/auth_session/requests.rs +++ b/backend/src/lib/domain/warren/models/auth_session/requests.rs @@ -2,6 +2,8 @@ use thiserror::Error; use crate::domain::warren::models::user::User; +use super::{AuthSession, AuthSessionId}; + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct CreateAuthSessionRequest { user: User, @@ -43,3 +45,48 @@ impl SessionExpirationTime { self.0 } } + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FetchAuthSessionRequest { + session_id: AuthSessionId, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FetchAuthSessionResponse { + session: AuthSession, + user: User, +} + +impl FetchAuthSessionResponse { + pub fn new(session: AuthSession, user: User) -> Self { + Self { session, user } + } + + pub fn session(&self) -> &AuthSession { + &self.session + } + + pub fn user(&self) -> &User { + &self.user + } +} + +impl FetchAuthSessionRequest { + pub fn new(session_id: AuthSessionId) -> Self { + Self { session_id } + } + + pub fn session_id(&self) -> &AuthSessionId { + &self.session_id + } +} + +#[derive(Debug, Error)] +pub enum FetchAuthSessionError { + #[error("There is no auth session with this id")] + NotFound, + #[error("The auth session has expired")] + Expired, + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} diff --git a/backend/src/lib/domain/warren/ports/metrics.rs b/backend/src/lib/domain/warren/ports/metrics.rs index 3fe2241..d6782f5 100644 --- a/backend/src/lib/domain/warren/ports/metrics.rs +++ b/backend/src/lib/domain/warren/ports/metrics.rs @@ -58,4 +58,7 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static { fn record_auth_session_creation_success(&self) -> impl Future + Send; fn record_auth_session_creation_failure(&self) -> impl Future + Send; + + fn record_auth_session_fetch_success(&self) -> impl Future + Send; + fn record_auth_session_fetch_failure(&self) -> impl Future + Send; } diff --git a/backend/src/lib/domain/warren/ports/mod.rs b/backend/src/lib/domain/warren/ports/mod.rs index bd9bac6..7a80091 100644 --- a/backend/src/lib/domain/warren/ports/mod.rs +++ b/backend/src/lib/domain/warren/ports/mod.rs @@ -9,7 +9,10 @@ pub use repository::*; use super::models::{ auth_session::{ AuthSession, - requests::{CreateAuthSessionError, CreateAuthSessionRequest}, + requests::{ + CreateAuthSessionError, CreateAuthSessionRequest, FetchAuthSessionError, + FetchAuthSessionRequest, FetchAuthSessionResponse, + }, }, file::{ CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest, @@ -110,4 +113,8 @@ pub trait AuthService: Clone + Send + Sync + 'static { &self, request: CreateAuthSessionRequest, ) -> impl Future> + Send; + fn fetch_auth_session( + &self, + request: FetchAuthSessionRequest, + ) -> impl Future> + Send; } diff --git a/backend/src/lib/domain/warren/ports/notifier.rs b/backend/src/lib/domain/warren/ports/notifier.rs index cdf4d3a..51ce7ea 100644 --- a/backend/src/lib/domain/warren/ports/notifier.rs +++ b/backend/src/lib/domain/warren/ports/notifier.rs @@ -1,6 +1,7 @@ use uuid::Uuid; use crate::domain::warren::models::{ + auth_session::requests::FetchAuthSessionResponse, file::{AbsoluteFilePath, File, FilePath}, user::User, warren::Warren, @@ -76,4 +77,8 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static { fn user_registered(&self, user: &User) -> impl Future + Send; fn user_logged_in(&self, user: &User) -> impl Future + Send; fn auth_session_created(&self, user_id: &Uuid) -> impl Future + Send; + fn auth_session_fetched( + &self, + response: &FetchAuthSessionResponse, + ) -> impl Future + Send; } diff --git a/backend/src/lib/domain/warren/ports/repository.rs b/backend/src/lib/domain/warren/ports/repository.rs index e414bec..6bb3149 100644 --- a/backend/src/lib/domain/warren/ports/repository.rs +++ b/backend/src/lib/domain/warren/ports/repository.rs @@ -1,7 +1,10 @@ use crate::domain::warren::models::{ auth_session::{ AuthSession, - requests::{CreateAuthSessionError, CreateAuthSessionRequest}, + requests::{ + CreateAuthSessionError, CreateAuthSessionRequest, FetchAuthSessionError, + FetchAuthSessionRequest, FetchAuthSessionResponse, + }, }, file::{ CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest, @@ -70,4 +73,8 @@ pub trait AuthRepository: Clone + Send + Sync + 'static { &self, request: CreateAuthSessionRequest, ) -> impl Future> + Send; + fn fetch_auth_session( + &self, + request: FetchAuthSessionRequest, + ) -> impl Future> + Send; } diff --git a/backend/src/lib/domain/warren/service/auth.rs b/backend/src/lib/domain/warren/service/auth.rs index 98ff10b..1790b09 100644 --- a/backend/src/lib/domain/warren/service/auth.rs +++ b/backend/src/lib/domain/warren/service/auth.rs @@ -5,7 +5,8 @@ use crate::{ auth_session::{ AuthSession, requests::{ - CreateAuthSessionError, CreateAuthSessionRequest, SessionExpirationTime, + CreateAuthSessionError, CreateAuthSessionRequest, FetchAuthSessionError, + FetchAuthSessionRequest, FetchAuthSessionResponse, SessionExpirationTime, }, }, user::{ @@ -131,4 +132,20 @@ where result } + + async fn fetch_auth_session( + &self, + request: FetchAuthSessionRequest, + ) -> Result { + let result = self.repository.fetch_auth_session(request).await; + + if let Ok(response) = result.as_ref() { + self.metrics.record_auth_session_fetch_success().await; + self.notifier.auth_session_fetched(response).await; + } else { + self.metrics.record_auth_session_fetch_failure().await; + } + + result + } } diff --git a/backend/src/lib/inbound/http/errors.rs b/backend/src/lib/inbound/http/errors.rs index 3fc621d..67a53b5 100644 --- a/backend/src/lib/inbound/http/errors.rs +++ b/backend/src/lib/inbound/http/errors.rs @@ -1,5 +1,6 @@ use crate::{ domain::warren::models::{ + auth_session::requests::FetchAuthSessionError, file::{CreateDirectoryError, DeleteDirectoryError, DeleteFileError, ListFilesError}, user::{LoginUserError, RegisterUserError, VerifyUserPasswordError}, warren::{ @@ -152,3 +153,17 @@ impl From for ApiError { } } } + +impl From for ApiError { + fn from(value: FetchAuthSessionError) -> Self { + match value { + FetchAuthSessionError::NotFound => { + Self::BadRequest("This session does not exist".to_string()) + } + FetchAuthSessionError::Expired => { + Self::BadRequest("This session has expired".to_string()) + } + FetchAuthSessionError::Unknown(e) => Self::InternalServerError(e.to_string()), + } + } +} diff --git a/backend/src/lib/inbound/http/handlers/auth/fetch_session.rs b/backend/src/lib/inbound/http/handlers/auth/fetch_session.rs new file mode 100644 index 0000000..e9eda5c --- /dev/null +++ b/backend/src/lib/inbound/http/handlers/auth/fetch_session.rs @@ -0,0 +1,82 @@ +use axum::{ + extract::State, + http::{HeaderMap, StatusCode}, +}; +use serde::Serialize; +use uuid::Uuid; + +use crate::{ + domain::warren::{ + models::{ + auth_session::{ + AuthSessionId, + requests::{FetchAuthSessionRequest, FetchAuthSessionResponse}, + }, + user::User, + }, + ports::{AuthService, WarrenService}, + }, + inbound::http::{ + AppState, + responses::{ApiError, ApiSuccess}, + }, +}; + +#[derive(Debug, Clone, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct SessionUser { + id: Uuid, + name: String, + email: String, + admin: bool, +} + +impl From for SessionUser { + fn from(value: User) -> Self { + Self { + id: *value.id(), + name: value.name().to_string(), + email: value.email().to_string(), + admin: value.admin(), + } + } +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct FetchSessionResponseBody { + user: SessionUser, + expires_at: i64, +} + +impl From for FetchSessionResponseBody { + fn from(value: FetchAuthSessionResponse) -> Self { + Self { + user: value.user().clone().into(), + expires_at: value.session().expires_at().and_utc().timestamp_millis(), + } + } +} + +pub async fn fetch_session( + State(state): State>, + headers: HeaderMap, +) -> Result, ApiError> { + let Some(Ok(Ok(session_id))) = headers.get("authorization").map(|h| { + h.to_str() + .map(|h| AuthSessionId::new(&h["WarrenAuth ".len()..])) + }) else { + return Err(ApiError::BadRequest( + "No authorization header set".to_string(), + )); + }; + + let domain_request = FetchAuthSessionRequest::new(session_id); + + state + .auth_service + .fetch_auth_session(domain_request) + .await + .map(|response| ApiSuccess::new(StatusCode::OK, response.into())) + .map_err(ApiError::from) +} diff --git a/backend/src/lib/inbound/http/handlers/auth/login.rs b/backend/src/lib/inbound/http/handlers/auth/login.rs index 963242d..34e8ea2 100644 --- a/backend/src/lib/inbound/http/handlers/auth/login.rs +++ b/backend/src/lib/inbound/http/handlers/auth/login.rs @@ -66,6 +66,7 @@ impl LoginUserHttpRequestBody { } } +// TODO: Include `user` and `expires_at` fields #[derive(Debug, Clone, PartialEq, Serialize)] pub struct LoginResponseBody { token: String, diff --git a/backend/src/lib/inbound/http/handlers/auth/mod.rs b/backend/src/lib/inbound/http/handlers/auth/mod.rs index d071c15..959a1ca 100644 --- a/backend/src/lib/inbound/http/handlers/auth/mod.rs +++ b/backend/src/lib/inbound/http/handlers/auth/mod.rs @@ -1,9 +1,14 @@ +mod fetch_session; mod login; mod register; +use fetch_session::fetch_session; use login::login; use register::register; -use axum::{Router, routing::post}; +use axum::{ + Router, + routing::{get, post}, +}; use crate::{ domain::warren::ports::{AuthService, WarrenService}, @@ -14,4 +19,5 @@ pub fn routes() -> Router> Router::new() .route("/register", post(register)) .route("/login", post(login)) + .route("/session", get(fetch_session)) } diff --git a/backend/src/lib/outbound/metrics_debug_logger.rs b/backend/src/lib/outbound/metrics_debug_logger.rs index 46bae26..e14ef7c 100644 --- a/backend/src/lib/outbound/metrics_debug_logger.rs +++ b/backend/src/lib/outbound/metrics_debug_logger.rs @@ -144,4 +144,11 @@ impl AuthMetrics for MetricsDebugLogger { async fn record_auth_session_creation_failure(&self) { tracing::debug!("[Metrics] Auth session creation failed"); } + + async fn record_auth_session_fetch_success(&self) { + tracing::debug!("[Metrics] Auth session fetch succeeded"); + } + async fn record_auth_session_fetch_failure(&self) { + tracing::debug!("[Metrics] Auth session fetch failed"); + } } diff --git a/backend/src/lib/outbound/notifier_debug_logger.rs b/backend/src/lib/outbound/notifier_debug_logger.rs index 5c37349..47a5634 100644 --- a/backend/src/lib/outbound/notifier_debug_logger.rs +++ b/backend/src/lib/outbound/notifier_debug_logger.rs @@ -2,6 +2,7 @@ use uuid::Uuid; use crate::domain::warren::{ models::{ + auth_session::requests::FetchAuthSessionResponse, file::{File, FilePath}, user::User, warren::Warren, @@ -128,4 +129,11 @@ impl AuthNotifier for NotifierDebugLogger { async fn auth_session_created(&self, user_id: &Uuid) { tracing::debug!("[Notifier] Created auth session for user {}", user_id); } + + async fn auth_session_fetched(&self, response: &FetchAuthSessionResponse) { + tracing::debug!( + "[Notifier] Fetched auth session for user {}", + response.user().id() + ); + } } diff --git a/backend/src/lib/outbound/postgres.rs b/backend/src/lib/outbound/postgres.rs index f92d85d..83eca3f 100644 --- a/backend/src/lib/outbound/postgres.rs +++ b/backend/src/lib/outbound/postgres.rs @@ -18,8 +18,11 @@ use uuid::Uuid; use crate::domain::warren::{ models::{ auth_session::{ - AuthSession, - requests::{CreateAuthSessionError, CreateAuthSessionRequest, SessionExpirationTime}, + AuthSession, AuthSessionId, + requests::{ + CreateAuthSessionError, CreateAuthSessionRequest, FetchAuthSessionError, + FetchAuthSessionRequest, FetchAuthSessionResponse, SessionExpirationTime, + }, }, user::{ RegisterUserError, RegisterUserRequest, User, UserEmail, UserName, UserPassword, @@ -163,6 +166,28 @@ impl Postgres { Ok(user) } + async fn get_user_from_id( + &self, + connection: &mut PgConnection, + id: &Uuid, + ) -> Result { + let user: User = sqlx::query_as( + " + SELECT + * + FROM + users + WHERE + id = $1 + ", + ) + .bind(id) + .fetch_one(connection) + .await?; + + Ok(user) + } + async fn get_user_from_email( &self, connection: &mut PgConnection, @@ -245,6 +270,28 @@ impl Postgres { Ok(session) } + + async fn get_auth_session( + &self, + connection: &mut PgConnection, + session_id: &AuthSessionId, + ) -> anyhow::Result { + let session: AuthSession = sqlx::query_as( + " + SELECT + * + FROM + auth_sessions + WHERE + session_id = $1 + ", + ) + .bind(session_id) + .fetch_one(connection) + .await?; + + Ok(session) + } } impl WarrenRepository for Postgres { @@ -355,6 +402,28 @@ impl AuthRepository for Postgres { Ok(session) } + + async fn fetch_auth_session( + &self, + request: FetchAuthSessionRequest, + ) -> Result { + let mut connection = self + .pool + .acquire() + .await + .context("Failed to get a PostgreSQL connection")?; + + let session = self + .get_auth_session(&mut connection, request.session_id()) + .await + .context("Failed to get auth session")?; + let user = self + .get_user_from_id(&mut connection, session.user_id()) + .await + .context("Failed to get user")?; + + Ok(FetchAuthSessionResponse::new(session, user)) + } } fn is_not_found_error(err: &sqlx::Error) -> bool { diff --git a/frontend/components/AppSidebar.vue b/frontend/components/AppSidebar.vue index 978e992..90610bf 100644 --- a/frontend/components/AppSidebar.vue +++ b/frontend/components/AppSidebar.vue @@ -15,6 +15,7 @@ import { const route = useRoute(); const store = useWarrenStore(); +const session = useAuthSession(); async function selectWarren(id: string) { await store.setCurrentWarren(id, '/'); @@ -77,7 +78,7 @@ async function selectWarren(id: string) { - + diff --git a/frontend/components/SidebarUser.vue b/frontend/components/SidebarUser.vue index a3c54a1..ad60b6c 100644 --- a/frontend/components/SidebarUser.vue +++ b/frontend/components/SidebarUser.vue @@ -13,14 +13,15 @@ import { } from '@/components/ui/dropdown-menu'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { logout } from '~/lib/api/auth/logout'; +import type { AuthUser } from '~/types/auth'; const { isMobile } = useSidebar(); +const { user } = defineProps<{ + user: AuthUser; +}>(); -const user = { - name: '409', - email: 'user@example.com', - avatar: 'https://cdn.discordapp.com/avatars/285424924049276939/0368b00056c416cae689ab1434c0aac0.webp', -}; +const AVATAR = + 'https://cdn.discordapp.com/avatars/285424924049276939/0368b00056c416cae689ab1434c0aac0.webp';