DRE customer portal: FastAPI + SQLite backend (18 endpoints)
- 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
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
-- ============================================================
|
||||
-- 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
|
||||
);
|
||||
Reference in New Issue
Block a user