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",
"runtime-tokio",
"rust_decimal",
"uuid"
] }
syslog-tracing = "0.3.1"
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_email_uniq_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
public.status
AS ENUM
@ -18,7 +34,8 @@ CREATE TYPE
AS ENUM
('Account');
CREATE TABLE IF NOT EXISTS public.user (
CREATE TABLE IF NOT EXISTS
public.user (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL,
password TEXT NOT NULL,
@ -58,7 +75,7 @@ INSERT INTO
CREATE TABLE IF NOT EXISTS
public.account (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
id UUID PRIMARY KEY DEFAULT uuidv7(),
account_type account_type NOT NULL,
name TEXT NOT NULL,
description TEXT NULL,
@ -72,7 +89,7 @@ CREATE TABLE IF NOT EXISTS
public.user_account_permission (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
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),
status status NOT NULL DEFAULT 'Active',
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
public.budget (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
id UUID PRIMARY KEY DEFAULT uuidv7(),
name TEXT NOT NULL,
description TEXT NULL,
icon TEXT NULL,
@ -95,9 +112,9 @@ CREATE TABLE IF NOT EXISTS
CREATE TABLE IF NOT EXISTS
public.transaction (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
id UUID PRIMARY KEY DEFAULT uuidv7(),
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',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
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
public.transaction_line_item (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
transaction_id INT NOT NULL REFERENCES public.transaction(id),
account_id INT NOT NULL REFERENCES public.account(id),
id UUID PRIMARY KEY DEFAULT uuidv7(),
transaction_id UUID NOT NULL REFERENCES public.transaction(id),
account_id UUID NOT NULL REFERENCES public.account(id),
entry_type entry_type NOT NULL,
value NUMERIC(12, 2) NOT NULL,
status status NOT NULL DEFAULT 'Active',

View file

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

View file

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

View file

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

View file

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