feat: OIDC-Anmeldebutton frei beschriftbar, Standard Mit SSO anmelden

This commit is contained in:
opencode committed 2026-10-18 19:38:58 +02:00
1 parent ef8662130c
commit 076a6b0cd4
6 files changed
+51 -13

No files matched your search

+6 -2
View File
@@ -1,11 +1,15 @@
import { NextResponse } from "next/server";
import { isOidcConfigured } from "@/lib/oidc";
import { getOidcSettings } from "@/lib/integration-settings";
import { isPasswordLoginAllowed } from "@/lib/authz";
/** Öffentlich: zeigt an, ob SSO angeboten wird und ob Passwort-Login erlaubt ist. */
/** Öffentlich: zeigt an, ob SSO angeboten wird, mit welcher Button-Beschriftung, und ob Passwort-Login erlaubt ist. */
export async function GET() {
const enabled = await isOidcConfigured();
const { buttonLabel } = enabled ? await getOidcSettings() : { buttonLabel: "Mit SSO anmelden" };
return NextResponse.json({
oidcEnabled: await isOidcConfigured(),
oidcEnabled: enabled,
oidcButtonLabel: buttonLabel,
passwordAllowed: isPasswordLoginAllowed(),
});
}
+5 -1
View File
@@ -9,6 +9,7 @@ export default function LoginPage() {
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [oidcEnabled, setOidcEnabled] = useState(false);
const [oidcLabel, setOidcLabel] = useState("Mit SSO anmelden");
const [passwordAllowed, setPasswordAllowed] = useState(true);
const [ssoError, setSsoError] = useState("");
@@ -25,6 +26,9 @@ export default function LoginPage() {
.then((s) => {
if (!s) return;
setOidcEnabled(!!s.oidcEnabled);
if (typeof s.oidcButtonLabel === "string" && s.oidcButtonLabel.trim() !== "") {
setOidcLabel(s.oidcButtonLabel);
}
setPasswordAllowed(s.passwordAllowed !== false);
})
.catch(() => {});
@@ -80,7 +84,7 @@ export default function LoginPage() {
href="/api/auth/oidc/login"
className="mb-5 flex h-[47px] items-center justify-center rounded-field border border-line bg-white text-sm font-semibold transition-colors hover:border-brand-600 hover:text-brand-700"
>
Mit SSO anmelden
{oidcLabel}
</a>
)}
{ssoError && (
+16 -2
View File
@@ -7,7 +7,7 @@ export type IntegrationStatus = {
smtp: { host: string; port: number; user: string; from: string; passGesetzt: boolean };
telegram: { botTokenGesetzt: boolean };
logo: { secretGesetzt: boolean; publishableKey: string };
oidc: { issuer: string; clientId: string; secretGesetzt: boolean; signingAlg: string };
oidc: { issuer: string; clientId: string; secretGesetzt: boolean; signingAlg: string; buttonLabel: string };
verschluesselungBereit: boolean;
};
@@ -207,6 +207,7 @@ export function OidcSection({ status, onSaved }: { status: IntegrationStatus; on
const [clientId, setClientId] = useState(status.oidc.clientId);
const [secret, setSecret] = useState("");
const [signingAlg, setSigningAlg] = useState(status.oidc.signingAlg || "RS256");
const [buttonLabel, setButtonLabel] = useState(status.oidc.buttonLabel || "");
return (
<Section title="SSO (OIDC)">
<p className="text-[13px] text-muted">Generischer OIDC-Anbieter, z. B. Authentik oder Keycloak. Nach dem Speichern erscheint auf der Anmeldeseite „Mit SSO anmelden“.</p>
@@ -262,13 +263,26 @@ export function OidcSection({ status, onSaved }: { status: IntegrationStatus; on
</select>
<p className="mt-1 text-[13px] text-muted">Muss zum Anbieter passen (dort: id_token_signed_response_alg).</p>
</div>
<div className="sm:col-span-2">
<label htmlFor="oidc-label" className="block text-[13px] font-semibold">Beschriftung des Anmelde-Buttons</label>
<input
id="oidc-label"
value={buttonLabel}
onChange={(e) => setButtonLabel(e.target.value)}
autoComplete="off"
maxLength={60}
placeholder="Mit SSO anmelden"
className={inputClass}
/>
<p className="mt-1 text-[13px] text-muted">Leer lassen für „Mit SSO anmelden“.</p>
</div>
</div>
<button
type="button"
onClick={async () => {
setNotice("");
try {
const s = await saveSection("oidc", { oidc_issuer: issuer, oidc_client_id: clientId, oidc_client_secret: secret, oidc_signing_alg: signingAlg });
const s = await saveSection("oidc", { oidc_issuer: issuer, oidc_client_id: clientId, oidc_client_secret: secret, oidc_signing_alg: signingAlg, oidc_button_label: buttonLabel });
setSecret("");
onSaved(s);
setNotice("OIDC-Einstellungen gespeichert.");
+9 -7
View File
@@ -1,6 +1,6 @@
import { getDb } from "./db";
import { canEncrypt, decryptSecret, encryptSecret } from "./settings-crypto";
import { normalizeSigningAlg, OIDC_SIGNING_ALGS } from "./oidc-algs";
import { normalizeSigningAlg, normalizeButtonLabel, OIDC_SIGNING_ALGS, DEFAULT_OIDC_BUTTON_LABEL } from "./oidc-algs";
export const INTEGRATION_SECTIONS = ["smtp", "telegram", "logo", "oidc"] as const;
export type IntegrationSection = (typeof INTEGRATION_SECTIONS)[number];
@@ -25,24 +25,26 @@ const ENV_FALLBACK: Record<string, string> = {
oidc_client_id: "OIDC_CLIENT_ID",
oidc_client_secret: "OIDC_CLIENT_SECRET",
oidc_signing_alg: "OIDC_SIGNING_ALG",
oidc_button_label: "OIDC_BUTTON_LABEL",
};
export type SmtpSettings = { host: string; port: number; user: string; pass: string; from: string };
export type TelegramSettings = { botToken: string };
export type LogoSettings = { secretKey: string; publishableKey: string };
export type OidcSettings = { issuer: string; clientId: string; clientSecret: string; signingAlg: string };
export type OidcSettings = { issuer: string; clientId: string; clientSecret: string; signingAlg: string; buttonLabel: string };
export async function getOidcSettings(): Promise<OidcSettings> {
const [issuer, clientId, clientSecret, signingAlg] = await Promise.all([
const [issuer, clientId, clientSecret, signingAlg, buttonLabel] = await Promise.all([
resolveValue("oidc_issuer"),
resolveValue("oidc_client_id"),
resolveValue("oidc_client_secret"),
resolveValue("oidc_signing_alg"),
resolveValue("oidc_button_label"),
]);
return { issuer: issuer.replace(/\/$/, ""), clientId, clientSecret, signingAlg: normalizeSigningAlg(signingAlg) };
return { issuer: issuer.replace(/\/$/, ""), clientId, clientSecret, signingAlg: normalizeSigningAlg(signingAlg), buttonLabel: normalizeButtonLabel(buttonLabel) };
}
export { OIDC_SIGNING_ALGS, normalizeSigningAlg };
export { OIDC_SIGNING_ALGS, normalizeSigningAlg, normalizeButtonLabel, DEFAULT_OIDC_BUTTON_LABEL };
async function readStored(key: string): Promise<string | null> {
const db = getDb();
@@ -107,14 +109,14 @@ export async function getIntegrationStatus() {
async function getOidcStatus() {
const s = await getOidcSettings();
return { issuer: s.issuer, clientId: s.clientId, secretGesetzt: s.clientSecret !== "", signingAlg: s.signingAlg };
return { issuer: s.issuer, clientId: s.clientId, secretGesetzt: s.clientSecret !== "", signingAlg: s.signingAlg, buttonLabel: s.buttonLabel };
}
const SECTION_FIELDS: Record<IntegrationSection, string[]> = {
smtp: ["smtp_host", "smtp_port", "smtp_user", "smtp_pass", "mail_from"],
telegram: ["telegram_bot_token"],
logo: ["logo_dev_secret_key", "logo_dev_publishable_key"],
oidc: ["oidc_issuer", "oidc_client_id", "oidc_client_secret", "oidc_signing_alg"],
oidc: ["oidc_issuer", "oidc_client_id", "oidc_client_secret", "oidc_signing_alg", "oidc_button_label"],
};
export function validateIntegrationSection(section: string, values: Record<string, unknown>): string[] {
+8
View File
@@ -5,3 +5,11 @@ export function normalizeSigningAlg(raw: string | null | undefined): string {
const alg = (raw ?? "").trim().toUpperCase();
return (OIDC_SIGNING_ALGS as readonly string[]).includes(alg) ? alg : "RS256";
}
/** Anmelde-Button-Beschriftung: leer wird Standard, längere Texte auf 60 Zeichen gekürzt. */
export const DEFAULT_OIDC_BUTTON_LABEL = "Mit SSO anmelden";
export function normalizeButtonLabel(raw: string | null | undefined): string {
const label = (raw ?? "").trim().slice(0, 60);
return label === "" ? DEFAULT_OIDC_BUTTON_LABEL : label;
}
+7 -1
View File
@@ -4,7 +4,7 @@ import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { createServer, type Server } from "https";
import { generateKeyPair, exportJWK, SignJWT, type KeyLike } from "jose";
import { normalizeSigningAlg } from "../lib/oidc-algs";
import { normalizeSigningAlg, normalizeButtonLabel, DEFAULT_OIDC_BUTTON_LABEL } from "../lib/oidc-algs";
import { buildLoginUrl, verifyCallbackCode, type OidcDiscovery } from "../lib/oidc";
const here = dirname(fileURLToPath(import.meta.url));
@@ -62,6 +62,12 @@ describe("normalizeSigningAlg", () => {
expect(normalizeSigningAlg("ES384")).toBe("ES384");
expect(normalizeSigningAlg("ps512")).toBe("PS512");
});
it("leere Beschriftung fällt auf Standard zurück, lange wird gekürzt", () => {
expect(normalizeButtonLabel(undefined)).toBe(DEFAULT_OIDC_BUTTON_LABEL);
expect(normalizeButtonLabel(" ")).toBe(DEFAULT_OIDC_BUTTON_LABEL);
expect(normalizeButtonLabel("Mit Keycloak anmelden")).toBe("Mit Keycloak anmelden");
expect(normalizeButtonLabel("x".repeat(100))).toHaveLength(60);
});
});
describe("buildLoginUrl", () => {