Convert datetime to integer

Add roles to admin user profile screen
This commit is contained in:
Chris Jean-Marie 2024-10-07 02:51:14 +00:00
parent 9870b11664
commit e7d0780b1b
16 changed files with 208 additions and 90 deletions

View File

@ -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)
);

View File

@ -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
);

View File

@ -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');

View File

@ -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
);

View File

@ -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');

View File

@ -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');

View File

@ -70,7 +70,7 @@ pub async fn login(
.remove("return_url") .remove("return_url")
.unwrap_or_else(|| "/".to_string()); .unwrap_or_else(|| "/".to_string());
// TODO: check if return_url is valid // TODO: check if return_url is valid
let client = get_client(hostname)?; let client = get_client(hostname)?;
let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256(); 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()) .bind(email.as_str())
.fetch_one(&db_pool) .fetch_one(&db_pool)
.await; .await;
let user_id = if let Ok(query) = query let user_id = if let Ok(query) = query {
{
query.0 query.0
} else { } 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(email.clone())
.bind(name.clone()) .bind(name.clone())
.bind(family_name.clone()) .bind(family_name.clone())
@ -194,8 +200,12 @@ pub async fn google_auth_return(
.await?; .await?;
// Add public role // Add public role
sqlx::query("INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)") sqlx::query("INSERT INTO user_roles (created_at, created_by, updated_at, updated_by, user_id, role_id) VALUES (?, ?, ?, ?, ?, ?)")
.bind(query.0) .bind(now)
.bind(0 as i64)// Created by system
.bind(now)
.bind(0 as i64) // Updated by system
.bind(query.0)
.bind("1") .bind("1")
.execute(&db_pool) .execute(&db_pool)
.await?; .await?;
@ -207,7 +217,6 @@ pub async fn google_auth_return(
if let Some(cookie) = cookie { if let Some(cookie) = cookie {
if let Some(_session_token) = cookie.get("session_token") { if let Some(_session_token) = cookie.get("session_token") {
} else { } else {
println!("google_auth_return : No session token");
// Create a session for the user // Create a session for the user
let session_token_p1 = Uuid::new_v4().to_string(); let session_token_p1 = Uuid::new_v4().to_string();
let session_token_p2 = Uuid::new_v4().to_string(); let session_token_p2 = Uuid::new_v4().to_string();

View File

@ -2,8 +2,7 @@ use std::net::SocketAddr;
use axum::{ use axum::{
middleware, routing::{get, get_service}, Extension, Router middleware, routing::{get, get_service}, Extension, Router
}; };
use serde::{Deserialize, Serialize}; use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
use sqlx::{prelude::FromRow, sqlite::SqlitePoolOptions, SqlitePool};
use sqlx::migrate::Migrator; use sqlx::migrate::Migrator;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
@ -11,27 +10,19 @@ mod error_handling;
mod google_oauth; mod google_oauth;
mod middlewares; mod middlewares;
mod routes; mod routes;
mod user;
use error_handling::AppError; use error_handling::AppError;
use middlewares::inject_user_data; use middlewares::inject_user_data;
use google_oauth::{login, logout, google_auth_return}; use google_oauth::{login, logout, google_auth_return};
use routes::{dashboard, index, about, profile, user_profile, useradmin}; use routes::{dashboard, index, about, profile, user_profile, useradmin};
use user::UserData;
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
pub db_pool: SqlitePool 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] #[tokio::main]
async fn main() { async fn main() {
// initialize tracing // initialize tracing
@ -46,10 +37,10 @@ async fn main() {
static MIGRATOR: Migrator = sqlx::migrate!(); static MIGRATOR: Migrator = sqlx::migrate!();
MIGRATOR /* MIGRATOR
.run(&app_state.db_pool) .run(&app_state.db_pool)
.await .await
.expect("Failed to run migrations"); .expect("Failed to run migrations"); */
let user_data: Option<UserData> = None; let user_data: Option<UserData> = None;
@ -66,7 +57,6 @@ async fn main() {
.route("/login", get(login)) .route("/login", get(login))
.route("/logout", get(logout)) .route("/logout", get(logout))
.route("/google_auth_return", get(google_auth_return)) .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)) .route_layer(middleware::from_fn_with_state(app_state.db_pool.clone(), inject_user_data))
.with_state(app_state.db_pool) .with_state(app_state.db_pool)
.layer(Extension(user_data)) .layer(Extension(user_data))

View File

@ -55,6 +55,10 @@ pub async fn inject_user_data(
request.extensions_mut().insert(Some(UserData { request.extensions_mut().insert(Some(UserData {
id: row.id, 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, email: row.email,
name: row.name, name: row.name,
family_name: row.family_name, family_name: row.family_name,

View File

@ -3,14 +3,23 @@ use axum::{extract::{Path, State}, response::{Html, IntoResponse, Redirect}, Ext
use http::StatusCode; use http::StatusCode;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::{middlewares::is_authorized, UserData}; use crate::{middlewares::is_authorized, user::get_user_roles, UserData};
#[derive(Template)] #[derive(Template)]
#[template(path = "profile.html")] #[template(path = "profile.html")]
struct ProfileTemplate { struct ProfileTemplate {
logged_in: bool, logged_in: bool,
name: String, name: String,
user: UserData user: UserData,
}
#[derive(Template)]
#[template(path = "user.html")]
struct UserProfileTemplate {
logged_in: bool,
name: String,
user: UserData,
user_roles: Vec<crate::user::UserRoles>
} }
struct HtmlTemplate<T>(T); struct HtmlTemplate<T>(T);
@ -87,13 +96,17 @@ pub async fn profile(
let logged_in = user_name.is_some(); let logged_in = user_name.is_some();
let name = user_name.unwrap_or_default(); let name = user_name.unwrap_or_default();
// Extract the user data. if logged_in {
let user = user_data.as_ref().unwrap().clone(); // Extract the user data.
let user = user_data.as_ref().unwrap().clone();
if is_authorized("/profile", user_data, db_pool).await { if is_authorized("/profile", user_data, db_pool).await {
// Create the profile template. // Create the profile template.
let template = ProfileTemplate { logged_in, name, user: user.clone() }; let template = ProfileTemplate { logged_in, name, user: user.clone() };
return HtmlTemplate(template).into_response() return HtmlTemplate(template).into_response()
} else {
Redirect::to("/").into_response()
}
} else { } else {
Redirect::to("/").into_response() Redirect::to("/").into_response()
} }
@ -119,9 +132,12 @@ pub async fn profile(
.await .await
.unwrap(); .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. // 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() return HtmlTemplate(template).into_response()
} else { } else {
Redirect::to("/").into_response() Redirect::to("/").into_response()

51
backend/src/user.rs Normal file
View File

@ -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<UserRoles> {
// 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
}

View File

@ -1,9 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}User Profile{% endblock %} {% block title %}User Profile{% endblock %}
{% block content %} {% block content %}
<h1>User Profile</h1> <h1>My Profile</h1>
Full name: {{ user.name }}<br/> Full name: {{ user.name }}<br/>
Given name: {{ user.given_name }}<br/> Given name: {{ user.given_name }}<br/>
Family name: {{ user.family_name }}<br/> Family name: {{ user.family_name }}<br/>
Your email address: {{ user.email }}<br Your email address: {{ user.email }}<br/>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,27 @@
{% extends "authorized.html" %}
{% block title %}User Profile{% endblock %}
{% block content %}
<h1>User Profile</h1>
Full name: {{ user.name }}<br/>
Given name: {{ user.given_name }}<br/>
Family name: {{ user.family_name }}<br/>
Your email address: {{ user.email }}<br/>
<br/>
<h2>User Roles</h2>
<table>
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
</tr>
</thead>
<tbody>
{% for user_role in user_roles %}
<tr>
<td><a href="/roles/{{ user_role.id }}">{{ user_role.id }}</a></td>
<td>{{ user_role.role_id }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}