From e7d0780b1befa805520488e3ac732f0b144106e7 Mon Sep 17 00:00:00 2001 From: Chris Jean-Marie Date: Mon, 7 Oct 2024 02:51:14 +0000 Subject: [PATCH] Convert datetime to integer Add roles to admin user profile screen --- .../20240926181906_initial_setup.up.sql | 25 -------- .../20240926235210_role_tables.up.sql | 23 -------- .../20240929223122_initial_roles.up.sql | 8 --- ... => 20241006175727_initial_setup.down.sql} | 0 .../20241006175727_initial_setup.up.sql | 58 +++++++++++++++++++ ... => 20241006211932_initial_roles.down.sql} | 0 .../20241006211932_initial_roles.up.sql | 6 ++ ...l => 20241007024816_initial_data.down.sql} | 0 .../20241007024816_initial_data.up.sql | 13 +++++ backend/src/google_oauth.rs | 23 +++++--- backend/src/main.rs | 20 ++----- backend/src/middlewares.rs | 4 ++ backend/src/routes.rs | 36 ++++++++---- backend/src/user.rs | 51 ++++++++++++++++ backend/templates/profile.html | 4 +- backend/templates/user.html | 27 +++++++++ 16 files changed, 208 insertions(+), 90 deletions(-) delete mode 100644 backend/migrations/20240926181906_initial_setup.up.sql delete mode 100644 backend/migrations/20240926235210_role_tables.up.sql delete mode 100644 backend/migrations/20240929223122_initial_roles.up.sql rename backend/migrations/{20240926181906_initial_setup.down.sql => 20241006175727_initial_setup.down.sql} (100%) create mode 100644 backend/migrations/20241006175727_initial_setup.up.sql rename backend/migrations/{20240926235210_role_tables.down.sql => 20241006211932_initial_roles.down.sql} (100%) create mode 100644 backend/migrations/20241006211932_initial_roles.up.sql rename backend/migrations/{20240929223122_initial_roles.down.sql => 20241007024816_initial_data.down.sql} (100%) create mode 100644 backend/migrations/20241007024816_initial_data.up.sql create mode 100644 backend/src/user.rs create mode 100644 backend/templates/user.html diff --git a/backend/migrations/20240926181906_initial_setup.up.sql b/backend/migrations/20240926181906_initial_setup.up.sql deleted file mode 100644 index f1d5474..0000000 --- a/backend/migrations/20240926181906_initial_setup.up.sql +++ /dev/null @@ -1,25 +0,0 @@ --- Add up migration script here -CREATE TABLE "oauth2_state_storage" ( - "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, - "csrf_state" text NOT NULL, - "pkce_code_verifier" text NOT NULL, - "return_url" text NOT NULL -); - -CREATE TABLE "user_sessions" ( - "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, - "user_id" integer NOT NULL, - "session_token_p1" text NOT NULL, - "session_token_p2" text NOT NULL, - "created_at" integer NOT NULL, - "expires_at" integer NOT NULL -); - -CREATE TABLE "users" ( - "id" integer NOT NULL, - "email" text NOT NULL UNIQUE, - "name" text NOT NULL, - "family_name" text NOT NULL, - "given_name" text NOT NULL, - PRIMARY KEY("id" AUTOINCREMENT) -); diff --git a/backend/migrations/20240926235210_role_tables.up.sql b/backend/migrations/20240926235210_role_tables.up.sql deleted file mode 100644 index e3015f9..0000000 --- a/backend/migrations/20240926235210_role_tables.up.sql +++ /dev/null @@ -1,23 +0,0 @@ --- Add up migration script here - -CREATE TABLE IF NOT EXISTS roles ( - "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, - "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "name" TEXT NOT NULL, - "description" TEXT -); - -CREATE TABLE IF NOT EXISTS user_roles ( - "user_id" integer NOT NULL, - "role_id" integer NOT NULL, - "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -create TABLE IF NOT EXISTS role_permissions ( - "role_id" integer NOT NULL, - "item" text NOT NULL, - "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -); diff --git a/backend/migrations/20240929223122_initial_roles.up.sql b/backend/migrations/20240929223122_initial_roles.up.sql deleted file mode 100644 index a11ec9f..0000000 --- a/backend/migrations/20240929223122_initial_roles.up.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Add up migration script here -INSERT INTO "main"."roles" ("id", "created_at", "updated_at", "name", "description") VALUES ('1', '2024-09-27 00:58:54', '2024-09-27 00:58:54', 'public', 'Users with only anonymous access'); -INSERT INTO "main"."roles" ("id", "created_at", "updated_at", "name", "description") VALUES ('2', '2024-09-27 00:59:24', '2024-09-27 00:59:24', 'normal', 'Users with no elevated privileges'); -INSERT INTO "main"."roles" ("id", "created_at", "updated_at", "name", "description") VALUES ('3', '2024-09-27 01:00:16', '2024-09-27 01:00:16', 'editor', 'Users with basic elevated privileges'); -INSERT INTO "main"."roles" ("id", "created_at", "updated_at", "name", "description") VALUES ('4', '2024-09-27 01:00:53', '2024-09-27 01:00:53', 'admin', 'Users with full administrative privileges'); - -INSERT INTO "main"."role_permissions" ("role_id", "item", "created_at", "updated_at") VALUES ('4', '/useradmin', '2024-09-27 01:01:59', '2024-09-27 01:01:59'); -INSERT INTO "main"."role_permissions" ("role_id", "item", "created_at", "updated_at") VALUES ('4', '/users', '2024-09-28 12:14:49', '2024-09-28 12:14:49'); \ No newline at end of file diff --git a/backend/migrations/20240926181906_initial_setup.down.sql b/backend/migrations/20241006175727_initial_setup.down.sql similarity index 100% rename from backend/migrations/20240926181906_initial_setup.down.sql rename to backend/migrations/20241006175727_initial_setup.down.sql diff --git a/backend/migrations/20241006175727_initial_setup.up.sql b/backend/migrations/20241006175727_initial_setup.up.sql new file mode 100644 index 0000000..87d36ea --- /dev/null +++ b/backend/migrations/20241006175727_initial_setup.up.sql @@ -0,0 +1,58 @@ +-- Add up migration script here +CREATE TABLE "oauth2_state_storage" ( + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, + "csrf_state" text NOT NULL, + "pkce_code_verifier" text NOT NULL, + "return_url" text NOT NULL +); + +CREATE TABLE "user_sessions" ( + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, + "user_id" integer NOT NULL, + "session_token_p1" text NOT NULL, + "session_token_p2" text NOT NULL, + "created_at" integer NOT NULL, + "expires_at" integer NOT NULL +); + +CREATE TABLE "users" ( + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, + "created_at" integer NOT NULL, + "created_by" integer NOT NULL, + "updated_at" integer NOT NULL, + "updated_by" integer NOT NULL, + "email" text NOT NULL UNIQUE, + "name" text NOT NULL, + "family_name" text NOT NULL, + "given_name" text NOT NULL +); + +CREATE TABLE IF NOT EXISTS roles ( + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, + "created_at" integer NOT NULL, + "created_by" integer NOT NULL, + "updated_at" integer NOT NULL, + "updated_by" integer NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT +); + +CREATE TABLE IF NOT EXISTS user_roles ( + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, + "created_at" integer NOT NULL, + "created_by" integer NOT NULL, + "updated_at" integer NOT NULL, + "updated_by" integer NOT NULL, + "user_id" integer NOT NULL, + "role_id" integer NOT NULL +); + +create TABLE IF NOT EXISTS role_permissions ( + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, + "created_at" integer NOT NULL, + "created_by" integer NOT NULL, + "updated_at" integer NOT NULL, + "updated_by" integer NOT NULL, + "role_id" integer NOT NULL, + "item" text NOT NULL +); diff --git a/backend/migrations/20240926235210_role_tables.down.sql b/backend/migrations/20241006211932_initial_roles.down.sql similarity index 100% rename from backend/migrations/20240926235210_role_tables.down.sql rename to backend/migrations/20241006211932_initial_roles.down.sql diff --git a/backend/migrations/20241006211932_initial_roles.up.sql b/backend/migrations/20241006211932_initial_roles.up.sql new file mode 100644 index 0000000..3273d6d --- /dev/null +++ b/backend/migrations/20241006211932_initial_roles.up.sql @@ -0,0 +1,6 @@ +-- Add up migration script here +INSERT INTO "main"."roles" ("id", "created_at", "created_by", "updated_at", "updated_by", "name", "description") VALUES ('1', '0', '0', '0', '0', 'public', 'Users with only anonymous access'); +INSERT INTO "main"."roles" ("id", "created_at", "created_by", "updated_at", "updated_by", "name", "description") VALUES ('2', '0', '0', '0', '0', 'normal', 'Users with no elevated privileges'); +INSERT INTO "main"."roles" ("id", "created_at", "created_by", "updated_at", "updated_by", "name", "description") VALUES ('3', '0', '0', '0', '0', 'editor', 'Users with basic elevated privileges'); +INSERT INTO "main"."roles" ("id", "created_at", "created_by", "updated_at", "updated_by", "name", "description") VALUES ('4', '0', '0', '0', '0', 'admin', 'Users with full administrative privileges'); + diff --git a/backend/migrations/20240929223122_initial_roles.down.sql b/backend/migrations/20241007024816_initial_data.down.sql similarity index 100% rename from backend/migrations/20240929223122_initial_roles.down.sql rename to backend/migrations/20241007024816_initial_data.down.sql diff --git a/backend/migrations/20241007024816_initial_data.up.sql b/backend/migrations/20241007024816_initial_data.up.sql new file mode 100644 index 0000000..f881b88 --- /dev/null +++ b/backend/migrations/20241007024816_initial_data.up.sql @@ -0,0 +1,13 @@ +-- Add up migration script here +-- Role permissions +INSERT INTO "main"."role_permissions" ("id", "created_at", "created_by", "updated_at", "updated_by", "role_id", "item") VALUES ('1', '0', '0', '0', '0', '1', '/'); +INSERT INTO "main"."role_permissions" ("id", "created_at", "created_by", "updated_at", "updated_by", "role_id", "item") VALUES ('2', '0', '0', '0', '0', '1', '/login'); +INSERT INTO "main"."role_permissions" ("id", "created_at", "created_by", "updated_at", "updated_by", "role_id", "item") VALUES ('3', '0', '0', '0', '0', '1', '/logout'); +INSERT INTO "main"."role_permissions" ("id", "created_at", "created_by", "updated_at", "updated_by", "role_id", "item") VALUES ('4', '0', '0', '0', '0', '2', '/dashboard'); +INSERT INTO "main"."role_permissions" ("id", "created_at", "created_by", "updated_at", "updated_by", "role_id", "item") VALUES ('5', '0', '0', '0', '0', '2', '/profile'); +INSERT INTO "main"."role_permissions" ("id", "created_at", "created_by", "updated_at", "updated_by", "role_id", "item") VALUES ('6', '0', '0', '0', '0', '4', '/useradmin'); +INSERT INTO "main"."role_permissions" ("id", "created_at", "created_by", "updated_at", "updated_by", "role_id", "item") VALUES ('7', '0', '0', '0', '0', '4', '/users'); + +-- First user is an admin +INSERT INTO "main"."user_roles" ("id", "created_at", "created_by", "updated_at", "updated_by", "user_id", "role_id") VALUES ('1', '1728247301', '0', '1728247301', '0', '1', '1'); +INSERT INTO "main"."user_roles" ("id", "created_at", "created_by", "updated_at", "updated_by", "user_id", "role_id") VALUES ('2', '0', '0', '0', '0', '1', '4'); diff --git a/backend/src/google_oauth.rs b/backend/src/google_oauth.rs index 709b95d..7062783 100644 --- a/backend/src/google_oauth.rs +++ b/backend/src/google_oauth.rs @@ -70,7 +70,7 @@ pub async fn login( .remove("return_url") .unwrap_or_else(|| "/".to_string()); // TODO: check if return_url is valid - + let client = get_client(hostname)?; let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256(); @@ -181,11 +181,17 @@ pub async fn google_auth_return( .bind(email.as_str()) .fetch_one(&db_pool) .await; - let user_id = if let Ok(query) = query - { + let user_id = if let Ok(query) = query { query.0 } else { - let query: (i64,) = sqlx::query_as("INSERT INTO users (email, name, family_name, given_name) VALUES (?, ?, ?, ?) RETURNING id") + let now = Utc::now().timestamp(); + + // Add user + let query: (i64,) = sqlx::query_as("INSERT INTO users (created_at, created_by, updated_at, updated_by, email, name, family_name, given_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id") + .bind(now) + .bind(0 as i64)// Created by system + .bind(now) + .bind(0 as i64) // Updated by system .bind(email.clone()) .bind(name.clone()) .bind(family_name.clone()) @@ -194,8 +200,12 @@ pub async fn google_auth_return( .await?; // Add public role - sqlx::query("INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)") - .bind(query.0) + sqlx::query("INSERT INTO user_roles (created_at, created_by, updated_at, updated_by, user_id, role_id) VALUES (?, ?, ?, ?, ?, ?)") + .bind(now) + .bind(0 as i64)// Created by system + .bind(now) + .bind(0 as i64) // Updated by system + .bind(query.0) .bind("1") .execute(&db_pool) .await?; @@ -207,7 +217,6 @@ pub async fn google_auth_return( if let Some(cookie) = cookie { if let Some(_session_token) = cookie.get("session_token") { } else { - println!("google_auth_return : No session token"); // Create a session for the user let session_token_p1 = Uuid::new_v4().to_string(); let session_token_p2 = Uuid::new_v4().to_string(); diff --git a/backend/src/main.rs b/backend/src/main.rs index b38514e..09516ac 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -2,8 +2,7 @@ use std::net::SocketAddr; use axum::{ middleware, routing::{get, get_service}, Extension, Router }; -use serde::{Deserialize, Serialize}; -use sqlx::{prelude::FromRow, sqlite::SqlitePoolOptions, SqlitePool}; +use sqlx::{sqlite::SqlitePoolOptions, SqlitePool}; use sqlx::migrate::Migrator; use tower_http::services::ServeDir; @@ -11,27 +10,19 @@ mod error_handling; mod google_oauth; mod middlewares; mod routes; +mod user; 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; #[derive(Clone)] pub struct AppState { pub db_pool: SqlitePool } -#[derive(Default, Clone, Debug, FromRow, Serialize, Deserialize)] -pub struct UserData { - #[allow(dead_code)] - pub id: i64, - pub email: String, - pub name: String, - pub family_name: String, - pub given_name: String, -} - #[tokio::main] async fn main() { // initialize tracing @@ -46,10 +37,10 @@ async fn main() { static MIGRATOR: Migrator = sqlx::migrate!(); - MIGRATOR +/* MIGRATOR .run(&app_state.db_pool) .await - .expect("Failed to run migrations"); + .expect("Failed to run migrations"); */ let user_data: Option = None; @@ -66,7 +57,6 @@ async fn main() { .route("/login", get(login)) .route("/logout", get(logout)) .route("/google_auth_return", get(google_auth_return)) - //.route_layer(middleware::from_fn_with_state(app_state.db_pool.clone(), check_auth)) .route_layer(middleware::from_fn_with_state(app_state.db_pool.clone(), inject_user_data)) .with_state(app_state.db_pool) .layer(Extension(user_data)) diff --git a/backend/src/middlewares.rs b/backend/src/middlewares.rs index 96771ac..201c4d3 100644 --- a/backend/src/middlewares.rs +++ b/backend/src/middlewares.rs @@ -55,6 +55,10 @@ pub async fn inject_user_data( request.extensions_mut().insert(Some(UserData { id: row.id, + created_at: row.created_at, + created_by: row.created_by, + updated_at: row.updated_at, + updated_by: row.updated_by, email: row.email, name: row.name, family_name: row.family_name, diff --git a/backend/src/routes.rs b/backend/src/routes.rs index 3752818..a52a719 100644 --- a/backend/src/routes.rs +++ b/backend/src/routes.rs @@ -3,14 +3,23 @@ use axum::{extract::{Path, State}, response::{Html, IntoResponse, Redirect}, Ext use http::StatusCode; use sqlx::SqlitePool; -use crate::{middlewares::is_authorized, UserData}; +use crate::{middlewares::is_authorized, user::get_user_roles, UserData}; #[derive(Template)] #[template(path = "profile.html")] struct ProfileTemplate { logged_in: bool, name: String, - user: UserData + user: UserData, +} + +#[derive(Template)] +#[template(path = "user.html")] +struct UserProfileTemplate { + logged_in: bool, + name: String, + user: UserData, + user_roles: Vec } struct HtmlTemplate(T); @@ -87,13 +96,17 @@ pub async fn profile( let logged_in = user_name.is_some(); let name = user_name.unwrap_or_default(); - // Extract the user data. - let user = user_data.as_ref().unwrap().clone(); + if logged_in { + // Extract the user data. + let user = user_data.as_ref().unwrap().clone(); - if is_authorized("/profile", user_data, db_pool).await { - // Create the profile template. - let template = ProfileTemplate { logged_in, name, user: user.clone() }; - return HtmlTemplate(template).into_response() + if is_authorized("/profile", user_data, db_pool).await { + // Create the profile template. + let template = ProfileTemplate { logged_in, name, user: user.clone() }; + return HtmlTemplate(template).into_response() + } else { + Redirect::to("/").into_response() + } } else { Redirect::to("/").into_response() } @@ -119,9 +132,12 @@ pub async fn profile( .await .unwrap(); - if is_authorized("/users", user_data, db_pool).await { + if is_authorized("/users", user_data, db_pool.clone()).await { + // Get user roles + let user_roles = get_user_roles(user_id, &db_pool.clone()).await; + // Create the profile template. - let template = ProfileTemplate { logged_in, name, user: user }; + let template = UserProfileTemplate { logged_in, name, user: user, user_roles }; return HtmlTemplate(template).into_response() } else { Redirect::to("/").into_response() diff --git a/backend/src/user.rs b/backend/src/user.rs new file mode 100644 index 0000000..b915afa --- /dev/null +++ b/backend/src/user.rs @@ -0,0 +1,51 @@ +///User related structs and functions +use serde::{Deserialize, Serialize}; +use sqlx::{prelude::FromRow, SqlitePool}; + +#[derive(Default, Clone, Debug, FromRow, Serialize, Deserialize)] +pub struct UserData { + pub id: i64, + pub created_at: i64, + pub created_by: i64, + pub updated_at: i64, + pub updated_by: i64, + pub email: String, + pub name: String, + pub family_name: String, + pub given_name: String, +} + +#[derive(Serialize, Deserialize)] +pub struct RoleData { + pub id: i64, + pub created_at: i64, + pub created_by: i64, + pub updated_at: i64, + pub updated_by: i64, + pub name: String, + pub description: String, +} + +#[derive(Default, Clone, Debug, FromRow, Serialize, Deserialize)] +pub struct UserRoles { + pub id: i64, + pub created_at: i64, + pub created_by: i64, + pub updated_at: i64, + pub updated_by: i64, + pub user_id: i64, + pub role_id: i64, +} + +pub async fn get_user_roles(user_id: i64, db_pool: &SqlitePool) -> Vec { + // Get user roles + let user_roles = sqlx::query_as( + r#"SELECT id, created_at, created_by, updated_at, updated_by, user_id, role_id FROM user_roles WHERE user_id = ?"# + ) + .bind(user_id) + .fetch_all(db_pool) + .await + .unwrap(); + + user_roles +} \ No newline at end of file diff --git a/backend/templates/profile.html b/backend/templates/profile.html index d1e9c7b..8a4ff26 100644 --- a/backend/templates/profile.html +++ b/backend/templates/profile.html @@ -1,9 +1,9 @@ {% extends "base.html" %} {% block title %}User Profile{% endblock %} {% block content %} -

User Profile

+

My Profile

Full name: {{ user.name }}
Given name: {{ user.given_name }}
Family name: {{ user.family_name }}
-Your email address: {{ user.email }}
{% endblock %} diff --git a/backend/templates/user.html b/backend/templates/user.html new file mode 100644 index 0000000..4379b3e --- /dev/null +++ b/backend/templates/user.html @@ -0,0 +1,27 @@ +{% extends "authorized.html" %} +{% block title %}User Profile{% endblock %} +{% block content %} +

User Profile

+Full name: {{ user.name }}
+Given name: {{ user.given_name }}
+Family name: {{ user.family_name }}
+Your email address: {{ user.email }}
+
+

User Roles

+ + + + + + + + + {% for user_role in user_roles %} + + + + + {% endfor %} + +
IDName
{{ user_role.id }}{{ user_role.role_id }}
+{% endblock %}