edit users
This commit is contained in:
60
backend/src/lib/domain/warren/models/user/requests/edit.rs
Normal file
60
backend/src/lib/domain/warren/models/user/requests/edit.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::user::{UserEmail, UserName, UserPassword};
|
||||||
|
|
||||||
|
/// An admin request to edit an existing user
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct EditUserRequest {
|
||||||
|
user_id: Uuid,
|
||||||
|
name: UserName,
|
||||||
|
email: UserEmail,
|
||||||
|
password: Option<UserPassword>,
|
||||||
|
admin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EditUserRequest {
|
||||||
|
pub fn new(
|
||||||
|
user_id: Uuid,
|
||||||
|
name: UserName,
|
||||||
|
email: UserEmail,
|
||||||
|
password: Option<UserPassword>,
|
||||||
|
admin: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
user_id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
admin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_id(&self) -> &Uuid {
|
||||||
|
&self.user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &UserName {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn email(&self) -> &UserEmail {
|
||||||
|
&self.email
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password(&self) -> Option<&UserPassword> {
|
||||||
|
self.password.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn admin(&self) -> bool {
|
||||||
|
self.admin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum EditUserError {
|
||||||
|
#[error("There is no user with this id")]
|
||||||
|
NotFound,
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::{
|
||||||
|
user::User,
|
||||||
|
user_warren::{UserWarren, requests::ListUserWarrensError},
|
||||||
|
warren::{FetchWarrensError, Warren},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::ListUsersError;
|
||||||
|
|
||||||
|
/// An admin request to list all users, user warrens and warrens
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct ListAllUsersAndWarrensRequest {}
|
||||||
|
|
||||||
|
impl ListAllUsersAndWarrensRequest {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ListAllUsersAndWarrensError {
|
||||||
|
#[error(transparent)]
|
||||||
|
ListUsers(#[from] ListUsersError),
|
||||||
|
#[error(transparent)]
|
||||||
|
ListUserWarrens(#[from] ListUserWarrensError),
|
||||||
|
#[error(transparent)]
|
||||||
|
FetchWarrens(#[from] FetchWarrensError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct ListAllUsersAndWarrensResponse {
|
||||||
|
users: Vec<User>,
|
||||||
|
user_warrens: Vec<UserWarren>,
|
||||||
|
warrens: Vec<Warren>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListAllUsersAndWarrensResponse {
|
||||||
|
pub fn new(users: Vec<User>, user_warrens: Vec<UserWarren>, warrens: Vec<Warren>) -> Self {
|
||||||
|
Self {
|
||||||
|
users,
|
||||||
|
user_warrens,
|
||||||
|
warrens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn users(&self) -> &Vec<User> {
|
||||||
|
&self.users
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_warrens(&self) -> &Vec<UserWarren> {
|
||||||
|
&self.user_warrens
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn warrens(&self) -> &Vec<Warren> {
|
||||||
|
&self.warrens
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unpack(self) -> (Vec<User>, Vec<UserWarren>, Vec<Warren>) {
|
||||||
|
(self.users, self.user_warrens, self.warrens)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
mod create;
|
mod create;
|
||||||
mod delete;
|
mod delete;
|
||||||
|
mod edit;
|
||||||
mod list;
|
mod list;
|
||||||
|
mod list_all;
|
||||||
mod login;
|
mod login;
|
||||||
mod register;
|
mod register;
|
||||||
mod verify_password;
|
mod verify_password;
|
||||||
|
|
||||||
pub use create::*;
|
pub use create::*;
|
||||||
pub use delete::*;
|
pub use delete::*;
|
||||||
|
pub use edit::*;
|
||||||
pub use list::*;
|
pub use list::*;
|
||||||
|
pub use list_all::*;
|
||||||
pub use login::*;
|
pub use login::*;
|
||||||
pub use register::*;
|
pub use register::*;
|
||||||
pub use verify_password::*;
|
pub use verify_password::*;
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct FetchUserWarrenRequest {
|
||||||
|
user_id: Uuid,
|
||||||
|
warren_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FetchUserWarrenRequest {
|
||||||
|
pub fn new(user_id: Uuid, warren_id: Uuid) -> Self {
|
||||||
|
Self { user_id, warren_id }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_id(&self) -> &Uuid {
|
||||||
|
&self.user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn warren_id(&self) -> &Uuid {
|
||||||
|
&self.warren_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum FetchUserWarrenError {
|
||||||
|
#[error("This user warren does not exist")]
|
||||||
|
NotFound,
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
@@ -42,31 +42,3 @@ pub enum ListWarrensError {
|
|||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Unknown(#[from] anyhow::Error),
|
Unknown(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct FetchUserWarrenRequest {
|
|
||||||
user_id: Uuid,
|
|
||||||
warren_id: Uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FetchUserWarrenRequest {
|
|
||||||
pub fn new(user_id: Uuid, warren_id: Uuid) -> Self {
|
|
||||||
Self { user_id, warren_id }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn user_id(&self) -> &Uuid {
|
|
||||||
&self.user_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn warren_id(&self) -> &Uuid {
|
|
||||||
&self.warren_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum FetchUserWarrenError {
|
|
||||||
#[error("This user warren does not exist")]
|
|
||||||
NotFound,
|
|
||||||
#[error(transparent)]
|
|
||||||
Unknown(#[from] anyhow::Error),
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// A request to list all user warrens (admin only)
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct ListUserWarrensRequest {}
|
||||||
|
|
||||||
|
impl ListUserWarrensRequest {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ListUserWarrensError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
mod fetch_user_warren;
|
||||||
|
mod fetch_user_warrens;
|
||||||
|
mod list_user_warrens;
|
||||||
|
pub use fetch_user_warren::*;
|
||||||
|
pub use fetch_user_warrens::*;
|
||||||
|
pub use list_user_warrens::*;
|
||||||
@@ -59,9 +59,15 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static {
|
|||||||
fn record_user_creation_success(&self) -> impl Future<Output = ()> + Send;
|
fn record_user_creation_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_user_creation_failure(&self) -> impl Future<Output = ()> + Send;
|
fn record_user_creation_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_user_edit_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_user_edit_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
fn record_user_list_success(&self) -> impl Future<Output = ()> + Send;
|
fn record_user_list_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_user_list_failure(&self) -> impl Future<Output = ()> + Send;
|
fn record_user_list_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_list_all_users_and_warrens_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_list_all_users_and_warrens_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
fn record_user_deletion_success(&self) -> impl Future<Output = ()> + Send;
|
fn record_user_deletion_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_user_deletion_failure(&self) -> impl Future<Output = ()> + Send;
|
fn record_user_deletion_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ use super::models::{
|
|||||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||||
},
|
},
|
||||||
user::{
|
user::{
|
||||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, ListUsersError,
|
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||||
ListUsersRequest, LoginUserError, LoginUserRequest, LoginUserResponse, RegisterUserError,
|
EditUserRequest, ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest,
|
||||||
RegisterUserRequest, User,
|
ListAllUsersAndWarrensResponse, ListUsersError, ListUsersRequest, LoginUserError,
|
||||||
|
LoginUserRequest, LoginUserResponse, RegisterUserError, RegisterUserRequest, User,
|
||||||
},
|
},
|
||||||
user_warren::{
|
user_warren::{
|
||||||
UserWarren,
|
UserWarren,
|
||||||
@@ -121,11 +122,17 @@ pub trait AuthService: Clone + Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
request: LoginUserRequest,
|
request: LoginUserRequest,
|
||||||
) -> impl Future<Output = Result<LoginUserResponse, LoginUserError>> + Send;
|
) -> impl Future<Output = Result<LoginUserResponse, LoginUserError>> + Send;
|
||||||
|
|
||||||
/// An action that creates a user (MUST REQUIRE ADMIN PRIVILEGES)
|
/// An action that creates a user (MUST REQUIRE ADMIN PRIVILEGES)
|
||||||
fn create_user(
|
fn create_user(
|
||||||
&self,
|
&self,
|
||||||
request: AuthRequest<CreateUserRequest>,
|
request: AuthRequest<CreateUserRequest>,
|
||||||
) -> impl Future<Output = Result<User, AuthError<CreateUserError>>> + Send;
|
) -> impl Future<Output = Result<User, AuthError<CreateUserError>>> + Send;
|
||||||
|
/// An action that edits a user (MUST REQUIRE ADMIN PRIVILEGES)
|
||||||
|
fn edit_user(
|
||||||
|
&self,
|
||||||
|
request: AuthRequest<EditUserRequest>,
|
||||||
|
) -> impl Future<Output = Result<User, AuthError<EditUserError>>> + Send;
|
||||||
/// An action that deletes a user (MUST REQUIRE ADMIN PRIVILEGES)
|
/// An action that deletes a user (MUST REQUIRE ADMIN PRIVILEGES)
|
||||||
fn delete_user(
|
fn delete_user(
|
||||||
&self,
|
&self,
|
||||||
@@ -137,6 +144,15 @@ pub trait AuthService: Clone + Send + Sync + 'static {
|
|||||||
request: AuthRequest<ListUsersRequest>,
|
request: AuthRequest<ListUsersRequest>,
|
||||||
) -> impl Future<Output = Result<Vec<User>, AuthError<ListUsersError>>> + Send;
|
) -> impl Future<Output = Result<Vec<User>, AuthError<ListUsersError>>> + Send;
|
||||||
|
|
||||||
|
/// An action that lists all users, user warrens and warrens (MUST REQUIRE ADMIN PRIVILEGES)
|
||||||
|
fn list_all_users_and_warrens<WS: WarrenService>(
|
||||||
|
&self,
|
||||||
|
request: AuthRequest<ListAllUsersAndWarrensRequest>,
|
||||||
|
warren_service: &WS,
|
||||||
|
) -> impl Future<
|
||||||
|
Output = Result<ListAllUsersAndWarrensResponse, AuthError<ListAllUsersAndWarrensError>>,
|
||||||
|
> + Send;
|
||||||
|
|
||||||
fn create_auth_session(
|
fn create_auth_session(
|
||||||
&self,
|
&self,
|
||||||
request: CreateAuthSessionRequest,
|
request: CreateAuthSessionRequest,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
|||||||
use crate::domain::warren::models::{
|
use crate::domain::warren::models::{
|
||||||
auth_session::requests::FetchAuthSessionResponse,
|
auth_session::requests::FetchAuthSessionResponse,
|
||||||
file::{File, FilePath},
|
file::{File, FilePath},
|
||||||
user::{LoginUserResponse, User},
|
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
||||||
user_warren::UserWarren,
|
user_warren::UserWarren,
|
||||||
warren::{
|
warren::{
|
||||||
CreateWarrenDirectoryResponse, DeleteWarrenDirectoryResponse, DeleteWarrenFileResponse,
|
CreateWarrenDirectoryResponse, DeleteWarrenDirectoryResponse, DeleteWarrenFileResponse,
|
||||||
@@ -74,12 +74,18 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
|||||||
fn user_registered(&self, user: &User) -> impl Future<Output = ()> + Send;
|
fn user_registered(&self, user: &User) -> impl Future<Output = ()> + Send;
|
||||||
fn user_logged_in(&self, response: &LoginUserResponse) -> impl Future<Output = ()> + Send;
|
fn user_logged_in(&self, response: &LoginUserResponse) -> impl Future<Output = ()> + Send;
|
||||||
fn user_created(&self, creator: &User, created: &User) -> impl Future<Output = ()> + Send;
|
fn user_created(&self, creator: &User, created: &User) -> impl Future<Output = ()> + Send;
|
||||||
|
fn user_edited(&self, editor: &User, edited: &User) -> impl Future<Output = ()> + Send;
|
||||||
fn user_deleted(&self, deleter: &User, user: &User) -> impl Future<Output = ()> + Send;
|
fn user_deleted(&self, deleter: &User, user: &User) -> impl Future<Output = ()> + Send;
|
||||||
/// Lists all the users (admin action)
|
/// Lists all the users (admin action)
|
||||||
///
|
///
|
||||||
/// * `user`: The user who requested the list
|
/// * `user`: The user who requested the list
|
||||||
/// * `users`: The users from the list
|
/// * `users`: The users from the list
|
||||||
fn users_listed(&self, user: &User, users: &Vec<User>) -> impl Future<Output = ()> + Send;
|
fn users_listed(&self, user: &User, users: &Vec<User>) -> impl Future<Output = ()> + Send;
|
||||||
|
fn all_users_and_warrens_listed(
|
||||||
|
&self,
|
||||||
|
user: &User,
|
||||||
|
response: &ListAllUsersAndWarrensResponse,
|
||||||
|
) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
fn auth_session_created(&self, user_id: &Uuid) -> impl Future<Output = ()> + Send;
|
fn auth_session_created(&self, user_id: &Uuid) -> impl Future<Output = ()> + Send;
|
||||||
fn auth_session_fetched(
|
fn auth_session_fetched(
|
||||||
|
|||||||
@@ -12,14 +12,16 @@ use crate::domain::warren::models::{
|
|||||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||||
},
|
},
|
||||||
user::{
|
user::{
|
||||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, ListUsersError,
|
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||||
ListUsersRequest, User, VerifyUserPasswordError, VerifyUserPasswordRequest,
|
EditUserRequest, ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest,
|
||||||
|
ListAllUsersAndWarrensResponse, ListUsersError, ListUsersRequest, User,
|
||||||
|
VerifyUserPasswordError, VerifyUserPasswordRequest,
|
||||||
},
|
},
|
||||||
user_warren::{
|
user_warren::{
|
||||||
UserWarren,
|
UserWarren,
|
||||||
requests::{
|
requests::{
|
||||||
FetchUserWarrenError, FetchUserWarrenRequest, FetchUserWarrensError,
|
FetchUserWarrenError, FetchUserWarrenRequest, FetchUserWarrensError,
|
||||||
FetchUserWarrensRequest,
|
FetchUserWarrensRequest, ListUserWarrensError, ListUserWarrensRequest,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
warren::{
|
warren::{
|
||||||
@@ -27,6 +29,8 @@ use crate::domain::warren::models::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::WarrenService;
|
||||||
|
|
||||||
pub trait WarrenRepository: Clone + Send + Sync + 'static {
|
pub trait WarrenRepository: Clone + Send + Sync + 'static {
|
||||||
fn list_warrens(
|
fn list_warrens(
|
||||||
&self,
|
&self,
|
||||||
@@ -74,6 +78,11 @@ pub trait AuthRepository: Clone + Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
request: CreateUserRequest,
|
request: CreateUserRequest,
|
||||||
) -> impl Future<Output = Result<User, CreateUserError>> + Send;
|
) -> impl Future<Output = Result<User, CreateUserError>> + Send;
|
||||||
|
/// An action that edits a user (MUST REQUIRE ADMIN PRIVILEGES)
|
||||||
|
fn edit_user(
|
||||||
|
&self,
|
||||||
|
request: EditUserRequest,
|
||||||
|
) -> impl Future<Output = Result<User, EditUserError>> + Send;
|
||||||
fn list_users(
|
fn list_users(
|
||||||
&self,
|
&self,
|
||||||
request: ListUsersRequest,
|
request: ListUsersRequest,
|
||||||
@@ -83,11 +92,23 @@ pub trait AuthRepository: Clone + Send + Sync + 'static {
|
|||||||
request: DeleteUserRequest,
|
request: DeleteUserRequest,
|
||||||
) -> impl Future<Output = Result<User, DeleteUserError>> + Send;
|
) -> impl Future<Output = Result<User, DeleteUserError>> + Send;
|
||||||
|
|
||||||
|
/// An action that lists all users, user warrens and warrens (MUST REQUIRE ADMIN PRIVILEGES)
|
||||||
|
fn list_all_users_and_warrens<WS: WarrenService>(
|
||||||
|
&self,
|
||||||
|
request: ListAllUsersAndWarrensRequest,
|
||||||
|
warren_service: &WS,
|
||||||
|
) -> impl Future<Output = Result<ListAllUsersAndWarrensResponse, ListAllUsersAndWarrensError>> + Send;
|
||||||
|
|
||||||
fn verify_user_password(
|
fn verify_user_password(
|
||||||
&self,
|
&self,
|
||||||
request: VerifyUserPasswordRequest,
|
request: VerifyUserPasswordRequest,
|
||||||
) -> impl Future<Output = Result<User, VerifyUserPasswordError>> + Send;
|
) -> impl Future<Output = Result<User, VerifyUserPasswordError>> + Send;
|
||||||
|
|
||||||
|
fn list_user_warrens(
|
||||||
|
&self,
|
||||||
|
request: ListUserWarrensRequest,
|
||||||
|
) -> impl Future<Output = Result<Vec<UserWarren>, ListUserWarrensError>> + Send;
|
||||||
|
|
||||||
fn create_auth_session(
|
fn create_auth_session(
|
||||||
&self,
|
&self,
|
||||||
request: CreateAuthSessionRequest,
|
request: CreateAuthSessionRequest,
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ use crate::{
|
|||||||
},
|
},
|
||||||
user::{
|
user::{
|
||||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest,
|
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest,
|
||||||
ListUsersError, ListUsersRequest, LoginUserError, LoginUserRequest,
|
EditUserError, EditUserRequest, ListAllUsersAndWarrensError,
|
||||||
LoginUserResponse, RegisterUserError, RegisterUserRequest, User,
|
ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse, ListUsersError,
|
||||||
|
ListUsersRequest, LoginUserError, LoginUserRequest, LoginUserResponse,
|
||||||
|
RegisterUserError, RegisterUserRequest, User,
|
||||||
},
|
},
|
||||||
user_warren::{
|
user_warren::{
|
||||||
UserWarren,
|
UserWarren,
|
||||||
@@ -166,6 +168,32 @@ where
|
|||||||
result.map_err(AuthError::Custom)
|
result.map_err(AuthError::Custom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn edit_user(
|
||||||
|
&self,
|
||||||
|
request: AuthRequest<EditUserRequest>,
|
||||||
|
) -> Result<User, AuthError<EditUserError>> {
|
||||||
|
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.edit_user(request).await;
|
||||||
|
|
||||||
|
if let Ok(user) = result.as_ref() {
|
||||||
|
self.metrics.record_user_edit_success().await;
|
||||||
|
self.notifier.user_edited(response.user(), user).await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_user_edit_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.map_err(AuthError::Custom)
|
||||||
|
}
|
||||||
|
|
||||||
async fn delete_user(
|
async fn delete_user(
|
||||||
&self,
|
&self,
|
||||||
request: AuthRequest<DeleteUserRequest>,
|
request: AuthRequest<DeleteUserRequest>,
|
||||||
@@ -222,6 +250,42 @@ where
|
|||||||
result.map_err(AuthError::Custom)
|
result.map_err(AuthError::Custom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_all_users_and_warrens<WS: WarrenService>(
|
||||||
|
&self,
|
||||||
|
request: AuthRequest<ListAllUsersAndWarrensRequest>,
|
||||||
|
warren_service: &WS,
|
||||||
|
) -> Result<ListAllUsersAndWarrensResponse, AuthError<ListAllUsersAndWarrensError>> {
|
||||||
|
let (session, request) = request.unpack();
|
||||||
|
|
||||||
|
let session_response = self
|
||||||
|
.fetch_auth_session(FetchAuthSessionRequest::new(session.session_id().clone()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !session_response.user().admin() {
|
||||||
|
return Err(AuthError::InsufficientPermissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.repository
|
||||||
|
.list_all_users_and_warrens(request, warren_service)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(response) = result.as_ref() {
|
||||||
|
self.metrics
|
||||||
|
.record_list_all_users_and_warrens_success()
|
||||||
|
.await;
|
||||||
|
self.notifier
|
||||||
|
.all_users_and_warrens_listed(session_response.user(), response)
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
self.metrics
|
||||||
|
.record_list_all_users_and_warrens_failure()
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.map_err(AuthError::Custom)
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_auth_session(
|
async fn create_auth_session(
|
||||||
&self,
|
&self,
|
||||||
request: CreateAuthSessionRequest,
|
request: CreateAuthSessionRequest,
|
||||||
|
|||||||
103
backend/src/lib/inbound/http/handlers/admin/edit_user.rs
Normal file
103
backend/src/lib/inbound/http/handlers/admin/edit_user.rs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
use axum::{Json, extract::State, http::StatusCode};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::warren::{
|
||||||
|
models::{
|
||||||
|
auth_session::AuthRequest,
|
||||||
|
user::{
|
||||||
|
EditUserRequest, UserEmail, UserEmailError, UserName, UserNameError, UserPassword,
|
||||||
|
UserPasswordError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ports::{AuthService, WarrenService},
|
||||||
|
},
|
||||||
|
inbound::http::{
|
||||||
|
AppState,
|
||||||
|
handlers::{UserData, extractors::SessionIdHeader},
|
||||||
|
responses::{ApiError, ApiSuccess},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)]
|
||||||
|
pub(super) enum ParseEditUserHttpRequestError {
|
||||||
|
#[error(transparent)]
|
||||||
|
UserName(#[from] UserNameError),
|
||||||
|
#[error(transparent)]
|
||||||
|
UserEmail(#[from] UserEmailError),
|
||||||
|
#[error(transparent)]
|
||||||
|
UserPassword(#[from] UserPasswordError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseEditUserHttpRequestError> for ApiError {
|
||||||
|
fn from(value: ParseEditUserHttpRequestError) -> Self {
|
||||||
|
match value {
|
||||||
|
ParseEditUserHttpRequestError::UserName(err) => match err {
|
||||||
|
UserNameError::Empty => {
|
||||||
|
Self::BadRequest("The username must not be empty".to_string())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ParseEditUserHttpRequestError::UserEmail(err) => match err {
|
||||||
|
UserEmailError::Invalid => Self::BadRequest("The email is invalid".to_string()),
|
||||||
|
UserEmailError::Empty => {
|
||||||
|
Self::BadRequest("The email must not be empty".to_string())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ParseEditUserHttpRequestError::UserPassword(err) => Self::BadRequest(
|
||||||
|
match err {
|
||||||
|
UserPasswordError::Empty => "The provided password is empty",
|
||||||
|
// Best not give a potential bad actor any hints since this is the login and
|
||||||
|
// not the registration
|
||||||
|
UserPasswordError::LeadingWhitespace
|
||||||
|
| UserPasswordError::TrailingWhitespace
|
||||||
|
| UserPasswordError::TooShort
|
||||||
|
| UserPasswordError::TooLong => "",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(super) struct EditUserHttpRequestBody {
|
||||||
|
id: Uuid,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
password: Option<String>,
|
||||||
|
admin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EditUserHttpRequestBody {
|
||||||
|
fn try_into_domain(self) -> Result<EditUserRequest, ParseEditUserHttpRequestError> {
|
||||||
|
let name = UserName::new(&self.name)?;
|
||||||
|
let email = UserEmail::new(&self.email)?;
|
||||||
|
let password = if let Some(password) = self.password.as_ref() {
|
||||||
|
Some(UserPassword::new(password)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(EditUserRequest::new(
|
||||||
|
self.id, name, email, password, self.admin,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn edit_user<WS: WarrenService, AS: AuthService>(
|
||||||
|
State(state): State<AppState<WS, AS>>,
|
||||||
|
SessionIdHeader(session): SessionIdHeader,
|
||||||
|
Json(request): Json<EditUserHttpRequestBody>,
|
||||||
|
) -> Result<ApiSuccess<UserData>, ApiError> {
|
||||||
|
let domain_request = request.try_into_domain()?;
|
||||||
|
|
||||||
|
state
|
||||||
|
.auth_service
|
||||||
|
.edit_user(AuthRequest::new(session, domain_request))
|
||||||
|
.await
|
||||||
|
.map(|user| ApiSuccess::new(StatusCode::CREATED, user.into()))
|
||||||
|
.map_err(ApiError::from)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
use axum::{extract::State, http::StatusCode};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::warren::{
|
||||||
|
models::{
|
||||||
|
auth_session::AuthRequest,
|
||||||
|
user::{ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse},
|
||||||
|
},
|
||||||
|
ports::{AuthService, WarrenService},
|
||||||
|
},
|
||||||
|
inbound::http::{
|
||||||
|
AppState,
|
||||||
|
handlers::{UserData, UserWarrenData, WarrenData, extractors::SessionIdHeader},
|
||||||
|
responses::{ApiError, ApiSuccess},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(super) struct ListAllUsersAndWarrensHttpResponseBody {
|
||||||
|
users: Vec<UserData>,
|
||||||
|
user_warrens: Vec<UserWarrenData>,
|
||||||
|
warrens: Vec<WarrenData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ListAllUsersAndWarrensResponse> for ListAllUsersAndWarrensHttpResponseBody {
|
||||||
|
fn from(value: ListAllUsersAndWarrensResponse) -> Self {
|
||||||
|
let (users, user_warrens, warrens) = value.unpack();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
users: users.into_iter().map(Into::into).collect(),
|
||||||
|
user_warrens: user_warrens.into_iter().map(Into::into).collect(),
|
||||||
|
warrens: warrens.into_iter().map(Into::into).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_all_users_and_warrens<WS: WarrenService, AS: AuthService>(
|
||||||
|
State(state): State<AppState<WS, AS>>,
|
||||||
|
SessionIdHeader(session): SessionIdHeader,
|
||||||
|
) -> Result<ApiSuccess<ListAllUsersAndWarrensHttpResponseBody>, ApiError> {
|
||||||
|
state
|
||||||
|
.auth_service
|
||||||
|
.list_all_users_and_warrens(
|
||||||
|
AuthRequest::new(session, ListAllUsersAndWarrensRequest::new()),
|
||||||
|
state.warren_service.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|response| ApiSuccess::new(StatusCode::OK, response.into()))
|
||||||
|
.map_err(ApiError::from)
|
||||||
|
}
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
mod create_user;
|
mod create_user;
|
||||||
mod delete_user;
|
mod delete_user;
|
||||||
|
mod edit_user;
|
||||||
|
mod list_all_users_and_warrens;
|
||||||
mod list_users;
|
mod list_users;
|
||||||
|
|
||||||
use create_user::create_user;
|
use create_user::create_user;
|
||||||
use delete_user::delete_user;
|
use delete_user::delete_user;
|
||||||
|
use edit_user::edit_user;
|
||||||
|
use list_all_users_and_warrens::list_all_users_and_warrens;
|
||||||
use list_users::list_users;
|
use list_users::list_users;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
routing::{delete, get, post},
|
routing::{delete, get, patch, post},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -18,7 +22,9 @@ use crate::{
|
|||||||
|
|
||||||
pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>> {
|
pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
.route("/all", get(list_all_users_and_warrens))
|
||||||
.route("/users", get(list_users))
|
.route("/users", get(list_users))
|
||||||
.route("/users", post(create_user))
|
.route("/users", post(create_user))
|
||||||
|
.route("/users", patch(edit_user))
|
||||||
.route("/users", delete(delete_user))
|
.route("/users", delete(delete_user))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::domain::warren::models::user::User;
|
use crate::domain::warren::models::{user::User, user_warren::UserWarren, warren::Warren};
|
||||||
|
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
@@ -28,3 +28,49 @@ impl From<User> for UserData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// A user warren that can be safely sent to the client
|
||||||
|
pub(super) struct UserWarrenData {
|
||||||
|
user_id: Uuid,
|
||||||
|
warren_id: Uuid,
|
||||||
|
can_create_children: bool,
|
||||||
|
can_list_files: bool,
|
||||||
|
can_read_files: bool,
|
||||||
|
can_modify_files: bool,
|
||||||
|
can_delete_files: bool,
|
||||||
|
can_delete_warren: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UserWarren> for UserWarrenData {
|
||||||
|
fn from(value: UserWarren) -> Self {
|
||||||
|
Self {
|
||||||
|
user_id: *value.user_id(),
|
||||||
|
warren_id: *value.warren_id(),
|
||||||
|
can_create_children: value.can_create_children(),
|
||||||
|
can_list_files: value.can_list_files(),
|
||||||
|
can_read_files: value.can_read_files(),
|
||||||
|
can_modify_files: value.can_modify_files(),
|
||||||
|
can_delete_files: value.can_delete_files(),
|
||||||
|
can_delete_warren: value.can_delete_warren(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// A warren that can be safely sent to the client
|
||||||
|
pub(super) struct WarrenData {
|
||||||
|
id: Uuid,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Warren> for WarrenData {
|
||||||
|
fn from(value: Warren) -> Self {
|
||||||
|
Self {
|
||||||
|
id: *value.id(),
|
||||||
|
name: value.name().to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -145,6 +145,13 @@ impl AuthMetrics for MetricsDebugLogger {
|
|||||||
tracing::debug!("[Metrics] User creation failed");
|
tracing::debug!("[Metrics] User creation failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn record_user_edit_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] User edit succeeded");
|
||||||
|
}
|
||||||
|
async fn record_user_edit_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] User edit failed");
|
||||||
|
}
|
||||||
|
|
||||||
async fn record_user_deletion_success(&self) {
|
async fn record_user_deletion_success(&self) {
|
||||||
tracing::debug!("[Metrics] User deletion succeeded");
|
tracing::debug!("[Metrics] User deletion succeeded");
|
||||||
}
|
}
|
||||||
@@ -152,13 +159,20 @@ impl AuthMetrics for MetricsDebugLogger {
|
|||||||
tracing::debug!("[Metrics] User deletion failed");
|
tracing::debug!("[Metrics] User deletion failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn record_user_list_success(&self) -> () {
|
async fn record_user_list_success(&self) {
|
||||||
tracing::debug!("[Metrics] User list succeeded");
|
tracing::debug!("[Metrics] User list succeeded");
|
||||||
}
|
}
|
||||||
async fn record_user_list_failure(&self) -> () {
|
async fn record_user_list_failure(&self) {
|
||||||
tracing::debug!("[Metrics] User list failed");
|
tracing::debug!("[Metrics] User list failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn record_list_all_users_and_warrens_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Users, warrens and user warrens list succeeded");
|
||||||
|
}
|
||||||
|
async fn record_list_all_users_and_warrens_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Users, warrens and user warrens list failed");
|
||||||
|
}
|
||||||
|
|
||||||
async fn record_auth_session_creation_success(&self) {
|
async fn record_auth_session_creation_success(&self) {
|
||||||
tracing::debug!("[Metrics] Auth session creation succeeded");
|
tracing::debug!("[Metrics] Auth session creation succeeded");
|
||||||
}
|
}
|
||||||
@@ -180,10 +194,10 @@ impl AuthMetrics for MetricsDebugLogger {
|
|||||||
tracing::debug!("[Metrics] Auth warren list failed");
|
tracing::debug!("[Metrics] Auth warren list failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn record_auth_fetch_user_warrens_success(&self) -> () {
|
async fn record_auth_fetch_user_warrens_success(&self) {
|
||||||
tracing::debug!("[Metrics] Auth user warren id fetch succeeded");
|
tracing::debug!("[Metrics] Auth user warren id fetch succeeded");
|
||||||
}
|
}
|
||||||
async fn record_auth_fetch_user_warrens_failure(&self) -> () {
|
async fn record_auth_fetch_user_warrens_failure(&self) {
|
||||||
tracing::debug!("[Metrics] Auth user warren id fetch failed");
|
tracing::debug!("[Metrics] Auth user warren id fetch failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::domain::warren::{
|
|||||||
models::{
|
models::{
|
||||||
auth_session::requests::FetchAuthSessionResponse,
|
auth_session::requests::FetchAuthSessionResponse,
|
||||||
file::{File, FilePath},
|
file::{File, FilePath},
|
||||||
user::{LoginUserResponse, User},
|
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
||||||
user_warren::UserWarren,
|
user_warren::UserWarren,
|
||||||
warren::{
|
warren::{
|
||||||
CreateWarrenDirectoryResponse, DeleteWarrenDirectoryResponse, DeleteWarrenFileResponse,
|
CreateWarrenDirectoryResponse, DeleteWarrenDirectoryResponse, DeleteWarrenFileResponse,
|
||||||
@@ -129,6 +129,14 @@ impl AuthNotifier for NotifierDebugLogger {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn user_edited(&self, editor: &User, user: &User) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Admin user {} edited user {}",
|
||||||
|
editor.name(),
|
||||||
|
user.id()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async fn user_deleted(&self, deleter: &User, user: &User) {
|
async fn user_deleted(&self, deleter: &User, user: &User) {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"[Notifier] Admin user {} deleted user {}",
|
"[Notifier] Admin user {} deleted user {}",
|
||||||
@@ -145,6 +153,20 @@ impl AuthNotifier for NotifierDebugLogger {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn all_users_and_warrens_listed(
|
||||||
|
&self,
|
||||||
|
user: &User,
|
||||||
|
response: &ListAllUsersAndWarrensResponse,
|
||||||
|
) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Admin user {} listed {} user(s), {} warren(s) and {} user warren(s)",
|
||||||
|
user.name(),
|
||||||
|
response.users().len(),
|
||||||
|
response.warrens().len(),
|
||||||
|
response.user_warrens().len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async fn user_logged_in(&self, response: &LoginUserResponse) {
|
async fn user_logged_in(&self, response: &LoginUserResponse) {
|
||||||
tracing::debug!("[Notifier] Logged in user {}", response.user().name());
|
tracing::debug!("[Notifier] Logged in user {}", response.user().name());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,22 +25,23 @@ use crate::domain::warren::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
user::{
|
user::{
|
||||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, ListUsersError,
|
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||||
ListUsersRequest, User, UserEmail, UserName, UserPassword, VerifyUserPasswordError,
|
EditUserRequest, ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest,
|
||||||
VerifyUserPasswordRequest,
|
ListAllUsersAndWarrensResponse, ListUsersError, ListUsersRequest, User, UserEmail,
|
||||||
|
UserName, UserPassword, VerifyUserPasswordError, VerifyUserPasswordRequest,
|
||||||
},
|
},
|
||||||
user_warren::{
|
user_warren::{
|
||||||
UserWarren,
|
UserWarren,
|
||||||
requests::{
|
requests::{
|
||||||
FetchUserWarrenError, FetchUserWarrenRequest, FetchUserWarrensError,
|
FetchUserWarrenError, FetchUserWarrenRequest, FetchUserWarrensError,
|
||||||
FetchUserWarrensRequest,
|
FetchUserWarrensRequest, ListUserWarrensError, ListUserWarrensRequest,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
warren::{
|
warren::{
|
||||||
FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, Warren,
|
FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, Warren,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ports::{AuthRepository, WarrenRepository},
|
ports::{AuthRepository, WarrenRepository, WarrenService},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -138,12 +139,8 @@ impl Postgres {
|
|||||||
password: &UserPassword,
|
password: &UserPassword,
|
||||||
is_admin: bool,
|
is_admin: bool,
|
||||||
) -> anyhow::Result<User> {
|
) -> anyhow::Result<User> {
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
let password_hash =
|
||||||
let argon2 = Argon2::default();
|
hash_password(password).map_err(|e| anyhow!("Failed to hash password: {e:?}"))?;
|
||||||
let password_hash = argon2
|
|
||||||
.hash_password(password.as_str().as_bytes(), &salt)
|
|
||||||
.map_err(|_| anyhow!("Failed to hash password"))?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let mut tx = connection.begin().await?;
|
let mut tx = connection.begin().await?;
|
||||||
|
|
||||||
@@ -176,6 +173,73 @@ impl Postgres {
|
|||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn edit_user(
|
||||||
|
&self,
|
||||||
|
connection: &mut PgConnection,
|
||||||
|
id: &Uuid,
|
||||||
|
name: &UserName,
|
||||||
|
email: &UserEmail,
|
||||||
|
password: Option<&UserPassword>,
|
||||||
|
is_admin: bool,
|
||||||
|
) -> anyhow::Result<User> {
|
||||||
|
let password_hash = if let Some(password) = password {
|
||||||
|
Some(hash_password(password).map_err(|e| anyhow!("Failed to hash password: {e:?}"))?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut tx = connection.begin().await?;
|
||||||
|
|
||||||
|
let user: User = sqlx::query_as(
|
||||||
|
"UPDATE
|
||||||
|
users
|
||||||
|
SET
|
||||||
|
name = $2,
|
||||||
|
email = $3,
|
||||||
|
hash = COALESCE($4, hash),
|
||||||
|
admin = $5
|
||||||
|
WHERE
|
||||||
|
id = $1
|
||||||
|
RETURNING
|
||||||
|
*
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.bind(name)
|
||||||
|
.bind(email)
|
||||||
|
.bind(password_hash)
|
||||||
|
.bind(is_admin)
|
||||||
|
.fetch_one(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.delete_user_sessions(&mut *tx, id).await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_user_sessions(
|
||||||
|
&self,
|
||||||
|
connection: &mut PgConnection,
|
||||||
|
user_id: &Uuid,
|
||||||
|
) -> Result<u64, sqlx::Error> {
|
||||||
|
let rows_affected = sqlx::query(
|
||||||
|
"
|
||||||
|
DELETE FROM
|
||||||
|
auth_sessions
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.execute(connection)
|
||||||
|
.await?
|
||||||
|
.rows_affected();
|
||||||
|
|
||||||
|
Ok(rows_affected)
|
||||||
|
}
|
||||||
|
|
||||||
async fn delete_user_from_database(
|
async fn delete_user_from_database(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut PgConnection,
|
||||||
@@ -307,7 +371,7 @@ impl Postgres {
|
|||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut PgConnection,
|
||||||
session_id: &AuthSessionId,
|
session_id: &AuthSessionId,
|
||||||
) -> anyhow::Result<AuthSession> {
|
) -> Result<AuthSession, sqlx::Error> {
|
||||||
let session: AuthSession = sqlx::query_as(
|
let session: AuthSession = sqlx::query_as(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -330,7 +394,7 @@ impl Postgres {
|
|||||||
connection: &mut PgConnection,
|
connection: &mut PgConnection,
|
||||||
user_id: &Uuid,
|
user_id: &Uuid,
|
||||||
) -> Result<Vec<UserWarren>, sqlx::Error> {
|
) -> Result<Vec<UserWarren>, sqlx::Error> {
|
||||||
let ids: Vec<UserWarren> = sqlx::query_as(
|
let user_warrens: Vec<UserWarren> = sqlx::query_as(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
*
|
*
|
||||||
@@ -344,7 +408,25 @@ impl Postgres {
|
|||||||
.fetch_all(connection)
|
.fetch_all(connection)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(ids)
|
Ok(user_warrens)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_all_user_warrens(
|
||||||
|
&self,
|
||||||
|
connection: &mut PgConnection,
|
||||||
|
) -> Result<Vec<UserWarren>, sqlx::Error> {
|
||||||
|
let user_warrens: Vec<UserWarren> = sqlx::query_as(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
user_warrens
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.fetch_all(connection)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(user_warrens)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user_warren(
|
async fn get_user_warren(
|
||||||
@@ -379,6 +461,8 @@ impl Postgres {
|
|||||||
*
|
*
|
||||||
FROM
|
FROM
|
||||||
users
|
users
|
||||||
|
ORDER BY
|
||||||
|
created_at ASC
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.fetch_all(connection)
|
.fetch_all(connection)
|
||||||
@@ -453,6 +537,28 @@ impl AuthRepository for Postgres {
|
|||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn edit_user(&self, request: EditUserRequest) -> Result<User, EditUserError> {
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a PostgreSQL connection")?;
|
||||||
|
|
||||||
|
let user = self
|
||||||
|
.edit_user(
|
||||||
|
&mut connection,
|
||||||
|
request.user_id(),
|
||||||
|
request.name(),
|
||||||
|
request.email(),
|
||||||
|
request.password(),
|
||||||
|
request.admin(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context(format!("Failed to edit user"))?;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
async fn delete_user(&self, request: DeleteUserRequest) -> Result<User, DeleteUserError> {
|
async fn delete_user(&self, request: DeleteUserRequest) -> Result<User, DeleteUserError> {
|
||||||
let mut connection = self
|
let mut connection = self
|
||||||
.pool
|
.pool
|
||||||
@@ -528,7 +634,13 @@ impl AuthRepository for Postgres {
|
|||||||
let session = self
|
let session = self
|
||||||
.get_auth_session(&mut connection, request.session_id())
|
.get_auth_session(&mut connection, request.session_id())
|
||||||
.await
|
.await
|
||||||
.context("Failed to get auth session")?;
|
.map_err(|e| {
|
||||||
|
if is_not_found_error(&e) {
|
||||||
|
FetchAuthSessionError::NotFound
|
||||||
|
} else {
|
||||||
|
anyhow!("Failed to get auth session: {e:?}").into()
|
||||||
|
}
|
||||||
|
})?;
|
||||||
let user = self
|
let user = self
|
||||||
.get_user_from_id(&mut connection, session.user_id())
|
.get_user_from_id(&mut connection, session.user_id())
|
||||||
.await
|
.await
|
||||||
@@ -547,12 +659,30 @@ impl AuthRepository for Postgres {
|
|||||||
.await
|
.await
|
||||||
.context("Failed to get a PostgreSQL connection")?;
|
.context("Failed to get a PostgreSQL connection")?;
|
||||||
|
|
||||||
let warren_ids = self
|
let user_warrens = self
|
||||||
.get_user_warrens(&mut connection, request.user_id())
|
.get_user_warrens(&mut connection, request.user_id())
|
||||||
.await
|
.await
|
||||||
.context("Failed to get user warrens")?;
|
.context("Failed to get user warrens")?;
|
||||||
|
|
||||||
Ok(warren_ids)
|
Ok(user_warrens)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_user_warrens(
|
||||||
|
&self,
|
||||||
|
_request: ListUserWarrensRequest,
|
||||||
|
) -> Result<Vec<UserWarren>, ListUserWarrensError> {
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a PostgreSQL connection")?;
|
||||||
|
|
||||||
|
let user_warrens = self
|
||||||
|
.get_all_user_warrens(&mut connection)
|
||||||
|
.await
|
||||||
|
.context("Failed to get all user warrens")?;
|
||||||
|
|
||||||
|
Ok(user_warrens)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_user_warren(
|
async fn fetch_user_warren(
|
||||||
@@ -590,8 +720,53 @@ impl AuthRepository for Postgres {
|
|||||||
|
|
||||||
Ok(users)
|
Ok(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_all_users_and_warrens<WS: WarrenService>(
|
||||||
|
&self,
|
||||||
|
_request: ListAllUsersAndWarrensRequest,
|
||||||
|
warren_service: &WS,
|
||||||
|
) -> Result<ListAllUsersAndWarrensResponse, ListAllUsersAndWarrensError> {
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a PostgreSQL connection")?;
|
||||||
|
|
||||||
|
let users = self
|
||||||
|
.fetch_users(&mut connection)
|
||||||
|
.await
|
||||||
|
.context("Failed to fetch all users")?;
|
||||||
|
let user_warrens = self
|
||||||
|
.get_all_user_warrens(&mut connection)
|
||||||
|
.await
|
||||||
|
.context("Failed to fetch all user warrens")?;
|
||||||
|
let warrens = warren_service
|
||||||
|
.list_warrens(FetchWarrensRequest::new(
|
||||||
|
user_warrens
|
||||||
|
.iter()
|
||||||
|
.map(|uw| uw.warren_id().clone())
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.context("Failed to get warrens")?;
|
||||||
|
|
||||||
|
Ok(ListAllUsersAndWarrensResponse::new(
|
||||||
|
users,
|
||||||
|
user_warrens,
|
||||||
|
warrens,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_not_found_error(err: &sqlx::Error) -> bool {
|
fn is_not_found_error(err: &sqlx::Error) -> bool {
|
||||||
matches!(err, sqlx::Error::RowNotFound)
|
matches!(err, sqlx::Error::RowNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn hash_password(password: &UserPassword) -> Result<String, argon2::password_hash::Error> {
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
|
||||||
|
argon2
|
||||||
|
.hash_password(password.as_str().as_bytes(), &salt)
|
||||||
|
.map(|h| h.to_string())
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"@nuxt/test-utils": "3.19.2",
|
"@nuxt/test-utils": "3.19.2",
|
||||||
"@pinia/nuxt": "^0.11.1",
|
"@pinia/nuxt": "^0.11.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@vee-validate/yup": "^4.15.1",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/core": "^13.5.0",
|
"@vueuse/core": "^13.5.0",
|
||||||
"byte-size": "^9.0.1",
|
"byte-size": "^9.0.1",
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vue-sonner": "^2.0.1",
|
"vue-sonner": "^2.0.1",
|
||||||
"zod": "^4.0.5",
|
"yup": "^1.6.1",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/lucide": "^1.2.57",
|
"@iconify-json/lucide": "^1.2.57",
|
||||||
@@ -538,6 +539,8 @@
|
|||||||
|
|
||||||
"@unhead/vue": ["@unhead/vue@2.0.12", "", { "dependencies": { "hookable": "^5.5.3", "unhead": "2.0.12" }, "peerDependencies": { "vue": ">=3.5.13" } }, "sha512-WFaiCVbBd39FK6Bx3GQskhgT9s45Vjx6dRQegYheVwU1AnF+FAfJVgWbrl21p6fRJcLAFp0xDz6wE18JYBM0eQ=="],
|
"@unhead/vue": ["@unhead/vue@2.0.12", "", { "dependencies": { "hookable": "^5.5.3", "unhead": "2.0.12" }, "peerDependencies": { "vue": ">=3.5.13" } }, "sha512-WFaiCVbBd39FK6Bx3GQskhgT9s45Vjx6dRQegYheVwU1AnF+FAfJVgWbrl21p6fRJcLAFp0xDz6wE18JYBM0eQ=="],
|
||||||
|
|
||||||
|
"@vee-validate/yup": ["@vee-validate/yup@4.15.1", "", { "dependencies": { "type-fest": "^4.8.3", "vee-validate": "4.15.1" }, "peerDependencies": { "yup": "^1.3.2" } }, "sha512-+u6lI1IZftjHphj+mTCPJRruwBBwv1IKKCI1EFm6ipQroAPibkS5M8UNX+yeVYG5++ix6m1rsv4/SJvJJQTWJg=="],
|
||||||
|
|
||||||
"@vee-validate/zod": ["@vee-validate/zod@4.15.1", "", { "dependencies": { "type-fest": "^4.8.3", "vee-validate": "4.15.1" }, "peerDependencies": { "zod": "^3.24.0" } }, "sha512-329Z4TDBE5Vx0FdbA8S4eR9iGCFFUNGbxjpQ20ff5b5wGueScjocUIx9JHPa79LTG06RnlUR4XogQsjN4tecKA=="],
|
"@vee-validate/zod": ["@vee-validate/zod@4.15.1", "", { "dependencies": { "type-fest": "^4.8.3", "vee-validate": "4.15.1" }, "peerDependencies": { "zod": "^3.24.0" } }, "sha512-329Z4TDBE5Vx0FdbA8S4eR9iGCFFUNGbxjpQ20ff5b5wGueScjocUIx9JHPa79LTG06RnlUR4XogQsjN4tecKA=="],
|
||||||
|
|
||||||
"@vercel/nft": ["@vercel/nft@0.29.4", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^10.4.5", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-6lLqMNX3TuycBPABycx7A9F1bHQR7kiQln6abjFbPrf5C/05qHM9M5E4PeTE59c7z8g6vHnx1Ioihb2AQl7BTA=="],
|
"@vercel/nft": ["@vercel/nft@0.29.4", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^10.4.5", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-6lLqMNX3TuycBPABycx7A9F1bHQR7kiQln6abjFbPrf5C/05qHM9M5E4PeTE59c7z8g6vHnx1Ioihb2AQl7BTA=="],
|
||||||
@@ -1548,6 +1551,8 @@
|
|||||||
|
|
||||||
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
|
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
|
||||||
|
|
||||||
|
"property-expr": ["property-expr@2.0.6", "", {}, "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="],
|
||||||
|
|
||||||
"protocols": ["protocols@2.0.2", "", {}, "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ=="],
|
"protocols": ["protocols@2.0.2", "", {}, "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ=="],
|
||||||
|
|
||||||
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||||
@@ -1752,6 +1757,8 @@
|
|||||||
|
|
||||||
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
|
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
|
||||||
|
|
||||||
|
"tiny-case": ["tiny-case@1.0.3", "", {}, "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="],
|
||||||
|
|
||||||
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||||
|
|
||||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||||
@@ -1770,6 +1777,8 @@
|
|||||||
|
|
||||||
"toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="],
|
"toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="],
|
||||||
|
|
||||||
|
"toposort": ["toposort@2.0.2", "", {}, "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="],
|
||||||
|
|
||||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||||
|
|
||||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
@@ -1928,6 +1937,8 @@
|
|||||||
|
|
||||||
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
|
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
|
||||||
|
|
||||||
|
"yup": ["yup@1.6.1", "", { "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", "toposort": "^2.0.2", "type-fest": "^2.19.0" } }, "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA=="],
|
||||||
|
|
||||||
"zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="],
|
"zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="],
|
||||||
|
|
||||||
"zod": ["zod@4.0.5", "", {}, "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA=="],
|
"zod": ["zod@4.0.5", "", {}, "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA=="],
|
||||||
@@ -2242,6 +2253,8 @@
|
|||||||
|
|
||||||
"yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
"yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
||||||
|
|
||||||
|
"yup/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||||
|
|||||||
@@ -81,9 +81,14 @@ const warrenCrumbs = computed<WarrenBreadcrumbData[]>(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Breadcrumb>
|
<Breadcrumb class="flex-nowrap overflow-hidden">
|
||||||
<BreadcrumbList>
|
<BreadcrumbList class="flex-nowrap">
|
||||||
<template v-if="store.current == null">
|
<template
|
||||||
|
v-if="
|
||||||
|
store.current == null ||
|
||||||
|
!route.path.startsWith('/warrens/files')
|
||||||
|
"
|
||||||
|
>
|
||||||
<template v-for="(crumb, i) in routeCrumbs" :key="i">
|
<template v-for="(crumb, i) in routeCrumbs" :key="i">
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
@@ -97,10 +102,7 @@ const warrenCrumbs = computed<WarrenBreadcrumbData[]>(() => {
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<BreadcrumbPage v-else>{{ crumb.name }}</BreadcrumbPage>
|
<BreadcrumbPage v-else>{{ crumb.name }}</BreadcrumbPage>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator
|
<BreadcrumbSeparator v-if="i < routeCrumbs.length - 1" />
|
||||||
v-if="i < routeCrumbs.length - 1"
|
|
||||||
class="hidden md:block"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
@@ -118,10 +120,7 @@ const warrenCrumbs = computed<WarrenBreadcrumbData[]>(() => {
|
|||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbPage v-else>{{ crumb.name }}</BreadcrumbPage>
|
<BreadcrumbPage v-else>{{ crumb.name }}</BreadcrumbPage>
|
||||||
<BreadcrumbSeparator
|
<BreadcrumbSeparator v-if="i < warrenCrumbs.length - 1" />
|
||||||
v-if="i < warrenCrumbs.length - 1"
|
|
||||||
class="hidden md:block"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
|
|||||||
@@ -2,50 +2,48 @@
|
|||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { useForm } from 'vee-validate';
|
import { useForm } from 'vee-validate';
|
||||||
import { createUserSchema } from '~/lib/schemas/admin';
|
import { createUserSchema } from '~/lib/schemas/admin';
|
||||||
import { toTypedSchema } from '@vee-validate/zod';
|
|
||||||
import type z from 'zod';
|
|
||||||
import { createUser } from '~/lib/api/admin/createUser';
|
import { createUser } from '~/lib/api/admin/createUser';
|
||||||
|
import { toTypedSchema } from '@vee-validate/yup';
|
||||||
|
|
||||||
const adminStore = useAdminStore();
|
const adminStore = useAdminStore();
|
||||||
const creating = ref(false);
|
const creating = ref(false);
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
adminStore.closeCreateUserDialog();
|
adminStore.closeCreateUserDialog();
|
||||||
|
form.resetForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
validationSchema: toTypedSchema(createUserSchema),
|
validationSchema: toTypedSchema(createUserSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit(
|
const onSubmit = form.handleSubmit(async (values) => {
|
||||||
async (values: z.output<typeof createUserSchema>) => {
|
if (creating.value) {
|
||||||
if (creating.value) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
creating.value = true;
|
|
||||||
|
|
||||||
const result = await createUser(values);
|
|
||||||
|
|
||||||
creating.value = false;
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
adminStore.closeCreateUserDialog();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
creating.value = true;
|
||||||
|
|
||||||
|
const result = await createUser(values);
|
||||||
|
|
||||||
|
creating.value = false;
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
adminStore.closeCreateUserDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog :open="adminStore.createUserDialog != null">
|
<AlertDialog :open="adminStore.createUserDialogOpen">
|
||||||
<DialogTrigger><slot /></DialogTrigger>
|
<AlertDialogTrigger><slot /></AlertDialogTrigger>
|
||||||
<DialogContent @escape-key-down="cancel">
|
<AlertDialogContent @escape-key-down="cancel">
|
||||||
<DialogHeader>
|
<AlertDialogHeader>
|
||||||
<DialogTitle>Create user</DialogTitle>
|
<AlertDialogTitle>Create user</AlertDialogTitle>
|
||||||
<DialogDescription>
|
<AlertDialogDescription>
|
||||||
Enter a username, email and password to create a new user
|
Enter a username, email and password to create a new user
|
||||||
</DialogDescription>
|
</AlertDialogDescription>
|
||||||
</DialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
id="create-user-form"
|
id="create-user-form"
|
||||||
@@ -67,8 +65,8 @@ const onSubmit = form.handleSubmit(
|
|||||||
data-bwignore
|
data-bwignore
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormMessage />
|
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField v-slot="{ componentField }" name="email">
|
<FormField v-slot="{ componentField }" name="email">
|
||||||
@@ -104,8 +102,8 @@ const onSubmit = form.handleSubmit(
|
|||||||
data-bwignore
|
data-bwignore
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormMessage />
|
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField v-slot="{ value, handleChange }" name="admin">
|
<FormField v-slot="{ value, handleChange }" name="admin">
|
||||||
@@ -118,15 +116,19 @@ const onSubmit = form.handleSubmit(
|
|||||||
@update:model-value="handleChange"
|
@update:model-value="handleChange"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormMessage />
|
|
||||||
</FormField>
|
</FormField>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter>
|
<AlertDialogFooter>
|
||||||
<Button variant="outline" @click="cancel">Cancel</Button>
|
<AlertDialogCancel variant="outline" @click="cancel"
|
||||||
<Button type="submit" form="create-user-form">Create</Button>
|
>Cancel</AlertDialogCancel
|
||||||
</DialogFooter>
|
>
|
||||||
</DialogContent>
|
<AlertDialogAction type="submit" form="create-user-form"
|
||||||
</Dialog>
|
>Create</AlertDialogAction
|
||||||
|
>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ adminStore.$subscribe(async (_mutation, state) => {
|
|||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
confirmEmail.value = '';
|
confirmEmail.value = '';
|
||||||
adminStore.clearDeleteUserDialog();
|
adminStore.closeDeleteUserDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
|
|||||||
185
frontend/components/admin/EditUserDialog.vue
Normal file
185
frontend/components/admin/EditUserDialog.vue
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
import { useForm } from 'vee-validate';
|
||||||
|
import { toTypedSchema } from '@vee-validate/yup';
|
||||||
|
import { editUserSchema } from '~/lib/schemas/admin';
|
||||||
|
import { editUser } from '~/lib/api/admin/editUser';
|
||||||
|
import type { AuthUser } from '~/shared/types/auth';
|
||||||
|
|
||||||
|
const adminStore = useAdminStore();
|
||||||
|
|
||||||
|
const isValid = computed(() => Object.keys(form.errors.value).length < 1);
|
||||||
|
|
||||||
|
// We'll only update this value if there is a user to prevent layout shifts on close
|
||||||
|
const user = ref<AuthUser>();
|
||||||
|
const editing = ref(false);
|
||||||
|
|
||||||
|
const isChanged = computed(() => {
|
||||||
|
if (user.value == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const values = editUserSchema.validateSync(form.controlledValues.value);
|
||||||
|
return (
|
||||||
|
values.name !== user.value.name ||
|
||||||
|
values.email !== user.value.email ||
|
||||||
|
values.password != null ||
|
||||||
|
values.admin !== user.value.admin
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
adminStore.closeEditUserDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
validationSchema: toTypedSchema(editUserSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
adminStore.$subscribe((_mutation, state) => {
|
||||||
|
if (state.editUserDialog != null && !editing.value) {
|
||||||
|
user.value = state.editUserDialog.user;
|
||||||
|
form.setValues(user.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit(async (values) => {
|
||||||
|
if (user.value == null || !isChanged.value || editing.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editing.value = true;
|
||||||
|
|
||||||
|
const result = await editUser({
|
||||||
|
id: user.value.id,
|
||||||
|
name: values.name,
|
||||||
|
email: values.email,
|
||||||
|
password: values.password ?? null,
|
||||||
|
admin: values.admin,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
editing.value = false;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AlertDialog :open="adminStore.editUserDialog != null">
|
||||||
|
<AlertDialogTrigger><slot /></AlertDialogTrigger>
|
||||||
|
<AlertDialogContent @escape-key-down="close">
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Edit user</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Edit the user's fields, manage permissions or assign warrens
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<form
|
||||||
|
id="edit-user-form"
|
||||||
|
class="flex flex-col gap-2"
|
||||||
|
@submit.prevent="onSubmit"
|
||||||
|
>
|
||||||
|
<FormField v-slot="{ componentField }" name="name">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
v-bind="componentField"
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="confused-cat"
|
||||||
|
autocomplete="off"
|
||||||
|
data-1p-ignore
|
||||||
|
data-protonpass-ignore
|
||||||
|
data-bwignore
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-slot="{ componentField }" name="email">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
v-bind="componentField"
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="confusedcat@example.com"
|
||||||
|
autocomplete="off"
|
||||||
|
data-1p-ignore
|
||||||
|
data-protonpass-ignore
|
||||||
|
data-bwignore
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-slot="{ componentField }" name="password">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
v-bind="componentField"
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
data-1p-ignore
|
||||||
|
data-protonpass-ignore
|
||||||
|
data-bwignore
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<FormDescription
|
||||||
|
>Leave empty to keep the current
|
||||||
|
password</FormDescription
|
||||||
|
>
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-slot="{ value, handleChange }" name="admin">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Admin</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
id="admin"
|
||||||
|
:model-value="value"
|
||||||
|
@update:model-value="handleChange"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<AlertDialogFooter class="gap-y-0">
|
||||||
|
<AlertDialogCancel @click="close">Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
type="submit"
|
||||||
|
form="edit-user-form"
|
||||||
|
:disabled="!isChanged || !isValid"
|
||||||
|
>Save</AlertDialogAction
|
||||||
|
>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</template>
|
||||||
@@ -11,15 +11,21 @@ const AVATAR =
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="group/user flex flex-row items-center justify-between gap-4">
|
<div
|
||||||
|
class="group/user bg-accent/30 flex cursor-pointer flex-row items-center justify-between gap-4 overflow-hidden rounded-lg p-2 pl-3"
|
||||||
|
>
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage :src="AVATAR" />
|
<AvatarImage :src="AVATAR" />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div class="flex grow flex-col leading-4">
|
<div class="flex min-w-0 shrink grow flex-col leading-4">
|
||||||
<span class="text-sm font-medium">{{ user.name }}</span>
|
<span class="truncate text-sm font-medium">{{ user.name }}</span>
|
||||||
<span class="text-muted-foreground text-xs">{{ user.email }}</span>
|
<span class="text-muted-foreground truncate text-xs">{{
|
||||||
|
user.email
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="opacity-0 transition-all group-hover/user:opacity-100">
|
<div
|
||||||
|
class="flex justify-end transition-all not-pointer-coarse:opacity-0 not-pointer-coarse:group-hover/user:opacity-100"
|
||||||
|
>
|
||||||
<slot name="actions" />
|
<slot name="actions" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { listUsers } from '~/lib/api/admin/listUsers';
|
import { fetchAllAdminResources } from '~/lib/api/admin/fetchAll';
|
||||||
|
|
||||||
const adminStore = useAdminStore();
|
const adminStore = useAdminStore();
|
||||||
|
|
||||||
await useAsyncData('users', async () => {
|
await useAsyncData('admin-resources', async () => {
|
||||||
const response = await listUsers();
|
const response = await fetchAllAdminResources();
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
adminStore.users = response.users;
|
adminStore.resources = response.data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -15,6 +15,7 @@ await useAsyncData('users', async () => {
|
|||||||
<template>
|
<template>
|
||||||
<NuxtLayout name="default">
|
<NuxtLayout name="default">
|
||||||
<AdminCreateUserDialog />
|
<AdminCreateUserDialog />
|
||||||
|
<AdminEditUserDialog />
|
||||||
<AdminDeleteUserDialog />
|
<AdminDeleteUserDialog />
|
||||||
<slot />
|
<slot />
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
|
|||||||
@@ -14,27 +14,24 @@ store.warrens = await getWarrens();
|
|||||||
<template>
|
<template>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<main class="flex w-full grow flex-col overflow-hidden">
|
<main
|
||||||
|
class="flex w-full grow flex-col-reverse overflow-hidden md:flex-col"
|
||||||
|
>
|
||||||
<header
|
<header
|
||||||
class="flex h-16 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12"
|
class="flex h-16 items-center gap-2 border-t transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12 md:border-t-0 md:border-b"
|
||||||
>
|
>
|
||||||
<div class="flex w-full items-center gap-2 px-4">
|
<div class="flex w-full items-center gap-4 px-4">
|
||||||
<SidebarTrigger class="[&_svg]:size-4" />
|
<SidebarTrigger class="[&_svg]:size-4" />
|
||||||
<Separator orientation="vertical" class="mr-2 !h-4" />
|
<div class="hidden flex-row items-center gap-4 md:flex">
|
||||||
<AppBreadcrumbs />
|
<Separator orientation="vertical" class="mr-2 !h-4" />
|
||||||
|
<AppBreadcrumbs />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="ml-auto flex flex-row items-center gap-2">
|
||||||
class="ml-auto flex flex-row-reverse items-center gap-2"
|
<Separator
|
||||||
>
|
orientation="vertical"
|
||||||
<CreateDirectoryDialog>
|
class="mr-2 hidden !h-4 md:block"
|
||||||
<Button
|
/>
|
||||||
v-if="route.path.startsWith('/warrens/')"
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
>
|
|
||||||
<Icon name="lucide:folder-plus" />
|
|
||||||
</Button>
|
|
||||||
</CreateDirectoryDialog>
|
|
||||||
<UploadDialog>
|
<UploadDialog>
|
||||||
<Button
|
<Button
|
||||||
class="relative"
|
class="relative"
|
||||||
@@ -48,11 +45,20 @@ store.warrens = await getWarrens();
|
|||||||
></div>
|
></div>
|
||||||
</Button>
|
</Button>
|
||||||
</UploadDialog>
|
</UploadDialog>
|
||||||
|
<CreateDirectoryDialog>
|
||||||
|
<Button
|
||||||
|
v-if="route.path.startsWith('/warrens/')"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<Icon name="lucide:folder-plus" />
|
||||||
|
</Button>
|
||||||
|
</CreateDirectoryDialog>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col p-4 pt-0">
|
<div class="flex flex-1 flex-col p-4">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export async function createUser(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await refreshNuxtData('users');
|
await refreshNuxtData('admin-resources');
|
||||||
|
|
||||||
toast.success('Create user', {
|
toast.success('Create user', {
|
||||||
description: 'Successfully created user',
|
description: 'Successfully created user',
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export async function deleteUser(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await refreshNuxtData('users');
|
await refreshNuxtData('admin-resources');
|
||||||
|
|
||||||
toast.success('Delete user', {
|
toast.success('Delete user', {
|
||||||
description: 'Successfully delete user',
|
description: 'Successfully delete user',
|
||||||
|
|||||||
46
frontend/lib/api/admin/editUser.ts
Normal file
46
frontend/lib/api/admin/editUser.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
import type { ApiResponse } from '#shared/types/api';
|
||||||
|
import type { AuthUser } from '#shared/types/auth';
|
||||||
|
import { getApiHeaders } from '..';
|
||||||
|
|
||||||
|
/** Admin function to edit an existing user */
|
||||||
|
export async function editUser(
|
||||||
|
user: AuthUser & { password: string | null }
|
||||||
|
): Promise<{ success: true; user: AuthUser } | { success: false }> {
|
||||||
|
const { data, error } = await useFetch<ApiResponse<AuthUser>>(
|
||||||
|
getApiUrl('admin/users'),
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: getApiHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
password: user.password,
|
||||||
|
admin: user.admin,
|
||||||
|
}),
|
||||||
|
responseType: 'json',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.value == null) {
|
||||||
|
toast.error('Edit user', {
|
||||||
|
description: error.value?.data ?? 'Failed to edit user',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshNuxtData('admin-resources');
|
||||||
|
|
||||||
|
toast.success('Edit user', {
|
||||||
|
description: 'Successfully edited user',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user: data.value.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
56
frontend/lib/api/admin/fetchAll.ts
Normal file
56
frontend/lib/api/admin/fetchAll.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import type { ApiResponse } from '~/shared/types/api';
|
||||||
|
import type { UserWarren, Warren } from '~/shared/types/warrens';
|
||||||
|
import { getApiHeaders } from '..';
|
||||||
|
import type { AdminResources, AuthUserWithWarrens } from '~/shared/types/admin';
|
||||||
|
import type { AuthUser } from '~/shared/types/auth';
|
||||||
|
|
||||||
|
export async function fetchAllAdminResources(): Promise<
|
||||||
|
| {
|
||||||
|
success: true;
|
||||||
|
data: AdminResources;
|
||||||
|
}
|
||||||
|
| { success: false }
|
||||||
|
> {
|
||||||
|
const { data } = await useFetch<
|
||||||
|
ApiResponse<{
|
||||||
|
users: AuthUser[];
|
||||||
|
userWarrens: UserWarren[];
|
||||||
|
warrens: Warren[];
|
||||||
|
}>
|
||||||
|
>(getApiUrl('admin/all'), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: getApiHeaders(),
|
||||||
|
responseType: 'json',
|
||||||
|
deep: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.value == null) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const users: Record<string, AuthUserWithWarrens> = data.value.data.users
|
||||||
|
.map((u) => ({
|
||||||
|
...u,
|
||||||
|
warrens: [],
|
||||||
|
}))
|
||||||
|
.reduce((acc, u) => ({ ...acc, [u.id]: u }), {});
|
||||||
|
const warrens: Record<string, Warren> = {};
|
||||||
|
|
||||||
|
for (const warren of data.value.data.warrens) {
|
||||||
|
warrens[warren.id] = warren;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const userWarren of data.value.data.userWarrens) {
|
||||||
|
users[userWarren.userId].warrens.push(userWarren);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
users: Object.values(users),
|
||||||
|
warrens,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
|
export function getAuthHeader(): ['authorization', string] | null {
|
||||||
|
const authSession = useAuthSession().value;
|
||||||
|
if (authSession == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['authorization', `${authSession.type} ${authSession.id}`];
|
||||||
|
}
|
||||||
export function getApiHeaders(
|
export function getApiHeaders(
|
||||||
includeAuth: boolean = true
|
includeAuth: boolean = true
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
|
|
||||||
if (includeAuth) {
|
if (includeAuth) {
|
||||||
const authSession = useAuthSession().value;
|
const header = getAuthHeader();
|
||||||
|
if (header != null) {
|
||||||
if (authSession != null) {
|
headers[header[0]] = header[1];
|
||||||
headers['authorization'] = `${authSession.type} ${authSession.id}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { toast } from 'vue-sonner';
|
|||||||
import type { DirectoryEntry } from '#shared/types';
|
import type { DirectoryEntry } from '#shared/types';
|
||||||
import type { ApiResponse } from '#shared/types/api';
|
import type { ApiResponse } from '#shared/types/api';
|
||||||
import type { Warren } from '#shared/types/warrens';
|
import type { Warren } from '#shared/types/warrens';
|
||||||
import { getApiHeaders } from '.';
|
import { getApiHeaders, getAuthHeader } from '.';
|
||||||
|
|
||||||
export async function getWarrens(): Promise<Record<string, Warren>> {
|
export async function getWarrens(): Promise<Record<string, Warren>> {
|
||||||
const { data, error } = await useFetch<ApiResponse<{ warrens: Warren[] }>>(
|
const { data, error } = await useFetch<ApiResponse<{ warrens: Warren[] }>>(
|
||||||
@@ -201,9 +201,9 @@ export async function uploadToWarren(
|
|||||||
body.append('files', file);
|
body.append('files', file);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = getApiHeaders();
|
const header = getAuthHeader();
|
||||||
for (const [key, value] of Object.entries(headers)) {
|
if (header != null) {
|
||||||
xhr.setRequestHeader(key, value);
|
xhr.setRequestHeader(header[0], header[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
xhr.send(body);
|
xhr.send(body);
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import z from 'zod';
|
import { boolean, object, string } from 'yup';
|
||||||
import { registerSchema } from './auth';
|
import { registerSchema } from './auth';
|
||||||
|
|
||||||
export const createUserSchema = registerSchema.extend({
|
export const createUserSchema = registerSchema.concat(
|
||||||
admin: z
|
object({
|
||||||
.boolean()
|
admin: boolean().default(false),
|
||||||
.default(false)
|
})
|
||||||
.prefault(() => false),
|
);
|
||||||
|
export const editUserSchema = object({
|
||||||
|
name: string().trim().min(1).required('required'),
|
||||||
|
email: string().email().trim().required('required'),
|
||||||
|
password: string()
|
||||||
|
.trim()
|
||||||
|
.min(12)
|
||||||
|
.max(32)
|
||||||
|
.transform((s: string) => (s.length > 0 ? s : undefined))
|
||||||
|
.optional(),
|
||||||
|
admin: boolean().required('required'),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
import z from 'zod';
|
import { object, string } from 'yup';
|
||||||
|
|
||||||
export const registerSchema = z.object({
|
export const registerSchema = object({
|
||||||
name: z.string('This field is required').trim().min(1),
|
name: string().trim().min(1).required('required'),
|
||||||
email: z
|
email: string().trim().email('Expected a valid email').required('required'),
|
||||||
.email({
|
password: string().trim().min(12).max(32).required('required'),
|
||||||
error: 'This field is required',
|
|
||||||
pattern: z.regexes.rfc5322Email,
|
|
||||||
})
|
|
||||||
.trim(),
|
|
||||||
password: z.string('This field is required').trim().min(12).max(32),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const loginSchema = z.object({
|
export const loginSchema = object({
|
||||||
email: z
|
email: string().trim().email('Expected a valid email').required('required'),
|
||||||
.email({
|
|
||||||
error: 'This field is required',
|
|
||||||
pattern: z.regexes.rfc5322Email,
|
|
||||||
})
|
|
||||||
.trim(),
|
|
||||||
// Don't include the min and max here to let bad actors waste their time
|
// Don't include the min and max here to let bad actors waste their time
|
||||||
password: z.string('This field is required').trim(),
|
password: string().trim().required('required'),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import tailwindcss from '@tailwindcss/vite';
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2025-05-15',
|
compatibilityDate: '2025-05-15',
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: false },
|
||||||
|
|
||||||
modules: [
|
modules: [
|
||||||
'@nuxt/eslint',
|
'@nuxt/eslint',
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@nuxt/test-utils": "3.19.2",
|
"@nuxt/test-utils": "3.19.2",
|
||||||
"@pinia/nuxt": "^0.11.1",
|
"@pinia/nuxt": "^0.11.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@vee-validate/yup": "^4.15.1",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/core": "^13.5.0",
|
"@vueuse/core": "^13.5.0",
|
||||||
"byte-size": "^9.0.1",
|
"byte-size": "^9.0.1",
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vue-sonner": "^2.0.1",
|
"vue-sonner": "^2.0.1",
|
||||||
"zod": "^4.0.5"
|
"yup": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/lucide": "^1.2.57",
|
"@iconify-json/lucide": "^1.2.57",
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ const adminStore = useAdminStore();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
<Card>
|
<Card class="overflow-hidden">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle
|
<CardTitle
|
||||||
><NuxtLink to="/admin/users">Users</NuxtLink></CardTitle
|
><NuxtLink to="/admin/users">Users</NuxtLink></CardTitle
|
||||||
@@ -20,16 +20,26 @@ const adminStore = useAdminStore();
|
|||||||
warrens</CardDescription
|
warrens</CardDescription
|
||||||
>
|
>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="max-h-64">
|
<CardContent class="max-h-64 overflow-hidden">
|
||||||
<ScrollArea class="h-full">
|
<ScrollArea class="h-full w-full overflow-hidden">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex w-full flex-col gap-2 overflow-hidden">
|
||||||
<AdminUserListing
|
<AdminUserListing
|
||||||
v-for="user in adminStore.users"
|
v-for="user in adminStore.resources.users"
|
||||||
:key="user.id"
|
:key="user.id"
|
||||||
:user
|
:user
|
||||||
class="group/user flex flex-row items-center justify-between gap-4"
|
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
|
<Button
|
||||||
|
class="m-1"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
@click="
|
||||||
|
() =>
|
||||||
|
adminStore.openEditUserDialog(user)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Icon name="lucide:pencil" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
class="m-1"
|
class="m-1"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
@@ -50,9 +60,9 @@ const adminStore = useAdminStore();
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
<div class="mt-4 flex flex-row">
|
<div class="mt-4 flex grow flex-row justify-end">
|
||||||
<Button @click="adminStore.openCreateUserDialog"
|
<Button @click="adminStore.openCreateUserDialog"
|
||||||
>Create user</Button
|
>Create</Button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
|
layout: 'admin',
|
||||||
middleware: ['is-admin'],
|
middleware: ['is-admin'],
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
|
layout: 'admin',
|
||||||
middleware: ['is-admin'],
|
middleware: ['is-admin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const adminStore = useAdminStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<p>/admin/users</p>
|
<div class="flex h-full w-full">
|
||||||
|
<ScrollArea class="h-full grow">
|
||||||
|
<div class="flex w-full flex-col gap-2">
|
||||||
|
<AdminUserListing
|
||||||
|
v-for="user in adminStore.resources.users"
|
||||||
|
:key="user.id"
|
||||||
|
:user
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
|
layout: 'admin',
|
||||||
middleware: ['is-admin'],
|
middleware: ['is-admin'],
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ import {
|
|||||||
CardContent,
|
CardContent,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import { toTypedSchema } from '@vee-validate/zod';
|
import { toTypedSchema } from '@vee-validate/yup';
|
||||||
import { useForm } from 'vee-validate';
|
import { useForm } from 'vee-validate';
|
||||||
import type z from 'zod';
|
|
||||||
import { loginUser } from '~/lib/api/auth/login';
|
import { loginUser } from '~/lib/api/auth/login';
|
||||||
import { loginSchema } from '~/lib/schemas/auth';
|
import { loginSchema } from '~/lib/schemas/auth';
|
||||||
|
|
||||||
@@ -29,23 +28,21 @@ const form = useForm({
|
|||||||
validationSchema: toTypedSchema(loginSchema),
|
validationSchema: toTypedSchema(loginSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit(
|
const onSubmit = form.handleSubmit(async (values) => {
|
||||||
async (values: z.output<typeof loginSchema>) => {
|
if (loggingIn.value) {
|
||||||
if (loggingIn.value) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loggingIn.value = true;
|
|
||||||
|
|
||||||
const { success } = await loginUser(values.email, values.password);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
await navigateTo({ path: '/' });
|
|
||||||
}
|
|
||||||
|
|
||||||
loggingIn.value = false;
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
loggingIn.value = true;
|
||||||
|
|
||||||
|
const { success } = await loginUser(values.email, values.password);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
await navigateTo({ path: '/' });
|
||||||
|
}
|
||||||
|
|
||||||
|
loggingIn.value = false;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -86,8 +83,8 @@ const onSubmit = form.handleSubmit(
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormMessage />
|
|
||||||
</FormField>
|
</FormField>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ import {
|
|||||||
CardContent,
|
CardContent,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import { toTypedSchema } from '@vee-validate/zod';
|
import { toTypedSchema } from '@vee-validate/yup';
|
||||||
import { useForm } from 'vee-validate';
|
import { useForm } from 'vee-validate';
|
||||||
import type z from 'zod';
|
|
||||||
import { registerUser } from '~/lib/api/auth/register';
|
import { registerUser } from '~/lib/api/auth/register';
|
||||||
import { registerSchema } from '~/lib/schemas/auth';
|
import { registerSchema } from '~/lib/schemas/auth';
|
||||||
|
|
||||||
@@ -27,24 +26,22 @@ const form = useForm({
|
|||||||
validationSchema: toTypedSchema(registerSchema),
|
validationSchema: toTypedSchema(registerSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit(
|
const onSubmit = form.handleSubmit(async (values) => {
|
||||||
async (values: z.output<typeof registerSchema>) => {
|
registering.value = true;
|
||||||
registering.value = true;
|
|
||||||
|
|
||||||
const { success } = await registerUser(
|
const { success } = await registerUser(
|
||||||
values.name,
|
values.name,
|
||||||
values.email,
|
values.email,
|
||||||
values.password
|
values.password
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
await navigateTo({ path: '/login' });
|
await navigateTo({ path: '/login' });
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
registering.value = false;
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
registering.value = false;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -71,8 +68,8 @@ const onSubmit = form.handleSubmit(
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormMessage />
|
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField v-slot="{ componentField }" name="email">
|
<FormField v-slot="{ componentField }" name="email">
|
||||||
@@ -102,8 +99,8 @@ const onSubmit = form.handleSubmit(
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormMessage />
|
|
||||||
</FormField>
|
</FormField>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
11
frontend/shared/types/admin.ts
Normal file
11
frontend/shared/types/admin.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { AuthUser } from './auth';
|
||||||
|
import type { UserWarren, Warren } from './warrens';
|
||||||
|
|
||||||
|
export type AdminResources = {
|
||||||
|
users: AuthUserWithWarrens[];
|
||||||
|
warrens: Record<string, Warren>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthUserWithWarrens = AuthUser & {
|
||||||
|
warrens: UserWarren[];
|
||||||
|
};
|
||||||
@@ -2,3 +2,14 @@ export type Warren = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UserWarren = {
|
||||||
|
userId: string;
|
||||||
|
warrenId: string;
|
||||||
|
canCreateChildren: boolean;
|
||||||
|
canListFiles: boolean;
|
||||||
|
canReadFiles: boolean;
|
||||||
|
canModifyFiles: boolean;
|
||||||
|
canDeleteFiles: boolean;
|
||||||
|
canDeleteWarren: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,30 +1,35 @@
|
|||||||
import type { AuthUser, AuthUserFields } from '#shared/types/auth';
|
import type { AuthUser } from '#shared/types/auth';
|
||||||
|
import type { AdminResources } from '~/shared/types/admin';
|
||||||
|
|
||||||
export const useAdminStore = defineStore('admin', {
|
export const useAdminStore = defineStore('admin', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
users: [] as AuthUser[],
|
resources: {
|
||||||
createUserDialog: null as { user: AuthUserFields } | null,
|
users: [],
|
||||||
|
warrens: {},
|
||||||
|
} as AdminResources,
|
||||||
|
createUserDialogOpen: false,
|
||||||
|
editUserDialog: null as { user: AuthUser } | null,
|
||||||
deleteUserDialog: null as { user: AuthUser } | null,
|
deleteUserDialog: null as { user: AuthUser } | null,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
openCreateUserDialog() {
|
openCreateUserDialog() {
|
||||||
this.createUserDialog = {
|
this.createUserDialogOpen = true;
|
||||||
user: {
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
admin: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
closeCreateUserDialog() {
|
closeCreateUserDialog() {
|
||||||
this.createUserDialog = null;
|
this.createUserDialogOpen = false;
|
||||||
|
},
|
||||||
|
openEditUserDialog(user: AuthUser) {
|
||||||
|
this.editUserDialog = { user };
|
||||||
|
},
|
||||||
|
closeEditUserDialog() {
|
||||||
|
this.editUserDialog = null;
|
||||||
},
|
},
|
||||||
openDeleteUserDialog(user: AuthUser) {
|
openDeleteUserDialog(user: AuthUser) {
|
||||||
this.deleteUserDialog = {
|
this.deleteUserDialog = {
|
||||||
user: user,
|
user: user,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
clearDeleteUserDialog() {
|
closeDeleteUserDialog() {
|
||||||
this.deleteUserDialog = null;
|
this.deleteUserDialog = null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user