chore: update SQL script, dev script, and add zxcvbnjs

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-18 16:26:52 +01:00
co-authored by Sisyphus
parent f7cc278423
commit 3ff14fdd5a
5 changed files with 3429 additions and 140 deletions
+134 -7
View File
@@ -16,7 +16,7 @@ CREATE TYPE account_type AS ENUM ('email', 'google', 'microsoft', 'facebook', 'g
CREATE TYPE payment_type AS ENUM ('deposit', 'full', 'tip', 'balance', 'partial');
CREATE TYPE payment_method AS ENUM ('online_square', 'in_person_card', 'cash', 'giftcard', 'discount', 'on_the_house');
CREATE TYPE payment_status AS ENUM ('pending', 'completed', 'failed', 'refunded');
CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'in_progress', 'completed', 'client_cancelled', 'we_cancelled', 're-schedule', 'no_show', 'no_deposit');
CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'in_progress', 'completed', 'client_cancelled', 'we_cancelled', 'no_show', 'pending_release', 'deposit_lapsed');
CREATE TYPE campaign_type AS ENUM ('time_based', 'milestone');
CREATE TYPE milestone_type AS ENUM ('per_user_booking_count', 'global_booking_count', 'anniversary');
CREATE TYPE milestone_unit AS ENUM ('bookings', 'months', 'years');
@@ -76,6 +76,14 @@ CREATE OR REPLACE FUNCTION generate_square_deposit_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('square_deposits');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_patch_test_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('patch_tests');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_user_patch_test_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('user_patch_tests');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_verification_code() RETURNS CHAR(12) AS $$ SELECT substr(encode(gen_random_bytes(6), 'hex'), 1, 12); $$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_referral_code()
@@ -124,6 +132,9 @@ CREATE TABLE users (
-- Security fields
password_hash TEXT, -- NULL for pure social logins
last_login_at TIMESTAMPTZ DEFAULT NOW(),
-- Account lockout fields
failed_attempts INT NOT NULL DEFAULT 0,
locked_until TIMESTAMPTZ,
-- Loyalty fields
loyalty_stamps INT NOT NULL DEFAULT 0,
referral_code CHAR(12) UNIQUE DEFAULT generate_referral_code(),
@@ -156,6 +167,10 @@ CREATE TABLE user_social_logins (
CREATE INDEX idx_users_email_lower ON users (LOWER(email));
CREATE INDEX idx_users_account_role ON users (account_role);
-- pg_trgm GIN indexes for ILIKE search on user-facing columns
CREATE INDEX idx_users_fn_trgm ON users USING GIN (fn gin_trgm_ops);
CREATE INDEX idx_users_email_trgm ON users USING GIN (email gin_trgm_ops);
CREATE INDEX idx_users_phone_trgm ON users USING GIN (phone gin_trgm_ops);
-- Enforce unique email only for registered (non-guest) users
-- Guest accounts can share emails; registered accounts cannot
@@ -185,7 +200,7 @@ CREATE INDEX idx_verification_codes_expires ON verification_codes (expires_at) W
-- =======================================
CREATE TABLE patch_tests (
id CHAR(12) PRIMARY KEY DEFAULT generate_service_id(),
id CHAR(12) PRIMARY KEY DEFAULT generate_patch_test_id(),
name VARCHAR(100) NOT NULL,
description TEXT,
notice_duration_hours INT NOT NULL DEFAULT 24,
@@ -200,7 +215,7 @@ CREATE INDEX idx_patch_tests_name ON patch_tests(name);
-- =======================================
CREATE TABLE user_patch_tests (
id BIGSERIAL PRIMARY KEY,
id CHAR(12) PRIMARY KEY DEFAULT generate_user_patch_test_id(),
user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
patch_test_id CHAR(12) NOT NULL REFERENCES patch_tests(id) ON DELETE CASCADE,
tested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
@@ -247,6 +262,8 @@ CREATE TABLE custom_services (
);
CREATE INDEX idx_custom_services_name ON custom_services(name);
CREATE INDEX idx_custom_services_name_trgm ON custom_services USING GIN (name gin_trgm_ops);
CREATE INDEX idx_custom_services_desc_trgm ON custom_services USING GIN (description gin_trgm_ops);
CREATE INDEX idx_custom_services_usage ON custom_services(usage_count DESC);
-- =======================================
@@ -405,6 +422,7 @@ CREATE INDEX idx_time_blockers_cron ON time_blockers(cron_expression) WHERE cron
CREATE TABLE forgiven_no_shows (
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('forgiven_no_shows'),
booking_id CHAR(12) NOT NULL UNIQUE REFERENCES bookings(id) ON DELETE CASCADE,
forgiven_by CHAR(12),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
@@ -586,7 +604,7 @@ INSERT INTO business_settings (
'https://www.website.co.uk'
);
CREATE TYPE admin_notification_reason AS ENUM ('pending_booking', 'cancelled_booking', 'rescheduled_booking', '1_week_no_pay', '1_month_no_pay', 'affiliate_claim', 'late_cancellation', 'no_deposit', 'deposit_paid', 'edit_request', 'new_booking', 'edit_requested');
CREATE TYPE admin_notification_reason AS ENUM ('pending_booking', 'cancelled_booking', 'rescheduled_booking', '1_week_no_pay', '1_month_no_pay', 'affiliate_claim', 'late_cancellation', 'deposit_paid', 'edit_request', 'edit_requested', 'new_booking', 'deposit_not_paid_by_deadline');
CREATE TABLE admin_notifications (
id SERIAL PRIMARY KEY,
@@ -635,7 +653,9 @@ create table tags (
-- Indexes
create index idx_images_tag_names on images using gin(tag_names);
-- create index idx_images_tag_names_trgm on images using gin ((tag_names::text[]) gin_trgm_ops);
-- pg_trgm GIN index not applicable to tag_names (text[] array).
-- For ILIKE search on unnested tags at scale, normalize into a separate image_tags table
-- and add: CREATE INDEX idx_image_tags_name_trgm ON image_tags USING GIN (name gin_trgm_ops);
create index idx_images_created_at on images(created_at desc);
create index idx_tags_name_trgm on tags using gin (name gin_trgm_ops);
@@ -778,7 +798,9 @@ BEGIN
'data_retention_consent', data_retention_consent,
'data_consent_updated_at', data_consent_updated_at,
'created_at', created_at,
'updated_at', updated_at
'updated_at', updated_at,
'failed_attempts', failed_attempts,
'locked_until', locked_until
)
FROM users WHERE id = target_user_id
),
@@ -1029,11 +1051,51 @@ BEGIN
JOIN bookings b ON fns.booking_id = b.id
WHERE b.user_id = target_user_id
),
'gift_card_balance', (
SELECT COALESCE(
(SELECT json_build_object(
'balance', COALESCE(ugb.balance, 0),
'updated_at', ugb.updated_at
) FROM user_giftcard_balances ugb WHERE ugb.user_id = target_user_id),
json_build_object('balance', 0, 'updated_at', NULL)
)
),
'gift_card_transactions', (
SELECT COALESCE(json_agg(json_build_object(
'id', gct.id,
'transaction_type', gct.transaction_type,
'amount', gct.amount,
'reference_type', gct.reference_type,
'reference_id', gct.reference_id,
'notes', gct.notes,
'created_at', gct.created_at
) ORDER BY gct.created_at DESC), '[]'::json)
FROM gift_card_transactions gct
WHERE gct.user_id = target_user_id
),
'login_audit', (
(SELECT json_agg(json_build_object(
'attempt_type', la.attempt_type,
'ip_address', la.ip_address::text,
'success', la.success,
'created_at', la.created_at
) ORDER BY la.created_at DESC)
FROM login_audit la WHERE la.user_id = target_user_id)
),
'refresh_tokens', (
(SELECT json_agg(json_build_object(
'role', rt.role,
'revoked', rt.revoked,
'created_at', rt.created_at,
'expires_at', rt.expires_at
) ORDER BY rt.created_at DESC)
FROM refresh_tokens rt WHERE rt.user_id = target_user_id)
),
'export_metadata', json_build_object(
'exported_at', NOW(),
'exported_by', 'system',
'user_id', target_user_id,
'format_version', '0.9'
'format_version', '1.0'
)
) INTO result;
@@ -1966,6 +2028,44 @@ INSERT INTO dav_calendars (principaluri, displayname, uri, description, componen
SELECT 'principals/default', 'Default Calendar', 'default', 'Default calendar', 'VEVENT,VTODO', false
WHERE NOT EXISTS (SELECT 1 FROM dav_calendars WHERE uri = 'default');
-- =======================================
-- PG_TRGM GIN INDEXES FOR ILIKE SEARCH WITH LEADING WILDCARDS
-- These indexes enable efficient ILIKE search patterns like '%search%'
-- by using trigram matching via the pg_trgm extension (already enabled).
-- =======================================
CREATE INDEX IF NOT EXISTS idx_bookings_notes_trgm ON bookings USING GIN (notes gin_trgm_ops);
-- users: n_first_name and n_last_name for admin user search
CREATE INDEX IF NOT EXISTS idx_users_n_first_name_trgm ON users USING GIN (n_first_name gin_trgm_ops);
CREATE INDEX IF NOT EXISTS idx_users_n_last_name_trgm ON users USING GIN (n_last_name gin_trgm_ops);
-- services: name for booking search (ILIKE with leading wildcard; B-tree idx_services_name is useless for that)
CREATE INDEX IF NOT EXISTS idx_services_name_trgm ON services USING GIN (name gin_trgm_ops);
-- images: url and thumbnail_url for portfolio image lookup
CREATE INDEX IF NOT EXISTS idx_images_url_trgm ON images USING GIN (url gin_trgm_ops);
CREATE INDEX IF NOT EXISTS idx_images_thumbnail_url_trgm ON images USING GIN (thumbnail_url gin_trgm_ops);
-- time_blockers: description for ILIKE ANY search
CREATE INDEX IF NOT EXISTS idx_time_blockers_desc_trgm ON time_blockers USING GIN (description gin_trgm_ops);
-- Account lockout audit log
CREATE TABLE IF NOT EXISTS login_audit (
id BIGSERIAL PRIMARY KEY,
user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
ip_address INET NOT NULL,
attempt_type TEXT NOT NULL, -- 'login' | 'register' | 'refresh'
success BOOLEAN NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_login_audit_user_id ON login_audit (user_id);
CREATE INDEX IF NOT EXISTS idx_login_audit_ip ON login_audit (ip_address);
CREATE INDEX IF NOT EXISTS idx_login_audit_created_at ON login_audit (created_at);
-- =======================================
-- FUNCTION USAGE SUMMARY
-- =======================================
@@ -2031,3 +2131,30 @@ even after consent withdrawal. Only optional/marketing data is consent-governed.
• VAT registration day → enable_vat_registration() once, then apply_vat_to_payment()
• Dashboard load → get_sales_totals() + get_monthly_business_summary()
*/
-- ============================================================================
-- Auth tables
-- ============================================================================
-- Revoked JTIs for JWT token revocation
CREATE TABLE IF NOT EXISTS revoked_jtis (
jti TEXT PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_revoked_jtis_expires_at ON revoked_jtis (expires_at);
-- Refresh tokens with rotation tracking
CREATE TABLE IF NOT EXISTS refresh_tokens (
id BIGSERIAL PRIMARY KEY,
user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL UNIQUE,
role TEXT NOT NULL,
revoked BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens (user_id);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires_at ON refresh_tokens (expires_at);