diff --git a/.github/workflows/automated-tests.yml b/.github/workflows/automated-tests.yml
index 22438c06d..c67f7c3cc 100644
--- a/.github/workflows/automated-tests.yml
+++ b/.github/workflows/automated-tests.yml
@@ -15,15 +15,13 @@ jobs:
- name: Checkout Code Repository Layers
uses: actions/checkout@v7
- - uses: pnpm/action-setup@v6
- with:
- version: 11
-
- name: Initialize Node.js Environment
uses: actions/setup-node@v6
with:
node-version: 22
- cache: 'pnpm'
+
+ - name: Enable pnpm
+ run: corepack enable && corepack prepare pnpm@11.9.0 --activate
- name: Install Project Dependencies (Clean Architecture Ingest)
run: pnpm install --frozen-lockfile
@@ -33,4 +31,3 @@ jobs:
- name: Execute Security & Code Quality Monitoring Audits
run: echo "Code coverage thresholds verified at 80%+. Monitoring data packages fully initialized."
-
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 428bb6922..b4a780194 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,13 +19,12 @@ jobs:
steps:
- uses: actions/checkout@v7
- - uses: pnpm/action-setup@v6
- with:
- version: 11
- uses: actions/setup-node@v6
with:
node-version: 22
- cache: pnpm
+
+ - name: Enable pnpm
+ run: corepack enable && corepack prepare pnpm@11.9.0 --activate
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -39,13 +38,12 @@ jobs:
steps:
- uses: actions/checkout@v7
- - uses: pnpm/action-setup@v6
- with:
- version: 11
- uses: actions/setup-node@v6
with:
node-version: 22
- cache: pnpm
+
+ - name: Enable pnpm
+ run: corepack enable && corepack prepare pnpm@11.9.0 --activate
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -62,13 +60,12 @@ jobs:
steps:
- uses: actions/checkout@v7
- - uses: pnpm/action-setup@v6
- with:
- version: 11
- uses: actions/setup-node@v6
with:
node-version: 22
- cache: pnpm
+
+ - name: Enable pnpm
+ run: corepack enable && corepack prepare pnpm@11.9.0 --activate
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -108,13 +105,12 @@ jobs:
steps:
- uses: actions/checkout@v7
- - uses: pnpm/action-setup@v6
- with:
- version: 11
- uses: actions/setup-node@v6
with:
node-version: 22
- cache: pnpm
+
+ - name: Enable pnpm
+ run: corepack enable && corepack prepare pnpm@11.9.0 --activate
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -128,13 +124,12 @@ jobs:
steps:
- uses: actions/checkout@v7
- - uses: pnpm/action-setup@v6
- with:
- version: 11
- uses: actions/setup-node@v6
with:
node-version: 22
- cache: pnpm
+
+ - name: Enable pnpm
+ run: corepack enable && corepack prepare pnpm@11.9.0 --activate
- name: Install dependencies
run: pnpm install --frozen-lockfile
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 99da69366..b2dcc9c38 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -28,13 +28,12 @@ jobs:
steps:
- uses: actions/checkout@v7
- - uses: pnpm/action-setup@v6
- with:
- version: 11
- uses: actions/setup-node@v6
with:
node-version: 22
- cache: pnpm
+
+ - name: Enable pnpm
+ run: corepack enable && corepack prepare pnpm@11.9.0 --activate
- name: Install app dependencies
run: pnpm install --frozen-lockfile
diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml
index 401300788..311b20a8f 100644
--- a/.github/workflows/visual-regression.yml
+++ b/.github/workflows/visual-regression.yml
@@ -42,15 +42,13 @@ jobs:
- name: Checkout
uses: actions/checkout@v7
- - uses: pnpm/action-setup@v6
- with:
- version: 11
-
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 22
- cache: pnpm
+
+ - name: Enable pnpm
+ run: corepack enable && corepack prepare pnpm@11.9.0 --activate
- name: Install dependencies
run: pnpm install --frozen-lockfile
diff --git a/Dockerfile b/Dockerfile
index 800716ddc..354f8ec99 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,15 +1,16 @@
-FROM node:20-alpine AS base
+FROM node:22-alpine AS base
+RUN corepack enable && corepack prepare pnpm@11.9.0 --activate
# Install dependencies only when needed
FROM base AS deps
-RUN apk add --no-cache libc6-compat
+RUN apk add --no-cache libc6-compat python3 make g++
WORKDIR /app
# Install dependencies based on the preferred package manager
-COPY package.json package-lock.json* pnpm-lock.yaml* ./
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN \
- if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
+ if [ -f pnpm-lock.yaml ]; then pnpm i --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
else echo "Lockfile not found." && exit 1; \
fi
@@ -18,12 +19,12 @@ RUN \
FROM base AS development
WORKDIR /app
-RUN apk add --no-cache libc6-compat
+RUN apk add --no-cache libc6-compat python3 make g++
-COPY package.json package-lock.json* pnpm-lock.yaml* ./
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN \
- if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm install; \
+ if [ -f pnpm-lock.yaml ]; then pnpm install; \
elif [ -f package-lock.json ]; then npm install; \
else echo "Lockfile not found." && exit 1; \
fi
@@ -48,7 +49,7 @@ COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN \
- if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
+ if [ -f pnpm-lock.yaml ]; then pnpm run build; \
elif [ -f package-lock.json ]; then npm run build; \
else echo "Lockfile not found." && exit 1; \
fi
@@ -76,4 +77,4 @@ EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
-CMD ["node", "server.js"]
\ No newline at end of file
+CMD ["node", "server.js"]
diff --git a/README.md b/README.md
index 93184d72a..4befb2adc 100644
--- a/README.md
+++ b/README.md
@@ -168,7 +168,189 @@ The full walkthrough — Supabase migrations, OAuth app setup, every environment
---
-## Tech Stack
+## Docker Development Setup
+
+DevTrack includes Docker support for local development, allowing contributors to get started quickly without manually installing dependencies or configuring environments.
+
+### Prerequisites
+
+- Docker Desktop (Windows/macOS) or Docker Engine (Linux)
+- Docker Compose v2+
+
+Verify installation:
+
+```bash
+docker --version
+docker compose version
+```
+
+### Configure Environment Variables
+
+Copy the example environment file:
+
+```bash
+cp .env.example .env.local
+```
+
+Fill in the required values as described in the Environment Variables section above.
+
+### Start the Application
+
+Build and start the development container:
+
+```bash
+docker compose up --build
+```
+
+The application will be available at:
+
+```text
+http://localhost:3000
+```
+
+### Stop the Application
+
+```bash
+docker compose down
+```
+
+### Hot Reload Support
+
+The project source code is mounted into the container using Docker volumes.
+
+Any changes made to files on your host machine are automatically reflected inside the container, enabling Next.js hot reload during development without rebuilding the image.
+
+### Rebuild After Dependency Changes
+
+If you modify `package.json` or install new dependencies:
+
+```bash
+docker compose down
+docker compose up --build
+```
+
+### Troubleshooting
+
+Remove containers and rebuild from scratch:
+
+```bash
+docker compose down -v
+docker compose up --build
+```
+
+View container logs:
+
+```bash
+docker compose logs -f
+```
+
+Common self-hosting build issues (pnpm/Corepack mismatch, `ERR_PNPM_IGNORED_BUILDS`, missing native build tools, GitHub OAuth `error=github`) are documented in [docs/self-hosting.md](docs/self-hosting.md#docker-build-troubleshooting). Set `AUTH_DEBUG=true` in your env file for detailed NextAuth logs during OAuth setup.
+
+---
+
+## FAQ
+
+Answers to questions new contributors and self-hosters ask most often. For deeper setup detail, see [DEVELOPMENT.md](./DEVELOPMENT.md) and the [Self-Hosting Guide](./docs/self-hosting.md).
+
+
+How do I configure GitHub OAuth?
+
+1. Go to [GitHub → Settings → Developer Settings → OAuth Apps](https://github.com/settings/applications/new) and create a new OAuth App.
+2. Set the **Authorization callback URL** to:
+ - Local dev: `http://localhost:3000/api/auth/callback/github`
+ - Production: `https:///api/auth/callback/github`
+3. Copy the generated **Client ID** and **Client Secret** into `GITHUB_ID` and `GITHUB_SECRET` in `.env.local`.
+4. Make sure `NEXTAUTH_URL` matches the base URL you're running on, and `NEXTAUTH_SECRET` is set (generate one with `openssl rand -base64 32`).
+
+
+
+
+Why is login not working?
+
+This is almost always one of the following:
+
+- **Callback URL mismatch** — the URL registered on your GitHub OAuth App must exactly match `NEXTAUTH_URL` + `/api/auth/callback/github`, including protocol and trailing slashes.
+- **Missing/incorrect `NEXTAUTH_SECRET`** — sessions will silently fail without it.
+- **Stale `.env.local`** — restart `pnpm dev` after changing any auth-related env vars; Next.js doesn't hot-reload env files.
+- **Supabase RLS blocking the user row** — check that the relevant migrations from `supabase/migrations/` have been applied in order.
+
+If the issue persists, check your browser console and terminal logs for the specific NextAuth error code, then search/open a [Discussion](https://github.com/Priyanshu-byte-coder/devtrack/discussions).
+
+
+
+
+How do I obtain Supabase credentials?
+
+1. Create a free project at [supabase.com](https://supabase.com).
+2. Open **Project Settings → API**.
+3. Copy:
+ - **Project URL** → `NEXT_PUBLIC_SUPABASE_URL`
+ - **anon public key** → `NEXT_PUBLIC_SUPABASE_ANON_KEY`
+ - **service_role key** → `SUPABASE_SERVICE_ROLE_KEY` (server-side only — never expose this in client code or commit it)
+4. Run all SQL files in `supabase/migrations/` via the Supabase SQL editor, in order, before starting the app.
+
+
+
+
+Why are GitHub metrics not loading?
+
+- You're likely hitting **GitHub's unauthenticated API rate limit**. Set the optional `GITHUB_TOKEN` (a personal access token) in `.env.local` to raise the limit significantly.
+- Check that your GitHub OAuth scopes were granted during sign-in — if you denied a permission, re-authenticate by signing out and back in.
+- If you're self-hosting behind a proxy or firewall, confirm outbound requests to `api.github.com` aren't being blocked.
+- Look at the server logs/terminal for the specific API error (403 usually means rate-limited; 401 means a token problem).
+
+
+
+
+How do I run tests?
+
+```bash
+# Unit tests
+pnpm test
+
+# End-to-end tests (Playwright, first-time setup)
+npx playwright install --with-deps chromium
+pnpm run test:e2e
+
+# Run a single e2e spec
+npx playwright test e2e/goals.spec.ts
+
+# Visual regression tests
+npx playwright test -c playwright.visual.config.mjs
+```
+
+E2E tests use mocked external calls (no real GitHub/Supabase credentials needed) and also run automatically on every PR via `.github/workflows/e2e.yml`.
+
+
+
+
+How can I contribute?
+
+1. Browse [open issues](https://github.com/Priyanshu-byte-coder/devtrack/issues) and start with one labeled `good first issue`.
+2. Comment on the issue to get assigned before starting work.
+3. Fork the repo, branch off `main` (e.g. `feat/issue-42-description`), and open a PR.
+4. Before pushing, make sure CI passes locally:
+ ```bash
+ pnpm run lint && pnpm run type-check
+ ```
+5. See [CONTRIBUTING.md](./CONTRIBUTING.md) for commit message style, branch naming conventions, and the review process.
+
+Questions are welcome anytime in [Discussions](https://github.com/Priyanshu-byte-coder/devtrack/discussions).
+
+
+
+
+Which Node.js and pnpm versions are supported?
+
+| Tool | Version | Check |
+|------|---------|-------|
+| Node.js | >= 20 | `node -v` |
+| pnpm | >= 9 | `pnpm -v` |
+| Git | any | `git --version` |
+
+Install pnpm via `corepack enable` or `npm install -g pnpm` if you don't already have it.
+
+If your local versions differ and you hit install/build errors, aligning your Node.js/pnpm version with the table above is the first thing to check.
| Layer | Technology |
|---|---|
diff --git a/docs/self-hosting.md b/docs/self-hosting.md
index 1b38cf93b..d3f98830e 100644
--- a/docs/self-hosting.md
+++ b/docs/self-hosting.md
@@ -53,6 +53,7 @@ DevTrack uses `@supabase/supabase-js` which relies on the Supabase REST API, so
| `WAKATIME_CLIENT_ID` | Optional | WakaTime OAuth Client ID (for WakaTime integration) | `waka_...` |
| `WAKATIME_CLIENT_SECRET` | Optional | WakaTime OAuth Client Secret | `waka_sec_...` |
| `ALLOWED_ORIGINS` | Optional | Comma-separated extra origins allowed for CSRF validation | `https://staging.example.com` |
+| `AUTH_DEBUG` | Optional | Set to `true` for verbose NextAuth OAuth logs (self-hosting troubleshooting) | `true` |
---
@@ -96,6 +97,8 @@ When running DevTrack with Docker, set the following environment variables in yo
```
5. DevTrack will be available at `http://localhost:3000`.
+> **Note:** The bundled `docker-compose.yml` targets the `development` stage and runs `npm run dev` with hot reload. For a production-style container, build the `production` target from the Dockerfile directly (see [Troubleshooting](#docker-build-troubleshooting)).
+
---
## 2. Railway
@@ -129,6 +132,25 @@ DevTrack includes a `render.yaml` Blueprint for easy deployment on Render's free
## 🔧 Troubleshooting
+### Docker build troubleshooting
+
+- **`pnpm` / Corepack version mismatch during `docker compose build`**:
+ The Dockerfile uses Node 22 with pnpm pinned via Corepack (`corepack prepare pnpm@11.9.0 --activate`). CI uses the same Corepack pin. Pull the latest code and rebuild with `docker compose build --no-cache`.
+
+- **`ERR_PNPM_IGNORED_BUILDS` / `pnpm approve-builds`**:
+ Build-script approval is configured non-interactively in `pnpm-workspace.yaml` (`allowBuilds`). The Dockerfile copies this file before `pnpm install`. If you see this error on an older clone, update and rebuild with `--no-cache`. Do not run interactive `pnpm approve-builds` inside CI or Docker builds.
+
+- **Native module build failures (`sharp`, `esbuild`, etc.)**:
+ The Dockerfile installs `python3`, `make`, and `g++` in the deps/development stages for Alpine. Rebuild with `docker compose build --no-cache` after pulling fixes.
+
+- **GitHub OAuth redirects with `error=github`**:
+ 1. Confirm `GITHUB_ID` and `GITHUB_SECRET` match your GitHub OAuth app.
+ 2. Set `NEXTAUTH_URL` to your public URL with no trailing slash (e.g. `https://devtrack.example.com`).
+ 3. Set the GitHub OAuth **Authorization callback URL** to `/api/auth/callback/github`.
+ 4. Enable verbose auth logging: set `AUTH_DEBUG=true` in your `.env` and inspect container logs (`docker compose logs -f`). Look for `[auth]` and `[nextauth]` lines.
+
+### General troubleshooting
+
- **Server Error 500 on Login**:
Make sure your `NEXTAUTH_SECRET` and `ENCRYPTION_KEY` are set. If `ENCRYPTION_KEY` is missing or the wrong length (must be 32 bytes / 64 hex chars), the OAuth callback will crash when attempting to encrypt the GitHub token.
- **Login Redirects back to Home Page infinitely**:
diff --git a/package.json b/package.json
index 7a4549cba..822433ea1 100644
--- a/package.json
+++ b/package.json
@@ -101,7 +101,7 @@
"js-yaml": ">=4.1.0"
},
"engines": {
- "node": "20.x"
+ "node": "20.x || 22.x"
},
"optionalDependencies": {
"@esbuild/linux-x64": "^0.28.0",
@@ -111,21 +111,5 @@
"@parcel/watcher-linux-x64-glibc": "^2.5.6",
"@parcel/watcher-linux-x64-musl": "^2.5.6",
"@swc/core-linux-x64-gnu": "^1.15.43"
- },
- "pnpm": {
- "onlyBuiltDependencies": [
- "@parcel/watcher",
- "@scarf/scarf",
- "@sentry/cli",
- "@swc/core",
- "@tree-sitter-grammars/tree-sitter-yaml",
- "core-js-pure",
- "core-js",
- "esbuild",
- "sharp",
- "tree-sitter-json",
- "tree-sitter",
- "unrs-resolver"
- ]
}
}
diff --git a/public/openapi.yaml b/public/openapi.yaml
index 01f56c03f..1366c5eba 100644
--- a/public/openapi.yaml
+++ b/public/openapi.yaml
@@ -865,8 +865,10 @@ paths:
type: object
'401':
description: Unauthorized
+ '429':
+ description: Rate limit exceeded
'500':
- description: AI service unavailable (GROQ_API_KEY not configured)
+ description: AI service unavailable (GEMINI_API_KEY not configured)
/api/ai/weekly-summary:
post:
diff --git a/src/app/api/ai/roast/route.ts b/src/app/api/ai/roast/route.ts
index 2eb468a8c..50d4b0bcf 100644
--- a/src/app/api/ai/roast/route.ts
+++ b/src/app/api/ai/roast/route.ts
@@ -1,11 +1,52 @@
import { NextResponse } from 'next/server';
import { GoogleGenerativeAI } from '@google/generative-ai';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { resolveAppUser } from '@/lib/resolve-user';
+import {
+ upstashRateLimitFixedWindow,
+ getUpstashConfig,
+ upstashPipeline,
+} from '@/lib/upstash-rest';
+import { createMemoryFixedWindowRateLimiter } from '@/lib/rate-limit';
+import crypto from 'crypto';
+
+export const dynamic = 'force-dynamic';
// Initialize the Google Generative AI SDK
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || '');
+const ROAST_LIMIT = 5;
+const ROAST_WINDOW_SECONDS = 60 * 60; // 1 hour
+const CACHE_TTL_SECONDS = 5 * 60; // 5 minutes
+
+// In-memory fallback rate limiter
+const memoryLimiter = createMemoryFixedWindowRateLimiter({
+ windowMs: ROAST_WINDOW_SECONDS * 1000,
+ pruneIntervalMs: ROAST_WINDOW_SECONDS * 1000,
+ maxEntries: 10_000,
+});
+
+// In-memory fallback response cache
+type CacheEntry = { value: string; expiresAt: number };
+const localCache = new Map();
+
export async function POST(req: Request) {
try {
+ // 1. Session check
+ const session = await getServerSession(authOptions);
+ if (!session?.githubId) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // 2. Resolve application user
+ const user = await resolveAppUser(session.githubId, session.githubLogin);
+ if (!user) {
+ return NextResponse.json({ error: 'User not found' }, { status: 404 });
+ }
+ const userId = user.id;
+
+ // 3. Parse and validate body
const body = await req.json();
const { mode, stats } = body;
@@ -16,15 +57,83 @@ export async function POST(req: Request) {
);
}
+ if (mode !== 'roast' && mode !== 'hype') {
+ return NextResponse.json({ error: 'Invalid mode.' }, { status: 400 });
+ }
+
+ // 4. Generate stats hash and check cache
+ const sortedLanguages = stats.languages ? [...stats.languages].sort() : [];
+ const statsString = JSON.stringify({
+ commits: stats.commits || 0,
+ languages: sortedLanguages,
+ mergedPRs: stats.mergedPRs || 0,
+ failedGoals: stats.failedGoals || 0,
+ });
+ const statsHash = crypto.createHash('sha256').update(statsString).digest('hex');
+ const cacheKey = `roast-cache:${userId}:${mode}:${statsHash}`;
+
+ // Read cache (Upstash Redis or in-memory)
+ let cachedMessage: string | null = null;
+ if (getUpstashConfig()) {
+ try {
+ const results = await upstashPipeline([['GET', cacheKey]]);
+ cachedMessage = (results[0]?.result as string) || null;
+ } catch (err) {
+ console.error('Failed to read roast from Upstash Redis:', err);
+ }
+ } else {
+ const entry = localCache.get(cacheKey);
+ if (entry && Date.now() <= entry.expiresAt) {
+ cachedMessage = entry.value;
+ } else if (entry) {
+ localCache.delete(cacheKey);
+ }
+ }
+
+ if (cachedMessage) {
+ return NextResponse.json({ message: cachedMessage, cached: true });
+ }
+
+ // 5. Rate limiting check
+ let rateLimitDenied = false;
+ let retryAfterSeconds = ROAST_WINDOW_SECONDS;
+
+ if (getUpstashConfig()) {
+ const result = await upstashRateLimitFixedWindow({
+ key: `roast-limit:${userId}`,
+ limit: ROAST_LIMIT,
+ windowSeconds: ROAST_WINDOW_SECONDS,
+ });
+ if (!result.allowed) {
+ rateLimitDenied = true;
+ retryAfterSeconds = result.retryAfter ?? ROAST_WINDOW_SECONDS;
+ }
+ } else {
+ const result = memoryLimiter.check(`roast-limit:${userId}`, ROAST_LIMIT);
+ if (!result.allowed) {
+ rateLimitDenied = true;
+ retryAfterSeconds = Math.max(result.reset - Math.floor(Date.now() / 1000), 1);
+ }
+ }
+
+ if (rateLimitDenied) {
+ return NextResponse.json(
+ { error: 'Rate limit exceeded. Try again later.' },
+ {
+ status: 429,
+ headers: { 'Retry-After': String(retryAfterSeconds) },
+ }
+ );
+ }
+
+ // 6. Gemini Generation
const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' });
let systemInstruction = '';
if (mode === 'roast') {
systemInstruction = `You are a hilariously brutal, sarcastic senior developer reviewing a junior's code stats. Roast their coding habits, commit streaks, or languages used based on the provided stats. Keep it strictly safe for work (SFW), funny, and under 3 sentences. No cursing.`;
- } else if (mode === 'hype') {
- systemInstruction = `You are the ultimate enthusiastic developer hype-man. Look at the user's coding stats and hype them up! Make them feel like a 10x coding god. Keep it energetic, modern, and under 3 sentences.`;
} else {
- return NextResponse.json({ error: 'Invalid mode.' }, { status: 400 });
+ systemInstruction = `You are the ultimate enthusiastic developer hype-man. Look at the user's coding stats and hype them up! Make them feel like a 10x coding god. Keep it energetic, modern, and under 3 sentences.`;
}
const prompt = `
@@ -40,9 +149,27 @@ export async function POST(req: Request) {
`;
const result = await model.generateContent(prompt);
- const responseText = result.response.text();
+ const responseText = (result.response.text() || '').trim();
+
+ if (!responseText) {
+ throw new Error('Empty response received from Gemini.');
+ }
+
+ // 7. Write to cache
+ if (getUpstashConfig()) {
+ try {
+ await upstashPipeline([['SET', cacheKey, responseText, 'EX', CACHE_TTL_SECONDS]]);
+ } catch (err) {
+ console.error('Failed to write roast to Upstash Redis:', err);
+ }
+ } else {
+ localCache.set(cacheKey, {
+ value: responseText,
+ expiresAt: Date.now() + CACHE_TTL_SECONDS * 1000,
+ });
+ }
- return NextResponse.json({ message: responseText.trim() });
+ return NextResponse.json({ message: responseText });
} catch (error) {
console.error('Gemini API Error:', error);
return NextResponse.json(
diff --git a/src/lib/auth-config.ts b/src/lib/auth-config.ts
new file mode 100644
index 000000000..6203c9066
--- /dev/null
+++ b/src/lib/auth-config.ts
@@ -0,0 +1,61 @@
+const PLACEHOLDER_PATTERNS = [
+ /^your[-_]/i,
+ /^changeme$/i,
+ /^placeholder$/i,
+ /^xxx+$/i,
+ /^todo$/i,
+];
+
+function isUnset(value: string | undefined): boolean {
+ if (!value || value.trim() === "") return true;
+ return PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(value.trim()));
+}
+
+function logAuthConfigStatus(): void {
+ const issues: string[] = [];
+
+ if (isUnset(process.env.GITHUB_ID)) {
+ issues.push("GITHUB_ID is unset — GitHub sign-in will fail");
+ }
+ if (isUnset(process.env.GITHUB_SECRET)) {
+ issues.push("GITHUB_SECRET is unset — GitHub sign-in will fail");
+ }
+ if (isUnset(process.env.NEXTAUTH_SECRET)) {
+ issues.push("NEXTAUTH_SECRET is unset — sessions cannot be signed");
+ }
+ if (isUnset(process.env.NEXTAUTH_URL)) {
+ issues.push(
+ "NEXTAUTH_URL is unset — OAuth callbacks may redirect incorrectly"
+ );
+ } else if (process.env.NEXTAUTH_URL?.endsWith("/")) {
+ issues.push(
+ "NEXTAUTH_URL has a trailing slash — remove it (e.g. https://example.com)"
+ );
+ }
+
+ if (issues.length > 0) {
+ console.warn(
+ "[auth] Self-hosting configuration issues:\n - " + issues.join("\n - ")
+ );
+ } else if (process.env.AUTH_DEBUG === "true") {
+ console.info("[auth] GitHub OAuth and NextAuth env vars look configured");
+ }
+}
+
+logAuthConfigStatus();
+
+export const authDebugEnabled = process.env.AUTH_DEBUG === "true";
+
+export const nextAuthLogger = {
+ error(code: string, metadata: unknown) {
+ console.error("[nextauth]", code, metadata);
+ },
+ warn(code: string) {
+ console.warn("[nextauth]", code);
+ },
+ debug(code: string, metadata: unknown) {
+ if (authDebugEnabled) {
+ console.debug("[nextauth]", code, metadata);
+ }
+ },
+};
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index 66b24a92a..ce9fa5903 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -1,5 +1,6 @@
import { type NextAuthOptions } from "next-auth";
import GitHubProvider from "next-auth/providers/github";
+import { authDebugEnabled, nextAuthLogger } from "./auth-config";
import { syncGitHubAchievementsForUser } from "./github-achievements";
import { supabaseAdmin } from "./supabase";
@@ -14,6 +15,8 @@ const GITHUB_API = "https://api.github.com";
const TOKEN_VALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1000;
export const authOptions: NextAuthOptions = {
+ debug: authDebugEnabled,
+ logger: nextAuthLogger,
// Playwright runs on plain HTTP (127.0.0.1) and relies on the default
// `next-auth.session-token` cookie name. If NextAuth infers HTTPS via
// forwarded headers, it may switch to secure cookie prefixes and the E2E
@@ -47,6 +50,13 @@ export const authOptions: NextAuthOptions = {
if (account?.provider === "github" && profile) {
const p = profile as { id: number; login: string; email?: string };
+ if (authDebugEnabled) {
+ console.debug("[auth] GitHub signIn callback", {
+ login: p.login,
+ supabaseConfigured: Boolean(supabaseAdmin),
+ });
+ }
+
// Guard: supabaseAdmin is null when Supabase env vars are missing or
// contain placeholder values (see src/lib/supabase.ts). Calling .from()
// on null throws a TypeError which NextAuth silently converts to
@@ -54,8 +64,8 @@ export const authOptions: NextAuthOptions = {
// so authentication can still succeed with degraded functionality.
if (!supabaseAdmin) {
console.warn(
- "signIn: supabaseAdmin is not configured; skipping DB upsert. " +
- "Set NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in .env.local."
+ "[auth] supabaseAdmin is not configured; skipping DB upsert. " +
+ "Set NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY."
);
return true;
}