Use UUIDs for publically-facing exposed primary keys

This commit is contained in:
Z. Charles Dziura 2025-03-06 20:11:48 -05:00
parent 7f2e7e8e8d
commit efd9e80633
7 changed files with 42 additions and 18 deletions

View file

@ -38,6 +38,7 @@ sqlx = { version = "0.8", features = [
"postgres", "postgres",
"runtime-tokio", "runtime-tokio",
"rust_decimal", "rust_decimal",
"uuid"
] } ] }
syslog-tracing = "0.3.1" syslog-tracing = "0.3.1"
time = { version = "0.3", features = ["formatting", "macros"] } time = { version = "0.3", features = ["formatting", "macros"] }

View file

@ -1,3 +1,5 @@
DROP FUNCTION IF EXISTS uuidv7;
DROP INDEX IF EXISTS user_username_uniq_idx; DROP INDEX IF EXISTS user_username_uniq_idx;
DROP INDEX IF EXISTS user_email_uniq_idx; DROP INDEX IF EXISTS user_email_uniq_idx;
DROP INDEX IF EXISTS permission_category_idx; DROP INDEX IF EXISTS permission_category_idx;

View file

@ -1,3 +1,19 @@
CREATE FUNCTION uuidv7() RETURNS uuid
AS $$
-- Replace the first 48 bits of a uuidv4 with the current
-- number of milliseconds since 1970-01-01 UTC
-- and set the "ver" field to 7 by setting additional bits
SELECT encode(
set_bit(
set_bit(
overlay(uuid_send(gen_random_uuid()) placing
substring(int8send((extract(epoch FROM clock_timestamp())*1000)::bigint) FROM 3)
FROM 1 FOR 6),
52, 1),
53, 1), 'hex')::uuid;
$$ LANGUAGE sql volatile;
CREATE TYPE CREATE TYPE
public.status public.status
AS ENUM AS ENUM
@ -18,7 +34,8 @@ CREATE TYPE
AS ENUM AS ENUM
('Account'); ('Account');
CREATE TABLE IF NOT EXISTS public.user ( CREATE TABLE IF NOT EXISTS
public.user (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL, email TEXT NOT NULL,
password TEXT NOT NULL, password TEXT NOT NULL,
@ -58,7 +75,7 @@ INSERT INTO
CREATE TABLE IF NOT EXISTS CREATE TABLE IF NOT EXISTS
public.account ( public.account (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id UUID PRIMARY KEY DEFAULT uuidv7(),
account_type account_type NOT NULL, account_type account_type NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT NULL, description TEXT NULL,
@ -72,7 +89,7 @@ CREATE TABLE IF NOT EXISTS
public.user_account_permission ( public.user_account_permission (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id INT NOT NULL REFERENCES public.user(id), user_id INT NOT NULL REFERENCES public.user(id),
account_id INT NOT NULL REFERENCES public.account(id), account_id UUID NOT NULL REFERENCES public.account(id),
permission_id INT NOT NULL REFERENCES public.permission(id), permission_id INT NOT NULL REFERENCES public.permission(id),
status status NOT NULL DEFAULT 'Active', status status NOT NULL DEFAULT 'Active',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
@ -84,7 +101,7 @@ CREATE INDEX IF NOT EXISTS user_account_permission_account_id_idx ON public.user
CREATE TABLE IF NOT EXISTS CREATE TABLE IF NOT EXISTS
public.budget ( public.budget (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id UUID PRIMARY KEY DEFAULT uuidv7(),
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT NULL, description TEXT NULL,
icon TEXT NULL, icon TEXT NULL,
@ -95,9 +112,9 @@ CREATE TABLE IF NOT EXISTS
CREATE TABLE IF NOT EXISTS CREATE TABLE IF NOT EXISTS
public.transaction ( public.transaction (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id UUID PRIMARY KEY DEFAULT uuidv7(),
description TEXT NOT NULL, description TEXT NOT NULL,
budget_id INT NOT NULL REFERENCES public.budget(id), budget_id UUID NOT NULL REFERENCES public.budget(id),
status status NOT NULL DEFAULT 'Active', status status NOT NULL DEFAULT 'Active',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NULL updated_at TIMESTAMP WITH TIME ZONE NULL
@ -107,9 +124,9 @@ CREATE INDEX IF NOT EXISTS transaction_budget_id_idx ON public.transaction(budge
CREATE TABLE IF NOT EXISTS CREATE TABLE IF NOT EXISTS
public.transaction_line_item ( public.transaction_line_item (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id UUID PRIMARY KEY DEFAULT uuidv7(),
transaction_id INT NOT NULL REFERENCES public.transaction(id), transaction_id UUID NOT NULL REFERENCES public.transaction(id),
account_id INT NOT NULL REFERENCES public.account(id), account_id UUID NOT NULL REFERENCES public.account(id),
entry_type entry_type NOT NULL, entry_type entry_type NOT NULL,
value NUMERIC(12, 2) NOT NULL, value NUMERIC(12, 2) NOT NULL,
status status NOT NULL DEFAULT 'Active', status status NOT NULL DEFAULT 'Active',

View file

@ -1,5 +1,6 @@
use sqlx::prelude::*; use sqlx::prelude::*;
use tracing::error; use tracing::error;
use uuid::Uuid;
use crate::models::AppError; use crate::models::AppError;
@ -19,7 +20,7 @@ pub struct NewAccountEntity {
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, FromRow)] #[derive(Debug, FromRow)]
pub struct AccountEntity { pub struct AccountEntity {
pub id: i32, pub id: Uuid,
pub account_type: AccountType, pub account_type: AccountType,
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,
@ -30,7 +31,7 @@ pub struct AccountEntity {
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, Default, FromRow)] #[derive(Debug, Default, FromRow)]
pub struct AccountWithPermissionValueEntity { pub struct AccountWithPermissionValueEntity {
pub id: i32, pub id: Uuid,
pub account_type: AccountType, pub account_type: AccountType,
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,

View file

@ -1,5 +1,6 @@
use sqlx::prelude::*; use sqlx::prelude::*;
use tracing::error; use tracing::error;
use uuid::Uuid;
use crate::models::AppError; use crate::models::AppError;
@ -12,7 +13,7 @@ use super::{
#[derive(Clone)] #[derive(Clone)]
pub struct NewUserAccountPermissionEntity { pub struct NewUserAccountPermissionEntity {
pub user_id: i32, pub user_id: i32,
pub account_id: i32, pub account_id: Uuid,
pub permission_id: i32, pub permission_id: i32,
} }
@ -21,20 +22,20 @@ pub struct NewUserAccountPermissionEntity {
pub struct UserAccountPermissionEntity { pub struct UserAccountPermissionEntity {
pub id: i32, pub id: i32,
pub user_id: i32, pub user_id: i32,
pub account_id: i32, pub account_id: Uuid,
pub permission_id: i32, pub permission_id: i32,
pub status: StatusType, pub status: StatusType,
} }
pub async fn associate_account_with_user_as_owner( pub async fn associate_account_with_user_as_owner(
pool: &DbPool, pool: &DbPool,
account_id: i32, account_id: Uuid,
user_id: i32, user_id: i32,
) -> Result<Vec<PermissionEntity>, AppError> { ) -> Result<Vec<PermissionEntity>, AppError> {
let values = get_all_permissions_by_category(pool, PermissionCategoryType::Account) let values = get_all_permissions_by_category(pool, PermissionCategoryType::Account)
.await? .await?
.into_iter() .into_iter()
.map(|permission| format!("({user_id}, {account_id}, {})", permission.id)) .map(|permission| format!("({user_id}, '{account_id}', {})", permission.id))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(","); .join(",");
@ -48,7 +49,7 @@ pub async fn associate_account_with_user_as_owner(
.inspect_err(|err| { .inspect_err(|err| {
error!( error!(
?err, ?err,
account_id, user_id, "Cannot associate account with user as owner" %account_id, user_id, "Cannot associate account with user as owner"
) )
}) })
.map_err(|err| AppError::from(err)) .map_err(|err| AppError::from(err))

View file

@ -1,4 +1,5 @@
use serde::Serialize; use serde::Serialize;
use uuid::Uuid;
use crate::db::{AccountEntity, PermissionEntity}; use crate::db::{AccountEntity, PermissionEntity};
@ -6,7 +7,7 @@ use super::AccountType;
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct AccountCreationResponse { pub struct AccountCreationResponse {
pub id: i32, pub id: Uuid,
pub r#type: AccountType, pub r#type: AccountType,
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,

View file

@ -1,10 +1,11 @@
use serde::Serialize; use serde::Serialize;
use uuid::Uuid;
use crate::db::{AccountType, AccountWithPermissionValueEntity}; use crate::db::{AccountType, AccountWithPermissionValueEntity};
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct AccountsReadAllResponse { pub struct AccountsReadAllResponse {
pub id: i32, pub id: Uuid,
pub account_type: AccountType, pub account_type: AccountType,
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,