OTP overhaul: test-user bypass + hash-at-rest + Fazpass integration
- Test-OTP bypass allowlist for Apple reviewers / QA: phone-scoped static OTPs
managed in CC (Settings → Test OTP Bypass), bcrypt-hashed on save, kill-switch
toggle, per-entry expires_at. New `otp_requests` columns (is_bypass, code_hash)
+ DB CHECK enforcing bypass-row shape.
- Hash-at-rest for stub OTPs: replaced plaintext `<ref>:<code>` storage with
bcrypt(code_hash); reference goes to fazpass_reference alone. Verify routes on
sovereign is_bypass flag, defers code_hash-NULL rows to Fazpass.
- Fazpass integration (gated by FAZPASS_ENABLED env, default off): new
fazpass.service.js calling /v1/otp/{request,verify}; distinct errors for wrong
OTP (CODE_MISMATCH 401) vs provider outage (OTP_PROVIDER_FAILED 502).
- Removed redundant Free Trial CC section (was a back-compat shim for the same
pricing_promotions row as "Diskon Sesi Pertama") + unused alias in
pricing.service.js.
208 tests green (34 new for OTP + Fazpass). Fazpass API + dashboard PDFs added
at project root for reference (docs are auth-gated).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -403,6 +403,18 @@ const migrate = async () => {
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_auth_sessions_expires ON auth_sessions (expires_at) WHERE revoked_at IS NULL`
|
||||
|
||||
// OTP requests (Fazpass reference + rate-limit tracking)
|
||||
//
|
||||
// Storage shape rationale:
|
||||
// - is_bypass : explicit intent flag — true only when a row was created by
|
||||
// the test-OTP-bypass allowlist (phone-scoped static OTP for
|
||||
// App Store reviewers). Verify routes on this flag, NOT on
|
||||
// the mere presence of code_hash.
|
||||
// - code_hash : bcrypt hash of the OTP code, present whenever the backend
|
||||
// owns verification (stub-mode rows + bypass rows). NULL when
|
||||
// Fazpass owns verification (post-cutover, non-bypass rows).
|
||||
// - CHECK constraint: bypass rows MUST have code_hash and MUST NOT carry a
|
||||
// Fazpass reference — physically prevents a bypass row from
|
||||
// ever falling into the Fazpass-verify path.
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS otp_requests (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -414,12 +426,36 @@ const migrate = async () => {
|
||||
attempts INT NOT NULL DEFAULT 0,
|
||||
used_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
is_bypass BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
code_hash VARCHAR(255)
|
||||
)
|
||||
`
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_otp_requests_phone_created ON otp_requests (phone, created_at)`
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_otp_requests_ip_created ON otp_requests (ip_address, created_at)`
|
||||
|
||||
// Idempotent ALTERs for DBs created before is_bypass/code_hash were added.
|
||||
await sql`
|
||||
ALTER TABLE otp_requests
|
||||
ADD COLUMN IF NOT EXISTS is_bypass BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS code_hash VARCHAR(255)
|
||||
`
|
||||
|
||||
// Drop-then-add lets us tighten the invariant later without writing a v2.
|
||||
// The constraint is defense-in-depth alongside the verifyOtp branching: even
|
||||
// if app code regressed, the DB refuses to insert a corrupt bypass row.
|
||||
await sql`ALTER TABLE otp_requests DROP CONSTRAINT IF EXISTS otp_requests_bypass_shape`
|
||||
await sql`
|
||||
ALTER TABLE otp_requests
|
||||
ADD CONSTRAINT otp_requests_bypass_shape CHECK (
|
||||
is_bypass = FALSE OR (
|
||||
is_bypass = TRUE
|
||||
AND code_hash IS NOT NULL
|
||||
AND fazpass_reference IS NULL
|
||||
)
|
||||
)
|
||||
`
|
||||
|
||||
// Auth-related app_config defaults
|
||||
await sql`
|
||||
INSERT INTO app_config (key, value) VALUES
|
||||
|
||||
Reference in New Issue
Block a user