AcadeDoc/apps/client/src/features/acadenice/oidc-status/pages/security-page.tsx
Corentin 5b512e6324 feat(acadedoc): replace EE Settings with open source UI (audit log, API keys, OIDC status) — R4.5
Server (NestJS):
- AcadeniceAuditLogModule: GET /api/acadenice/audit-log (admin/owner, Kysely, paginated + filtered)
- AcadeniceApiKeysModule: GET/POST/DELETE /api/acadenice/api-keys (JWT, bcrypt hash, acdk_ prefix)
- AcadeniceSecurityModule: GET /api/acadenice/security/oidc-status (admin, no secrets exposed)
- Migration 20260510T100000: acadenice_api_key table with token_hash + bcrypt
- Permissions catalog: added audit_log:read

Client (React 18 + Mantine v7):
- Audit log page: paginated table with filters (event, userId, date range)
- API keys page: list/create/revoke personal tokens, one-time display modal
- Security/OIDC status page: read-only, env-var config reference
- Sidebar rewired: Security & SSO, API keys, Audit log -> acadenice/* routes (no EE feature gates)
- Prefetch functions for new routes

Tests: 36 server (Jest) + client typecheck clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 12:24:00 +02:00

154 lines
4.5 KiB
TypeScript

import { Alert, Badge, Code, Divider, Stack, Text, Title } from "@mantine/core";
import { IconInfoCircle, IconLock, IconShieldCheck } from "@tabler/icons-react";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import SettingsTitle from "@/components/settings/settings-title";
import { getAppName } from "@/lib/config";
import { useOidcStatusQuery } from "../queries/oidc-status.queries";
import useUserRole from "@/hooks/use-user-role";
export default function AcadeniceSecurityPage() {
const { t } = useTranslation();
const { isAdmin } = useUserRole();
const { data: oidc, isLoading } = useOidcStatusQuery();
if (!isAdmin) {
return (
<Text c="dimmed">
{t("You do not have permission to view security settings.")}
</Text>
);
}
return (
<>
<Helmet>
<title>
{t("Security")} - {getAppName()}
</title>
</Helmet>
<SettingsTitle title={t("Security")} />
<Alert
icon={<IconInfoCircle size={16} />}
color="blue"
variant="light"
mb="lg"
>
{t(
"Security settings are configured server-side via environment variables. Contact your system administrator to modify them.",
)}
</Alert>
<Title order={4} mb="sm">
<IconShieldCheck size={18} style={{ marginRight: 6, verticalAlign: "middle" }} />
{t("Single Sign-On (OIDC)")}
</Title>
{isLoading ? (
<Text c="dimmed">{t("Loading...")}</Text>
) : (
<Stack gap="sm" mb="lg">
<Text fz="sm">
{t("Status")}:{" "}
<Badge color={oidc?.enabled ? "green" : "gray"} variant="light">
{oidc?.enabled ? t("Enabled") : t("Disabled")}
</Badge>
</Text>
{oidc?.enabled && (
<>
{oidc.providerName && (
<Text fz="sm">
{t("Provider")}: <strong>{oidc.providerName}</strong>
</Text>
)}
{oidc.issuer && (
<Text fz="sm">
{t("Issuer")}: <Code>{oidc.issuer}</Code>
</Text>
)}
{oidc.scopes && (
<Text fz="sm">
{t("Scopes")}: <Code>{oidc.scopes}</Code>
</Text>
)}
{oidc.redirectUri && (
<Text fz="sm">
{t("Redirect URI")}: <Code>{oidc.redirectUri}</Code>
</Text>
)}
{oidc.loginUrl && (
<Text fz="sm">
{t("Login URL")}:{" "}
<Code>
{typeof window !== "undefined"
? window.location.origin + oidc.loginUrl
: oidc.loginUrl}
</Code>
</Text>
)}
</>
)}
</Stack>
)}
<Divider my="lg" />
<Title order={5} mb="xs">
{t("Configuration")}
</Title>
<Text fz="sm" c="dimmed" mb="md">
{t(
"OIDC is configured via environment variables on the server. The following variables are supported:",
)}
</Text>
<Stack gap="xs">
{[
{ key: "OIDC_ENABLED", desc: t("Enable OIDC login (true/false)") },
{ key: "OIDC_ISSUER", desc: t("Provider discovery URL") },
{ key: "OIDC_CLIENT_ID", desc: t("OAuth2 client ID") },
{
key: "OIDC_CLIENT_SECRET",
desc: t("OAuth2 client secret (server-only, never exposed)"),
},
{ key: "OIDC_REDIRECT_URI", desc: t("Callback URL (optional)") },
{
key: "OIDC_SCOPES",
desc: t("OAuth2 scopes (default: openid email profile)"),
},
{
key: "OIDC_PROVIDER_NAME",
desc: t("Label shown on login button"),
},
{
key: "OIDC_AUTO_PROVISION",
desc: t("Auto-create user on first login (true/false)"),
},
].map(({ key, desc }) => (
<Text fz="sm" key={key}>
<Code>{key}</Code> {desc}
</Text>
))}
</Stack>
<Divider my="lg" />
<Title order={5} mb="xs">
<IconLock size={16} style={{ marginRight: 4, verticalAlign: "middle" }} />
{t("API keys")}
</Title>
<Text fz="sm" c="dimmed">
{t(
"Personal API keys can be managed from Account > API keys. Rotate them every 90 days. Never commit tokens to source control.",
)}
</Text>
</>
);
}