Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -76,4 +77,4 @@ EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME=0.0.0.0

CMD ["node", "server.js"]
CMD ["node", "server.js"]
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,8 @@ View container logs:
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.

---

## Roadmap
Expand Down
22 changes: 22 additions & 0 deletions docs/self-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

---

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 the `packageManager` field in `package.json` (`pnpm@11.9.0`). Pull the latest code and rebuild with `docker compose build --no-cache`. Do not rely on unpinned Corepack downloads.

- **`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 `<NEXTAUTH_URL>/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**:
Expand Down
19 changes: 2 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,9 @@
"@opentelemetry/resources": ">=2.8.0",
"js-yaml": ">=4.1.0"
},
"packageManager": "pnpm@11.9.0",
"engines": {
"node": "20.x"
"node": "20.x || 22.x"
},
"optionalDependencies": {
"@esbuild/linux-x64": "^0.28.0",
Expand All @@ -111,21 +112,5 @@
"@parcel/watcher-linux-x64-glibc": "^2.5.6",
"@parcel/watcher-linux-x64-musl": "^2.5.6",
"@swc/core-linux-x64-gnu": "^1.15.41"
},
"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"
]
}
}
61 changes: 61 additions & 0 deletions src/lib/auth-config.ts
Original file line number Diff line number Diff line change
@@ -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);
}
},
};
14 changes: 12 additions & 2 deletions src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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
Expand Down Expand Up @@ -47,15 +50,22 @@ 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
// return false → error=github redirect. Skip the upsert gracefully
// 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;
}
Expand Down
Loading