mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-21 22:52:19 +03:00
feat(auth): add OIDC password-login disabler when OIDC is active (#10889)
Validado no worktree combinado: typecheck:core, changelog-integrity, complexity, cognitive-complexity, file-size, lint e testes focados (auth-login-route, login-bootstrap-route, feature-flags-settings — corrigi EXPECTED_FEATURE_FLAG_COUNT 51→52 fix-in-place, novo flag adicionado sem atualizar a própria contagem) todos verdes. CI vermelho é o base-red já rastreado em #9985. Obrigado!
This commit is contained in:
@@ -348,11 +348,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/api/auth/login/route.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/api/auth/oidc/callback/route.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@@ -813,14 +808,10 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/api/settings/require-login/route.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
|
||||
"src/app/api/settings/route.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/api/settings/system-prompt/route.ts": {
|
||||
@@ -1616,11 +1607,7 @@
|
||||
"count": 11
|
||||
}
|
||||
},
|
||||
"tests/unit/auth-login-route.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
|
||||
"tests/unit/auth-ollama-cloud-per-model-403-3027.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 11
|
||||
@@ -2499,11 +2486,7 @@
|
||||
"count": 12
|
||||
}
|
||||
},
|
||||
"tests/unit/login-bootstrap-route.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 10
|
||||
}
|
||||
},
|
||||
|
||||
"tests/unit/management-password.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 4
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getAuditRequestContext, logAuditEvent } from "@/lib/compliance/index";
|
||||
import { classifyIpScope } from "@/lib/ipUtils";
|
||||
import { getCachedSettings } from "@/lib/localDb";
|
||||
import { getCachedSettings } from "@/lib/db/settings";
|
||||
import { SignJWT } from "jose";
|
||||
import { cookies } from "next/headers";
|
||||
import {
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getStoredManagementPassword,
|
||||
verifyManagementPassword,
|
||||
} from "@/lib/auth/managementPassword";
|
||||
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
|
||||
import { loginSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { checkLoginGuard, clearLoginAttempts, recordLoginFailure } from "@/server/auth/loginGuard";
|
||||
@@ -74,8 +75,32 @@ export async function POST(request) {
|
||||
return NextResponse.json({ error: "Invalid password payload" }, { status: 400 });
|
||||
}
|
||||
const settings = await getCachedSettings();
|
||||
const bruteForceEnabled = settings.bruteForceProtection !== false;
|
||||
const clientIp = auditContext.ipAddress || null;
|
||||
const oidcDisabledPassword =
|
||||
settings.oidcEnabled === true &&
|
||||
(settings.oidcDisablePasswordLogin === true ||
|
||||
isFeatureFlagEnabled("OMNIROUTE_OIDC_DISABLE_PASSWORD_LOGIN") ||
|
||||
process.env.OMNIROUTE_OIDC_DISABLE_PASSWORD_LOGIN === "true" ||
|
||||
process.env.OIDC_DISABLE_PASSWORD_LOGIN === "true");
|
||||
|
||||
if (oidcDisabledPassword) {
|
||||
logAuditEvent({
|
||||
action: "auth.login.password_disabled_by_oidc",
|
||||
actor: "anonymous",
|
||||
target: "dashboard-auth",
|
||||
resourceType: "auth_session",
|
||||
status: "failed",
|
||||
ipAddress: clientIp || undefined,
|
||||
requestId: auditContext.requestId,
|
||||
metadata: { reason: "password_login_disabled_when_oidc_active" },
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: "Password login is disabled when OIDC is active. Please sign in with OIDC." },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const bruteForceEnabled = settings.bruteForceProtection !== false;
|
||||
|
||||
const guardCheck = checkLoginGuard(clientIp, { enabled: bruteForceEnabled });
|
||||
if (!guardCheck.allowed) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
import { jwtVerify } from "jose";
|
||||
import { getSettings, updateSettings } from "@/lib/localDb";
|
||||
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
|
||||
import { getSettings, updateSettings } from "@/lib/db/settings";
|
||||
import {
|
||||
hasManagementPasswordConfigured,
|
||||
hashManagementPassword,
|
||||
@@ -52,12 +53,19 @@ export async function GET() {
|
||||
const hasPassword = hasManagementPasswordConfigured(settings);
|
||||
const setupComplete = !!settings.setupComplete;
|
||||
const oidcEnabled = !!settings.oidcEnabled;
|
||||
const oidcDisablePasswordLogin =
|
||||
oidcEnabled &&
|
||||
(settings.oidcDisablePasswordLogin === true ||
|
||||
isFeatureFlagEnabled("OMNIROUTE_OIDC_DISABLE_PASSWORD_LOGIN") ||
|
||||
process.env.OMNIROUTE_OIDC_DISABLE_PASSWORD_LOGIN === "true" ||
|
||||
process.env.OIDC_DISABLE_PASSWORD_LOGIN === "true");
|
||||
return NextResponse.json({
|
||||
authenticated,
|
||||
requireLogin,
|
||||
hasPassword,
|
||||
setupComplete,
|
||||
oidcEnabled,
|
||||
oidcDisablePasswordLogin,
|
||||
...nodeInfo,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -69,6 +77,7 @@ export async function GET() {
|
||||
hasPassword: true,
|
||||
setupComplete: true,
|
||||
oidcEnabled: false,
|
||||
oidcDisablePasswordLogin: false,
|
||||
...nodeInfo,
|
||||
},
|
||||
{ status: 200 }
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { getSettings, getSettingsRevision, updateSettings } from "@/lib/localDb";
|
||||
import { SettingsRevisionConflictError } from "@/lib/db/settings";
|
||||
import {
|
||||
getSettings,
|
||||
getSettingsRevision,
|
||||
updateSettings,
|
||||
SettingsRevisionConflictError,
|
||||
} from "@/lib/db/settings";
|
||||
import { getRuntimePorts } from "@/lib/runtime/ports";
|
||||
import { updateSettingsSchema } from "@/shared/validation/settingsSchemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
@@ -118,6 +122,7 @@ const SECURITY_IMPACTING_KEYS = [
|
||||
"requireLogin",
|
||||
"newPassword",
|
||||
"oidcEnabled",
|
||||
"oidcDisablePasswordLogin",
|
||||
"oidcClientSecret",
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@ export default function LoginPage() {
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasPassword, setHasPassword] = useState(null);
|
||||
const [setupComplete, setSetupComplete] = useState(null);
|
||||
const [hasPassword, setHasPassword] = useState<boolean | null>(null);
|
||||
const [setupComplete, setSetupComplete] = useState<boolean | null>(null);
|
||||
const [oidcEnabled, setOidcEnabled] = useState<boolean | null>(null);
|
||||
const [oidcDisablePasswordLogin, setOidcDisablePasswordLogin] = useState<boolean | null>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [nodeVersion, setNodeVersion] = useState(null);
|
||||
const [nodeCompatible, setNodeCompatible] = useState(true);
|
||||
@@ -44,16 +45,19 @@ export default function LoginPage() {
|
||||
setHasPassword(!!data.hasPassword);
|
||||
setSetupComplete(!!data.setupComplete);
|
||||
setOidcEnabled(!!data.oidcEnabled);
|
||||
setOidcDisablePasswordLogin(!!data.oidcDisablePasswordLogin);
|
||||
} else {
|
||||
setHasPassword(true);
|
||||
setSetupComplete(true);
|
||||
setOidcEnabled(false);
|
||||
setOidcDisablePasswordLogin(false);
|
||||
}
|
||||
} catch (err) {
|
||||
clearTimeout(timeoutId);
|
||||
setHasPassword(true);
|
||||
setSetupComplete(true);
|
||||
setOidcEnabled(false);
|
||||
setOidcDisablePasswordLogin(false);
|
||||
}
|
||||
}
|
||||
checkAuth();
|
||||
@@ -122,7 +126,12 @@ export default function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
if (hasPassword === null || setupComplete === null || oidcEnabled === null) {
|
||||
if (
|
||||
hasPassword === null ||
|
||||
setupComplete === null ||
|
||||
oidcEnabled === null ||
|
||||
(oidcEnabled && oidcDisablePasswordLogin === null)
|
||||
) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-6">
|
||||
{nodeWarningBanner}
|
||||
@@ -235,60 +244,84 @@ export default function LoginPage() {
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-text-main tracking-tight">{t("signIn")}</h1>
|
||||
<p className="text-text-muted mt-1.5">{t("enterPassword")}</p>
|
||||
<p className="text-text-muted mt-1.5">
|
||||
{oidcEnabled && oidcDisablePasswordLogin
|
||||
? t("continueWithOidc")
|
||||
: t("enterPassword")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-text-main">{t("password")}</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t("enterPassword")}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
className="h-11"
|
||||
/>
|
||||
{error && (
|
||||
<p className="text-sm text-red-500 flex items-center gap-1.5 pt-1">
|
||||
<span className="material-symbols-outlined text-base">error</span>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-text-muted/60 pt-0.5">{t("defaultPasswordHint")}</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="w-full h-11 text-sm font-medium"
|
||||
loading={loading}
|
||||
>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</form>
|
||||
{oidcEnabled && (
|
||||
<div className="mt-4">
|
||||
{oidcEnabled && oidcDisablePasswordLogin ? (
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="w-full h-11 text-sm font-medium"
|
||||
variant="primary"
|
||||
className="w-full h-11 text-sm font-medium flex items-center justify-center gap-2"
|
||||
onClick={() => (window.location.href = "/api/auth/oidc/login")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-lg">login</span>
|
||||
{t("continueWithOidc")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<form onSubmit={handleLogin} className="space-y-5 w-full">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-text-main">{t("password")}</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t("enterPassword")}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
className="h-11"
|
||||
/>
|
||||
{error && (
|
||||
<p className="text-sm text-red-500 flex items-center gap-1.5 pt-1">
|
||||
<span className="material-symbols-outlined text-base">error</span>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-text-muted/60 pt-0.5">{t("defaultPasswordHint")}</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="w-full h-11 text-sm font-medium"
|
||||
loading={loading}
|
||||
>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{oidcEnabled && (
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="w-full h-11 text-sm font-medium flex items-center justify-center gap-2"
|
||||
onClick={() => (window.location.href = "/api/auth/oidc/login")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-lg">login</span>
|
||||
{t("continueWithOidc")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-border">
|
||||
<a
|
||||
href="/forgot-password"
|
||||
className="text-sm text-text-muted hover:text-primary transition-colors"
|
||||
>
|
||||
{t("forgotPassword")}
|
||||
</a>
|
||||
</div>
|
||||
{!oidcEnabled && (
|
||||
<div className="mt-6 pt-6 border-t border-border">
|
||||
<a
|
||||
href="/forgot-password"
|
||||
className="text-sm text-text-muted hover:text-primary transition-colors"
|
||||
>
|
||||
{t("forgotPassword")}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ export async function getSettings() {
|
||||
antigravitySignatureCacheMode: "enabled",
|
||||
requireLogin: true,
|
||||
oidcEnabled: false,
|
||||
oidcDisablePasswordLogin: false,
|
||||
oidcIssuer: "",
|
||||
oidcClientId: "",
|
||||
oidcClientSecret: "",
|
||||
|
||||
@@ -109,9 +109,9 @@ export const FEATURE_FLAG_DEFINITIONS: FeatureFlagDefinition[] = [
|
||||
key: "AUTH_LOG_INCLUDE_ACCOUNT_ID",
|
||||
label: "Log Account IDs",
|
||||
description:
|
||||
"Include the account ID prefix in AUTH log lines (e.g. \"Using <provider> account: abc12345...\"). " +
|
||||
"Disabled by default so the account identifier is redacted in shared/multi-tenant process logs. " +
|
||||
"Independent of Debug Mode — flipping Debug Mode on does not reveal this.",
|
||||
'Include account prefix in AUTH log lines (e.g. "Using <provider> account: abc12345..."). ' +
|
||||
"Disabled by default so account identifiers are redacted from shared/multi-tenant process logs. " +
|
||||
"Independent from Debug Mode; flipping Debug Mode does not reveal this.",
|
||||
descriptionI18nKey: "featureFlagAuthLogIncludeAccountIdDescription",
|
||||
category: "security",
|
||||
defaultValue: "false",
|
||||
@@ -119,6 +119,18 @@ export const FEATURE_FLAG_DEFINITIONS: FeatureFlagDefinition[] = [
|
||||
requiresRestart: false,
|
||||
warningLevel: "info",
|
||||
},
|
||||
{
|
||||
key: "OMNIROUTE_OIDC_DISABLE_PASSWORD_LOGIN",
|
||||
label: "Disable Password Login With OIDC",
|
||||
description:
|
||||
"When OIDC is enabled, disable password login so users can only authenticate via OIDC Single Sign-On. When disabled (default), both password login and OIDC are available.",
|
||||
descriptionI18nKey: "featureFlagOidcDisablePasswordLoginDescription",
|
||||
category: "security",
|
||||
defaultValue: "false",
|
||||
type: "boolean",
|
||||
requiresRestart: false,
|
||||
warningLevel: "info",
|
||||
},
|
||||
// ──────────────── Network (7) ────────────────
|
||||
{
|
||||
key: "ENABLE_TLS_FINGERPRINT",
|
||||
|
||||
@@ -108,6 +108,7 @@ export const updateSettingsSchema = z.object({
|
||||
language: z.string().max(10).optional(),
|
||||
requireLogin: z.boolean().optional(),
|
||||
oidcEnabled: z.boolean().optional(),
|
||||
oidcDisablePasswordLogin: z.boolean().optional(),
|
||||
oidcIssuer: z.string().max(500).optional(),
|
||||
oidcClientId: z.string().max(200).optional(),
|
||||
oidcClientSecret: z.string().max(500).optional(),
|
||||
|
||||
@@ -102,7 +102,7 @@ test("auth login route lazily migrates INITIAL_PASSWORD to a persisted hash befo
|
||||
assert.equal(
|
||||
await managementPassword.verifyManagementPassword(
|
||||
"bootstrap-secret",
|
||||
(settings as any).password
|
||||
(settings as Record<string, unknown>).password as string
|
||||
),
|
||||
true
|
||||
);
|
||||
@@ -133,3 +133,24 @@ test("auth login route sets a bounded maxAge on the auth_token cookie (Seg3)", a
|
||||
assert.equal(options.httpOnly, true);
|
||||
assert.equal(options.path, "/");
|
||||
});
|
||||
|
||||
test("auth login route returns 403 when OIDC password login is disabled", async () => {
|
||||
process.env.INITIAL_PASSWORD = "bootstrap-secret";
|
||||
await settingsDb.updateSettings({
|
||||
requireLogin: true,
|
||||
oidcEnabled: true,
|
||||
oidcDisablePasswordLogin: true,
|
||||
});
|
||||
|
||||
const response = await loginRoute.POST(
|
||||
new Request("http://localhost/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ password: "bootstrap-secret" }),
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(response.status, 403);
|
||||
const body = (await response.json()) as { error?: string };
|
||||
assert.match(body.error || "", /Password login is disabled when OIDC is active/);
|
||||
});
|
||||
|
||||
@@ -12,6 +12,14 @@ const core = await import("../../src/lib/db/core.ts");
|
||||
const settingsDb = await import("../../src/lib/db/settings.ts");
|
||||
const route = await import("../../src/app/api/settings/require-login/route.ts");
|
||||
|
||||
type BootstrapResponse = {
|
||||
nodeVersion: string;
|
||||
nodeCompatible: boolean;
|
||||
oidcEnabled: boolean;
|
||||
oidcDisablePasswordLogin: boolean;
|
||||
error?: { message: string; details?: { field: string; message: string }[] };
|
||||
};
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
@@ -35,23 +43,24 @@ test.after(() => {
|
||||
|
||||
const originalHash = bcrypt.hash;
|
||||
|
||||
test("public login bootstrap route exposes the metadata the login page consumes", async () => {
|
||||
test("public login bootstrap route exposes metadata login page consumes", async () => {
|
||||
await settingsDb.updateSettings({
|
||||
requireLogin: true,
|
||||
setupComplete: true,
|
||||
});
|
||||
|
||||
const response = await route.GET();
|
||||
const body = (await response.json()) as any;
|
||||
const body = (await response.json()) as BootstrapResponse;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(body, {
|
||||
// #9491 added `authenticated` so /login can redirect an active session.
|
||||
// #9491 added `authenticated` to help /login redirect if active session.
|
||||
authenticated: false,
|
||||
requireLogin: true,
|
||||
hasPassword: false,
|
||||
setupComplete: true,
|
||||
oidcEnabled: false,
|
||||
oidcDisablePasswordLogin: false,
|
||||
nodeVersion: body.nodeVersion,
|
||||
nodeCompatible: body.nodeCompatible,
|
||||
});
|
||||
@@ -66,22 +75,23 @@ test("public login bootstrap route reports env-provided bootstrap password metad
|
||||
});
|
||||
|
||||
const response = await route.GET();
|
||||
const body = (await response.json()) as any;
|
||||
const body = (await response.json()) as BootstrapResponse;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(body, {
|
||||
// #9491 added `authenticated` so /login can redirect an active session.
|
||||
// #9491 added `authenticated` to help /login redirect if active session.
|
||||
authenticated: false,
|
||||
requireLogin: true,
|
||||
hasPassword: true,
|
||||
setupComplete: true,
|
||||
oidcEnabled: false,
|
||||
oidcDisablePasswordLogin: false,
|
||||
nodeVersion: body.nodeVersion,
|
||||
nodeCompatible: body.nodeCompatible,
|
||||
});
|
||||
});
|
||||
|
||||
test("public login bootstrap route reports stored password metadata and disabled auth state", async () => {
|
||||
test("public login bootstrap route reports stored password metadata in disabled auth state", async () => {
|
||||
await settingsDb.updateSettings({
|
||||
requireLogin: false,
|
||||
password: "hashed-password",
|
||||
@@ -89,21 +99,38 @@ test("public login bootstrap route reports stored password metadata and disabled
|
||||
});
|
||||
|
||||
const response = await route.GET();
|
||||
const body = (await response.json()) as any;
|
||||
const body = (await response.json()) as BootstrapResponse;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(body, {
|
||||
// #9491 added `authenticated` so /login can redirect an active session.
|
||||
// #9491 added `authenticated` to help /login redirect if active session.
|
||||
authenticated: false,
|
||||
requireLogin: false,
|
||||
hasPassword: true,
|
||||
setupComplete: true,
|
||||
oidcEnabled: false,
|
||||
oidcDisablePasswordLogin: false,
|
||||
nodeVersion: body.nodeVersion,
|
||||
nodeCompatible: body.nodeCompatible,
|
||||
});
|
||||
});
|
||||
|
||||
test("public login bootstrap route reports oidcDisablePasswordLogin when oidc is enabled and flag is set", async () => {
|
||||
await settingsDb.updateSettings({
|
||||
requireLogin: true,
|
||||
setupComplete: true,
|
||||
oidcEnabled: true,
|
||||
oidcDisablePasswordLogin: true,
|
||||
});
|
||||
|
||||
const response = await route.GET();
|
||||
const body = (await response.json()) as BootstrapResponse;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.oidcEnabled, true);
|
||||
assert.equal(body.oidcDisablePasswordLogin, true);
|
||||
});
|
||||
|
||||
test("public login bootstrap route POST rejects invalid JSON bodies", async () => {
|
||||
const request = new Request("http://localhost/api/settings/require-login", {
|
||||
method: "POST",
|
||||
@@ -112,7 +139,7 @@ test("public login bootstrap route POST rejects invalid JSON bodies", async () =
|
||||
});
|
||||
|
||||
const response = await route.POST(request);
|
||||
const body = (await response.json()) as any;
|
||||
const body = (await response.json()) as BootstrapResponse;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(body.error.message, "Invalid request");
|
||||
@@ -127,7 +154,7 @@ test("public login bootstrap route POST rejects empty updates", async () => {
|
||||
});
|
||||
|
||||
const response = await route.POST(request);
|
||||
const body = (await response.json()) as any;
|
||||
const body = (await response.json()) as BootstrapResponse;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(body.error.message, "Invalid request");
|
||||
@@ -142,7 +169,7 @@ test("public login bootstrap route POST updates requireLogin without forcing pas
|
||||
});
|
||||
|
||||
const response = await route.POST(request);
|
||||
const body = (await response.json()) as any;
|
||||
const body = (await response.json()) as BootstrapResponse;
|
||||
const settings = await settingsDb.getSettings();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
@@ -160,7 +187,7 @@ test("public login bootstrap route POST hashes and stores passwords", async () =
|
||||
});
|
||||
|
||||
const response = await route.POST(request);
|
||||
const body = (await response.json()) as any;
|
||||
const body = (await response.json()) as BootstrapResponse;
|
||||
const settings = await settingsDb.getSettings();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
@@ -185,7 +212,7 @@ test("login bootstrap route POST rejects unauthenticated writes after setup is c
|
||||
});
|
||||
|
||||
const response = await route.POST(request);
|
||||
const body = (await response.json()) as any;
|
||||
const body = (await response.json()) as BootstrapResponse;
|
||||
const settings = await settingsDb.getSettings();
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
@@ -207,7 +234,7 @@ test("login bootstrap route POST allows first password creation after setup comp
|
||||
});
|
||||
|
||||
const response = await route.POST(request);
|
||||
const body = (await response.json()) as any;
|
||||
const body = (await response.json()) as BootstrapResponse;
|
||||
const settings = await settingsDb.getSettings();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
@@ -229,7 +256,7 @@ test("public login bootstrap route POST returns 500 when hashing fails", async (
|
||||
});
|
||||
|
||||
const response = await route.POST(request);
|
||||
const body = (await response.json()) as any;
|
||||
const body = (await response.json()) as BootstrapResponse;
|
||||
|
||||
assert.equal(response.status, 500);
|
||||
assert.deepEqual(body, { error: "hash failed" });
|
||||
|
||||
Reference in New Issue
Block a user