@cosmicdrift/kumiko-samples 0.341.0 → 0.342.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 +1 -1
- package/packages/bundled-features/package.json +1 -1
- package/packages/bundled-features/src/audit/constants.ts +5 -0
- package/packages/bundled-features/src/audit/feature.ts +47 -6
- package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +53 -0
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +18 -2
- package/packages/bundled-features/src/auth-email-password/changes.json +12 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-form-primitives.tsx +24 -6
- package/packages/bundled-features/src/auth-email-password/web/confirm-account-unlock-screen.tsx +12 -18
- package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +4 -4
- package/packages/bundled-features/src/auth-email-password/web/invite-accept-screen.tsx +65 -69
- package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +93 -95
- package/packages/bundled-features/src/auth-email-password/web/request-account-unlock-screen.tsx +4 -4
- package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +8 -10
- package/packages/bundled-features/src/auth-email-password/web/session-bootstrap-error.tsx +13 -15
- package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +43 -49
- package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +4 -4
- package/packages/bundled-features/src/auth-email-password/web/verify-email-screen.tsx +12 -18
- package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +83 -85
- package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +24 -26
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +8 -1
- package/packages/bundled-features/src/channel-email/pii-guard.ts +6 -5
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +27 -1
- package/packages/bundled-features/src/channel-email/types.ts +4 -0
- package/packages/bundled-features/src/waitlist/changes.json +6 -0
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +1 -1
- package/packages/bundled-features/src/waitlist/options.ts +3 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +27 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-samples",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.342.0",
|
|
4
4
|
"description": "Source trees of the Kumiko sample recipes, sample apps and bundled features in repo layout, for tooling such as few-shot corpus builds.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.342.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -24,3 +24,8 @@ export const AUDIT_LOG_SCREEN_ID = "audit-log" as const;
|
|
|
24
24
|
|
|
25
25
|
/** Single-event detail screen, breadcrumb-linked to the audit-log list. */
|
|
26
26
|
export const AUDIT_LOG_DETAIL_SCREEN_ID = "audit-log-detail" as const;
|
|
27
|
+
|
|
28
|
+
/** Config key (system scope) for how long `escapeHatchUse` audit events are kept. */
|
|
29
|
+
export const ESCAPE_HATCH_RETENTION_DAYS_KEY = "audit:config:escape-hatch-retention-days" as const;
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_ESCAPE_HATCH_RETENTION_DAYS = 90;
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
2
|
access,
|
|
3
|
+
createSystemConfig,
|
|
3
4
|
defineFeature,
|
|
4
5
|
type FeatureDefinition,
|
|
5
6
|
i18nKey,
|
|
7
|
+
SYSTEM_USER_ID,
|
|
6
8
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
AUDIT_LOG_DETAIL_SCREEN_ID,
|
|
11
|
+
AUDIT_LOG_SCREEN_ID,
|
|
12
|
+
AuditQueries,
|
|
13
|
+
DEFAULT_ESCAPE_HATCH_RETENTION_DAYS,
|
|
14
|
+
} from "./constants.js";
|
|
8
15
|
import { escapeHatchUsedSchema } from "./escape-hatch-audit-sink.js";
|
|
9
16
|
import { detailsQuery } from "./handlers/details.query.js";
|
|
10
17
|
import { listQuery } from "./handlers/list.query.js";
|
|
11
18
|
import { AUDIT_I18N } from "./i18n.js";
|
|
19
|
+
import { runEscapeHatchRetention } from "./run-escape-hatch-retention.js";
|
|
12
20
|
|
|
13
21
|
// Audit feature — exposes a filtered read over the framework's event log.
|
|
14
22
|
//
|
|
@@ -18,10 +26,11 @@ import { AUDIT_I18N } from "./i18n.js";
|
|
|
18
26
|
// the existing `events` table. See handlers/list.query.ts for the filter
|
|
19
27
|
// surface.
|
|
20
28
|
//
|
|
21
|
-
// Retention
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// the
|
|
29
|
+
// Retention: only `escapeHatchUse` events are pruned (daily job, config key
|
|
30
|
+
// `escapeHatchRetentionDays`). All other events stay as the source of truth
|
|
31
|
+
// for state; archive or compress policies are a separate concern (tracked
|
|
32
|
+
// with the snapshot/archive infrastructure that already exists in the
|
|
33
|
+
// framework).
|
|
25
34
|
export function createAuditFeature(): FeatureDefinition {
|
|
26
35
|
return defineFeature(
|
|
27
36
|
"audit",
|
|
@@ -35,11 +44,43 @@ export function createAuditFeature(): FeatureDefinition {
|
|
|
35
44
|
recommended: false,
|
|
36
45
|
});
|
|
37
46
|
r.translations({ keys: AUDIT_I18N });
|
|
38
|
-
r.requires("tenant", "user");
|
|
47
|
+
r.requires("tenant", "user", "config");
|
|
39
48
|
|
|
40
49
|
// Registered so ops tools/MSPs can discover the type — escape-hatch-audit-sink.ts appends it via the low-level event-store API.
|
|
41
50
|
r.defineEvent("escape-hatch-used", escapeHatchUsedSchema, { piiFields: "none" });
|
|
42
51
|
|
|
52
|
+
r.config(
|
|
53
|
+
"escapeHatchRetentionDays",
|
|
54
|
+
createSystemConfig("number", {
|
|
55
|
+
default: DEFAULT_ESCAPE_HATCH_RETENTION_DAYS,
|
|
56
|
+
bounds: { min: 1 },
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
r.job({
|
|
61
|
+
name: "escape-hatch-retention",
|
|
62
|
+
trigger: { cron: "30 3 * * *" },
|
|
63
|
+
concurrency: "skip",
|
|
64
|
+
// pruneEvents needs a transaction-capable connection, which only unsafeRaw() hands a job.
|
|
65
|
+
escapeHatch: {
|
|
66
|
+
reason: "prunes expired escapeHatchUse audit events through pruneEvents",
|
|
67
|
+
grants: ["unsafeRaw"],
|
|
68
|
+
},
|
|
69
|
+
handler: async (_payload, ctx) => {
|
|
70
|
+
if (!ctx.registry) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"escape-hatch-retention: ctx.registry required (JobContext incomplete)",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
await runEscapeHatchRetention({
|
|
76
|
+
db: ctx.db.unsafeRaw(),
|
|
77
|
+
registry: ctx.registry,
|
|
78
|
+
configResolver: ctx.configResolver,
|
|
79
|
+
userId: ctx._userId ?? SYSTEM_USER_ID,
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
43
84
|
const queries = {
|
|
44
85
|
list: r.queryHandler(listQuery),
|
|
45
86
|
details: r.queryHandler(detailsQuery),
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { DbConnection, DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import {
|
|
3
|
+
type ConfigResolver,
|
|
4
|
+
type Registry,
|
|
5
|
+
SYSTEM_TENANT_ID,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import { type PruneEventsResult, pruneEvents } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
8
|
+
import { createConfigAccessor } from "../config/index.js";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_ESCAPE_HATCH_RETENTION_DAYS,
|
|
11
|
+
ESCAPE_HATCH_RETENTION_DAYS_KEY,
|
|
12
|
+
} from "./constants.js";
|
|
13
|
+
import { ESCAPE_HATCH_USE_AGGREGATE_TYPE } from "./escape-hatch-audit-sink.js";
|
|
14
|
+
|
|
15
|
+
// pruneEvents opens its own transaction, so a runner that cannot begin one (a tx handle) is rejected instead of nested.
|
|
16
|
+
export function isDbConnection(runner: DbRunner): runner is DbConnection {
|
|
17
|
+
return "begin" in runner && typeof runner.begin === "function";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function resolveRetentionDays(args: {
|
|
21
|
+
readonly registry: Registry;
|
|
22
|
+
readonly configResolver: ConfigResolver | undefined;
|
|
23
|
+
readonly db: DbConnection;
|
|
24
|
+
readonly userId: string;
|
|
25
|
+
}): Promise<number> {
|
|
26
|
+
if (!args.configResolver) return DEFAULT_ESCAPE_HATCH_RETENTION_DAYS;
|
|
27
|
+
const config = createConfigAccessor(
|
|
28
|
+
args.registry,
|
|
29
|
+
args.configResolver,
|
|
30
|
+
SYSTEM_TENANT_ID,
|
|
31
|
+
args.userId,
|
|
32
|
+
args.db,
|
|
33
|
+
);
|
|
34
|
+
const raw = await config(ESCAPE_HATCH_RETENTION_DAYS_KEY);
|
|
35
|
+
return typeof raw === "number" && raw >= 1 ? raw : DEFAULT_ESCAPE_HATCH_RETENTION_DAYS;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function runEscapeHatchRetention(args: {
|
|
39
|
+
readonly db: DbRunner;
|
|
40
|
+
readonly registry: Registry;
|
|
41
|
+
readonly configResolver: ConfigResolver | undefined;
|
|
42
|
+
readonly userId: string;
|
|
43
|
+
}): Promise<PruneEventsResult> {
|
|
44
|
+
const { db } = args;
|
|
45
|
+
if (!isDbConnection(db)) {
|
|
46
|
+
throw new Error("audit escape-hatch retention: a transaction-free DbConnection is required");
|
|
47
|
+
}
|
|
48
|
+
const olderThanDays = await resolveRetentionDays({ ...args, db });
|
|
49
|
+
return pruneEvents(db, {
|
|
50
|
+
aggregateTypes: [ESCAPE_HATCH_USE_AGGREGATE_TYPE],
|
|
51
|
+
olderThanDays,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
// invitation row holds membership role + global roles until the invitee
|
|
6
6
|
// accepts through the regular invite-accept routes.
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
makeDispatchSystemWrite,
|
|
10
|
+
type SystemDispatchArgs,
|
|
11
|
+
} from "@cosmicdrift/kumiko-framework/api";
|
|
9
12
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
10
13
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
11
14
|
import {
|
|
12
15
|
ConfigScopes,
|
|
13
16
|
createSystemUser,
|
|
14
17
|
type TenantId,
|
|
18
|
+
type WriteResult,
|
|
15
19
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
16
20
|
import type { Dispatcher } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
17
21
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
@@ -48,6 +52,12 @@ export type BootstrapSeedDeps = {
|
|
|
48
52
|
readonly tenantId: TenantId;
|
|
49
53
|
readonly db: DbConnection;
|
|
50
54
|
readonly dispatchSystemWrite: ReturnType<typeof makeDispatchSystemWrite>;
|
|
55
|
+
/** Writes as the system user with the given tenant roles in the seeded
|
|
56
|
+
* tenant; use it when the app gates tenant data on its own roles rather
|
|
57
|
+
* than SystemAdmin. */
|
|
58
|
+
readonly dispatchWriteAs: (
|
|
59
|
+
args: Pick<SystemDispatchArgs, "handlerQn" | "payload"> & { readonly roles: readonly string[] },
|
|
60
|
+
) => Promise<WriteResult>;
|
|
51
61
|
};
|
|
52
62
|
|
|
53
63
|
export type BootstrapConfigValue = string | number | boolean;
|
|
@@ -276,7 +286,13 @@ export async function bootstrapTenants(
|
|
|
276
286
|
const outcome = await ensureTenant(deps, tenant);
|
|
277
287
|
const shouldSeed = outcome === "created" && plan.seed !== undefined;
|
|
278
288
|
if (shouldSeed) {
|
|
279
|
-
await plan.seed?.({
|
|
289
|
+
await plan.seed?.({
|
|
290
|
+
tenantId: tenant.id,
|
|
291
|
+
db: deps.db,
|
|
292
|
+
dispatchSystemWrite,
|
|
293
|
+
dispatchWriteAs: ({ handlerQn, payload, roles }) =>
|
|
294
|
+
deps.dispatcher.write(handlerQn, payload, createSystemUser(tenant.id, roles)),
|
|
295
|
+
});
|
|
280
296
|
}
|
|
281
297
|
const configApplied = await applyTenantConfig(deps, tenant);
|
|
282
298
|
tenants.push({ id: tenant.id, outcome, seeded: shouldSeed, configApplied });
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.342.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "AuthCard styling hooks (className, headerClassName, titleClassName, bodyClassName)",
|
|
6
|
+
"detail": "AuthCard takes className, headerClassName, titleClassName and bodyClassName\n`AuthCard` renders the body wrapper (`p-6 pt-0 flex flex-col gap-4`) itself and accepts `className` (Card), `headerClassName`, `titleClassName` and `bodyClassName`, merged with tailwind-merge over the defaults. The auth-email-password and auth-mfa screens no longer carry their own body wrapper; the rendered DOM stays the same. The session bootstrap error screen keeps its former bottom padding through `bodyClassName`."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.342.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Bootstrap seed can write with app tenant roles",
|
|
12
|
+
"detail": "Bootstrap seed gets `dispatchWriteAs` to write with app tenant roles instead of SystemAdmin."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.341.0",
|
|
4
16
|
"type": "breaking",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// die früheren authButtonClass/authMutedLinkClass sind dorthin gewandert.
|
|
16
16
|
|
|
17
17
|
import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
18
|
-
import { BareFormProvider } from "@cosmicdrift/kumiko-renderer-web";
|
|
18
|
+
import { BareFormProvider, cn } from "@cosmicdrift/kumiko-renderer-web";
|
|
19
19
|
import { createContext, type ReactNode, useContext, useEffect, useState } from "react";
|
|
20
20
|
|
|
21
21
|
// Wrappt die zentrierte Auth-Card in ihre Umgebung. Default = Fullscreen-
|
|
@@ -47,25 +47,41 @@ export function useAuthShell(): AuthShellRenderer | null {
|
|
|
47
47
|
export type AuthCardProps = {
|
|
48
48
|
readonly title?: string;
|
|
49
49
|
readonly subtitle?: ReactNode;
|
|
50
|
+
/** Merged onto the Card (default `w-full max-w-sm`). */
|
|
51
|
+
readonly className?: string;
|
|
52
|
+
readonly headerClassName?: string;
|
|
53
|
+
readonly titleClassName?: string;
|
|
54
|
+
/** Merged onto the body wrapper (default `p-6 pt-0 flex flex-col gap-4`). */
|
|
55
|
+
readonly bodyClassName?: string;
|
|
50
56
|
readonly children: ReactNode;
|
|
51
57
|
};
|
|
52
58
|
|
|
53
59
|
// kumiko-lint-ignore no-custom-primitives Wraps usePrimitives().Card, pure domain name for the auth-screen chrome, not a primitive reimplementation
|
|
54
|
-
export function AuthCard({
|
|
60
|
+
export function AuthCard({
|
|
61
|
+
title,
|
|
62
|
+
subtitle,
|
|
63
|
+
className,
|
|
64
|
+
headerClassName,
|
|
65
|
+
titleClassName,
|
|
66
|
+
bodyClassName,
|
|
67
|
+
children,
|
|
68
|
+
}: AuthCardProps): ReactNode {
|
|
55
69
|
const { Card } = usePrimitives();
|
|
56
70
|
const shell = useAuthShell() ?? defaultAuthShell;
|
|
57
71
|
// h1 (Seiten-Hauptüberschrift) via Header-Slot erhalten — die Card-Default-
|
|
58
72
|
// Header wäre h3. padded:false = Form sitzt randlos wie bisher (bare form).
|
|
59
73
|
const card = (
|
|
60
74
|
<Card
|
|
61
|
-
className="w-full max-w-sm"
|
|
75
|
+
className={cn("w-full max-w-sm", className)}
|
|
62
76
|
options={{ padded: false }}
|
|
63
77
|
slots={{
|
|
64
78
|
header:
|
|
65
79
|
title !== undefined || subtitle !== undefined ? (
|
|
66
|
-
<div className="flex flex-col space-y-1.5 p-6 pb-4">
|
|
80
|
+
<div className={cn("flex flex-col space-y-1.5 p-6 pb-4", headerClassName)}>
|
|
67
81
|
{title !== undefined && (
|
|
68
|
-
<h1 className="text-xl font-semibold tracking-tight"
|
|
82
|
+
<h1 className={cn("text-xl font-semibold tracking-tight", titleClassName)}>
|
|
83
|
+
{title}
|
|
84
|
+
</h1>
|
|
69
85
|
)}
|
|
70
86
|
{subtitle !== undefined && (
|
|
71
87
|
<p className="text-sm text-muted-foreground">{subtitle}</p>
|
|
@@ -74,7 +90,9 @@ export function AuthCard({ title, subtitle, children }: AuthCardProps): ReactNod
|
|
|
74
90
|
) : undefined,
|
|
75
91
|
}}
|
|
76
92
|
>
|
|
77
|
-
<BareFormProvider>
|
|
93
|
+
<BareFormProvider>
|
|
94
|
+
<div className={cn("p-6 pt-0 flex flex-col gap-4", bodyClassName)}>{children}</div>
|
|
95
|
+
</BareFormProvider>
|
|
78
96
|
</Card>
|
|
79
97
|
);
|
|
80
98
|
return shell(card);
|
package/packages/bundled-features/src/auth-email-password/web/confirm-account-unlock-screen.tsx
CHANGED
|
@@ -47,12 +47,10 @@ export function ConfirmAccountUnlockScreen({
|
|
|
47
47
|
if (status === "missing-token") {
|
|
48
48
|
return (
|
|
49
49
|
<AuthCard title={title ?? t("auth.unlockAccount.errorTitle")}>
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
</Link>
|
|
55
|
-
</div>
|
|
50
|
+
<p className="text-sm text-muted-foreground">{t("auth.unlockAccount.missingToken")}</p>
|
|
51
|
+
<Link href={loginHref} variant="muted">
|
|
52
|
+
{t("auth.unlockAccount.goToLogin")}
|
|
53
|
+
</Link>
|
|
56
54
|
</AuthCard>
|
|
57
55
|
);
|
|
58
56
|
}
|
|
@@ -72,12 +70,10 @@ export function ConfirmAccountUnlockScreen({
|
|
|
72
70
|
if (status === "success") {
|
|
73
71
|
return (
|
|
74
72
|
<AuthCard title={title ?? t("auth.unlockAccount.successTitle")}>
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
</Link>
|
|
80
|
-
</div>
|
|
73
|
+
<p className="text-sm text-muted-foreground">{t("auth.unlockAccount.successBody")}</p>
|
|
74
|
+
<Link href={loginHref} variant="button">
|
|
75
|
+
{t("auth.unlockAccount.goToLogin")}
|
|
76
|
+
</Link>
|
|
81
77
|
</AuthCard>
|
|
82
78
|
);
|
|
83
79
|
}
|
|
@@ -85,12 +81,10 @@ export function ConfirmAccountUnlockScreen({
|
|
|
85
81
|
// status === "error"
|
|
86
82
|
return (
|
|
87
83
|
<AuthCard title={title ?? t("auth.unlockAccount.errorTitle")}>
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
</Link>
|
|
93
|
-
</div>
|
|
84
|
+
<p className="text-sm text-muted-foreground">{t("auth.unlockAccount.errorBody")}</p>
|
|
85
|
+
<Link href={loginHref} variant="muted">
|
|
86
|
+
{t("auth.unlockAccount.goToLogin")}
|
|
87
|
+
</Link>
|
|
94
88
|
</AuthCard>
|
|
95
89
|
);
|
|
96
90
|
}
|
|
@@ -72,7 +72,7 @@ export function ForgotPasswordScreen({
|
|
|
72
72
|
return (
|
|
73
73
|
<AuthCard title={effectiveTitle} subtitle={subtitle}>
|
|
74
74
|
{done ? (
|
|
75
|
-
|
|
75
|
+
<>
|
|
76
76
|
<Banner variant="info">
|
|
77
77
|
<p className="font-medium text-foreground">{t("auth.forgotPassword.successTitle")}</p>
|
|
78
78
|
<p className="mt-1">{t("auth.forgotPassword.successBody")}</p>
|
|
@@ -80,9 +80,9 @@ export function ForgotPasswordScreen({
|
|
|
80
80
|
<Link href={loginHref} variant="muted">
|
|
81
81
|
{t("auth.forgotPassword.backToLogin")}
|
|
82
82
|
</Link>
|
|
83
|
-
|
|
83
|
+
</>
|
|
84
84
|
) : (
|
|
85
|
-
|
|
85
|
+
<>
|
|
86
86
|
<p className="text-sm text-muted-foreground">{t("auth.forgotPassword.intro")}</p>
|
|
87
87
|
<Form onSubmit={onSubmit}>
|
|
88
88
|
<Field id="forgot-email" label={t("auth.forgotPassword.email")} required>
|
|
@@ -104,7 +104,7 @@ export function ForgotPasswordScreen({
|
|
|
104
104
|
<Link href={loginHref} variant="muted" className="self-center">
|
|
105
105
|
{t("auth.forgotPassword.backToLogin")}
|
|
106
106
|
</Link>
|
|
107
|
-
|
|
107
|
+
</>
|
|
108
108
|
)}
|
|
109
109
|
</AuthCard>
|
|
110
110
|
);
|
|
@@ -186,87 +186,83 @@ export function InviteAcceptScreen({
|
|
|
186
186
|
if (token === "") {
|
|
187
187
|
return (
|
|
188
188
|
<AuthCard title={effectiveTitle}>
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
</Link>
|
|
194
|
-
</div>
|
|
189
|
+
<p className="text-sm text-muted-foreground">{t("auth.inviteAccept.missingToken")}</p>
|
|
190
|
+
<Link href={loginHref} variant="muted">
|
|
191
|
+
{t("auth.inviteAccept.goToLogin")}
|
|
192
|
+
</Link>
|
|
195
193
|
</AuthCard>
|
|
196
194
|
);
|
|
197
195
|
}
|
|
198
196
|
|
|
199
197
|
return (
|
|
200
198
|
<AuthCard title={effectiveTitle}>
|
|
201
|
-
<
|
|
202
|
-
<p className="text-sm text-muted-foreground">{t("auth.inviteAccept.intro")}</p>
|
|
199
|
+
<p className="text-sm text-muted-foreground">{t("auth.inviteAccept.intro")}</p>
|
|
203
200
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
<Input
|
|
228
|
-
kind="email"
|
|
229
|
-
id="invite-email"
|
|
230
|
-
name="invite-email"
|
|
231
|
-
value={email}
|
|
232
|
-
onChange={setEmail}
|
|
233
|
-
disabled={submitting}
|
|
234
|
-
required
|
|
235
|
-
autoComplete="email"
|
|
236
|
-
/>
|
|
237
|
-
</Field>
|
|
238
|
-
)}
|
|
239
|
-
<Field id="invite-password" label={t("auth.inviteAccept.password")} required>
|
|
201
|
+
{mode === "loggedin" ? (
|
|
202
|
+
<Form onSubmit={onSubmit}>
|
|
203
|
+
<p className="text-sm">
|
|
204
|
+
{t("auth.inviteAccept.loggedInAs", { email: session.user?.email ?? "" })}
|
|
205
|
+
</p>
|
|
206
|
+
{error !== null && <Banner variant="error">{error}</Banner>}
|
|
207
|
+
<Button type="submit" loading={submitting} disabled={submitting}>
|
|
208
|
+
{submitting ? t("auth.inviteAccept.submitting") : t("auth.inviteAccept.acceptButton")}
|
|
209
|
+
</Button>
|
|
210
|
+
<Button
|
|
211
|
+
type="button"
|
|
212
|
+
variant="secondary"
|
|
213
|
+
onClick={() => {
|
|
214
|
+
setMode("anon-existing");
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
{t("auth.inviteAccept.useOtherAccount")}
|
|
218
|
+
</Button>
|
|
219
|
+
</Form>
|
|
220
|
+
) : (
|
|
221
|
+
<Form onSubmit={onSubmit}>
|
|
222
|
+
{mode === "anon-existing" && (
|
|
223
|
+
<Field id="invite-email" label={t("auth.inviteAccept.email")} required>
|
|
240
224
|
<Input
|
|
241
|
-
kind="
|
|
242
|
-
id="invite-
|
|
243
|
-
name="invite-
|
|
244
|
-
value={
|
|
245
|
-
onChange={
|
|
225
|
+
kind="email"
|
|
226
|
+
id="invite-email"
|
|
227
|
+
name="invite-email"
|
|
228
|
+
value={email}
|
|
229
|
+
onChange={setEmail}
|
|
246
230
|
disabled={submitting}
|
|
247
231
|
required
|
|
248
|
-
autoComplete=
|
|
232
|
+
autoComplete="email"
|
|
249
233
|
/>
|
|
250
234
|
</Field>
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
</
|
|
268
|
-
|
|
269
|
-
|
|
235
|
+
)}
|
|
236
|
+
<Field id="invite-password" label={t("auth.inviteAccept.password")} required>
|
|
237
|
+
<Input
|
|
238
|
+
kind="password"
|
|
239
|
+
id="invite-password"
|
|
240
|
+
name="invite-password"
|
|
241
|
+
value={password}
|
|
242
|
+
onChange={setPassword}
|
|
243
|
+
disabled={submitting}
|
|
244
|
+
required
|
|
245
|
+
autoComplete={mode === "anon-existing" ? "current-password" : "new-password"}
|
|
246
|
+
/>
|
|
247
|
+
</Field>
|
|
248
|
+
{error !== null && <Banner variant="error">{error}</Banner>}
|
|
249
|
+
<Button type="submit" loading={submitting} disabled={submitting}>
|
|
250
|
+
{submitting ? t("auth.inviteAccept.submitting") : t("auth.inviteAccept.submit")}
|
|
251
|
+
</Button>
|
|
252
|
+
<Button
|
|
253
|
+
type="button"
|
|
254
|
+
variant="secondary"
|
|
255
|
+
onClick={() => {
|
|
256
|
+
setMode(mode === "anon-existing" ? "anon-new" : "anon-existing");
|
|
257
|
+
setError(null);
|
|
258
|
+
}}
|
|
259
|
+
>
|
|
260
|
+
{mode === "anon-existing"
|
|
261
|
+
? t("auth.inviteAccept.toggleNew")
|
|
262
|
+
: t("auth.inviteAccept.toggleExisting")}
|
|
263
|
+
</Button>
|
|
264
|
+
</Form>
|
|
265
|
+
)}
|
|
270
266
|
</AuthCard>
|
|
271
267
|
);
|
|
272
268
|
}
|