diff --git a/.env.local.example b/.env.local.example index e8fe612..c5a2137 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,3 +1,12 @@ # Get a free API key at https://dev.pokemontcg.io/ -# Then copy this file to .env.local and paste your key below +# Then copy this file to .env.local and paste your key(s) below + +# Client-side API key (exposed to browser) NEXT_PUBLIC_POKEMON_TCG_API_KEY= + +# Optional: Server-side API key (preferred). If set, server uses this and does not rely on public key. +POKEMON_TCG_API_KEY= + +# Optional: Enable very simple single-user Basic Auth (set both to enable) +BASIC_AUTH_USER= +BASIC_AUTH_PASS= diff --git a/docker-compose.yml b/docker-compose.yml index 1217299..7b8d8a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,9 @@ services: # Forward any API keys if needed - NEXT_PUBLIC_POKEMON_TCG_API_KEY=${NEXT_PUBLIC_POKEMON_TCG_API_KEY} - POKEMON_TCG_API_KEY=${POKEMON_TCG_API_KEY} + # Optional: enable very simple single-user Basic Auth + - BASIC_AUTH_USER=${BASIC_AUTH_USER} + - BASIC_AUTH_PASS=${BASIC_AUTH_PASS} ports: # Select host port via APP_PORT env var; default 3000 - "${APP_PORT:-3000}:3000" diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..eddbb4c --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,55 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +// Very simple single-user HTTP Basic Auth. +// Set BASIC_AUTH_USER and BASIC_AUTH_PASS in the environment to enable. +// If these are not set, auth is disabled and all requests pass through. +export function middleware(req: NextRequest) { + const user = process.env.BASIC_AUTH_USER; + const pass = process.env.BASIC_AUTH_PASS; + + if (!user || !pass) { + // Auth disabled + return NextResponse.next(); + } + + const header = req.headers.get('authorization') || ''; + const prefix = 'Basic '; + + if (!header.startsWith(prefix)) { + return unauthorized('Authentication required'); + } + + try { + const decoded = atob(header.slice(prefix.length)); + const idx = decoded.indexOf(':'); + const u = decoded.slice(0, idx); + const p = decoded.slice(idx + 1); + + if (u === user && p === pass) { + return NextResponse.next(); + } + } catch { + // fallthrough + } + + return unauthorized('Invalid credentials'); +} + +function unauthorized(message: string) { + return new NextResponse(message, { + status: 401, + headers: { + 'WWW-Authenticate': 'Basic realm="Restricted", charset="UTF-8"', + 'Content-Type': 'text/plain; charset=utf-8', + }, + }); +} + +// Apply to all routes (including API). Static assets will also be behind auth; browsers will reuse credentials. +export const config = { + matcher: [ + // Exclude Next static assets and image optimizer + '/((?!_next/static|_next/image).*)', + ], +};