Security: force first-login password change, gate admin tools, validate restore

Fixes from the code review:
- #1 Wire mustChangePassword: dashboard layout redirects to a new /change-password
  page + /api/account/change-password (verifies current pw, re-hashes, clears the
  flag, refreshes the session). Provisioned admins can no longer keep the default.
- #2 Assert NEXTAUTH_SECRET in production (not during build); set it on authOptions.
- #3 Backup page now server-gated to admins (split into server page + client panel),
  matching the Updates page.
- #4 Restore validates input: strict zod schema for the users table (known fields,
  role enum) + array checks for the rest; returns generic errors instead of String(err).

No version bump/changelog — left for you to fold into the next release (you're
editing those live). Not pushed, not deployed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 23:59:39 -05:00
parent c5bf108cb9
commit 0904790dca
8 changed files with 413 additions and 148 deletions

View File

@@ -29,7 +29,22 @@ declare module "next-auth/jwt" {
}
}
// Fail loudly in production if the JWT secret is missing or left at the
// placeholder — otherwise sessions are forgeable. Skipped during `next build`
// (no runtime env) and in development.
const NEXTAUTH_SECRET = process.env.NEXTAUTH_SECRET;
if (
process.env.NODE_ENV === "production" &&
process.env.NEXT_PHASE !== "phase-production-build" &&
(!NEXTAUTH_SECRET || NEXTAUTH_SECRET.length < 16 || NEXTAUTH_SECRET.includes("change-me"))
) {
throw new Error(
"NEXTAUTH_SECRET is missing or insecure — generate one with `openssl rand -base64 32` and set it in the environment."
);
}
export const authOptions: NextAuthOptions = {
secret: NEXTAUTH_SECRET,
session: { strategy: "jwt" },
pages: { signIn: "/login", error: "/login" },
providers: [