From 7b8863174f9834f0985d3351523ef3a0942772d5 Mon Sep 17 00:00:00 2001 From: Hector van der Aa Date: Mon, 1 Dec 2025 00:08:58 +0100 Subject: [PATCH] Basic user permissions UNTESTED --- src/backend/migrations/0002_create_users.sql | 2 +- src/backend/migrations/0003_create_perms.sql | 6 ++ src/backend/src/core/user_routines.rs | 78 +++++++++++++++++++- src/backend/src/domain/mod.rs | 1 + src/backend/src/domain/user.rs | 12 +++ src/backend/src/domain/user_prems.rs | 35 +++++++++ src/backend/src/infra/db/mod.rs | 1 + src/backend/src/infra/db/perms.rs | 55 ++++++++++++++ src/backend/src/infra/db/user.rs | 23 +++++- src/backend/src/router/mod.rs | 3 + src/backend/src/router/user_routes.rs | 16 +++- 11 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 src/backend/migrations/0003_create_perms.sql create mode 100644 src/backend/src/domain/user_prems.rs create mode 100644 src/backend/src/infra/db/perms.rs diff --git a/src/backend/migrations/0002_create_users.sql b/src/backend/migrations/0002_create_users.sql index 8e4c10a..0287c4a 100644 --- a/src/backend/migrations/0002_create_users.sql +++ b/src/backend/migrations/0002_create_users.sql @@ -1,4 +1,4 @@ -CREATE TABLE users ( +CREATE TABLE user_permissions ( uuid UUID PRIMARY KEY, username VARCHAR NOT NULL UNIQUE, email VARCHAR UNIQUE, diff --git a/src/backend/migrations/0003_create_perms.sql b/src/backend/migrations/0003_create_perms.sql new file mode 100644 index 0000000..4ab0d64 --- /dev/null +++ b/src/backend/migrations/0003_create_perms.sql @@ -0,0 +1,6 @@ +CREATE TABLE users ( + uuid UUID PRIMARY KEY, + root BOOL NOT NULL, + manage_users BOOL NOT NULL, + login BOOL NOT NULL +); diff --git a/src/backend/src/core/user_routines.rs b/src/backend/src/core/user_routines.rs index 9e302a1..c683b74 100644 --- a/src/backend/src/core/user_routines.rs +++ b/src/backend/src/core/user_routines.rs @@ -1,10 +1,19 @@ -use crate::{infra::db, prelude::*}; +use crate::{ + domain::{ + user::{self, InternalUser}, + user_prems::UserPermissions, + }, + infra::db, + prelude::*, +}; use std::sync::Arc; -use anyhow::Result; use axum::{Json, http::StatusCode}; +use uuid::Uuid; use validator::Validate; +use anyhow::{Context, anyhow}; + use crate::{ domain::user::{InternalNewUser, NewUser, User}, state::AppState, @@ -39,3 +48,68 @@ pub async fn get_all(state: Arc) -> Result, StatusCode> { Ok(users) } + +pub async fn get_safe_by_uuid( + state: Arc, + uuid: Uuid, +) -> Result, StatusCode> { + let user = db::user::get_safe_by_uuid(&state.db_pool, uuid.clone()) + .await + .map_err(|e| { + error!("Failed to fetch user: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + Ok(user) +} + +pub async fn get_by_uuid(state: Arc, uuid: Uuid) -> anyhow::Result> { + let mut user = match db::user::get_by_uuid(&state.db_pool, uuid) + .await + .context("failed to fetch user by uuid")? + { + Some(u) => u, + None => return Ok(None), + }; + + let perms_exist = db::perms::exists_by_uuid(&state.db_pool, user.uuid) + .await + .context("failed to check if user permissions exist")?; + + if perms_exist { + if let Some(perms) = db::perms::get_by_uuid(&state.db_pool, user.uuid) + .await + .context("failed to fetch user permissions")? + { + user.attach_permissions(perms); + } + } + + let user = user; + + Ok(Some(user)) +} + +pub async fn set_perms( + state: Arc, + user_perms: UserPermissions, +) -> Result<(), StatusCode> { + let exists = db::user::exists_by_uuid(&state.db_pool, user_perms.uuid) + .await + .map_err(|e| { + error!("Failed to verify user exists: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + if !exists { + return Err(StatusCode::BAD_REQUEST); + } + + db::perms::create(&state.db_pool, user_perms) + .await + .map_err(|e| { + error!("Failed to create user permissions entry: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + Ok(()) +} diff --git a/src/backend/src/domain/mod.rs b/src/backend/src/domain/mod.rs index 634c158..4cf35ef 100644 --- a/src/backend/src/domain/mod.rs +++ b/src/backend/src/domain/mod.rs @@ -1,2 +1,3 @@ pub mod user; +pub mod user_prems; pub mod validation; diff --git a/src/backend/src/domain/user.rs b/src/backend/src/domain/user.rs index a228f3e..c599d14 100644 --- a/src/backend/src/domain/user.rs +++ b/src/backend/src/domain/user.rs @@ -8,6 +8,7 @@ use sqlx::prelude::FromRow; use uuid::Uuid; use validator::Validate; +use crate::domain::user_prems::{MemberUserPermissions, UserPermissions}; use crate::domain::validation; use crate::auth; @@ -27,6 +28,7 @@ pub struct NewUser { first_name: Option, #[validate(length(min = 1, max = 64))] last_name: Option, + permissions: Option, } #[derive(Debug, Clone, Deserialize)] @@ -37,6 +39,7 @@ pub struct InternalNewUser { pub password_hash: String, pub first_name: Option, pub last_name: Option, + pub permissions: Option, } #[derive(Debug, Clone, Deserialize, FromRow)] @@ -47,6 +50,8 @@ pub struct InternalUser { pub password_hash: String, pub first_name: Option, pub last_name: Option, + #[sqlx(skip)] + pub permissions: Option, } #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] @@ -76,6 +81,7 @@ impl TryFrom for InternalNewUser { password_hash: password_hash, first_name: value.first_name, last_name: value.last_name, + permissions: value.permissions.clone(), }) } } @@ -99,3 +105,9 @@ impl Display for UserConversionError { } } } + +impl InternalUser { + pub fn attach_permissions(&mut self, permissions: UserPermissions) { + self.permissions = Some(MemberUserPermissions::from(permissions)); + } +} diff --git a/src/backend/src/domain/user_prems.rs b/src/backend/src/domain/user_prems.rs new file mode 100644 index 0000000..08155b7 --- /dev/null +++ b/src/backend/src/domain/user_prems.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, de::value}; +use sqlx::prelude::FromRow; +use uuid::Uuid; + +#[derive(Debug)] +pub enum UserActions { + Root, + ManageUsers, + Login, +} + +#[derive(Debug, Clone, Deserialize, FromRow)] +pub struct UserPermissions { + pub uuid: Uuid, + pub root: bool, + pub manage_users: bool, + pub login: bool, +} + +#[derive(Debug, Clone, Deserialize, FromRow)] +pub struct MemberUserPermissions { + pub root: bool, + pub manage_users: bool, + pub login: bool, +} + +impl From for MemberUserPermissions { + fn from(value: UserPermissions) -> Self { + Self { + root: value.root, + manage_users: value.manage_users, + login: value.login, + } + } +} diff --git a/src/backend/src/infra/db/mod.rs b/src/backend/src/infra/db/mod.rs index 7f72eba..5948dd7 100644 --- a/src/backend/src/infra/db/mod.rs +++ b/src/backend/src/infra/db/mod.rs @@ -1,3 +1,4 @@ +pub mod perms; pub mod user; use std::time::Duration; diff --git a/src/backend/src/infra/db/perms.rs b/src/backend/src/infra/db/perms.rs new file mode 100644 index 0000000..48bae1f --- /dev/null +++ b/src/backend/src/infra/db/perms.rs @@ -0,0 +1,55 @@ +use anyhow::Result; +use sqlx::PgPool; +use uuid::Uuid; + +use crate::domain::{user::User, user_prems::UserPermissions}; + +pub async fn create(pool: &PgPool, new_perms: UserPermissions) -> Result { + let perms = sqlx::query_as::<_, UserPermissions>( + r#" + INSERT INTO user_permissions (uuid, root, manage_users, login) + VALUES ($1, $2, $3, $4) + RETURNING uuid, root, manage_users, login + "#, + ) + .bind(new_perms.uuid) + .bind(new_perms.root) + .bind(new_perms.manage_users) + .bind(new_perms.login) + .fetch_one(pool) + .await?; + + Ok(perms) +} + +pub async fn get_by_uuid(pool: &PgPool, uuid: Uuid) -> Result> { + let perms = sqlx::query_as::<_, UserPermissions>( + r#" + SELECT uuid, root, manage_users, login + FROM user_permissions + WHERE uuid = $1 + "#, + ) + .bind(uuid) + .fetch_optional(pool) + .await?; + + Ok(perms) +} + +pub async fn exists_by_uuid(pool: &PgPool, uuid: Uuid) -> Result { + let exists = sqlx::query_scalar::<_, bool>( + r#" + SELECT EXISTS( + SELECT 1 + FROM user_permissions + WHERE uuis = $1 + ) + "#, + ) + .bind(uuid) + .fetch_one(pool) + .await?; + + Ok(exists) +} diff --git a/src/backend/src/infra/db/user.rs b/src/backend/src/infra/db/user.rs index 2040e9e..64f6557 100644 --- a/src/backend/src/infra/db/user.rs +++ b/src/backend/src/infra/db/user.rs @@ -2,7 +2,7 @@ use crate::{ domain::user::{InternalNewUser, InternalUser, User}, prelude::*, }; -use anyhow::{Ok, Result, anyhow}; +use anyhow::Result; use sqlx::PgPool; use uuid::Uuid; @@ -29,7 +29,7 @@ pub async fn create(pool: &PgPool, new_user: InternalNewUser) -> Result Result> { let user = sqlx::query_as::<_, InternalUser>( r#" - SELECT uuid, username, email, password_hash, first_name, last_name FROM users WHERE uuid = $ + SELECT uuid, username, email, password_hash, first_name, last_name FROM users WHERE uuid = $1 "#, ) .bind(uuid) @@ -42,7 +42,7 @@ pub async fn get_by_uuid(pool: &PgPool, uuid: Uuid) -> Result Result> { let user = sqlx::query_as::<_, User>( r#" - SELECT uuid, username, email, first_name, last_name FROM users WHERE uuid = $ + SELECT uuid, username, email, first_name, last_name FROM users WHERE uuid = $1 "#, ) .bind(uuid) @@ -79,3 +79,20 @@ pub async fn get_safe_all(pool: &PgPool) -> Result> { Ok(users) } + +pub async fn exists_by_uuid(pool: &PgPool, uuid: Uuid) -> Result { + let exists = sqlx::query_scalar::<_, bool>( + r#" + SELECT EXISTS( + SELECT 1 + FROM users + WHERE uuid = $1 + ) + "#, + ) + .bind(uuid) + .fetch_one(pool) + .await?; + + Ok(exists) +} diff --git a/src/backend/src/router/mod.rs b/src/backend/src/router/mod.rs index dd18cf1..5006c89 100644 --- a/src/backend/src/router/mod.rs +++ b/src/backend/src/router/mod.rs @@ -31,6 +31,9 @@ pub async fn init_router(app_state: Arc) -> Router { .layer(ServiceBuilder::new().layer(middleware::cors())) .with_state(app_state.clone()), ) + .route("/api/users/{uuid}", get(user_routes::get_uuid)) + .layer(ServiceBuilder::new().layer(middleware::cors())) + .with_state(app_state.clone()) } async fn ping() -> Result, StatusCode> { diff --git a/src/backend/src/router/user_routes.rs b/src/backend/src/router/user_routes.rs index 65a6b43..ee41310 100644 --- a/src/backend/src/router/user_routes.rs +++ b/src/backend/src/router/user_routes.rs @@ -6,7 +6,13 @@ use crate::{ domain::user::{InternalNewUser, NewUser, User}, state::AppState, }; -use axum::{Json, extract::State, http::StatusCode}; +use anyhow::Result; +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, +}; +use uuid::Uuid; pub async fn create( State(state): State>, @@ -20,3 +26,11 @@ pub async fn get_all(State(state): State>) -> Result>, + Path(uuid): Path, +) -> Result>, StatusCode> { + let user = core::user_routines::get_safe_by_uuid(state, uuid).await?; + Ok(Json(user)) +}