diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2d1b5a1..4512bb3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,7 +15,7 @@ // "features": {}, // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [5432], + "forwardPorts": [5432], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "rustc --version", diff --git a/backend/src/main.rs b/backend/src/main.rs index bda3347..280adeb 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -16,7 +16,7 @@ use error_handling::AppError; use middlewares::inject_user_data; use google_oauth::{login, logout, google_auth_return}; use routes::{dashboard, index, about, profile, user_profile, useradmin}; -use user::UserData; +use user::{add_user_role, delete_user_role, UserData}; #[derive(Clone)] pub struct AppState { @@ -50,6 +50,8 @@ async fn main() { .route("/profile", get(profile)) .route("/useradmin", get(useradmin)) .route("/users/:user_id", get(user_profile)) + .route("/roles/:user_id/:role_id/add", get(add_user_role)) + .route("/roles/:user_role_id/delete", get(delete_user_role)) .nest_service("/assets", ServeDir::new("templates/assets") .fallback(get_service(ServeDir::new("templates/assets")))) .route("/", get(index)) diff --git a/backend/src/middlewares.rs b/backend/src/middlewares.rs index 201c4d3..8d81472 100644 --- a/backend/src/middlewares.rs +++ b/backend/src/middlewares.rs @@ -85,7 +85,7 @@ pub async fn is_authorized(path: &str, user_data: Option, db_pool: Sql // loop through path to find a permission let mut remaining_path = Path::new(path); loop { - let query: Result<(String,), _> = sqlx::query_as(r#"select r.item from role_permissions r join user_roles ur on ur.role_id = r.role_id left join users u on u.id = ur.user_id where item = ?"#) + let query: Result<(String,), _> = sqlx::query_as(r#"select r.item from role_permissions r where item = ?"#) .bind(remaining_path.to_str().unwrap()) .fetch_one(&db_pool) .await; diff --git a/backend/src/routes.rs b/backend/src/routes.rs index c1a513a..12bf930 100644 --- a/backend/src/routes.rs +++ b/backend/src/routes.rs @@ -3,7 +3,7 @@ use axum::{extract::{Path, State}, response::{Html, IntoResponse, Redirect}, Ext use http::StatusCode; use sqlx::SqlitePool; -use crate::{middlewares::is_authorized, user::{get_user_roles, get_user_roles_display}, UserData}; +use crate::{middlewares::is_authorized, user::{get_other_roles_display, get_user_roles_display}, UserData}; #[derive(Template)] #[template(path = "profile.html")] @@ -19,7 +19,8 @@ struct UserProfileTemplate { logged_in: bool, name: String, user: UserData, - user_roles: Vec + user_roles: Vec, + non_user_roles: Vec } struct HtmlTemplate(T); @@ -136,8 +137,11 @@ pub async fn profile( // Get user roles let user_roles = get_user_roles_display(user_id, &db_pool.clone()).await; + // Get roles user does not have + let non_user_roles = get_other_roles_display(user_id, &db_pool.clone()).await; + // Create the profile template. - let template = UserProfileTemplate { logged_in, name, user: user, user_roles }; + let template = UserProfileTemplate { logged_in, name, user: user, user_roles , non_user_roles}; return HtmlTemplate(template).into_response() } else { Redirect::to("/").into_response() diff --git a/backend/src/user.rs b/backend/src/user.rs index 9da1c6f..3dbbea9 100644 --- a/backend/src/user.rs +++ b/backend/src/user.rs @@ -1,7 +1,16 @@ +use askama_axum::IntoResponse; +use axum::{ + extract::{Path, State}, + response::Redirect, + Extension, +}; +use chrono::Utc; ///User related structs and functions use serde::{Deserialize, Serialize}; use sqlx::{prelude::FromRow, SqlitePool}; +use crate::middlewares::is_authorized; + #[derive(Default, Clone, Debug, FromRow, Serialize, Deserialize)] pub struct UserData { pub id: i64, @@ -44,9 +53,12 @@ pub struct UserRolesDisplay { pub created_by: i64, pub updated_at: i64, pub updated_by: i64, - pub role: String, + pub user_id: i64, + pub user_name: String, + pub role_id: i64, + pub role_name: String, } - +/* pub async fn get_user_roles(user_id: i64, db_pool: &SqlitePool) -> Vec { // Get user roles let user_roles = sqlx::query_as( @@ -58,12 +70,12 @@ pub async fn get_user_roles(user_id: i64, db_pool: &SqlitePool) -> Vec Vec { // Get user roles let user_roles = sqlx::query_as( - r#"select r.id, r.name as role, r.created_at, r.created_by, r.updated_at, r.updated_by from roles r join user_roles ur on ur.role_id = r.id WHERE ur.user_id = ?"# + r#"select ur.id, u.id as user_id, u.name as user_name, r.id as role_id, r.name as role_name, r.created_at, r.created_by, r.updated_at, r.updated_by from roles r join user_roles ur on ur.role_id = r.id join users u on u.id = ur.user_id WHERE ur.user_id = ?"# ) .bind(user_id) .fetch_all(db_pool) @@ -71,4 +83,57 @@ pub async fn get_user_roles_display(user_id: i64, db_pool: &SqlitePool) -> Vec Vec { + // Get roles user does not have + let user_roles = sqlx::query_as( + r#"select 0 as id, r.created_at, r.created_by, r.updated_at, r.updated_by, ? as user_id, '' as user_name, r.id as role_id, r.name as role_name from roles r where r.id not in (select ur.role_id from user_roles ur where ur.user_id = ?)"# + ) + .bind(user_id.clone()) + .bind(user_id) + .fetch_all(db_pool) + .await + .unwrap(); + + user_roles +} + +pub async fn add_user_role( + Path((user_id, role_id)): Path<(i64, i64)>, + State(db_pool): State, + Extension(user_data): Extension>, +) -> impl IntoResponse { + if is_authorized("/roles", user_data.clone(), db_pool.clone()).await { + let now = Utc::now().timestamp(); + + println!("Adding role {} to user {}", role_id, user_id); + + sqlx::query("INSERT INTO user_roles (created_at, created_by, updated_at, updated_by, user_id, role_id) VALUES (?, ?, ?, ?, ?, ?)") + .bind(now)// Created now + .bind(user_data.as_ref().unwrap().id)// Created by current user + .bind(now) // Updated now + .bind(user_data.as_ref().unwrap().id) // Updated by current user + .bind(user_id) + .bind(role_id) + .execute(&db_pool) + .await + .unwrap(); + } + Redirect::to("/").into_response() +} + +pub async fn delete_user_role( + Path(user_role_id): Path, + State(db_pool): State, + Extension(user_data): Extension>, +) -> impl IntoResponse { + if is_authorized("/roles", user_data, db_pool.clone()).await { + sqlx::query("DELETE FROM user_roles WHERE id = ?") + .bind(user_role_id) + .execute(&db_pool) + .await + .unwrap(); + } + Redirect::to("/").into_response() +} diff --git a/backend/templates/user.html b/backend/templates/user.html index 4243b52..bac9670 100644 --- a/backend/templates/user.html +++ b/backend/templates/user.html @@ -8,18 +8,29 @@ Family name: {{ user.family_name }}
Your email address: {{ user.email }}

User Roles

- + + +
+ {% for user_role in user_roles %} - - + + + + + {% endfor %} + {% for non_user_role in non_user_roles %} + + + + {% endfor %} diff --git a/backend/templates/useradmin.html b/backend/templates/useradmin.html index 9d7ca93..8fb0d04 100644 --- a/backend/templates/useradmin.html +++ b/backend/templates/useradmin.html @@ -2,7 +2,7 @@ {% block title %}User Administration{% endblock %} {% block center %}

Users

-
ID Name
{{ user_role.id }}{{ user_role.role }}{{ user_role.id }}{{ user_role.role_name }}Delete
New{{ non_user_role.role_name }}Add
+
ID