name: status-page-auth description: > Single-password admin authentication for this status-page app. Use this skill whenever working on login, sessions, the session cookie, middleware route protection, or the Node-vs-Edge runtime split for auth. Trigger on phrases like "protect the admin", "login", "session cookie", "middleware auth", "verify the session", "admin password", or anything touching lib/auth.ts, lib/auth-edge.ts, or middleware.ts. Apply automatically when building or changing authentication here.
Status Page Auth (single password)
There is one admin, protected by a single password and a signed session cookie. No users table.
The Node/Edge split (critical)
Next.js middleware.ts runs in the Edge runtime — it has Web Crypto but no sqlite3 and no Node
crypto. Importing either into middleware breaks the build. Therefore auth is split into two files:
lib/auth.ts(Node runtime, used by route handlers only):hashPassword(pw)/verifyPassword(pw, hash)via Nodecrypto.scrypt+timingSafeEqual.createSession()→josenew SignJWT({ role:'admin' }).setProtectedHeader({alg:'HS256'}).setExpirationTime('7d').sign(secret).SESSION_COOKIE = 'sp_session', plus a helper to build theSet-Cookieoptions.
lib/auth-edge.ts(Edge-safe, imported ONLY bymiddleware.ts):verifySession(token)→jose.jwtVerify(token, secret); returns boolean. No DB, no Node crypto.joseworks in Edge because it uses Web Crypto — this is why we usejoseinstead of Node HMAC.
The signing secret comes from process.env.SESSION_SECRET (available in both runtimes). It MUST be stable
across restarts or all sessions invalidate.
middleware.ts
export const config = { matcher: ['/admin/:path*', '/api/admin/:path*'] };
- Allow
/admin/loginand/api/admin/auth/loginthrough unauthenticated (entry points). - Read the
sp_sessioncookie →verifySession. On failure:/api/admin/*→401JSON; page paths →NextResponse.redirect(new URL('/admin/login', req.url)).
Flows
- Login
POST /api/admin/auth/login {password}→getAdminAuth()resolves the expected credential (DBadmin.passwordHashfirst, elseenv.ADMIN_PASSWORD) → verify →createSession()→ set cookieHttpOnly; SameSite=Lax; Path=/; Secure (prod); Max-Age=7d. - Logout
POST /api/admin/auth/logout→ clear cookie (Max-Age=0).
Bootstrap rules
- If no DB password hash is set, accept
env.ADMIN_PASSWORD(timing-safe compare). - If neither a DB hash nor
ADMIN_PASSWORDis set, the login page shows a "set ADMIN_PASSWORD" notice and blank-password login is rejected. - Setting a password in the admin settings stores a scrypt hash in
settings.adminand takes precedence over env.