feat: add FORM_LOGIN_ENABLED auth toggle (#334)

This commit is contained in:
Daniel Volz
2026-02-27 00:48:58 +01:00
committed by GitHub
parent 8b3901c1e1
commit 19ba4bb7d2
7 changed files with 61 additions and 32 deletions
+6 -3
View File
@@ -47,7 +47,7 @@ export async function getAnonymousUserId(): Promise<number> {
export interface AuthState {
authEnabled: boolean;
registrationEnabled: boolean;
localAuthEnabled: boolean;
formLoginEnabled: boolean;
oidcEnabled: boolean;
oidcProviderName: string;
hasUsers: boolean;
@@ -59,15 +59,18 @@ export async function getAuthState(): Promise<AuthState> {
const [result] = await db.select({ count: count() }).from(users).where(sql`${users.id} != ${ANONYMOUS_USER_ID}`);
const hasUsers = result.count > 0;
const needsSetup = env.AUTH_ENABLED && !hasUsers;
return {
authEnabled: env.AUTH_ENABLED,
// Registration: enabled via ENV OR no users exist (first-time setup)
registrationEnabled: env.REGISTRATION_ENABLED || !hasUsers,
localAuthEnabled: env.AUTH_ENABLED, // Password auth available when auth is enabled
// Form login: enabled when auth + form login are both on, or forced on for first-user setup
formLoginEnabled: needsSetup || (env.AUTH_ENABLED && env.FORM_LOGIN_ENABLED),
oidcEnabled: env.OIDC_ENABLED,
oidcProviderName: env.OIDC_PROVIDER_NAME,
hasUsers,
needsSetup: env.AUTH_ENABLED && !hasUsers,
needsSetup,
};
}
+27 -1
View File
@@ -28,7 +28,11 @@ const EnvSchema = z.object({
.string()
.transform((v) => v === "true")
.default("false"),
// Disable local auth when using SSO only
// Disable username/password form login (useful for OIDC-only setups)
FORM_LOGIN_ENABLED: z
.string()
.transform((v) => v === "true")
.default("true"),
// JWT Secrets - only required when AUTH_ENABLED=true
JWT_SECRET: z.string().min(10).optional(),
@@ -128,4 +132,26 @@ if (parsed.OIDC_ENABLED) {
}
}
// Validate that at least one login method is available when auth is enabled
if (parsed.AUTH_ENABLED && !parsed.FORM_LOGIN_ENABLED && !parsed.OIDC_ENABLED) {
console.error("=".repeat(60));
console.error("AUTHENTICATION CONFIGURATION ERROR");
console.error("=".repeat(60));
console.error("AUTH_ENABLED=true but no login method is available.");
console.error("FORM_LOGIN_ENABLED=false and OIDC_ENABLED=false means users cannot log in.");
console.error("");
console.error("To fix this, either:");
console.error(" 1. Set FORM_LOGIN_ENABLED=true to allow username/password login");
console.error(" 2. Set OIDC_ENABLED=true to allow SSO login");
console.error("=".repeat(60));
process.exit(1);
}
// Warn about ineffective registration when form login is disabled
if (parsed.REGISTRATION_ENABLED && !parsed.FORM_LOGIN_ENABLED) {
console.warn(
"[config] REGISTRATION_ENABLED=true has no effect when FORM_LOGIN_ENABLED=false (no registration form available)"
);
}
export const env = parsed;
+4 -4
View File
@@ -123,8 +123,8 @@ export async function authRoutes(app: FastifyInstance) {
return reply.status(400).send({ error: "Registration is disabled", code: "REGISTRATION_DISABLED" });
}
if (!state.localAuthEnabled) {
return reply.status(400).send({ error: "Local authentication is disabled", code: "LOCAL_AUTH_DISABLED" });
if (!state.formLoginEnabled) {
return reply.status(400).send({ error: "Form login is disabled", code: "FORM_LOGIN_DISABLED" });
}
// Validate input
@@ -185,8 +185,8 @@ export async function authRoutes(app: FastifyInstance) {
return reply.status(400).send({ error: "Authentication is disabled", code: "AUTH_DISABLED" });
}
if (!state.localAuthEnabled) {
return reply.status(400).send({ error: "Local authentication is disabled", code: "LOCAL_AUTH_DISABLED" });
if (!state.formLoginEnabled) {
return reply.status(400).send({ error: "Form login is disabled", code: "FORM_LOGIN_DISABLED" });
}
const parsed = loginSchema.safeParse(request.body);
+2 -2
View File
@@ -28,7 +28,7 @@ vi.mock("../db/client.js", () => ({
vi.mock("../plugins/env.js", () => ({
env: {
AUTH_ENABLED: true,
LOCAL_AUTH_ENABLED: true,
FORM_LOGIN_ENABLED: true,
REGISTRATION_ENABLED: true,
OIDC_ENABLED: false,
NODE_ENV: "test",
@@ -144,7 +144,7 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
const data = response.json();
expect(data.authEnabled).toBe(true);
expect(data.registrationEnabled).toBe(true);
expect(data.localAuthEnabled).toBe(true);
expect(data.formLoginEnabled).toBe(true);
});
});