Upgrade to latest cargo packages

Refactor code to use the person table
This commit is contained in:
Chris Jean-Marie 2025-03-25 18:34:07 +00:00
parent c9dd17ae14
commit 1589ebfd37
12 changed files with 799 additions and 568 deletions

1133
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,10 +7,10 @@ edition = "2021"
# Update all dependencies with `cargo upgrade -i allow && cargo update`
[dependencies]
axum = { version = "0.7.6" }
axum_session = { version = "0.14.2" }
axum-server = { version = "0.7.1" }
axum-extra = { version = "0.9.4", features = ["cookie-private", "typed-header"] }
axum = { version = "0.8.1" }
axum_session = { version = "0.16.0" }
axum-server = { version = "0.7.2" }
axum-extra = { version = "0.10.0", features = ["cookie-private", "typed-header"] }
askama = "0.12.0"
askama_axum = "0.4.0"
headers = "0.4"

View File

@ -19,8 +19,12 @@ ADD
ALTER TABLE if exists users
ADD COLUMN person_id uuid REFERENCES people (id) ON DELETE SET NULL;
-- Copy data
-- Copy accounts(users) to profiles(people)
insert into people (created_by, updated_by, email, name, family_name, given_name)
select created_by, updated_by, email, name, family_name, given_name from users;
update users u set person_id = p.id from people p where p.email = u.email;
-- Link accounts to profiles
update users u set person_id = p.id from people p where p.email = u.email;
-- Move wishlist items from accounts to profiles
update wishlist_items wi set user_id = p.person_id from users p where p.id = wi.user_id;

View File

@ -1,7 +1,6 @@
use askama::Template;
use askama_axum::{IntoResponse, Response};
use axum::{
body::Body, extract::{Path, Query, Request, State}, response::{Html, Redirect}, Extension
body::Body, extract::{Path, Query, Request, State}, response::{Html, IntoResponse, Redirect, Response}, Extension
};
use http::StatusCode;
use serde::{Deserialize, Serialize};

View File

@ -5,9 +5,9 @@
// GOOGLE_CLIENT_SECRET=yyy
use axum::{
extract::{Extension, Host, Query, State}, response::{IntoResponse, Redirect}
extract::{Extension, Query, State}, response::{IntoResponse, Redirect}
};
use axum_extra::TypedHeader;
use axum_extra::{extract::Host, TypedHeader};
use dotenvy::var;
use headers::Cookie;
use http::{HeaderMap, HeaderValue};

View File

@ -24,8 +24,8 @@ use calendar::{calendar, get_events};
use error_handling::AppError;
use google_oauth::{google_auth_return, login, logout};
use middlewares::inject_user_data;
use routes::{about, contact, dashboard, index, profile, user_profile, useradmin};
use user::{add_user_role, delete_user_role, UserData};
use routes::{about, contact, dashboard, index, profile, user_profile, user_profile_account, useradmin};
use user::{add_user_role, delete_user_role, user_account, UserData};
use wishlist::{
user_wishlist, user_wishlist_add, user_wishlist_add_item, user_wishlist_bought_item,
user_wishlist_delete_item, user_wishlist_edit_item, user_wishlist_received_item,
@ -77,46 +77,47 @@ async fn main() {
// User
.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("/user/{user_id}", get(user_profile))
.route("/user/{user_id}/{account_id}", get(user_profile_account))
.route("/roles/{user_id}/{role_id}/add", get(add_user_role))
.route(
"/roles/:user_id/:user_role_id/delete",
"/roles/{user_id}/{user_role_id}/delete",
get(delete_user_role),
)
// Calendar
.route("/calendar", get(calendar))
.route("/getevents/:calendar", get(get_events))
.route("/getevents/{calendar}", get(get_events))
// Wishlist
.route("/wishlists", get(wishlists))
.route("/userwishlist/:user_id", get(user_wishlist))
.route("/userwishlist/{user_id}", get(user_wishlist))
.route(
"/userwishlist/add/:user_id",
"/userwishlist/add/{user_id}",
get(user_wishlist_add).post(user_wishlist_add_item),
)
.route(
"/userwishlist/edit/:item_id",
"/userwishlist/edit/{item_id}",
get(user_wishlist_edit_item).post(user_wishlist_save_item),
)
.route(
"/userwishlist/bought/:user_id",
"/userwishlist/bought/{user_id}",
get(user_wishlist_bought_item),
)
.route(
"/userwishlist/received/:user_id",
"/userwishlist/received/{user_id}",
get(user_wishlist_received_item),
)
.route(
"/userwishlist/delete/:item_id",
"/userwishlist/delete/{item_id}",
get(user_wishlist_delete_item),
)
.route(
"/userwishlist/returned/:item_id",
"/userwishlist/returned/{item_id}",
get(user_wishlist_returned_item),
)
// Secret Gift Exchange - Not ready for public use yet
.route("/giftexchanges", get(giftexchanges))
.route(
"/giftexchange/:giftexchange_id",
"/giftexchange/{giftexchange_id}",
get(giftexchange).post(giftexchange_save),
)
.nest_service(

View File

@ -1,15 +1,17 @@
use askama_axum::{Response, Template};
use askama_axum::Template;
use axum::{
extract::{Path, State},
response::{Html, IntoResponse, Redirect},
Extension,
};
use http::StatusCode;
use reqwest::redirect;
use sqlx::PgPool;
use uuid::Uuid;
use crate::{
middlewares::is_authorized,
user::{get_other_roles_display, get_user_roles_display},
user::{get_other_roles_display, get_user_roles_display, AccountData},
UserData,
};
@ -28,7 +30,8 @@ struct UserProfileTemplate {
user: UserData,
user_roles: Vec<crate::user::UserRolesDisplay>,
profile: UserData,
profile_accounts: Vec<UserData>,
profile_accounts: Vec<AccountData>,
account: AccountData,
profile_roles: Vec<crate::user::UserRolesDisplay>,
non_profile_roles: Vec<crate::user::UserRolesDisplay>,
}
@ -39,7 +42,7 @@ impl<T> IntoResponse for HtmlTemplate<T>
where
T: Template,
{
fn into_response(self) -> Response {
fn into_response(self) -> http::Response<axum::body::Body> {
match self.0.render() {
Ok(html) => Html(html).into_response(),
Err(err) => (
@ -172,7 +175,45 @@ pub async fn user_profile(
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
// Extract the user data.
let profile = sqlx::query_as( "SELECT * FROM users WHERE id = $1")
let profile: UserData = sqlx::query_as( "SELECT * FROM people WHERE id = $1")
.bind(user_id)
.fetch_one(&db_pool)
.await
.unwrap();
if is_authorized("/users", user_data, db_pool.clone()).await {
// Get first user account
let profile_account: Uuid = sqlx::query_scalar( "SELECT id FROM users WHERE person_id = $1 LIMIT 1")
.bind(user_id)
.fetch_one(&db_pool)
.await
.unwrap();
let redirect_url = format!("/user/{user_id}/{}", profile_account);
Redirect::to(&redirect_url).into_response()
} else {
Redirect::to("/").into_response()
}
} else {
Redirect::to("/").into_response()
}
}
pub async fn user_profile_account(
Path((user_id, account_id)): Path<(uuid::Uuid, uuid::Uuid)>,
State(db_pool): State<PgPool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
// Is the user logged in?
let logged_in = user_data.is_some();
if logged_in {
// Extract the user data.
let user = user_data.as_ref().unwrap().clone();
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
// Extract the user data.
let profile = sqlx::query_as( "SELECT * FROM people WHERE id = $1")
.bind(user_id)
.fetch_one(&db_pool)
.await
@ -189,21 +230,29 @@ pub async fn user_profile(
created_by,
updated_at,
updated_by,
person_id,
email,
name,
family_name,
given_name
FROM users WHERE person_id = $1"#)
FROM users WHERE person_id = $1 order by name"#)
.bind(user_id)
.fetch_all(&db_pool)
.await
.unwrap();
// Get user account
let account = sqlx::query_as( "SELECT * FROM users WHERE id = $1")
.bind(account_id)
.fetch_one(&db_pool)
.await
.unwrap();
// Get user roles
let profile_roles = get_user_roles_display(user_id, &db_pool.clone()).await;
let profile_roles = get_user_roles_display(account_id, &db_pool.clone()).await;
// Get roles user does not have
let non_profile_roles = get_other_roles_display(user_id, &db_pool.clone()).await;
let non_profile_roles = get_other_roles_display(account_id, &db_pool.clone()).await;
// Create the profile template.
let template = UserProfileTemplate {
@ -212,6 +261,7 @@ pub async fn user_profile(
user_roles,
profile,
profile_accounts,
account,
profile_roles,
non_profile_roles,
};
@ -245,7 +295,7 @@ pub async fn useradmin(
let user = user_data.as_ref().unwrap().clone();
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
let users = sqlx::query_as::<_, UserData>("SELECT * FROM users")
let users = sqlx::query_as::<_, UserData>("SELECT * FROM people order by name")
.fetch_all(&db_pool)
.await
.unwrap();

View File

@ -1,12 +1,10 @@
use askama::Template;
use askama_axum::{IntoResponse, Response};
use axum::{
body::{self, Body},
extract::{FromRequest, Path, Request, State},
response::Redirect,
response::{Html, IntoResponse, Redirect, Response},
Extension, Form, Json, RequestExt,
};
use axum_extra::response::Html;
use http::{header::CONTENT_TYPE, StatusCode};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool};

View File

@ -1,13 +1,13 @@
use askama_axum::IntoResponse;
use axum::{
extract::{Path, State},
response::Redirect,
response::{IntoResponse, Redirect},
Extension,
};
///User related structs and functions
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool};
use uuid::Uuid;
use crate::middlewares::is_authorized;
@ -24,6 +24,20 @@ pub struct UserData {
pub given_name: String,
}
#[derive(Default, Clone, Debug, Serialize, Deserialize, FromRow)]
pub struct AccountData {
pub id: uuid::Uuid,
pub created_at: chrono::NaiveDateTime,
pub created_by: uuid::Uuid,
pub updated_at: chrono::NaiveDateTime,
pub updated_by: uuid::Uuid,
pub person_id: uuid::Uuid,
pub email: String,
pub name: String,
pub family_name: String,
pub given_name: String,
}
#[derive(Serialize, Deserialize)]
pub struct RoleData {
pub id: uuid::Uuid,
@ -87,10 +101,13 @@ pub async fn get_user_roles(user_id: i64, db_pool: &PgPool) -> Vec<UserRoles> {
user_roles
} */
pub async fn get_user_roles_display(user_id: uuid::Uuid, db_pool: &PgPool) -> Vec<UserRolesDisplay> {
pub async fn get_user_roles_display(
user_id: uuid::Uuid,
db_pool: &PgPool,
) -> Vec<UserRolesDisplay> {
// Get user roles
let user_roles = sqlx::query_as(
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 = $1"#
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 = $1 order by r.name"#
)
.bind(user_id)
.fetch_all(db_pool)
@ -100,10 +117,13 @@ pub async fn get_user_roles_display(user_id: uuid::Uuid, db_pool: &PgPool) -> Ve
user_roles
}
pub async fn get_other_roles_display(user_id: uuid::Uuid, db_pool: &PgPool) -> Vec<UserRolesDisplay> {
pub async fn get_other_roles_display(
user_id: uuid::Uuid,
db_pool: &PgPool,
) -> Vec<UserRolesDisplay> {
// Get roles user does not have
let user_roles = sqlx::query_as(
r#"select r.id as id, r.created_at, r.created_by, r.updated_at, r.updated_by, $1 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 = $2)"#
r#"select r.id as id, r.created_at, r.created_by, r.updated_at, r.updated_by, $1 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 = $2) order by r.name"#
)
.bind(user_id.clone())
.bind(user_id)
@ -115,7 +135,7 @@ pub async fn get_other_roles_display(user_id: uuid::Uuid, db_pool: &PgPool) -> V
}
pub async fn add_user_role(
Path((user_id, role_id)): Path<(uuid::Uuid, uuid::Uuid)>,
Path((account_id, role_id)): Path<(uuid::Uuid, uuid::Uuid)>,
State(db_pool): State<PgPool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
@ -123,15 +143,20 @@ pub async fn add_user_role(
sqlx::query("INSERT INTO user_roles (created_by, updated_by, user_id, role_id) VALUES ($1, $2, $3, $4)")
.bind(user_data.as_ref().unwrap().id)// Created by current user
.bind(user_data.as_ref().unwrap().id) // Updated by current user
.bind(user_id)
.bind(account_id)
.bind(role_id)
.execute(&db_pool)
.await
.unwrap();
// TODO - send email to user regarding role changes
let profile_id: Uuid = sqlx::query_scalar("SELECT person_id FROM users WHERE id = $1")
.bind(account_id)
.fetch_one(&db_pool)
.await
.unwrap();
let redirect_url = format!("/users/{user_id}");
let redirect_url = format!("/user/{profile_id}/{account_id}");
Redirect::to(&redirect_url).into_response()
} else {
Redirect::to("/").into_response()
@ -139,7 +164,7 @@ pub async fn add_user_role(
}
pub async fn delete_user_role(
Path((user_id, user_role_id)): Path<(uuid::Uuid, uuid::Uuid)>,
Path((account_id, user_role_id)): Path<(uuid::Uuid, uuid::Uuid)>,
State(db_pool): State<PgPool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
@ -150,14 +175,23 @@ pub async fn delete_user_role(
.await
.unwrap();
let redirect_url = format!("/users/{user_id}");
let profile_id: Uuid = sqlx::query_scalar("SELECT person_id FROM users WHERE id = $1")
.bind(account_id)
.fetch_one(&db_pool)
.await
.unwrap();
let redirect_url = format!("/user/{profile_id}/{account_id}");
Redirect::to(&redirect_url).into_response()
} else {
Redirect::to("/").into_response()
}
}
pub async fn get_user_wishlist_item_by_id(item_id: uuid::Uuid, db_pool: &PgPool) -> UserWishlistItem {
pub async fn get_user_wishlist_item_by_id(
item_id: uuid::Uuid,
db_pool: &PgPool,
) -> UserWishlistItem {
// Get wish list items for the user
let user_wishlist_item = sqlx::query_as(
r#"select id, created_at, created_by, updated_at, updated_by, user_id, item, item_url, purchased_by, received_at
@ -171,7 +205,10 @@ pub async fn get_user_wishlist_item_by_id(item_id: uuid::Uuid, db_pool: &PgPool)
user_wishlist_item
}
pub async fn get_user_wishlist_items(user_id: uuid::Uuid, db_pool: &PgPool) -> Vec<UserWishlistItem> {
pub async fn get_user_wishlist_items(
user_id: uuid::Uuid,
db_pool: &PgPool,
) -> Vec<UserWishlistItem> {
// Get wish list items for the user
let user_wishlist_items = sqlx::query_as(
r#"select id, created_at, created_by, updated_at, updated_by, user_id, item, item_url, purchased_by, received_at
@ -193,4 +230,24 @@ pub async fn get_useremails_by_role(role_name: String, db_pool: &PgPool) -> Stri
.unwrap();
useremails
}
}
pub async fn user_account(
Path(account_id): Path<uuid::Uuid>,
State(db_pool): State<PgPool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
// Is the user logged in?
let logged_in = user_data.is_some();
if logged_in {
// Extract the user data.
if is_authorized("/users", user_data.clone(), db_pool.clone()).await {
Redirect::to("/").into_response()
} else {
Redirect::to("/").into_response()
}
} else {
Redirect::to("/").into_response()
}
}

View File

@ -1,10 +1,9 @@
use askama_axum::{IntoResponse, Response, Template};
use askama_axum::Template;
use axum::{
extract::{Path, State},
response::Redirect,
response::{Html, IntoResponse, Redirect},
Extension, Form,
};
use axum_extra::response::Html;
use chrono::Utc;
use http::StatusCode;
use serde::Deserialize;
@ -25,7 +24,7 @@ impl<T> IntoResponse for HtmlTemplate<T>
where
T: Template,
{
fn into_response(self) -> Response {
fn into_response(self) -> http::Response<axum::body::Body> {
match self.0.render() {
Ok(html) => Html(html).into_response(),
Err(err) => (
@ -58,7 +57,7 @@ pub async fn wishlists(
let user = user_data.as_ref().unwrap().clone();
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
let users = sqlx::query_as::<_, UserData>("SELECT * FROM users")
let users = sqlx::query_as::<_, UserData>("SELECT * FROM people order by name")
.fetch_all(&db_pool)
.await
.unwrap();
@ -107,7 +106,7 @@ pub async fn user_wishlist(
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
// Extract the user data.
let person = sqlx::query_as("SELECT * FROM users WHERE id = $1")
let person = sqlx::query_as("SELECT * FROM people WHERE id = $1")
.bind(user_id)
.fetch_one(&db_pool)
.await

View File

@ -5,7 +5,7 @@
Full name: {{ profile.name }}<br/>
Given name: {{ profile.given_name }}<br/>
Family name: {{ profile.family_name }}<br/>
Your email address: {{ profile.email }}<br/>
Logged in email address: {{ profile.email }}<br/>
<br/>
<h2>Accounts</h2>
<button type="button" class="btn btn-primary">Merge</button>
@ -13,19 +13,21 @@ Your email address: {{ profile.email }}<br/>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Email</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
{% for account in profile_accounts %}
{% for user_account in profile_accounts %}
<tr>
<td><a href="/accounts/{{ account.id }}">{{ account.name }}</a></td>
<td><a href="/accounts/{{ account.id }}/delete">Delete</a></td>
<td><a href="/user/{{profile.id}}/{{ user_account.id }}">{{ account.name }}</a></td>
<td>{{ user_account.email }}</td>
<td><a href="/account/{{ user_account.id }}/delete">Delete</a></td>
</tr>
{% endfor %}
</tbody>
</table>
<h2>User Roles</h2>
<h2>Account Roles ({{ account.email }})</h2>
<button type="button" class="btn btn-primary">Edit</button>
<button type="button" class="btn btn-primary">Add</button>
<table class="table table-striped table-bordered">

View File

@ -12,7 +12,7 @@
<tbody>
{% for user in users %}
<tr>
<td><a href="/users/{{ user.id }}">{{ user.name }}</a></td>
<td><a href="/user/{{ user.id }}">{{ user.name }}</a></td>
<td>{{ user.email }}</td>
</tr>
{% endfor %}