Security & Stability: - Add Next.js 16 proxy.ts for BetterAuth cookie-based auth protection - Add rate limiting for API routes (src/lib/rate-limit.ts) - Add Zod validation for chat API request bodies - Add session auth check to chat and diagnostics endpoints - Add security headers in next.config.ts (CSP, X-Frame-Options, etc.) - Add file upload validation and sanitization in storage.ts Core UX Components: - Add error boundaries (error.tsx, not-found.tsx, chat/error.tsx) - Add loading states (skeleton.tsx, spinner.tsx, loading.tsx files) - Add toast notifications with Sonner - Add form components (input.tsx, textarea.tsx, label.tsx) - Add database indexes for performance (schema.ts) - Enhance chat UX: timestamps, copy-to-clipboard, thinking indicator, error display, localStorage message persistence Polish & Accessibility: - Add Open Graph and Twitter card metadata - Add JSON-LD structured data for SEO - Add sitemap.ts, robots.ts, manifest.ts - Add skip-to-content link and ARIA labels in site-header - Enable profile page quick action buttons with dialogs - Update Next.js 15 references to Next.js 16 Developer Experience: - Add GitHub Actions CI workflow (lint, typecheck, build) - Add Prettier configuration (.prettierrc, .prettierignore) - Add .nvmrc pinning Node 20 - Add ESLint rules: import/order, react-hooks/exhaustive-deps - Add stricter TypeScript settings (exactOptionalPropertyTypes, noImplicitOverride) - Add interactive setup script (scripts/setup.ts) - Add session utility functions (src/lib/session.ts) All changes mirrored to create-agentic-app/template/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
170 lines
4.9 KiB
TypeScript
170 lines
4.9 KiB
TypeScript
import { headers } from "next/headers";
|
|
import { NextResponse } from "next/server";
|
|
import { auth } from "@/lib/auth";
|
|
|
|
type StatusLevel = "ok" | "warn" | "error";
|
|
|
|
interface DiagnosticsResponse {
|
|
timestamp: string;
|
|
env: {
|
|
POSTGRES_URL: boolean;
|
|
BETTER_AUTH_SECRET: boolean;
|
|
GOOGLE_CLIENT_ID: boolean;
|
|
GOOGLE_CLIENT_SECRET: boolean;
|
|
OPENROUTER_API_KEY: boolean;
|
|
NEXT_PUBLIC_APP_URL: boolean;
|
|
};
|
|
database: {
|
|
connected: boolean;
|
|
schemaApplied: boolean;
|
|
error?: string;
|
|
};
|
|
auth: {
|
|
configured: boolean;
|
|
routeResponding: boolean | null;
|
|
};
|
|
ai: {
|
|
configured: boolean;
|
|
};
|
|
storage: {
|
|
configured: boolean;
|
|
type: "local" | "remote";
|
|
};
|
|
overallStatus: StatusLevel;
|
|
}
|
|
|
|
export async function GET(req: Request) {
|
|
// Require authentication for diagnostics endpoint
|
|
const session = await auth.api.getSession({ headers: await headers() });
|
|
if (!session) {
|
|
return NextResponse.json(
|
|
{ error: "Unauthorized. Please sign in to access diagnostics." },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
const env = {
|
|
POSTGRES_URL: Boolean(process.env.POSTGRES_URL),
|
|
BETTER_AUTH_SECRET: Boolean(process.env.BETTER_AUTH_SECRET),
|
|
GOOGLE_CLIENT_ID: Boolean(process.env.GOOGLE_CLIENT_ID),
|
|
GOOGLE_CLIENT_SECRET: Boolean(process.env.GOOGLE_CLIENT_SECRET),
|
|
OPENROUTER_API_KEY: Boolean(process.env.OPENROUTER_API_KEY),
|
|
NEXT_PUBLIC_APP_URL: Boolean(process.env.NEXT_PUBLIC_APP_URL),
|
|
} as const;
|
|
|
|
// Database checks with timeout
|
|
let dbConnected = false;
|
|
let schemaApplied = false;
|
|
let dbError: string | undefined;
|
|
if (env.POSTGRES_URL) {
|
|
try {
|
|
// Add timeout to prevent hanging on unreachable database
|
|
const dbCheckPromise = (async () => {
|
|
const [{ db }, { sql }, schema] = await Promise.all([
|
|
import("@/lib/db"),
|
|
import("drizzle-orm"),
|
|
import("@/lib/schema"),
|
|
]);
|
|
|
|
// Ping DB - this will actually attempt to connect
|
|
const result = await db.execute(sql`SELECT 1 as ping`);
|
|
if (!result) {
|
|
throw new Error("Database query returned no result");
|
|
}
|
|
dbConnected = true;
|
|
|
|
try {
|
|
// Touch a known table to verify migrations
|
|
await db.select().from(schema.user).limit(1);
|
|
schemaApplied = true;
|
|
} catch {
|
|
schemaApplied = false;
|
|
// If we can't query the user table, it's likely migrations haven't run
|
|
if (!dbError) {
|
|
dbError = "Schema not applied. Run: npm run db:migrate";
|
|
}
|
|
}
|
|
})();
|
|
|
|
const timeoutPromise = new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error("Database connection timeout (5s)")), 5000)
|
|
);
|
|
|
|
await Promise.race([dbCheckPromise, timeoutPromise]);
|
|
} catch {
|
|
dbConnected = false;
|
|
schemaApplied = false;
|
|
|
|
// Provide user-friendly error messages
|
|
dbError = "Database not connected. Please start your PostgreSQL database and verify your POSTGRES_URL in .env";
|
|
}
|
|
} else {
|
|
dbConnected = false;
|
|
schemaApplied = false;
|
|
dbError = "POSTGRES_URL is not set";
|
|
}
|
|
|
|
// Auth route check: we consider the route responding if it returns any HTTP response
|
|
// for /api/auth/session (status codes in the 2xx-4xx range are acceptable for readiness)
|
|
const origin = (() => {
|
|
try {
|
|
return new URL(req.url).origin;
|
|
} catch {
|
|
return process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
|
}
|
|
})();
|
|
|
|
let authRouteResponding: boolean | null = null;
|
|
try {
|
|
const res = await fetch(`${origin}/api/auth/session`, {
|
|
method: "GET",
|
|
headers: { Accept: "application/json" },
|
|
cache: "no-store",
|
|
});
|
|
authRouteResponding = res.status >= 200 && res.status < 500;
|
|
} catch {
|
|
authRouteResponding = false;
|
|
}
|
|
|
|
const authConfigured =
|
|
env.BETTER_AUTH_SECRET && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET;
|
|
const aiConfigured = env.OPENROUTER_API_KEY; // We avoid live-calling the AI provider here
|
|
|
|
// Storage configuration check
|
|
const storageConfigured = Boolean(process.env.BLOB_READ_WRITE_TOKEN);
|
|
const storageType: "local" | "remote" = storageConfigured ? "remote" : "local";
|
|
|
|
const overallStatus: StatusLevel = (() => {
|
|
if (!env.POSTGRES_URL || !dbConnected || !schemaApplied) return "error";
|
|
if (!authConfigured) return "error";
|
|
// AI is optional; warn if not configured
|
|
if (!aiConfigured) return "warn";
|
|
return "ok";
|
|
})();
|
|
|
|
const body: DiagnosticsResponse = {
|
|
timestamp: new Date().toISOString(),
|
|
env,
|
|
database: {
|
|
connected: dbConnected,
|
|
schemaApplied,
|
|
...(dbError !== undefined && { error: dbError }),
|
|
},
|
|
auth: {
|
|
configured: authConfigured,
|
|
routeResponding: authRouteResponding,
|
|
},
|
|
ai: {
|
|
configured: aiConfigured,
|
|
},
|
|
storage: {
|
|
configured: storageConfigured,
|
|
type: storageType,
|
|
},
|
|
overallStatus,
|
|
};
|
|
|
|
return NextResponse.json(body, {
|
|
status: 200,
|
|
});
|
|
}
|