Google OAuth cannot provide phone numbers (no scope returns them reliably), so we collect it ourselves. Optional, stored unverified. - Migration 0011: users.phone text column (+ debug-migration hot-apply step) - schema/auth.ts: add phone field - onboarding: optional phone input on step 1; saved to users.phone via the onboarding API (normalised: leading + then digits, 8-15 digit validation) - profile page: editable Phone field; loaded from + saved to /api/auth/profile - /api/auth/profile: GET returns phone; POST accepts & normalises it (empty string clears, undefined leaves untouched) Capture point covers both Google and email/password signups since both land on onboarding. Verification (OTP) and marketing-consent flag intentionally deferred per product decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
120 lines
7.9 KiB
TypeScript
120 lines
7.9 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { sql } from "@/db";
|
|
import { requireFamily } from "@/lib/auth";
|
|
|
|
export async function GET() {
|
|
const auth = await requireFamily();
|
|
if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
|
|
|
// Each probe is independent so one failure never blanks the whole diagnostic.
|
|
const out: Record<string, unknown> = {};
|
|
|
|
// pgvector status — FIRST and standalone (this is what diagnoses the
|
|
// "could not access file vector" production error). pg_available_extensions
|
|
// reads the on-disk extension catalog and never loads the vector library, so
|
|
// it works even when the image is missing the pgvector binaries.
|
|
try {
|
|
const vectorRows = await sql.unsafe(
|
|
`SELECT name, default_version, installed_version FROM pg_available_extensions WHERE name = 'vector'`
|
|
);
|
|
const v = vectorRows[0] as { default_version?: string; installed_version?: string } | undefined;
|
|
out.pgvector = {
|
|
binaryAvailable: !!v, // false → wrong Postgres image (no pgvector binaries)
|
|
installed: !!v?.installed_version, // false → needs CREATE EXTENSION vector
|
|
availableVersion: v?.default_version ?? null,
|
|
installedVersion: v?.installed_version ?? null,
|
|
};
|
|
} catch (e) {
|
|
out.pgvectorError = String(e);
|
|
}
|
|
|
|
// Does the error_events table exist yet? (admin Errors page depends on it)
|
|
try {
|
|
const r = await sql.unsafe(`SELECT to_regclass('public.error_events') AS reg`);
|
|
out.errorEventsExists = !!(r[0] as { reg?: string })?.reg;
|
|
} catch (e) {
|
|
out.errorEventsError = String(e);
|
|
}
|
|
|
|
// Applied drizzle migrations (the journal lives in the "drizzle" schema).
|
|
try {
|
|
out.migrations = await sql.unsafe(
|
|
`SELECT hash, created_at FROM drizzle.__drizzle_migrations ORDER BY created_at DESC LIMIT 12`
|
|
);
|
|
} catch (e) {
|
|
out.migrationsError = String(e);
|
|
}
|
|
|
|
try {
|
|
out.circleTables = await sql.unsafe(
|
|
`SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND (tablename LIKE 'circle%' OR tablename = 'post_reports') ORDER BY tablename`
|
|
);
|
|
} catch (e) {
|
|
out.circleTablesError = String(e);
|
|
}
|
|
|
|
return NextResponse.json(out);
|
|
}
|
|
|
|
// One-shot: apply circles migration via the app DB connection (requires login)
|
|
export async function POST(req: Request) {
|
|
const auth = await requireFamily();
|
|
if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
|
|
|
// Extra gate: require a confirmation header
|
|
if (req.headers.get("x-run-migration") !== "yes") {
|
|
return NextResponse.json({ error: "Missing confirmation header" }, { status: 400 });
|
|
}
|
|
|
|
const steps = [
|
|
// One-time fix: memories stuck in 'processing' (vision pipeline was marking them failed)
|
|
// Reset to 'ready' so they're visible again — vision captions are optional
|
|
`UPDATE memories SET processing_status = 'ready', updated_at = now() WHERE processing_status = 'processing' AND created_at < NOW() - INTERVAL '10 minutes'`,
|
|
// family_invites missing columns (0006)
|
|
`ALTER TABLE family_invites ADD COLUMN IF NOT EXISTS display_name text`,
|
|
`ALTER TABLE family_invites ADD COLUMN IF NOT EXISTS accepted_at timestamp`,
|
|
// subscription_status on families (0007) — payment-provider abstraction
|
|
`ALTER TABLE families ADD COLUMN IF NOT EXISTS subscription_status varchar(20) DEFAULT NULL`,
|
|
// pediatrician_name on families (0008)
|
|
`ALTER TABLE families ADD COLUMN IF NOT EXISTS pediatrician_name text`,
|
|
// circles tables (0003)
|
|
`CREATE TABLE IF NOT EXISTS circles (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, created_by uuid NOT NULL REFERENCES families(id), created_at timestamptz NOT NULL DEFAULT now())`,
|
|
`CREATE TABLE IF NOT EXISTS circle_members (circle_id uuid NOT NULL REFERENCES circles(id) ON DELETE CASCADE, family_id uuid NOT NULL REFERENCES families(id), role text NOT NULL DEFAULT 'member', joined_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (circle_id, family_id))`,
|
|
`CREATE TABLE IF NOT EXISTS circle_invites (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), circle_id uuid NOT NULL REFERENCES circles(id) ON DELETE CASCADE, token text NOT NULL UNIQUE, created_by uuid NOT NULL REFERENCES families(id), expires_at timestamptz NOT NULL, consumed_at timestamptz, created_at timestamptz NOT NULL DEFAULT now())`,
|
|
`CREATE TABLE IF NOT EXISTS circle_posts (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), circle_id uuid NOT NULL REFERENCES circles(id) ON DELETE CASCADE, author_family_id uuid NOT NULL REFERENCES families(id), body text, image_key text, source_kind text, created_at timestamptz NOT NULL DEFAULT now())`,
|
|
`CREATE TABLE IF NOT EXISTS circle_post_comments (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), post_id uuid NOT NULL REFERENCES circle_posts(id) ON DELETE CASCADE, author_family_id uuid NOT NULL REFERENCES families(id), body text NOT NULL, created_at timestamptz NOT NULL DEFAULT now())`,
|
|
`CREATE TABLE IF NOT EXISTS circle_post_reactions (post_id uuid NOT NULL REFERENCES circle_posts(id) ON DELETE CASCADE, family_id uuid NOT NULL REFERENCES families(id), emoji text NOT NULL, PRIMARY KEY (post_id, family_id, emoji))`,
|
|
`CREATE TABLE IF NOT EXISTS post_reports (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), post_id uuid NOT NULL REFERENCES circle_posts(id) ON DELETE CASCADE, reported_by uuid NOT NULL REFERENCES families(id), reason text, created_at timestamptz NOT NULL DEFAULT now())`,
|
|
`CREATE INDEX IF NOT EXISTS circle_members_family_idx ON circle_members(family_id)`,
|
|
`CREATE INDEX IF NOT EXISTS circle_members_circle_idx ON circle_members(circle_id)`,
|
|
`CREATE INDEX IF NOT EXISTS circle_posts_circle_idx ON circle_posts(circle_id)`,
|
|
`CREATE INDEX IF NOT EXISTS circle_posts_author_idx ON circle_posts(author_family_id)`,
|
|
`CREATE INDEX IF NOT EXISTS circle_comments_post_idx ON circle_post_comments(post_id)`,
|
|
`CREATE INDEX IF NOT EXISTS circle_reactions_post_idx ON circle_post_reactions(post_id)`,
|
|
`CREATE INDEX IF NOT EXISTS circle_invites_token_idx ON circle_invites(token)`,
|
|
// 0009 — notifications table
|
|
`CREATE TABLE IF NOT EXISTS notifications (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), family_id UUID NOT NULL REFERENCES families(id) ON DELETE CASCADE, child_id UUID REFERENCES children(id) ON DELETE CASCADE, type VARCHAR(80) NOT NULL, title TEXT NOT NULL, message TEXT NOT NULL, action_url TEXT, is_read BOOLEAN NOT NULL DEFAULT false, scheduled_for DATE NOT NULL, metadata JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW())`,
|
|
`CREATE UNIQUE INDEX IF NOT EXISTS notifications_unique_slot ON notifications(family_id, child_id, type, scheduled_for)`,
|
|
`CREATE INDEX IF NOT EXISTS notifications_family_child_idx ON notifications(family_id, child_id, is_read, created_at DESC)`,
|
|
// 0010 — error_events table (admin error/crash tracker)
|
|
`CREATE TABLE IF NOT EXISTS error_events (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), level varchar(20) NOT NULL DEFAULT 'error', source varchar(20) NOT NULL DEFAULT 'client', message text NOT NULL, stack text, url text, digest varchar(120), user_id uuid, family_id uuid, user_agent text, metadata jsonb DEFAULT '{}', created_at timestamptz NOT NULL DEFAULT now())`,
|
|
`CREATE INDEX IF NOT EXISTS idx_error_events_created ON error_events (created_at DESC)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_error_events_source ON error_events (source)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_error_events_message ON error_events (message)`,
|
|
// 0011 — optional user phone number
|
|
`ALTER TABLE users ADD COLUMN IF NOT EXISTS phone text`,
|
|
];
|
|
|
|
const results: string[] = [];
|
|
for (const step of steps) {
|
|
try {
|
|
await sql.unsafe(step);
|
|
results.push(`OK: ${step.slice(0, 60)}`);
|
|
} catch (err: unknown) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
results.push(`ERR: ${step.slice(0, 60)} → ${msg}`);
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({ results });
|
|
}
|