a478462aa4
- Add requireAdmin() to all unprotected admin server actions (clients/new, clients/[id], timer-actions — 17 functions total) - Protect /api/internal/* endpoints with X-Internal-Secret header (proxy.ts sends it; routes reject requests without it) - Randomize auto-generated client slugs with 4-char suffix to prevent enumeration via predictable name-based slugs - Add in-memory rate limiting to /api/client/approve (20/min) and /api/client/comment (10/min) per IP - Add security headers: X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, X-DNS-Prefetch-Control - Reduce JWT session from 30 days to 7 days with daily rotation - Remove hardcoded NEXTAUTH_URL from Dockerfile (pass via Coolify env) - Genericize client API error messages to not leak data structure - Update .env.example with all required variables including INTERNAL_SECRET Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
57 lines
1.7 KiB
TypeScript
57 lines
1.7 KiB
TypeScript
import type { NextAuthOptions } from "next-auth";
|
|
import CredentialsProvider from "next-auth/providers/credentials";
|
|
|
|
export const authOptions: NextAuthOptions = {
|
|
providers: [
|
|
CredentialsProvider({
|
|
name: "credentials",
|
|
credentials: {
|
|
email: { label: "Email", type: "email" },
|
|
password: { label: "Password", type: "password" },
|
|
},
|
|
async authorize(credentials) {
|
|
if (!credentials?.email || !credentials?.password) return null;
|
|
|
|
const adminEmail = process.env.ADMIN_EMAIL;
|
|
const adminPassword = process.env.ADMIN_PASSWORD;
|
|
|
|
if (!adminEmail || !adminPassword) {
|
|
throw new Error("ADMIN_EMAIL and ADMIN_PASSWORD env vars must be set");
|
|
}
|
|
|
|
if (
|
|
credentials.email === adminEmail &&
|
|
credentials.password === adminPassword
|
|
) {
|
|
// Return minimal session user — no DB lookup needed
|
|
return { id: "admin", email: adminEmail, name: "Admin" };
|
|
}
|
|
|
|
return null; // null = unauthorized (NextAuth returns 401)
|
|
},
|
|
}),
|
|
],
|
|
session: {
|
|
strategy: "jwt", // stateless JWT — no DB session table (per D-03)
|
|
maxAge: 7 * 24 * 60 * 60, // 7 days (reduced from 30 for security)
|
|
updateAge: 24 * 60 * 60, // rotate token daily on active use
|
|
},
|
|
pages: {
|
|
signIn: "/admin/login", // custom login page (per D-07)
|
|
},
|
|
callbacks: {
|
|
async jwt({ token, user }) {
|
|
if (user) {
|
|
token.id = user.id;
|
|
}
|
|
return token;
|
|
},
|
|
async session({ session, token }) {
|
|
if (session.user) {
|
|
(session.user as { id?: string }).id = token.id as string;
|
|
}
|
|
return session;
|
|
},
|
|
},
|
|
};
|