- Magic-link auth (sha256-only token storage, 15-min single-use, 7-day sessions) - Staff-key auth via X-DRE-Staff-Key (constant-time compare) - SQLite WAL, foreign_keys, parameterized queries, atomic DRE/CLT sequence allocation - Intake validator rejects SSN/PAN patterns (FDCPA/TDCPA compliance) - Document upload allowlist + magic-byte check, 20MB cap - Unified error envelope, money as integer cents - systemd unit (port 8093, User=root, hardening directives) - Fixes import bug (auth.py relative imports) and audit_log placeholder mismatch
140 lines
6.7 KiB
SQL
140 lines
6.7 KiB
SQL
-- ============================================================
|
|
-- DRE Customer Portal — SQLite Schema (spec §1)
|
|
-- DB file: /opt/dre-portal/data/dre.db
|
|
-- Pragmas (set on every connection): foreign_keys=ON, journal_mode=WAL, busy_timeout=5000
|
|
-- All timestamps ISO-8601 UTC TEXT. Money as INTEGER cents. PKs TEXT UUID4.
|
|
-- ============================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS clients (
|
|
id TEXT PRIMARY KEY, -- uuid4
|
|
client_number TEXT UNIQUE NOT NULL, -- CLT-YYYY-NNNN
|
|
company_name TEXT NOT NULL,
|
|
contact_name TEXT NOT NULL,
|
|
email TEXT UNIQUE NOT NULL, -- lowercased; magic-link identity
|
|
phone TEXT,
|
|
tos_accepted_at TEXT, -- set when ToS accepted at intake
|
|
twentycrm_id TEXT, -- nullable; set by future CRM sync
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_clients_email ON clients(email);
|
|
|
|
CREATE TABLE IF NOT EXISTS debtors (
|
|
id TEXT PRIMARY KEY, -- uuid4
|
|
name TEXT NOT NULL, -- business or individual name
|
|
business_type TEXT NOT NULL DEFAULT 'OTHER'
|
|
CHECK (business_type IN
|
|
('INDIVIDUAL','SOLE_PROPRIETORSHIP','LLC','CORPORATION','PARTNERSHIP','OTHER')),
|
|
contact_email TEXT,
|
|
contact_phone TEXT,
|
|
physical_address TEXT, -- free-text single line
|
|
twentycrm_id TEXT, -- nullable
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS claims (
|
|
id TEXT PRIMARY KEY, -- uuid4
|
|
claim_number TEXT UNIQUE NOT NULL, -- DRE-YYYY-NNNN
|
|
client_id TEXT NOT NULL REFERENCES clients(id) ON DELETE RESTRICT,
|
|
debtor_id TEXT NOT NULL REFERENCES debtors(id) ON DELETE RESTRICT,
|
|
amount_cents INTEGER NOT NULL CHECK (amount_cents > 0),
|
|
currency TEXT NOT NULL DEFAULT 'USD',
|
|
status TEXT NOT NULL DEFAULT 'NEW'
|
|
CHECK (status IN
|
|
('NEW','UNDER_REVIEW','ACTIVE','NEGOTIATION','LEGAL','SETTLED','CLOSED','WRITE_OFF','REJECTED')),
|
|
tier TEXT NOT NULL DEFAULT 'TIER_1'
|
|
CHECK (tier IN ('TIER_1','TIER_2','TIER_2_5','TIER_3','TIER_4')),
|
|
description TEXT,
|
|
client_reference TEXT,
|
|
invoice_date TEXT, -- ISO date
|
|
date_assigned TEXT, -- set when moved out of NEW
|
|
date_resolved TEXT, -- set on SETTLED/CLOSED/WRITE_OFF
|
|
twentycrm_id TEXT, -- nullable
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_claims_client ON claims(client_id);
|
|
CREATE INDEX IF NOT EXISTS idx_claims_status ON claims(status);
|
|
CREATE INDEX IF NOT EXISTS idx_claims_debtor ON claims(debtor_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS documents (
|
|
id TEXT PRIMARY KEY, -- uuid4
|
|
claim_id TEXT NOT NULL REFERENCES claims(id) ON DELETE CASCADE,
|
|
original_name TEXT NOT NULL, -- sanitized display name
|
|
stored_path TEXT NOT NULL, -- absolute path on disk (uuid-named)
|
|
mime_type TEXT NOT NULL,
|
|
size_bytes INTEGER NOT NULL,
|
|
sha256 TEXT NOT NULL, -- integrity + dedupe
|
|
uploaded_by TEXT NOT NULL DEFAULT 'CLIENT' -- CLIENT | STAFF
|
|
CHECK (uploaded_by IN ('CLIENT','STAFF')),
|
|
twentycrm_id TEXT, -- nullable
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_documents_claim ON documents(claim_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS case_notes (
|
|
id TEXT PRIMARY KEY, -- uuid4
|
|
claim_id TEXT NOT NULL REFERENCES claims(id) ON DELETE CASCADE,
|
|
author_type TEXT NOT NULL
|
|
CHECK (author_type IN ('CLIENT','STAFF','SYSTEM')),
|
|
author_name TEXT NOT NULL,
|
|
subject TEXT, -- for client->team structured messages
|
|
content TEXT NOT NULL, -- plaintext; rendered escaped
|
|
visibility TEXT NOT NULL DEFAULT 'SHARED'
|
|
CHECK (visibility IN ('SHARED','INTERNAL')),
|
|
twentycrm_id TEXT, -- nullable
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_notes_claim ON case_notes(claim_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS auth_tokens (
|
|
id TEXT PRIMARY KEY, -- uuid4
|
|
client_id TEXT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
|
token_hash TEXT UNIQUE NOT NULL, -- sha256 of raw token
|
|
expires_at TEXT NOT NULL, -- created_at + 15 min
|
|
consumed_at TEXT, -- NULL = unused
|
|
requested_ip TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_tokens_hash ON auth_tokens(token_hash);
|
|
CREATE INDEX IF NOT EXISTS idx_tokens_client ON auth_tokens(client_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id TEXT PRIMARY KEY, -- uuid4
|
|
client_id TEXT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
|
session_hash TEXT UNIQUE NOT NULL, -- sha256 of raw session token
|
|
expires_at TEXT NOT NULL, -- + 7 days
|
|
revoked_at TEXT,
|
|
created_at TEXT NOT NULL,
|
|
last_seen_at TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_hash ON sessions(session_hash);
|
|
|
|
CREATE TABLE IF NOT EXISTS audit_log (
|
|
id TEXT PRIMARY KEY, -- uuid4
|
|
entity_type TEXT NOT NULL, -- 'claim'|'client'|'document'|'note'
|
|
entity_id TEXT NOT NULL,
|
|
action TEXT NOT NULL, -- 'create'|'status_change'|'update'|'upload'|'note_add'
|
|
field TEXT,
|
|
old_value TEXT,
|
|
new_value TEXT,
|
|
actor TEXT NOT NULL,
|
|
reason TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_log(entity_type, entity_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS number_sequences (
|
|
prefix TEXT NOT NULL, -- 'DRE'|'CLT'
|
|
year INTEGER NOT NULL,
|
|
last_value INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (prefix, year)
|
|
);
|
|
|
|
-- Migration tracking table
|
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
version TEXT PRIMARY KEY,
|
|
applied_at TEXT NOT NULL
|
|
);
|