Complete SaaS self-service onboarding sprint: - Stripe-powered signup flow: pricing page → checkout → provisioning → activation - Refresh token infrastructure: 1h access tokens + 30-day httpOnly cookie refresh - TOTP MFA with QR setup, recovery codes, and login challenge flow - Google + Azure AD SSO (conditional on env vars) with account linking - WebAuthn passkey registration and passwordless login - Guided onboarding checklist with server-side progress tracking - Stubbed email service (console + DB logging, ready for real provider) - Settings page with tabbed security settings (MFA, passkeys, linked accounts) - Login page enhanced with MFA verification, SSO buttons, passkey login - Database migration 015 with all new tables and columns - Version bump to 2026.03.17 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
108 lines
5.3 KiB
SQL
108 lines
5.3 KiB
SQL
-- Migration 015: SaaS Onboarding + Auth (Stripe, Refresh Tokens, MFA, SSO, Passkeys)
|
|
-- Adds tables for refresh tokens, stripe event tracking, invite tokens,
|
|
-- onboarding progress, and WebAuthn passkeys.
|
|
|
|
-- ============================================================================
|
|
-- 1. Modify shared.organizations — add Stripe billing columns
|
|
-- ============================================================================
|
|
ALTER TABLE shared.organizations ADD COLUMN IF NOT EXISTS stripe_customer_id VARCHAR(255) UNIQUE;
|
|
ALTER TABLE shared.organizations ADD COLUMN IF NOT EXISTS stripe_subscription_id VARCHAR(255) UNIQUE;
|
|
ALTER TABLE shared.organizations ADD COLUMN IF NOT EXISTS trial_ends_at TIMESTAMPTZ;
|
|
|
|
-- Update plan_level CHECK constraint to include new SaaS plan tiers
|
|
-- (Drop and re-add since ALTER CHECK is not supported in PG)
|
|
ALTER TABLE shared.organizations DROP CONSTRAINT IF EXISTS organizations_plan_level_check;
|
|
ALTER TABLE shared.organizations ADD CONSTRAINT organizations_plan_level_check
|
|
CHECK (plan_level IN ('standard', 'premium', 'enterprise', 'starter', 'professional'));
|
|
|
|
-- ============================================================================
|
|
-- 2. New table: shared.refresh_tokens
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS shared.refresh_tokens (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES shared.users(id) ON DELETE CASCADE,
|
|
token_hash VARCHAR(255) UNIQUE NOT NULL,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
revoked_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON shared.refresh_tokens(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash ON shared.refresh_tokens(token_hash);
|
|
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires ON shared.refresh_tokens(expires_at);
|
|
|
|
-- ============================================================================
|
|
-- 3. New table: shared.stripe_events (idempotency for webhook processing)
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS shared.stripe_events (
|
|
id VARCHAR(255) PRIMARY KEY,
|
|
type VARCHAR(100) NOT NULL,
|
|
processed_at TIMESTAMPTZ DEFAULT NOW(),
|
|
payload JSONB
|
|
);
|
|
|
|
-- ============================================================================
|
|
-- 4. New table: shared.invite_tokens (magic link activation)
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS shared.invite_tokens (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
organization_id UUID NOT NULL REFERENCES shared.organizations(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES shared.users(id) ON DELETE CASCADE,
|
|
token_hash VARCHAR(255) UNIQUE NOT NULL,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
used_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_invite_tokens_hash ON shared.invite_tokens(token_hash);
|
|
CREATE INDEX IF NOT EXISTS idx_invite_tokens_user ON shared.invite_tokens(user_id);
|
|
|
|
-- ============================================================================
|
|
-- 5. New table: shared.onboarding_progress
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS shared.onboarding_progress (
|
|
organization_id UUID PRIMARY KEY REFERENCES shared.organizations(id) ON DELETE CASCADE,
|
|
completed_steps TEXT[] DEFAULT '{}',
|
|
completed_at TIMESTAMPTZ,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- ============================================================================
|
|
-- 6. New table: shared.user_passkeys (WebAuthn)
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS shared.user_passkeys (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES shared.users(id) ON DELETE CASCADE,
|
|
credential_id TEXT UNIQUE NOT NULL,
|
|
public_key TEXT NOT NULL,
|
|
counter BIGINT DEFAULT 0,
|
|
device_name VARCHAR(255),
|
|
transports TEXT[],
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
last_used_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_user_passkeys_user ON shared.user_passkeys(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_user_passkeys_cred ON shared.user_passkeys(credential_id);
|
|
|
|
-- ============================================================================
|
|
-- 7. Modify shared.users — add MFA/WebAuthn columns
|
|
-- ============================================================================
|
|
ALTER TABLE shared.users ADD COLUMN IF NOT EXISTS totp_verified_at TIMESTAMPTZ;
|
|
ALTER TABLE shared.users ADD COLUMN IF NOT EXISTS recovery_codes TEXT;
|
|
ALTER TABLE shared.users ADD COLUMN IF NOT EXISTS webauthn_challenge TEXT;
|
|
ALTER TABLE shared.users ADD COLUMN IF NOT EXISTS has_seen_intro BOOLEAN DEFAULT FALSE;
|
|
|
|
-- ============================================================================
|
|
-- 8. Stubbed email log table (for development — replaces real email sends)
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS shared.email_log (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
to_email VARCHAR(255) NOT NULL,
|
|
subject VARCHAR(500) NOT NULL,
|
|
body TEXT,
|
|
template VARCHAR(100),
|
|
metadata JSONB,
|
|
sent_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|