Basic user permissions UNTESTED
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
CREATE TABLE users (
|
CREATE TABLE user_permissions (
|
||||||
uuid UUID PRIMARY KEY,
|
uuid UUID PRIMARY KEY,
|
||||||
username VARCHAR NOT NULL UNIQUE,
|
username VARCHAR NOT NULL UNIQUE,
|
||||||
email VARCHAR UNIQUE,
|
email VARCHAR UNIQUE,
|
||||||
|
|||||||
6
src/backend/migrations/0003_create_perms.sql
Normal file
6
src/backend/migrations/0003_create_perms.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE users (
|
||||||
|
uuid UUID PRIMARY KEY,
|
||||||
|
root BOOL NOT NULL,
|
||||||
|
manage_users BOOL NOT NULL,
|
||||||
|
login BOOL NOT NULL
|
||||||
|
);
|
||||||
@@ -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 std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use axum::{Json, http::StatusCode};
|
use axum::{Json, http::StatusCode};
|
||||||
|
use uuid::Uuid;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
|
use anyhow::{Context, anyhow};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
domain::user::{InternalNewUser, NewUser, User},
|
domain::user::{InternalNewUser, NewUser, User},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
@@ -39,3 +48,68 @@ pub async fn get_all(state: Arc<AppState>) -> Result<Vec<User>, StatusCode> {
|
|||||||
|
|
||||||
Ok(users)
|
Ok(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_safe_by_uuid(
|
||||||
|
state: Arc<AppState>,
|
||||||
|
uuid: Uuid,
|
||||||
|
) -> Result<Option<User>, 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<AppState>, uuid: Uuid) -> anyhow::Result<Option<InternalUser>> {
|
||||||
|
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<AppState>,
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod user;
|
pub mod user;
|
||||||
|
pub mod user_prems;
|
||||||
pub mod validation;
|
pub mod validation;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use sqlx::prelude::FromRow;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
|
use crate::domain::user_prems::{MemberUserPermissions, UserPermissions};
|
||||||
use crate::domain::validation;
|
use crate::domain::validation;
|
||||||
|
|
||||||
use crate::auth;
|
use crate::auth;
|
||||||
@@ -27,6 +28,7 @@ pub struct NewUser {
|
|||||||
first_name: Option<String>,
|
first_name: Option<String>,
|
||||||
#[validate(length(min = 1, max = 64))]
|
#[validate(length(min = 1, max = 64))]
|
||||||
last_name: Option<String>,
|
last_name: Option<String>,
|
||||||
|
permissions: Option<MemberUserPermissions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
@@ -37,6 +39,7 @@ pub struct InternalNewUser {
|
|||||||
pub password_hash: String,
|
pub password_hash: String,
|
||||||
pub first_name: Option<String>,
|
pub first_name: Option<String>,
|
||||||
pub last_name: Option<String>,
|
pub last_name: Option<String>,
|
||||||
|
pub permissions: Option<MemberUserPermissions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Deserialize, FromRow)]
|
||||||
@@ -47,6 +50,8 @@ pub struct InternalUser {
|
|||||||
pub password_hash: String,
|
pub password_hash: String,
|
||||||
pub first_name: Option<String>,
|
pub first_name: Option<String>,
|
||||||
pub last_name: Option<String>,
|
pub last_name: Option<String>,
|
||||||
|
#[sqlx(skip)]
|
||||||
|
pub permissions: Option<MemberUserPermissions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
@@ -76,6 +81,7 @@ impl TryFrom<NewUser> for InternalNewUser {
|
|||||||
password_hash: password_hash,
|
password_hash: password_hash,
|
||||||
first_name: value.first_name,
|
first_name: value.first_name,
|
||||||
last_name: value.last_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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
35
src/backend/src/domain/user_prems.rs
Normal file
35
src/backend/src/domain/user_prems.rs
Normal file
@@ -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<UserPermissions> for MemberUserPermissions {
|
||||||
|
fn from(value: UserPermissions) -> Self {
|
||||||
|
Self {
|
||||||
|
root: value.root,
|
||||||
|
manage_users: value.manage_users,
|
||||||
|
login: value.login,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod perms;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|||||||
55
src/backend/src/infra/db/perms.rs
Normal file
55
src/backend/src/infra/db/perms.rs
Normal file
@@ -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<UserPermissions> {
|
||||||
|
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<Option<UserPermissions>> {
|
||||||
|
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<bool> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ use crate::{
|
|||||||
domain::user::{InternalNewUser, InternalUser, User},
|
domain::user::{InternalNewUser, InternalUser, User},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
use anyhow::{Ok, Result, anyhow};
|
use anyhow::Result;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ pub async fn create(pool: &PgPool, new_user: InternalNewUser) -> Result<Internal
|
|||||||
pub async fn get_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<Option<InternalUser>> {
|
pub async fn get_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<Option<InternalUser>> {
|
||||||
let user = sqlx::query_as::<_, InternalUser>(
|
let user = sqlx::query_as::<_, InternalUser>(
|
||||||
r#"
|
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)
|
.bind(uuid)
|
||||||
@@ -42,7 +42,7 @@ pub async fn get_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<Option<InternalUse
|
|||||||
pub async fn get_safe_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<Option<User>> {
|
pub async fn get_safe_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<Option<User>> {
|
||||||
let user = sqlx::query_as::<_, User>(
|
let user = sqlx::query_as::<_, User>(
|
||||||
r#"
|
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)
|
.bind(uuid)
|
||||||
@@ -79,3 +79,20 @@ pub async fn get_safe_all(pool: &PgPool) -> Result<Vec<User>> {
|
|||||||
|
|
||||||
Ok(users)
|
Ok(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn exists_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<bool> {
|
||||||
|
let exists = sqlx::query_scalar::<_, bool>(
|
||||||
|
r#"
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM users
|
||||||
|
WHERE uuid = $1
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(uuid)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(exists)
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ pub async fn init_router(app_state: Arc<AppState>) -> Router {
|
|||||||
.layer(ServiceBuilder::new().layer(middleware::cors()))
|
.layer(ServiceBuilder::new().layer(middleware::cors()))
|
||||||
.with_state(app_state.clone()),
|
.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<Json<Value>, StatusCode> {
|
async fn ping() -> Result<Json<Value>, StatusCode> {
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ use crate::{
|
|||||||
domain::user::{InternalNewUser, NewUser, User},
|
domain::user::{InternalNewUser, NewUser, User},
|
||||||
state::AppState,
|
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(
|
pub async fn create(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
@@ -20,3 +26,11 @@ pub async fn get_all(State(state): State<Arc<AppState>>) -> Result<Json<Vec<User
|
|||||||
let users = core::user_routines::get_all(state).await?;
|
let users = core::user_routines::get_all(state).await?;
|
||||||
Ok(Json(users))
|
Ok(Json(users))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_uuid(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path(uuid): Path<Uuid>,
|
||||||
|
) -> Result<Json<Option<User>>, StatusCode> {
|
||||||
|
let user = core::user_routines::get_safe_by_uuid(state, uuid).await?;
|
||||||
|
Ok(Json(user))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user