@cosmicdrift/kumiko-bundled-features 0.256.0 → 0.258.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -10
- package/src/auth-mfa/__tests__/enable-flow.integration.test.ts +78 -6
- package/src/auth-mfa/__tests__/screen.test.ts +58 -0
- package/src/auth-mfa/feature.ts +6 -16
- package/src/auth-mfa/handlers/enable-start.write.ts +21 -12
- package/src/auth-mfa/i18n.ts +19 -0
- package/src/auth-mfa/screens.ts +52 -0
- package/src/auth-mfa/web/client-plugin.ts +4 -5
- package/src/auth-mfa/web/i18n.ts +0 -7
- package/src/auth-mfa/web/index.ts +0 -2
- package/src/auth-mfa/web/mfa-disable-dialog.tsx +2 -2
- package/src/auth-mfa/web/mfa-recovery-codes-reveal.tsx +4 -4
- package/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +5 -5
- package/src/auth-mfa/web/qrcode-browser.d.ts +2 -2
- package/src/folders/__tests__/folders-read-gate.integration.test.ts +179 -0
- package/src/folders/entity.ts +47 -28
- package/src/folders/feature.ts +58 -4
- package/src/folders/handlers/clear-folder.write.ts +4 -4
- package/src/folders/handlers/set-folder.write.ts +4 -4
- package/src/ledger/__tests__/ledger.integration.test.ts +111 -4
- package/src/ledger/entity.ts +30 -0
- package/src/ledger/handlers/confirm-schedule-period.write.ts +7 -0
- package/src/ledger/handlers/create-transaction.write.ts +2 -0
- package/src/ledger/schemas.ts +3 -0
- package/src/notes-history/__tests__/add-note-author-name-kms.integration.test.ts +2 -1
- package/src/notes-history/__tests__/notes-history-ownership.integration.test.ts +4 -1
- package/src/notes-history/__tests__/notes-history-parent-visibility.integration.test.ts +3 -1
- package/src/notes-history/__tests__/notes-history.integration.test.ts +2 -1
- package/src/notes-history/__tests__/notes-read-gate.integration.test.ts +243 -0
- package/src/notes-history/entity.ts +11 -1
- package/src/notes-history/feature.ts +12 -11
- package/src/notes-history/handlers/add-note.write.ts +8 -12
- package/src/personal-access-tokens/__tests__/expiry.test.ts +19 -0
- package/src/personal-access-tokens/__tests__/feature.test.ts +93 -0
- package/src/personal-access-tokens/__tests__/pat.integration.test.ts +143 -13
- package/src/personal-access-tokens/constants.ts +5 -2
- package/src/personal-access-tokens/expiry.ts +7 -0
- package/src/personal-access-tokens/feature.ts +8 -17
- package/src/personal-access-tokens/handlers/list.query.ts +75 -16
- package/src/personal-access-tokens/i18n.ts +54 -0
- package/src/personal-access-tokens/resolver.ts +2 -7
- package/src/personal-access-tokens/screens.ts +107 -0
- package/src/shared/index.ts +1 -1
- package/src/shared/parent-visibility.ts +30 -0
- package/src/tags/__tests__/tags-read-gate.integration.test.ts +366 -0
- package/src/tags/__tests__/tags.integration.test.ts +1 -0
- package/src/tags/entity.ts +11 -1
- package/src/tags/feature.ts +25 -8
- package/src/tags/handlers/assign-tag.write.ts +2 -8
- package/src/tags/handlers/remove-tag.write.ts +2 -8
- package/src/tags/web/__tests__/entity-tags.test.tsx +13 -4
- package/src/tags/web/entity-tags.tsx +9 -2
- package/src/tags/web/tag-section.tsx +9 -4
- package/src/auth-mfa/web/__tests__/mfa-enable-screen.test.tsx +0 -182
- package/src/auth-mfa/web/mfa-enable-screen.tsx +0 -224
- package/src/personal-access-tokens/web/client-plugin.tsx +0 -29
- package/src/personal-access-tokens/web/i18n.ts +0 -40
- package/src/personal-access-tokens/web/index.ts +0 -7
- package/src/personal-access-tokens/web/pat-tokens-screen.tsx +0 -296
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
createStaticLocaleResolver,
|
|
4
|
-
LocaleProvider,
|
|
5
|
-
PrimitivesProvider,
|
|
6
|
-
} from "@cosmicdrift/kumiko-renderer";
|
|
7
|
-
import { defaultPrimitives } from "@cosmicdrift/kumiko-renderer-web";
|
|
8
|
-
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
9
|
-
import type { ReactNode } from "react";
|
|
10
|
-
import { makeSessionApi } from "../../../auth-email-password/web/__tests__/test-utils";
|
|
11
|
-
import { SessionContext } from "../../../auth-email-password/web/session";
|
|
12
|
-
import { defaultTranslations } from "../i18n";
|
|
13
|
-
|
|
14
|
-
type WriteResult =
|
|
15
|
-
| { readonly isSuccess: true; readonly data: unknown }
|
|
16
|
-
| { readonly isSuccess: false; readonly error: { readonly i18nKey: string } };
|
|
17
|
-
|
|
18
|
-
const dispatchSpy = mock<() => Promise<WriteResult>>(async () => ({
|
|
19
|
-
isSuccess: true,
|
|
20
|
-
data: {
|
|
21
|
-
setupToken: "setup-token-1",
|
|
22
|
-
otpauthUri: "otpauth://totp/App:user@example.com?secret=ABCD1234&issuer=App",
|
|
23
|
-
recoveryCodes: ["code-1", "code-2"],
|
|
24
|
-
},
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
const actual_renderer = await import("@cosmicdrift/kumiko-renderer");
|
|
28
|
-
mock.module("@cosmicdrift/kumiko-renderer", () => ({
|
|
29
|
-
...actual_renderer,
|
|
30
|
-
useDispatcher: mock(() => ({ write: dispatchSpy, query: mock(), batch: mock() })),
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
|
-
mock.module("qrcode/lib/browser", () => ({
|
|
34
|
-
default: { toString: mock(async () => "<svg></svg>") },
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
// bun's mock.module is process-global and outlives this file — without
|
|
38
|
-
// restoring it, any later suite in the same test run that needs the real
|
|
39
|
-
// useDispatcher from @cosmicdrift/kumiko-renderer silently gets this mock
|
|
40
|
-
// instead (green-by-accident or a confusing foreign-suite failure,
|
|
41
|
-
// depending on test order).
|
|
42
|
-
afterAll(() => {
|
|
43
|
-
mock.module("@cosmicdrift/kumiko-renderer", () => actual_renderer);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const { MfaEnableScreen } = await import("../mfa-enable-screen");
|
|
47
|
-
|
|
48
|
-
const session = makeSessionApi();
|
|
49
|
-
|
|
50
|
-
function Wrapper({ children }: { readonly children: ReactNode }): ReactNode {
|
|
51
|
-
return (
|
|
52
|
-
<PrimitivesProvider value={defaultPrimitives}>
|
|
53
|
-
<LocaleProvider
|
|
54
|
-
resolver={createStaticLocaleResolver({ locale: "en" })}
|
|
55
|
-
fallbackBundles={[defaultTranslations]}
|
|
56
|
-
>
|
|
57
|
-
<SessionContext.Provider value={session}>{children}</SessionContext.Provider>
|
|
58
|
-
</LocaleProvider>
|
|
59
|
-
</PrimitivesProvider>
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
beforeEach(() => {
|
|
64
|
-
dispatchSpy.mockClear();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe("MfaEnableScreen", () => {
|
|
68
|
-
test("rendert Intro + Start-Button, kein Setup-Bereich vor dem Start", () => {
|
|
69
|
-
render(
|
|
70
|
-
<Wrapper>
|
|
71
|
-
<MfaEnableScreen />
|
|
72
|
-
</Wrapper>,
|
|
73
|
-
);
|
|
74
|
-
expect(screen.getByText("Two-factor authentication")).toBeTruthy();
|
|
75
|
-
expect(screen.getByRole("button", { name: "Start setup" })).toBeTruthy();
|
|
76
|
-
expect(screen.queryByLabelText("Code aus der Authenticator-App")).toBeNull();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("Start-Klick → dispatcher.write ruft enable-start, danach Setup-Bereich mit Secret + Recovery-Codes", async () => {
|
|
80
|
-
render(
|
|
81
|
-
<Wrapper>
|
|
82
|
-
<MfaEnableScreen />
|
|
83
|
-
</Wrapper>,
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
fireEvent.click(screen.getByRole("button", { name: "Start setup" }));
|
|
87
|
-
|
|
88
|
-
await waitFor(() => {
|
|
89
|
-
expect(dispatchSpy).toHaveBeenCalledWith(
|
|
90
|
-
"auth-mfa:write:enable-start",
|
|
91
|
-
expect.objectContaining({ accountLabel: "user@example.com" }),
|
|
92
|
-
);
|
|
93
|
-
});
|
|
94
|
-
await waitFor(() => {
|
|
95
|
-
expect(screen.getByText("ABCD1234")).toBeTruthy();
|
|
96
|
-
});
|
|
97
|
-
expect(screen.getByText(/code-1/)).toBeTruthy();
|
|
98
|
-
expect(screen.getByText(/code-2/)).toBeTruthy();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("Bestätigen-Button bleibt disabled bis Recovery-Codes bestätigt + 6-stelliger Code eingegeben", async () => {
|
|
102
|
-
render(
|
|
103
|
-
<Wrapper>
|
|
104
|
-
<MfaEnableScreen />
|
|
105
|
-
</Wrapper>,
|
|
106
|
-
);
|
|
107
|
-
fireEvent.click(screen.getByRole("button", { name: "Start setup" }));
|
|
108
|
-
await waitFor(() => {
|
|
109
|
-
expect(screen.getByText("ABCD1234")).toBeTruthy();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const confirm = screen.getByRole("button", { name: "Enable" }) as HTMLButtonElement;
|
|
113
|
-
expect(confirm.disabled).toBe(true);
|
|
114
|
-
|
|
115
|
-
fireEvent.click(screen.getByLabelText("I've saved my recovery codes."));
|
|
116
|
-
expect(confirm.disabled).toBe(true);
|
|
117
|
-
|
|
118
|
-
fireEvent.change(screen.getByLabelText(/^Code from your authenticator app/), {
|
|
119
|
-
target: { value: "123456" },
|
|
120
|
-
});
|
|
121
|
-
expect(confirm.disabled).toBe(false);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("Bestätigen → dispatcher.write ruft enable-confirm, danach Success-Banner", async () => {
|
|
125
|
-
dispatchSpy
|
|
126
|
-
.mockImplementationOnce(async () => ({
|
|
127
|
-
isSuccess: true,
|
|
128
|
-
data: {
|
|
129
|
-
setupToken: "setup-token-1",
|
|
130
|
-
otpauthUri: "otpauth://totp/App:user@example.com?secret=ABCD1234&issuer=App",
|
|
131
|
-
recoveryCodes: ["code-1", "code-2"],
|
|
132
|
-
},
|
|
133
|
-
}))
|
|
134
|
-
.mockImplementationOnce(async () => ({ isSuccess: true, data: undefined }));
|
|
135
|
-
const onEnabled = mock(() => {});
|
|
136
|
-
render(
|
|
137
|
-
<Wrapper>
|
|
138
|
-
<MfaEnableScreen onEnabled={onEnabled} />
|
|
139
|
-
</Wrapper>,
|
|
140
|
-
);
|
|
141
|
-
fireEvent.click(screen.getByRole("button", { name: "Start setup" }));
|
|
142
|
-
await waitFor(() => {
|
|
143
|
-
expect(screen.getByText("ABCD1234")).toBeTruthy();
|
|
144
|
-
});
|
|
145
|
-
fireEvent.click(screen.getByLabelText("I've saved my recovery codes."));
|
|
146
|
-
fireEvent.change(screen.getByLabelText(/^Code from your authenticator app/), {
|
|
147
|
-
target: { value: "123456" },
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
fireEvent.click(screen.getByRole("button", { name: "Enable" }));
|
|
151
|
-
|
|
152
|
-
await waitFor(() => {
|
|
153
|
-
expect(dispatchSpy).toHaveBeenCalledWith(
|
|
154
|
-
"auth-mfa:write:enable-confirm",
|
|
155
|
-
expect.objectContaining({ setupToken: "setup-token-1", code: "123456" }),
|
|
156
|
-
);
|
|
157
|
-
});
|
|
158
|
-
await waitFor(() => {
|
|
159
|
-
expect(screen.getByText("Two-factor authentication is now enabled.")).toBeTruthy();
|
|
160
|
-
expect(onEnabled).toHaveBeenCalled();
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test("enable-start schlägt fehl → übersetztes Fehler-Banner, kein Setup-Bereich", async () => {
|
|
165
|
-
dispatchSpy.mockImplementationOnce(async () => ({
|
|
166
|
-
isSuccess: false,
|
|
167
|
-
error: { i18nKey: "auth.mfa.errors.mfaAlreadyEnabled" },
|
|
168
|
-
}));
|
|
169
|
-
render(
|
|
170
|
-
<Wrapper>
|
|
171
|
-
<MfaEnableScreen />
|
|
172
|
-
</Wrapper>,
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
fireEvent.click(screen.getByRole("button", { name: "Start setup" }));
|
|
176
|
-
|
|
177
|
-
await waitFor(() => {
|
|
178
|
-
expect(screen.getByText("Two-factor authentication is already enabled.")).toBeTruthy();
|
|
179
|
-
});
|
|
180
|
-
expect(screen.queryByText("ABCD1234")).toBeNull();
|
|
181
|
-
});
|
|
182
|
-
});
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
// @runtime client
|
|
2
|
-
// MfaEnableScreen — logged-in self-service TOTP-Enrollment. Kein Dialog:
|
|
3
|
-
// DefaultDialog schließt nach JEDEM onConfirm (siehe renderer-web/src/
|
|
4
|
-
// primitives/dialog.tsx — onOpenChange(false) im finally-Block), das passt
|
|
5
|
-
// nicht zu einem Mehrschritt-Flow (Secret zeigen → Recovery-Codes zeigen →
|
|
6
|
-
// Code bestätigen). Folgt stattdessen pat-tokens-screen.tsx's embedded-
|
|
7
|
-
// Screen-Konvention: Feature registriert das dormant via r.screen, App
|
|
8
|
-
// platziert es via r.nav.
|
|
9
|
-
|
|
10
|
-
import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
11
|
-
import { FormScreenShell } from "@cosmicdrift/kumiko-renderer-web";
|
|
12
|
-
// qrcode's package.json#browser remap avoids Node-only deps (yargs/pngjs)
|
|
13
|
-
// for bundlers that honor it — Metro doesn't, hence the explicit subpath.
|
|
14
|
-
// @types/qrcode only covers the main "qrcode" entry, not this subpath, and
|
|
15
|
-
// TypeScript can't auto-discover an ambient .d.ts sibling from inside a
|
|
16
|
-
// node_modules package (consuming apps typecheck this raw .tsx source).
|
|
17
|
-
// Apps mounting MfaEnableScreen need their own local ambient shim — see
|
|
18
|
-
// qrcode-browser.d.ts in this directory for the declaration to copy.
|
|
19
|
-
import QRCode from "qrcode/lib/browser";
|
|
20
|
-
import { type ReactNode, useState } from "react";
|
|
21
|
-
// kumiko-lint-ignore cross-feature-import client-only hook, the feature's server barrel has no web/ re-export
|
|
22
|
-
import { useSession } from "../../auth-email-password/web";
|
|
23
|
-
import { AuthMfaHandlers } from "../constants";
|
|
24
|
-
|
|
25
|
-
type EnableStartResponse = {
|
|
26
|
-
readonly setupToken: string;
|
|
27
|
-
readonly otpauthUri: string;
|
|
28
|
-
readonly recoveryCodes: readonly string[];
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
type SetupState = {
|
|
32
|
-
readonly setupToken: string;
|
|
33
|
-
readonly secretParam: string;
|
|
34
|
-
readonly recoveryCodes: readonly string[];
|
|
35
|
-
readonly qrSvg: string;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// otpauth:// URIs put the secret in the query string — extract it for the
|
|
39
|
-
// manual-entry fallback (authenticator apps without a camera / desktop use).
|
|
40
|
-
function extractSecret(otpauthUri: string): string {
|
|
41
|
-
const query = otpauthUri.split("?")[1] ?? "";
|
|
42
|
-
return new URLSearchParams(query).get("secret") ?? "";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export type MfaEnableScreenProps = {
|
|
46
|
-
readonly embedded?: boolean;
|
|
47
|
-
// Fired once enable-confirm succeeds — MfaEnableScreen has no query of its
|
|
48
|
-
// own to invalidate, so a host screen composing it alongside other MFA
|
|
49
|
-
// state (e.g. a status query gating which section renders) needs this to
|
|
50
|
-
// know when to refetch/swap views instead of leaving the success banner
|
|
51
|
-
// as a dead end.
|
|
52
|
-
readonly onEnabled?: () => void;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export function MfaEnableScreen({
|
|
56
|
-
embedded = false,
|
|
57
|
-
onEnabled,
|
|
58
|
-
}: MfaEnableScreenProps = {}): ReactNode {
|
|
59
|
-
const t = useTranslation();
|
|
60
|
-
const { Button, Banner, Field, Input, Section, Heading } = usePrimitives();
|
|
61
|
-
const dispatcher = useDispatcher();
|
|
62
|
-
const session = useSession();
|
|
63
|
-
|
|
64
|
-
const [setup, setSetup] = useState<SetupState | null>(null);
|
|
65
|
-
const [acknowledged, setAcknowledged] = useState(false);
|
|
66
|
-
const [code, setCode] = useState("");
|
|
67
|
-
const [busy, setBusy] = useState(false);
|
|
68
|
-
const [error, setError] = useState<string | null>(null);
|
|
69
|
-
const [enabled, setEnabled] = useState(false);
|
|
70
|
-
|
|
71
|
-
const startSetup = async (): Promise<void> => {
|
|
72
|
-
setBusy(true);
|
|
73
|
-
setError(null);
|
|
74
|
-
try {
|
|
75
|
-
const res = await dispatcher.write<EnableStartResponse>(AuthMfaHandlers.enableStart, {
|
|
76
|
-
accountLabel: session.user?.email ?? "",
|
|
77
|
-
});
|
|
78
|
-
if (!res.isSuccess) {
|
|
79
|
-
setError(res.error.i18nKey);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
// errorCorrectionLevel "H" (~30% redundancy) — more resilient to
|
|
83
|
-
// camera/lighting issues than the default, no downside for a code
|
|
84
|
-
// this short-lived.
|
|
85
|
-
const qrSvg = await QRCode.toString(res.data.otpauthUri, {
|
|
86
|
-
type: "svg",
|
|
87
|
-
errorCorrectionLevel: "H",
|
|
88
|
-
});
|
|
89
|
-
setSetup({
|
|
90
|
-
setupToken: res.data.setupToken,
|
|
91
|
-
secretParam: extractSecret(res.data.otpauthUri),
|
|
92
|
-
recoveryCodes: res.data.recoveryCodes,
|
|
93
|
-
qrSvg,
|
|
94
|
-
});
|
|
95
|
-
setAcknowledged(false);
|
|
96
|
-
setCode("");
|
|
97
|
-
} catch {
|
|
98
|
-
setError("auth.mfa.errors.setupFailed");
|
|
99
|
-
} finally {
|
|
100
|
-
setBusy(false);
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const confirmSetup = async (): Promise<void> => {
|
|
105
|
-
if (!setup) return;
|
|
106
|
-
setBusy(true);
|
|
107
|
-
setError(null);
|
|
108
|
-
try {
|
|
109
|
-
const res = await dispatcher.write(AuthMfaHandlers.enableConfirm, {
|
|
110
|
-
setupToken: setup.setupToken,
|
|
111
|
-
code,
|
|
112
|
-
});
|
|
113
|
-
if (!res.isSuccess) {
|
|
114
|
-
setError(res.error.i18nKey);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
setEnabled(true);
|
|
118
|
-
setSetup(null);
|
|
119
|
-
onEnabled?.();
|
|
120
|
-
} catch {
|
|
121
|
-
setError("auth.mfa.errors.setupFailed");
|
|
122
|
-
} finally {
|
|
123
|
-
setBusy(false);
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const content = (
|
|
128
|
-
<div className="flex flex-col gap-6">
|
|
129
|
-
<Heading>{t("auth.mfa.enable.title")}</Heading>
|
|
130
|
-
|
|
131
|
-
{enabled && <Banner variant="info">{t("auth.mfa.enable.success")}</Banner>}
|
|
132
|
-
{error !== null && <Banner variant="error">{t(error)}</Banner>}
|
|
133
|
-
|
|
134
|
-
{!setup && !enabled && (
|
|
135
|
-
<Section
|
|
136
|
-
testId="mfa-enable-intro"
|
|
137
|
-
actions={
|
|
138
|
-
<Button
|
|
139
|
-
variant="primary"
|
|
140
|
-
onClick={() => void startSetup()}
|
|
141
|
-
loading={busy}
|
|
142
|
-
disabled={busy}
|
|
143
|
-
>
|
|
144
|
-
{t("auth.mfa.enable.start")}
|
|
145
|
-
</Button>
|
|
146
|
-
}
|
|
147
|
-
>
|
|
148
|
-
<span className="text-sm text-muted-foreground">{t("auth.mfa.enable.intro")}</span>
|
|
149
|
-
</Section>
|
|
150
|
-
)}
|
|
151
|
-
|
|
152
|
-
{setup && (
|
|
153
|
-
<Section
|
|
154
|
-
testId="mfa-enable-setup"
|
|
155
|
-
actions={
|
|
156
|
-
<>
|
|
157
|
-
<Button variant="secondary" onClick={() => setSetup(null)} disabled={busy}>
|
|
158
|
-
{t("auth.mfa.enable.cancel")}
|
|
159
|
-
</Button>
|
|
160
|
-
<Button
|
|
161
|
-
variant="primary"
|
|
162
|
-
onClick={() => void confirmSetup()}
|
|
163
|
-
loading={busy}
|
|
164
|
-
disabled={busy || !acknowledged || code.length !== 6}
|
|
165
|
-
>
|
|
166
|
-
{t("auth.mfa.enable.confirm")}
|
|
167
|
-
</Button>
|
|
168
|
-
</>
|
|
169
|
-
}
|
|
170
|
-
>
|
|
171
|
-
<div className="flex flex-col items-center gap-2 text-center">
|
|
172
|
-
<span className="text-sm font-semibold">{t("auth.mfa.enable.scanTitle")}</span>
|
|
173
|
-
{/* qrcode's own SVG string output, not user input — safe to inline */}
|
|
174
|
-
<div
|
|
175
|
-
className="h-40 w-40"
|
|
176
|
-
// biome-ignore lint/security/noDangerouslySetInnerHtml: qrcode-generated SVG, no user input
|
|
177
|
-
dangerouslySetInnerHTML={{ __html: setup.qrSvg }}
|
|
178
|
-
/>
|
|
179
|
-
<span className="text-xs text-muted-foreground">
|
|
180
|
-
{t("auth.mfa.enable.manualEntry")}
|
|
181
|
-
</span>
|
|
182
|
-
<code className="inline-block break-all rounded bg-muted px-3 py-2 font-mono text-sm">
|
|
183
|
-
{setup.secretParam}
|
|
184
|
-
</code>
|
|
185
|
-
</div>
|
|
186
|
-
|
|
187
|
-
<div className="flex flex-col gap-2">
|
|
188
|
-
<span className="text-sm font-semibold">{t("auth.mfa.enable.recoveryTitle")}</span>
|
|
189
|
-
<span className="text-xs text-muted-foreground">
|
|
190
|
-
{t("auth.mfa.enable.recoveryHint")}
|
|
191
|
-
</span>
|
|
192
|
-
<code className="block whitespace-pre-wrap break-all rounded bg-muted px-3 py-2 font-mono text-sm">
|
|
193
|
-
{setup.recoveryCodes.join("\n")}
|
|
194
|
-
</code>
|
|
195
|
-
<Field id="mfa-enable-ack" label={t("auth.mfa.enable.acknowledge")}>
|
|
196
|
-
<Input
|
|
197
|
-
kind="boolean"
|
|
198
|
-
id="mfa-enable-ack"
|
|
199
|
-
name="mfa-enable-ack"
|
|
200
|
-
value={acknowledged}
|
|
201
|
-
onChange={setAcknowledged}
|
|
202
|
-
/>
|
|
203
|
-
</Field>
|
|
204
|
-
</div>
|
|
205
|
-
|
|
206
|
-
<Field id="mfa-enable-code" label={t("auth.mfa.enable.code")} required>
|
|
207
|
-
<Input
|
|
208
|
-
kind="text"
|
|
209
|
-
id="mfa-enable-code"
|
|
210
|
-
name="mfa-enable-code"
|
|
211
|
-
value={code}
|
|
212
|
-
onChange={setCode}
|
|
213
|
-
disabled={busy || !acknowledged}
|
|
214
|
-
autoComplete="one-time-code"
|
|
215
|
-
/>
|
|
216
|
-
</Field>
|
|
217
|
-
</Section>
|
|
218
|
-
)}
|
|
219
|
-
</div>
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
if (embedded) return content;
|
|
223
|
-
return <FormScreenShell testId="mfa-enable-screen">{content}</FormScreenShell>;
|
|
224
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// @runtime client
|
|
2
|
-
// Client-feature factory for personal-access-tokens. Ships the PatTokensScreen
|
|
3
|
-
// (mapped to the "api-tokens" screen id) + default translations. Apps mount it
|
|
4
|
-
// in createKumikoApp({ clientFeatures: [personalAccessTokensClient()] }); the
|
|
5
|
-
// server registers the screen dormant (r.screen) and the app places it via
|
|
6
|
-
// r.nav.
|
|
7
|
-
|
|
8
|
-
import { mergeTranslations, type TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
9
|
-
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
|
|
10
|
-
import { PAT_FEATURE, PAT_SCREEN_ID } from "../constants";
|
|
11
|
-
import { defaultTranslations } from "./i18n";
|
|
12
|
-
import { PatTokensScreen } from "./pat-tokens-screen";
|
|
13
|
-
|
|
14
|
-
export type PersonalAccessTokensClientOptions = {
|
|
15
|
-
/** Key-wise overrides over the default bundles (de/en). */
|
|
16
|
-
readonly translations?: TranslationsByLocale;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export function personalAccessTokensClient(
|
|
20
|
-
options?: PersonalAccessTokensClientOptions,
|
|
21
|
-
): ClientFeatureDefinition {
|
|
22
|
-
return {
|
|
23
|
-
name: PAT_FEATURE,
|
|
24
|
-
translations: mergeTranslations(defaultTranslations, options?.translations ?? {}),
|
|
25
|
-
components: {
|
|
26
|
-
[PAT_SCREEN_ID]: PatTokensScreen,
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// @runtime client
|
|
2
|
-
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
-
|
|
4
|
-
export const defaultTranslations: TranslationsByLocale = {
|
|
5
|
-
en: {
|
|
6
|
-
"pat.title": "Personal Access Tokens",
|
|
7
|
-
"pat.create.title": "Create a new token",
|
|
8
|
-
"pat.create.subtitle": "Pick permissions and an expiration. The token is shown only once.",
|
|
9
|
-
"pat.create.name": "Name",
|
|
10
|
-
"pat.create.namePlaceholder": "e.g. CLI automation",
|
|
11
|
-
"pat.create.scopes": "Per-API access",
|
|
12
|
-
"pat.level.none": "No access",
|
|
13
|
-
"pat.level.read": "Read",
|
|
14
|
-
"pat.level.write": "Read & write",
|
|
15
|
-
"pat.create.expiry": "Expiration",
|
|
16
|
-
"pat.create.submit": "Create token",
|
|
17
|
-
"pat.create.needName": "Please enter a name.",
|
|
18
|
-
"pat.create.needScope": "Please select at least one permission.",
|
|
19
|
-
"pat.create.needPassword": "Please enter your password to confirm.",
|
|
20
|
-
"pat.create.currentPassword": "Password (to confirm)",
|
|
21
|
-
"pat.create.mfaCode": "2FA code (if enabled)",
|
|
22
|
-
"pat.create.mfaCodePlaceholder": "6-digit code",
|
|
23
|
-
"pat.expiry.30d": "30 days",
|
|
24
|
-
"pat.expiry.90d": "90 days",
|
|
25
|
-
"pat.expiry.1y": "1 year",
|
|
26
|
-
"pat.expiry.never": "No expiration",
|
|
27
|
-
"pat.created.hint": "Copy this token now — for security it is shown only this once.",
|
|
28
|
-
"pat.created.copy": "Copy",
|
|
29
|
-
"pat.created.copied": "Copied ✓",
|
|
30
|
-
"pat.created.dismiss": "Done",
|
|
31
|
-
"pat.list.title": "Your tokens",
|
|
32
|
-
"pat.list.empty": "No tokens yet.",
|
|
33
|
-
"pat.list.revoke": "Revoke",
|
|
34
|
-
"pat.list.revoked": "Revoked",
|
|
35
|
-
"pat.list.validUntil": "Valid until {date}",
|
|
36
|
-
"pat.list.neverExpires": "Never expires",
|
|
37
|
-
"pat.list.created": "Created {date}",
|
|
38
|
-
"pat.error.generic": "Action failed.",
|
|
39
|
-
},
|
|
40
|
-
};
|