feat(auth): default to single-user mode with enable toggle

This commit is contained in:
Zimeng Xiong
2026-02-06 09:45:38 -08:00
parent 40a645b823
commit 7977a3eb09
11 changed files with 425 additions and 37 deletions
+131 -2
View File
@@ -45,10 +45,26 @@ const ensureSystemConfig = async () => {
return prisma.systemConfig.upsert({
where: { id: DEFAULT_SYSTEM_CONFIG_ID },
update: {},
create: { id: DEFAULT_SYSTEM_CONFIG_ID, registrationEnabled: false },
create: {
id: DEFAULT_SYSTEM_CONFIG_ID,
authEnabled: false,
registrationEnabled: false,
},
});
};
const ensureAuthEnabled = async (res: Response): Promise<boolean> => {
const systemConfig = await ensureSystemConfig();
if (!systemConfig.authEnabled) {
res.status(404).json({
error: "Not found",
message: "Authentication is disabled",
});
return false;
}
return true;
};
// Rate limiting for auth endpoints (stricter than general rate limiting)
const authRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
@@ -89,6 +105,10 @@ const adminRoleUpdateSchema = z.object({
role: z.enum(["ADMIN", "USER"]),
});
const authEnabledToggleSchema = z.object({
enabled: z.boolean(),
});
const findUserByIdentifier = async (identifier: string) => {
const trimmed = identifier.trim();
if (trimmed.length === 0) return null;
@@ -150,6 +170,7 @@ const getRefreshTokenExpiresAt = (): Date =>
*/
router.post("/register", authRateLimiter, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
const parsed = registerSchema.safeParse(req.body);
if (!parsed.success) {
@@ -380,6 +401,7 @@ router.post("/register", authRateLimiter, async (req: Request, res: Response) =>
*/
router.post("/login", authRateLimiter, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
const parsed = loginSchema.safeParse(req.body);
if (!parsed.success) {
@@ -507,6 +529,7 @@ router.post("/login", authRateLimiter, async (req: Request, res: Response) => {
*/
router.post("/refresh", async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
const { refreshToken: oldRefreshToken } = req.body;
if (!oldRefreshToken || typeof oldRefreshToken !== "string") {
@@ -639,6 +662,7 @@ router.post("/refresh", async (req: Request, res: Response) => {
*/
router.get("/me", requireAuth, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!req.user) {
return res.status(401).json({
error: "Unauthorized",
@@ -684,16 +708,31 @@ router.get("/me", requireAuth, async (req: Request, res: Response) => {
router.get("/status", optionalAuth, async (req: Request, res: Response) => {
try {
const systemConfig = await ensureSystemConfig();
if (!systemConfig.authEnabled) {
return res.json({
enabled: false,
authenticated: false,
authEnabled: false,
registrationEnabled: false,
bootstrapRequired: false,
user: null,
});
}
const bootstrapUser = await prisma.user.findUnique({
where: { id: BOOTSTRAP_USER_ID },
select: { id: true, isActive: true },
});
const activeUsers = await prisma.user.count({ where: { isActive: true } });
const bootstrapRequired =
Boolean(bootstrapUser && bootstrapUser.isActive === false) && activeUsers === 0;
res.json({
enabled: true,
authEnabled: true,
authenticated: Boolean(req.user),
registrationEnabled: systemConfig.registrationEnabled,
bootstrapRequired: Boolean(bootstrapUser && bootstrapUser.isActive === false),
bootstrapRequired,
user: req.user
? {
id: req.user.id,
@@ -714,12 +753,97 @@ router.get("/status", optionalAuth, async (req: Request, res: Response) => {
}
});
/**
* POST /auth/auth-enabled
* Enable/disable authentication mode.
*
* - Enabling auth is allowed without login (single-user mode).
* - Disabling auth requires an authenticated ADMIN.
*/
router.post("/auth-enabled", optionalAuth, async (req: Request, res: Response) => {
try {
const parsed = authEnabledToggleSchema.safeParse(req.body);
if (!parsed.success) {
return res
.status(400)
.json({ error: "Bad request", message: "Invalid toggle payload" });
}
const systemConfig = await ensureSystemConfig();
const current = systemConfig.authEnabled;
const next = parsed.data.enabled;
if (current && !next) {
if (!req.user) {
return res
.status(401)
.json({ error: "Unauthorized", message: "User not authenticated" });
}
if (req.user.role !== "ADMIN") {
return res
.status(403)
.json({ error: "Forbidden", message: "Admin access required" });
}
}
// Ensure the bootstrap user exists for the bootstrap registration flow.
if (!current && next) {
const bootstrap = await prisma.user.findUnique({
where: { id: BOOTSTRAP_USER_ID },
select: { id: true },
});
if (!bootstrap) {
await prisma.user.create({
data: {
id: BOOTSTRAP_USER_ID,
email: "bootstrap@excalidash.local",
username: null,
passwordHash: "",
name: "Bootstrap Admin",
role: "ADMIN",
mustResetPassword: true,
isActive: false,
},
});
}
}
const updated = await prisma.systemConfig.upsert({
where: { id: DEFAULT_SYSTEM_CONFIG_ID },
update: { authEnabled: next },
create: {
id: DEFAULT_SYSTEM_CONFIG_ID,
authEnabled: next,
registrationEnabled: systemConfig.registrationEnabled,
},
});
const bootstrapUser = await prisma.user.findUnique({
where: { id: BOOTSTRAP_USER_ID },
select: { id: true, isActive: true },
});
const activeUsers = await prisma.user.count({ where: { isActive: true } });
const bootstrapRequired =
Boolean(updated.authEnabled && bootstrapUser && bootstrapUser.isActive === false) &&
activeUsers === 0;
res.json({ authEnabled: updated.authEnabled, bootstrapRequired });
} catch (error) {
console.error("Auth enabled toggle error:", error);
res.status(500).json({
error: "Internal server error",
message: "Failed to update authentication mode",
});
}
});
/**
* POST /auth/registration/toggle
* Enable/disable registration (admin-only)
*/
router.post("/registration/toggle", requireAuth, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!req.user) {
return res.status(401).json({ error: "Unauthorized", message: "User not authenticated" });
}
@@ -754,6 +878,7 @@ router.post("/registration/toggle", requireAuth, async (req: Request, res: Respo
*/
router.post("/admins", requireAuth, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!req.user) {
return res.status(401).json({ error: "Unauthorized", message: "User not authenticated" });
}
@@ -805,6 +930,7 @@ const passwordResetRequestSchema = z.object({
});
router.post("/password-reset-request", authRateLimiter, async (req: Request, res: Response) => {
if (!(await ensureAuthEnabled(res))) return;
// Check if password reset feature is enabled
if (!config.enablePasswordReset) {
return res.status(404).json({
@@ -901,6 +1027,7 @@ const passwordResetConfirmSchema = z.object({
});
router.post("/password-reset-confirm", authRateLimiter, async (req: Request, res: Response) => {
if (!(await ensureAuthEnabled(res))) return;
// Check if password reset feature is enabled
if (!config.enablePasswordReset) {
return res.status(404).json({
@@ -1010,6 +1137,7 @@ const updateProfileSchema = z.object({
router.put("/profile", requireAuth, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!req.user) {
return res.status(401).json({
error: "Unauthorized",
@@ -1074,6 +1202,7 @@ const changePasswordSchema = z.object({
router.post("/change-password", requireAuth, authRateLimiter, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!req.user) {
return res.status(401).json({
error: "Unauthorized",