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>
76 lines
1.8 KiB
TypeScript
76 lines
1.8 KiB
TypeScript
import {
|
|
Alert,
|
|
Button,
|
|
Group,
|
|
Modal,
|
|
Stack,
|
|
Text,
|
|
TextInput,
|
|
} from "@mantine/core";
|
|
import { IconAlertTriangle } from "@tabler/icons-react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { CreateAcadeniceApiKeyResponse } from "../types/api-key.types";
|
|
import CopyTextButton from "@/components/common/copy";
|
|
|
|
interface TokenCreatedModalProps {
|
|
opened: boolean;
|
|
onClose: () => void;
|
|
result: CreateAcadeniceApiKeyResponse | null;
|
|
}
|
|
|
|
export function AcadeniceTokenCreatedModal({
|
|
opened,
|
|
onClose,
|
|
result,
|
|
}: TokenCreatedModalProps) {
|
|
const { t } = useTranslation();
|
|
|
|
if (!result) return null;
|
|
|
|
return (
|
|
<Modal
|
|
opened={opened}
|
|
onClose={onClose}
|
|
title={t("API key created")}
|
|
size="lg"
|
|
>
|
|
<Stack gap="md">
|
|
<Alert
|
|
icon={<IconAlertTriangle size={16} />}
|
|
title={t("Important")}
|
|
color="red"
|
|
>
|
|
{t(
|
|
"This is the only time you can see this token. Copy it now — it cannot be retrieved later.",
|
|
)}
|
|
</Alert>
|
|
|
|
<Text fz="sm" fw={500}>
|
|
{t("Your new API key")}
|
|
</Text>
|
|
|
|
<Group gap="xs" wrap="nowrap">
|
|
<TextInput
|
|
variant="filled"
|
|
style={{ flex: 1 }}
|
|
value={result.token}
|
|
readOnly
|
|
ff="monospace"
|
|
aria-label={t("Generated API key token")}
|
|
/>
|
|
<CopyTextButton text={result.token} />
|
|
</Group>
|
|
|
|
<Text fz="xs" c="dimmed">
|
|
{t(
|
|
"This token grants full access to your account. Treat it like a password.",
|
|
)}
|
|
</Text>
|
|
|
|
<Button fullWidth onClick={onClose} mt="sm" aria-label={t("Confirm token saved")}>
|
|
{t("I have saved my token")}
|
|
</Button>
|
|
</Stack>
|
|
</Modal>
|
|
);
|
|
}
|