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
@@ -0,0 +1,5 @@
-- Add authEnabled flag to SystemConfig to support single-user mode by default.
-- SQLite supports simple ADD COLUMN for non-null with default.
ALTER TABLE "SystemConfig" ADD COLUMN "authEnabled" BOOLEAN NOT NULL DEFAULT false;
+1
View File
@@ -32,6 +32,7 @@ model User {
model SystemConfig {
id String @id @default("default")
authEnabled Boolean @default(false)
registrationEnabled Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
+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",
+104
View File
@@ -7,6 +7,76 @@ import { config } from "../config";
import { PrismaClient } from "../generated/client";
const prisma = new PrismaClient();
const DEFAULT_SYSTEM_CONFIG_ID = "default";
const BOOTSTRAP_USER_ID = "bootstrap-admin";
type AuthEnabledCache = {
value: boolean;
fetchedAt: number;
};
let authEnabledCache: AuthEnabledCache | null = null;
const AUTH_ENABLED_TTL_MS = 0;
const getAuthEnabled = async (): Promise<boolean> => {
const now = Date.now();
if (authEnabledCache && now - authEnabledCache.fetchedAt < AUTH_ENABLED_TTL_MS) {
return authEnabledCache.value;
}
const systemConfig = await prisma.systemConfig.upsert({
where: { id: DEFAULT_SYSTEM_CONFIG_ID },
update: {},
create: {
id: DEFAULT_SYSTEM_CONFIG_ID,
authEnabled: false,
registrationEnabled: false,
},
select: { authEnabled: true },
});
authEnabledCache = { value: systemConfig.authEnabled, fetchedAt: now };
return systemConfig.authEnabled;
};
const getBootstrapActingUser = async () => {
const user = await prisma.user.findUnique({
where: { id: BOOTSTRAP_USER_ID },
select: {
id: true,
username: true,
email: true,
name: true,
role: true,
mustResetPassword: true,
isActive: true,
},
});
if (user) return user;
return prisma.user.create({
data: {
id: BOOTSTRAP_USER_ID,
email: "bootstrap@excalidash.local",
username: null,
passwordHash: "",
name: "Bootstrap Admin",
role: "ADMIN",
mustResetPassword: true,
isActive: false,
},
select: {
id: true,
username: true,
email: true,
name: true,
role: true,
mustResetPassword: true,
isActive: true,
},
});
};
// Extend Express Request type to include user
declare global {
@@ -87,6 +157,30 @@ export const requireAuth = async (
res: Response,
next: NextFunction
): Promise<void> => {
// Single-user mode: authentication disabled -> treat all requests as the bootstrap user.
try {
const authEnabled = await getAuthEnabled();
if (!authEnabled) {
const user = await getBootstrapActingUser();
req.user = {
id: user.id,
username: user.username,
email: user.email,
name: user.name,
role: user.role,
mustResetPassword: user.mustResetPassword,
};
return next();
}
} catch (error) {
console.error("Error reading auth mode:", error);
res.status(500).json({
error: "Internal server error",
message: "Failed to read authentication mode",
});
return;
}
const token = extractToken(req);
if (!token) {
@@ -159,6 +253,16 @@ export const optionalAuth = async (
res: Response,
next: NextFunction
): Promise<void> => {
try {
const authEnabled = await getAuthEnabled();
if (!authEnabled) {
return next();
}
} catch (error) {
console.error("Error reading auth mode:", error);
return next();
}
const token = extractToken(req);
if (!token) {