browser-based admin setup on first visit, no CLI needed
This commit is contained in:
@@ -40,7 +40,68 @@ async function ensureLinkedUser(adminId: string): Promise<string> {
|
||||
}, { isolationLevel: "Serializable" });
|
||||
}
|
||||
|
||||
const setupBody = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8, "Password must be at least 8 characters"),
|
||||
});
|
||||
|
||||
export default async function adminAuthRoutes(app: FastifyInstance) {
|
||||
// check if initial setup is needed (no admin exists)
|
||||
app.get(
|
||||
"/admin/setup-status",
|
||||
{ config: { rateLimit: { max: 30, timeWindow: "1 minute" } } },
|
||||
async (_req, reply) => {
|
||||
const count = await prisma.adminUser.count();
|
||||
reply.send({ needsSetup: count === 0 });
|
||||
}
|
||||
);
|
||||
|
||||
// initial admin setup (only works when no admin exists)
|
||||
app.post<{ Body: z.infer<typeof setupBody> }>(
|
||||
"/admin/setup",
|
||||
{ config: { rateLimit: { max: 3, timeWindow: "15 minutes" } } },
|
||||
async (req, reply) => {
|
||||
const count = await prisma.adminUser.count();
|
||||
if (count > 0) {
|
||||
reply.status(403).send({ error: "Setup already completed" });
|
||||
return;
|
||||
}
|
||||
|
||||
const body = setupBody.parse(req.body);
|
||||
const hash = await bcrypt.hash(body.password, 12);
|
||||
|
||||
const admin = await prisma.adminUser.create({
|
||||
data: { email: body.email, passwordHash: hash, role: "SUPER_ADMIN" },
|
||||
});
|
||||
|
||||
const linkedUserId = await ensureLinkedUser(admin.id);
|
||||
|
||||
const adminToken = jwt.sign(
|
||||
{ sub: admin.id, type: "admin", aud: "echoboard:admin", iss: "echoboard" },
|
||||
config.JWT_SECRET,
|
||||
{ expiresIn: "4h" }
|
||||
);
|
||||
const userToken = jwt.sign(
|
||||
{ sub: linkedUserId, type: "passkey", aud: "echoboard:user", iss: "echoboard" },
|
||||
config.JWT_SECRET,
|
||||
{ expiresIn: "4h" }
|
||||
);
|
||||
|
||||
reply
|
||||
.setCookie("echoboard_admin", adminToken, {
|
||||
path: "/", httpOnly: true, sameSite: "strict",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
maxAge: 60 * 60 * 4,
|
||||
})
|
||||
.setCookie("echoboard_passkey", userToken, {
|
||||
path: "/", httpOnly: true, sameSite: "strict",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
maxAge: 60 * 60 * 4,
|
||||
})
|
||||
.send({ ok: true });
|
||||
}
|
||||
);
|
||||
|
||||
app.post<{ Body: z.infer<typeof loginBody> }>(
|
||||
"/admin/login",
|
||||
{ config: { rateLimit: { max: 5, timeWindow: "15 minutes" } } },
|
||||
|
||||
Reference in New Issue
Block a user