diff --git a/backend/src/lib/domain/warren/models/user/requests.rs b/backend/src/lib/domain/warren/models/user/requests.rs index 5e2c006..665a0f6 100644 --- a/backend/src/lib/domain/warren/models/user/requests.rs +++ b/backend/src/lib/domain/warren/models/user/requests.rs @@ -228,3 +228,19 @@ pub enum CreateUserError { #[error(transparent)] Unknown(#[from] anyhow::Error), } + +/// An admin request to list all users +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ListUsersRequest {} + +impl ListUsersRequest { + pub fn new() -> Self { + Self {} + } +} + +#[derive(Debug, Error)] +pub enum ListUsersError { + #[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 45a57fb..a9e4578 100644 --- a/backend/src/lib/domain/warren/ports/metrics.rs +++ b/backend/src/lib/domain/warren/ports/metrics.rs @@ -59,6 +59,9 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static { fn record_user_creation_success(&self) -> impl Future + Send; fn record_user_creation_failure(&self) -> impl Future + Send; + fn record_user_list_success(&self) -> impl Future + Send; + fn record_user_list_failure(&self) -> impl Future + Send; + fn record_auth_session_creation_success(&self) -> impl Future + Send; fn record_auth_session_creation_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 9bb5404..e6e23b9 100644 --- a/backend/src/lib/domain/warren/ports/mod.rs +++ b/backend/src/lib/domain/warren/ports/mod.rs @@ -20,8 +20,8 @@ use super::models::{ FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest, }, user::{ - CreateUserError, CreateUserRequest, LoginUserError, LoginUserRequest, LoginUserResponse, - RegisterUserError, RegisterUserRequest, User, + CreateUserError, CreateUserRequest, ListUsersError, ListUsersRequest, LoginUserError, + LoginUserRequest, LoginUserResponse, RegisterUserError, RegisterUserRequest, User, }, user_warren::{ UserWarren, @@ -125,6 +125,10 @@ pub trait AuthService: Clone + Send + Sync + 'static { &self, request: AuthRequest, ) -> impl Future>> + Send; + fn list_users( + &self, + request: AuthRequest, + ) -> impl Future, AuthError>> + Send; fn create_auth_session( &self, diff --git a/backend/src/lib/domain/warren/ports/notifier.rs b/backend/src/lib/domain/warren/ports/notifier.rs index b64edc6..c0db56d 100644 --- a/backend/src/lib/domain/warren/ports/notifier.rs +++ b/backend/src/lib/domain/warren/ports/notifier.rs @@ -74,6 +74,11 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static { fn user_registered(&self, user: &User) -> impl Future + Send; fn user_logged_in(&self, response: &LoginUserResponse) -> impl Future + Send; fn user_created(&self, creator: &User, created: &User) -> impl Future + Send; + /// Lists all the users (admin action) + /// + /// * `user`: The user who requested the list + /// * `users`: The users from the list + fn users_listed(&self, user: &User, users: &Vec) -> impl Future + Send; fn auth_session_created(&self, user_id: &Uuid) -> impl Future + Send; fn auth_session_fetched( diff --git a/backend/src/lib/domain/warren/ports/repository.rs b/backend/src/lib/domain/warren/ports/repository.rs index 6f8f260..3734215 100644 --- a/backend/src/lib/domain/warren/ports/repository.rs +++ b/backend/src/lib/domain/warren/ports/repository.rs @@ -12,8 +12,8 @@ use crate::domain::warren::models::{ FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest, }, user::{ - CreateUserError, CreateUserRequest, User, VerifyUserPasswordError, - VerifyUserPasswordRequest, + CreateUserError, CreateUserRequest, ListUsersError, ListUsersRequest, User, + VerifyUserPasswordError, VerifyUserPasswordRequest, }, user_warren::{ UserWarren, @@ -74,10 +74,16 @@ pub trait AuthRepository: Clone + Send + Sync + 'static { &self, request: CreateUserRequest, ) -> impl Future> + Send; + fn list_users( + &self, + request: ListUsersRequest, + ) -> impl Future, ListUsersError>> + Send; + fn verify_user_password( &self, request: VerifyUserPasswordRequest, ) -> impl Future> + Send; + fn create_auth_session( &self, request: CreateAuthSessionRequest, diff --git a/backend/src/lib/domain/warren/service/auth.rs b/backend/src/lib/domain/warren/service/auth.rs index 8beb1a6..c7031d8 100644 --- a/backend/src/lib/domain/warren/service/auth.rs +++ b/backend/src/lib/domain/warren/service/auth.rs @@ -10,8 +10,9 @@ use crate::{ }, }, user::{ - CreateUserError, CreateUserRequest, LoginUserError, LoginUserRequest, - LoginUserResponse, RegisterUserError, RegisterUserRequest, User, + CreateUserError, CreateUserRequest, ListUsersError, ListUsersRequest, + LoginUserError, LoginUserRequest, LoginUserResponse, RegisterUserError, + RegisterUserRequest, User, }, user_warren::{ UserWarren, @@ -165,6 +166,32 @@ where result.map_err(AuthError::Custom) } + async fn list_users( + &self, + request: AuthRequest, + ) -> Result, AuthError> { + let (session, request) = request.unpack(); + + let response = self + .fetch_auth_session(FetchAuthSessionRequest::new(session.session_id().clone())) + .await?; + + if !response.user().admin() { + return Err(AuthError::InsufficientPermissions); + } + + let result = self.repository.list_users(request).await; + + if let Ok(users) = result.as_ref() { + self.metrics.record_user_list_success().await; + self.notifier.users_listed(response.user(), users).await; + } else { + self.metrics.record_user_list_failure().await; + } + + result.map_err(AuthError::Custom) + } + async fn create_auth_session( &self, request: CreateAuthSessionRequest, diff --git a/backend/src/lib/inbound/http/handlers/admin/list_users.rs b/backend/src/lib/inbound/http/handlers/admin/list_users.rs new file mode 100644 index 0000000..604f47e --- /dev/null +++ b/backend/src/lib/inbound/http/handlers/admin/list_users.rs @@ -0,0 +1,32 @@ +use axum::{extract::State, http::StatusCode}; + +use crate::{ + domain::warren::{ + models::{ + auth_session::AuthRequest, + user::{ListUsersRequest, User}, + }, + ports::{AuthService, WarrenService}, + }, + inbound::http::{ + AppState, + handlers::{UserData, extractors::SessionIdHeader}, + responses::{ApiError, ApiSuccess}, + }, +}; + +pub async fn list_users( + State(state): State>, + SessionIdHeader(session): SessionIdHeader, +) -> Result>, ApiError> { + state + .auth_service + .list_users(AuthRequest::new(session, ListUsersRequest::new())) + .await + .map(|users| ApiSuccess::new(StatusCode::OK, get_user_data(users))) + .map_err(ApiError::from) +} + +fn get_user_data(users: Vec) -> Vec { + users.into_iter().map(Into::into).collect() +} diff --git a/backend/src/lib/inbound/http/handlers/admin/mod.rs b/backend/src/lib/inbound/http/handlers/admin/mod.rs index 8478ac7..57cf33d 100644 --- a/backend/src/lib/inbound/http/handlers/admin/mod.rs +++ b/backend/src/lib/inbound/http/handlers/admin/mod.rs @@ -1,7 +1,13 @@ mod create_user; -use create_user::create_user; +mod list_users; -use axum::{Router, routing::post}; +use create_user::create_user; +use list_users::list_users; + +use axum::{ + Router, + routing::{get, post}, +}; use crate::{ domain::warren::ports::{AuthService, WarrenService}, @@ -9,5 +15,7 @@ use crate::{ }; pub fn routes() -> Router> { - Router::new().route("/users", post(create_user)) + Router::new() + .route("/users", get(list_users)) + .route("/users", post(create_user)) } diff --git a/backend/src/lib/inbound/http/handlers/mod.rs b/backend/src/lib/inbound/http/handlers/mod.rs index 98c4450..9ed671c 100644 --- a/backend/src/lib/inbound/http/handlers/mod.rs +++ b/backend/src/lib/inbound/http/handlers/mod.rs @@ -10,7 +10,7 @@ pub mod warrens; #[derive(Debug, Clone, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] -/// A session user that can be safely sent to the client +/// A user that can be safely sent to the client pub(super) struct UserData { id: Uuid, name: String, diff --git a/backend/src/lib/outbound/metrics_debug_logger.rs b/backend/src/lib/outbound/metrics_debug_logger.rs index debb2c9..c6037cf 100644 --- a/backend/src/lib/outbound/metrics_debug_logger.rs +++ b/backend/src/lib/outbound/metrics_debug_logger.rs @@ -145,6 +145,13 @@ impl AuthMetrics for MetricsDebugLogger { tracing::debug!("[Metrics] User creation failed"); } + async fn record_user_list_success(&self) -> () { + tracing::debug!("[Metrics] User list succeeded"); + } + async fn record_user_list_failure(&self) -> () { + tracing::debug!("[Metrics] User list failed"); + } + async fn record_auth_session_creation_success(&self) { tracing::debug!("[Metrics] Auth session creation succeeded"); } diff --git a/backend/src/lib/outbound/notifier_debug_logger.rs b/backend/src/lib/outbound/notifier_debug_logger.rs index bf1d917..b2564c6 100644 --- a/backend/src/lib/outbound/notifier_debug_logger.rs +++ b/backend/src/lib/outbound/notifier_debug_logger.rs @@ -129,6 +129,14 @@ impl AuthNotifier for NotifierDebugLogger { ); } + async fn users_listed(&self, user: &User, users: &Vec) { + tracing::debug!( + "[Notifier] Admin user {} listed {} user(s)", + user.name(), + users.len() + ); + } + async fn user_logged_in(&self, response: &LoginUserResponse) { tracing::debug!("[Notifier] Logged in user {}", response.user().name()); } diff --git a/backend/src/lib/outbound/postgres.rs b/backend/src/lib/outbound/postgres.rs index de9e0c3..86d0f55 100644 --- a/backend/src/lib/outbound/postgres.rs +++ b/backend/src/lib/outbound/postgres.rs @@ -25,8 +25,8 @@ use crate::domain::warren::{ }, }, user::{ - CreateUserError, CreateUserRequest, User, UserEmail, UserName, UserPassword, - VerifyUserPasswordError, VerifyUserPasswordRequest, + CreateUserError, CreateUserRequest, ListUsersError, ListUsersRequest, User, UserEmail, + UserName, UserPassword, VerifyUserPasswordError, VerifyUserPasswordRequest, }, user_warren::{ UserWarren, @@ -348,6 +348,21 @@ impl Postgres { Ok(ids) } + + async fn fetch_users(&self, connection: &mut PgConnection) -> Result, sqlx::Error> { + let users: Vec = sqlx::query_as( + " + SELECT + * + FROM + users + ", + ) + .fetch_all(connection) + .await?; + + Ok(users) + } } impl WarrenRepository for Postgres { @@ -519,6 +534,21 @@ impl AuthRepository for Postgres { } }) } + + async fn list_users(&self, _request: ListUsersRequest) -> Result, ListUsersError> { + let mut connection = self + .pool + .acquire() + .await + .context("Failed to get a PostgreSQL connection")?; + + let users = self + .fetch_users(&mut connection) + .await + .map_err(|e| anyhow!(e))?; + + Ok(users) + } } fn is_not_found_error(err: &sqlx::Error) -> bool { diff --git a/frontend/components/AppBreadcrumbs.vue b/frontend/components/AppBreadcrumbs.vue index a75ed6a..4cd94b0 100644 --- a/frontend/components/AppBreadcrumbs.vue +++ b/frontend/components/AppBreadcrumbs.vue @@ -7,7 +7,7 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from '@/components/ui/breadcrumb'; -import type { BreadcrumbData } from '~/types'; +import type { BreadcrumbData } from '#shared/types'; const route = useRoute(); const store = useWarrenStore(); diff --git a/frontend/components/DirectoryEntry.vue b/frontend/components/DirectoryEntry.vue index 1f2f73d..e83a1ed 100644 --- a/frontend/components/DirectoryEntry.vue +++ b/frontend/components/DirectoryEntry.vue @@ -7,7 +7,7 @@ import { ContextMenuSeparator, } from '@/components/ui/context-menu'; import { deleteWarrenDirectory, deleteWarrenFile } from '~/lib/api/warrens'; -import type { DirectoryEntry } from '~/types'; +import type { DirectoryEntry } from '#shared/types'; const warrenStore = useWarrenStore(); const renameDialog = useRenameDirectoryDialog(); diff --git a/frontend/components/DirectoryList.vue b/frontend/components/DirectoryList.vue index 9a81b8c..70723be 100644 --- a/frontend/components/DirectoryList.vue +++ b/frontend/components/DirectoryList.vue @@ -1,6 +1,6 @@ +