@cosmicdrift/kumiko-samples 0.383.0 → 0.384.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 +6 -1
- package/packages/bundled-features/src/audit/changes.json +6 -0
- package/packages/bundled-features/src/audit/event-classification.ts +83 -0
- package/packages/bundled-features/src/audit/feature.ts +3 -2
- package/packages/bundled-features/src/audit/handlers/audit-events.ts +63 -5
- package/packages/bundled-features/src/audit/handlers/details-all-tenants.query.ts +6 -5
- package/packages/bundled-features/src/audit/handlers/details.query.ts +12 -6
- package/packages/bundled-features/src/audit/handlers/list-all-tenants.query.ts +20 -12
- package/packages/bundled-features/src/audit/handlers/list.query.ts +14 -3
- package/packages/bundled-features/src/audit/screens.ts +32 -4
- package/packages/bundled-features/src/audit/web/audit-event-cell.tsx +22 -0
- package/packages/bundled-features/src/audit/web/audit-event-labels.ts +30 -0
- package/packages/bundled-features/src/audit/web/client-plugin.tsx +10 -1
- package/packages/bundled-features/src/audit/web/constants.ts +11 -0
- package/packages/bundled-features/src/audit/web/i18n.ts +48 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
- package/packages/bundled-features/src/auth-email-password/feature.ts +3 -2
- package/packages/bundled-features/src/auth-email-password/handlers/login-options.query.ts +19 -0
- package/packages/bundled-features/src/auth-email-password/index.ts +7 -1
- package/packages/bundled-features/src/auth-email-password/{auth-paths.ts → web/auth-paths.ts} +7 -4
- package/packages/bundled-features/src/auth-email-password/web/client-plugin.ts +7 -0
- package/packages/bundled-features/src/auth-email-password/web/constants.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +31 -0
- package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +68 -42
- package/packages/bundled-features/src/jobs/changes.json +6 -0
- package/packages/bundled-features/src/jobs/feature.ts +34 -127
- package/packages/bundled-features/src/jobs/handlers/consumer.write.ts +98 -0
- package/packages/bundled-features/src/jobs/handlers/consumers.query.ts +137 -0
- package/packages/bundled-features/src/jobs/screens.ts +262 -0
- package/packages/bundled-features/src/jobs/web/constants.ts +29 -0
- package/packages/bundled-features/src/jobs/web/i18n.ts +41 -0
- package/packages/bundled-features/src/secrets/i18n.ts +0 -3
- package/packages/bundled-features/src/sessions/changes.json +6 -0
- package/packages/bundled-features/src/sessions/constants.ts +3 -1
- package/packages/bundled-features/src/sessions/handlers/detail.query.ts +7 -3
- package/packages/bundled-features/src/sessions/handlers/list.query.ts +16 -10
- package/packages/bundled-features/src/sessions/handlers/mine.query.ts +15 -9
- package/packages/bundled-features/src/sessions/screens.ts +23 -8
- package/packages/bundled-features/src/sessions/web/client-plugin.tsx +17 -0
- package/packages/bundled-features/src/sessions/web/constants.ts +3 -0
- package/packages/bundled-features/src/sessions/web/default-translations.ts +8 -0
- package/packages/bundled-features/src/sessions/web/i18n.ts +5 -1
- package/packages/bundled-features/src/sessions/web/index.ts +1 -0
- package/packages/bundled-features/src/sessions/web/session-device-cell.tsx +19 -0
- package/packages/bundled-features/src/sessions/web/user-agent.ts +74 -0
- package/packages/bundled-features/src/template-resolver/changes.json +6 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +2 -0
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +34 -1
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +53 -0
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +51 -0
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +7 -38
- package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/changes.json +18 -0
- package/packages/bundled-features/src/user-data-rights/feature.ts +33 -33
- package/packages/bundled-features/src/user-data-rights/web/i18n.ts +4 -1
- package/samples/apps/showcase/src/features/items/schema/item.ts +8 -2
- package/samples/apps/ui-walkthrough/src/features/tasks/schema/task.ts +2 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +4 -0
- package/samples/apps/use-all-bundled/src/app/client.tsx +2 -0
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -1
- package/samples/apps/use-all-bundled/src/app/invoice-reports-feature.ts +30 -0
- package/samples/apps/use-all-bundled/src/app/seed.ts +51 -0
- package/samples/apps/workspaces/src/features/demo/schema/order.ts +2 -2
- package/samples/recipes/custom-fields-basic/src/feature.ts +13 -3
- package/samples/recipes/designer-demo/src/feature.ts +6 -2
|
@@ -24,8 +24,55 @@ export const AUDIT_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
|
24
24
|
"audit.log.col.tenant": { en: "Tenant" },
|
|
25
25
|
"audit.log.col.when": { en: "When" },
|
|
26
26
|
"audit.log.col.type": { en: "Event" },
|
|
27
|
-
"audit.log.col.aggregateType": { en: "Aggregate type" },
|
|
28
27
|
"audit.log.col.aggregateId": { en: "Aggregate ID" },
|
|
28
|
+
"audit.log.col.aggregateType": { en: "Object" },
|
|
29
|
+
"audit.log.event": { en: "{entity} {action}" },
|
|
30
|
+
"audit.log.action.created": { en: "created" },
|
|
31
|
+
"audit.log.action.updated": { en: "updated" },
|
|
32
|
+
"audit.log.action.deleted": { en: "deleted" },
|
|
33
|
+
"audit.log.action.restored": { en: "restored" },
|
|
34
|
+
"audit.log.facet.technical": { en: "Technical entries" },
|
|
35
|
+
"audit.log.facet.technical.include": { en: "Show technical entries" },
|
|
36
|
+
"audit.log.event.custom-field-set": { en: "Custom field value set" },
|
|
37
|
+
"audit.log.event.custom-field-cleared": { en: "Custom field value cleared" },
|
|
38
|
+
"audit.log.event.field-definition-deleted": { en: "Custom field deleted" },
|
|
39
|
+
"audit.log.event.secret-read": { en: "Secret read" },
|
|
40
|
+
"audit.log.event.session-revoked": { en: "Session ended" },
|
|
41
|
+
"audit.log.event.subject-forgotten": { en: "Person forgotten (data erased)" },
|
|
42
|
+
"audit.log.event.forget-denied": { en: "Erasure request denied" },
|
|
43
|
+
"audit.log.event.pat-revoked": { en: "Access token revoked" },
|
|
44
|
+
"audit.log.event.mail-account-connected": { en: "Mail account connected" },
|
|
45
|
+
"audit.log.event.mail-account-updated": { en: "Mail account updated" },
|
|
46
|
+
"audit.log.event.mail-account-disconnected": { en: "Mail account disconnected" },
|
|
47
|
+
"audit.log.event.subscription-created": { en: "Subscription created" },
|
|
48
|
+
"audit.log.event.subscription-updated": { en: "Subscription updated" },
|
|
49
|
+
"audit.log.event.subscription-canceled": { en: "Subscription canceled" },
|
|
50
|
+
"audit.log.event.invoice-paid": { en: "Invoice paid" },
|
|
51
|
+
"audit.log.event.invoice-payment-failed": { en: "Invoice payment failed" },
|
|
52
|
+
"audit.log.event.payment-received": { en: "Payment received" },
|
|
53
|
+
"audit.log.event.checkout-consent-recorded": { en: "Checkout consent recorded" },
|
|
54
|
+
"audit.log.event.contract-confirmation-issued": { en: "Contract confirmation issued" },
|
|
55
|
+
"audit.log.event.contract-termination-revoked": { en: "Contract termination revoked" },
|
|
56
|
+
"audit.log.event.destruction-requested": { en: "Tenant deletion requested" },
|
|
57
|
+
"audit.log.event.destruction-cancelled": { en: "Tenant deletion cancelled" },
|
|
58
|
+
"audit.log.event.tenant-destruction-started": { en: "Tenant deletion started" },
|
|
59
|
+
"audit.log.event.tenant-destruction-completed": { en: "Tenant deletion completed" },
|
|
60
|
+
"audit.log.event.tenant-destruction-failed": { en: "Tenant deletion failed" },
|
|
61
|
+
"audit.log.event.tenant-handover-claimed": { en: "Tenant handover claimed" },
|
|
62
|
+
"audit.log.event.waitlist-exported": { en: "Waitlist exported" },
|
|
63
|
+
"audit.log.event.waitlist-purged": { en: "Waitlist purged" },
|
|
64
|
+
"audit.log.event.feature-toggle-set": { en: "Feature toggle changed" },
|
|
65
|
+
"audit.log.event.contract-termination-requested": { en: "Contract termination requested" },
|
|
66
|
+
"audit.log.event.contract-termination-declared": { en: "Contract termination declared" },
|
|
67
|
+
"audit.log.event.contract-termination-unmatched": { en: "Contract termination not matched" },
|
|
68
|
+
"audit.log.entity.tenant": { en: "Tenant" },
|
|
69
|
+
"audit.log.entity.tenant-membership": { en: "Membership" },
|
|
70
|
+
"audit.log.entity.tenant-invitation": { en: "Invitation" },
|
|
71
|
+
"audit.log.entity.user": { en: "User" },
|
|
72
|
+
"audit.log.entity.folder": { en: "Folder" },
|
|
73
|
+
"audit.log.entity.folder-assignment": { en: "Folder assignment" },
|
|
74
|
+
"audit.log.entity.field-definition": { en: "Custom field" },
|
|
75
|
+
"audit.log.entity.template-resource": { en: "Template" },
|
|
29
76
|
"audit.log.col.actor": { en: "Actor" },
|
|
30
77
|
"audit.log.details": { en: "Details" },
|
|
31
78
|
"audit.log.detail.payload": { en: "Event payload" },
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.384.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Forgot-password and account-unlock work without app wiring",
|
|
6
|
+
"detail": "With auth.mail and SMTP configured, runProdApp and runDevApp now also wire the account-unlock flow (POST /api/auth/request-account-unlock and /api/auth/confirm-account-unlock, mail link to /unlock-account) because the login lockout is always on. emailPasswordClient serves ForgotPasswordScreen, ResetPasswordScreen (with token) and the unlock screens at /forgot-password, /reset-password and /unlock-account; gates registered earlier by the app win, and defaultAuthPages: false turns it off. LoginScreen reads the new anonymous login-options query and shows the forgot and unlock links when the flows exist; explicit forgotPasswordHref and unlockAccountHref still win and null hides a link. The query needs anonymousAccess on the app, otherwise no link appears."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.383.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { inviteInfoQuery } from "./handlers/invite-info.query.ts";
|
|
15
15
|
import { createInviteSignupCompleteHandler } from "./handlers/invite-signup-complete.write.ts";
|
|
16
16
|
import { createLoginHandler, type LoginHandlerOptions } from "./handlers/login.write.ts";
|
|
17
|
+
import { loginOptionsQuery } from "./handlers/login-options.query.ts";
|
|
17
18
|
import { logoutWrite } from "./handlers/logout.write.ts";
|
|
18
19
|
import {
|
|
19
20
|
createRequestAccountUnlockHandler,
|
|
@@ -197,8 +198,7 @@ export function createAuthEmailPasswordFeature(
|
|
|
197
198
|
{ key: "accountUnlock", label: "Account-Unlock-Flow", type: "boolean", default: false },
|
|
198
199
|
],
|
|
199
200
|
});
|
|
200
|
-
r.requires({ features: ["user"] });
|
|
201
|
-
r.requires({ features: ["tenant"] });
|
|
201
|
+
r.requires({ features: ["user", "tenant"] });
|
|
202
202
|
// All five magic-link flows (reset/verify/signup/invite/unlock) dispatch
|
|
203
203
|
// via ctx.notify → delivery must be mounted. Fail closed at boot instead
|
|
204
204
|
// of silently dropping the mail.
|
|
@@ -256,6 +256,7 @@ export function createAuthEmailPasswordFeature(
|
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
const queries = {
|
|
259
|
+
loginOptions: r.queryHandler(loginOptionsQuery),
|
|
259
260
|
...(opts.signup && {
|
|
260
261
|
signupRegistrationStatus: r.queryHandler(selfRegistrationStatusQuery),
|
|
261
262
|
}),
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { AuthHandlers } from "../web/constants.ts";
|
|
4
|
+
|
|
5
|
+
// Anonymous-readable so the (unauthenticated) login screen can show the
|
|
6
|
+
// forgot-password and unlock links only when the matching mail flows are
|
|
7
|
+
// actually mounted, without every app having to pass hrefs by hand.
|
|
8
|
+
export const loginOptionsQuery = defineQueryHandler({
|
|
9
|
+
name: "login-options",
|
|
10
|
+
schema: z.object({}),
|
|
11
|
+
access: { roles: ["anonymous", "User", "TenantAdmin", "SystemAdmin"] },
|
|
12
|
+
rateLimit: { per: "ip+handler", limit: 30, windowSeconds: 60 },
|
|
13
|
+
description:
|
|
14
|
+
"Reports which optional login-page flows (password reset, account unlock) are available, readable without signing in, so the login screen links only to flows that exist.",
|
|
15
|
+
handler: async (_query, ctx) => ({
|
|
16
|
+
passwordReset: ctx.registry.getWriteHandler(AuthHandlers.requestPasswordReset) !== undefined,
|
|
17
|
+
accountUnlock: ctx.registry.getWriteHandler(AuthHandlers.requestAccountUnlock) !== undefined,
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
// hashPassword/verifyPassword; removing it is a breaking change that needs
|
|
7
7
|
// its own changeset + deliberate deprecation window, not a silent drop.
|
|
8
8
|
export { hashPassword, verifyPassword } from "../shared/password-hashing.ts";
|
|
9
|
-
export { type AuthPath, type AuthPaths, DEFAULT_AUTH_PATHS, makeAuthPaths } from "./auth-paths.ts";
|
|
10
9
|
export type {
|
|
11
10
|
BootstrapConfigValue,
|
|
12
11
|
BootstrapDeps,
|
|
@@ -54,6 +53,13 @@ export {
|
|
|
54
53
|
SIGNUP_TOKEN_KEY_PREFIXES,
|
|
55
54
|
storeSignupToken,
|
|
56
55
|
} from "./signup-token-store.ts";
|
|
56
|
+
export {
|
|
57
|
+
type AuthPath,
|
|
58
|
+
type AuthPaths,
|
|
59
|
+
DEFAULT_AUTH_PATHS,
|
|
60
|
+
DEFAULT_FORGOT_PASSWORD_PATH,
|
|
61
|
+
makeAuthPaths,
|
|
62
|
+
} from "./web/auth-paths.ts";
|
|
57
63
|
export {
|
|
58
64
|
AUTH_EMAIL_PASSWORD_FEATURE,
|
|
59
65
|
AuthErrors,
|
package/packages/bundled-features/src/auth-email-password/{auth-paths.ts → web/auth-paths.ts}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Convention paths for the
|
|
1
|
+
// Convention paths for the five auth pages (relative to the app base URL).
|
|
2
2
|
// resolveAuthMail builds each flow's magic-link appUrl from baseUrl + these;
|
|
3
3
|
// the handlers append `?token=…` and mail via delivery (ctx.notify).
|
|
4
4
|
|
|
@@ -6,23 +6,26 @@
|
|
|
6
6
|
* (e.g. `/en/reset-password`, `/de/reset-password`). */
|
|
7
7
|
export type AuthPath = string | ((locale: string) => string);
|
|
8
8
|
|
|
9
|
-
/** Pfad-Konstanten der 4 Auth-Seiten (relativ zur App-baseUrl). */
|
|
10
9
|
export type AuthPaths = {
|
|
11
10
|
readonly resetPassword: AuthPath;
|
|
12
11
|
readonly verifyEmail: AuthPath;
|
|
13
12
|
readonly signupComplete: AuthPath;
|
|
14
13
|
readonly inviteAccept: AuthPath;
|
|
14
|
+
readonly unlockAccount: AuthPath;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* nur die Ausnahme via `makeAuthPaths({ ... })`. */
|
|
17
|
+
/** Shared by every Kumiko app; apps override only the exceptions via `makeAuthPaths({ ... })`. */
|
|
19
18
|
export const DEFAULT_AUTH_PATHS = {
|
|
20
19
|
resetPassword: "/reset-password",
|
|
21
20
|
verifyEmail: "/verify-email",
|
|
22
21
|
signupComplete: "/signup/complete",
|
|
23
22
|
inviteAccept: "/invite/accept",
|
|
23
|
+
unlockAccount: "/unlock-account",
|
|
24
24
|
} as const satisfies AuthPaths;
|
|
25
25
|
|
|
26
|
+
/** Client page that requests a reset mail; no mail links to it, so it is not part of AuthPaths. */
|
|
27
|
+
export const DEFAULT_FORGOT_PASSWORD_PATH = "/forgot-password";
|
|
28
|
+
|
|
26
29
|
export function makeAuthPaths(overrides: Partial<AuthPaths> = {}): AuthPaths {
|
|
27
30
|
return { ...DEFAULT_AUTH_PATHS, ...overrides };
|
|
28
31
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
makeSessionAuthGate,
|
|
14
14
|
} from "./auth-gate.tsx";
|
|
15
15
|
import { emailPasswordClientActions } from "./client-actions.ts";
|
|
16
|
+
import { DefaultAuthPagesGate } from "./default-auth-pages-gate.tsx";
|
|
16
17
|
import { defaultTranslations, mergeTranslations } from "./i18n.ts";
|
|
17
18
|
import type { LoginScreenProps } from "./login-screen.tsx";
|
|
18
19
|
|
|
@@ -45,6 +46,11 @@ export type EmailPasswordClientOptions = {
|
|
|
45
46
|
* Für Branding ("Sign in" → "Login to Acme") oder weitere Sprachen
|
|
46
47
|
* (`fr`, `es`, …) zusätzlich. */
|
|
47
48
|
readonly translations?: TranslationsByLocale;
|
|
49
|
+
/** Serve the forgot-password, reset-password and unlock-account pages at
|
|
50
|
+
* their convention paths for signed-out visitors. Apps with their own
|
|
51
|
+
* screens at those paths either register an earlier gate (it wins) or
|
|
52
|
+
* set this to false. Default: true. */
|
|
53
|
+
readonly defaultAuthPages?: boolean;
|
|
48
54
|
};
|
|
49
55
|
|
|
50
56
|
// Structurally identical to the renderer/web ClientFeatureDefinition, but
|
|
@@ -66,6 +72,7 @@ export function emailPasswordClient(
|
|
|
66
72
|
name: "auth-email-password",
|
|
67
73
|
providers: [],
|
|
68
74
|
gates: [
|
|
75
|
+
...(options.defaultAuthPages === false ? [] : [DefaultAuthPagesGate]),
|
|
69
76
|
makeSessionAuthGate({
|
|
70
77
|
loginScreen: options.loginScreen,
|
|
71
78
|
loginScreenProps: options.loginScreenProps,
|
|
@@ -48,6 +48,7 @@ export const AuthHandlers = {
|
|
|
48
48
|
// Qualified query names. Anonymous-readable status so the (unauthenticated)
|
|
49
49
|
// signup page can decide whether to show its own link/form.
|
|
50
50
|
export const AuthQueries = {
|
|
51
|
+
loginOptions: "auth-email-password:query:login-options",
|
|
51
52
|
signupRegistrationStatus: "auth-email-password:query:signup-registration-status",
|
|
52
53
|
// Anonymous, read-only invite lookup for the invite-acceptance page —
|
|
53
54
|
// see invite-info.query.ts. Does not consume the token.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { DEFAULT_AUTH_PATHS, DEFAULT_FORGOT_PASSWORD_PATH } from "./auth-paths.ts";
|
|
3
|
+
import { ConfirmAccountUnlockScreen } from "./confirm-account-unlock-screen.tsx";
|
|
4
|
+
import { ForgotPasswordScreen } from "./forgot-password-screen.tsx";
|
|
5
|
+
import { RequestAccountUnlockScreen } from "./request-account-unlock-screen.tsx";
|
|
6
|
+
import { ResetPasswordScreen } from "./reset-password-screen.tsx";
|
|
7
|
+
|
|
8
|
+
function hasToken(search: string): boolean {
|
|
9
|
+
return (new URLSearchParams(search).get("token") ?? "") !== "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function pickDefaultAuthPage(pathname: string, search: string): ReactNode {
|
|
13
|
+
const path = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
|
|
14
|
+
switch (path) {
|
|
15
|
+
case DEFAULT_FORGOT_PASSWORD_PATH:
|
|
16
|
+
return <ForgotPasswordScreen />;
|
|
17
|
+
case DEFAULT_AUTH_PATHS.resetPassword:
|
|
18
|
+
return hasToken(search) ? <ResetPasswordScreen /> : null;
|
|
19
|
+
case DEFAULT_AUTH_PATHS.unlockAccount:
|
|
20
|
+
return hasToken(search) ? <ConfirmAccountUnlockScreen /> : <RequestAccountUnlockScreen />;
|
|
21
|
+
default:
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Sits inside emailPasswordClient's gate list, so app gates registered earlier
|
|
27
|
+
// (outer) still win on the same path. The pages are public: no session needed.
|
|
28
|
+
export function DefaultAuthPagesGate({ children }: { readonly children: ReactNode }): ReactNode {
|
|
29
|
+
const { pathname, search } = window.location;
|
|
30
|
+
return pickDefaultAuthPage(pathname, search) ?? <>{children}</>;
|
|
31
|
+
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// (de+en) liefert das Feature selber mit (translations.ts); Apps die
|
|
4
|
-
// was anderes wollen, setzen entweder einen eigenen LocaleResolver auf
|
|
5
|
-
// App-Level oder reichen Key-Overrides an `emailPasswordClient()`.
|
|
6
|
-
//
|
|
7
|
-
// Die Reason-Codes aus dem Login-Handler werden 1:1 auf i18n-Keys
|
|
8
|
-
// gemappt (reasonToI18nKey) — neue Reason-Codes brauchen nur eine
|
|
9
|
-
// neue Zeile im Bundle + hier im Mapping.
|
|
1
|
+
// Login handler reason codes map 1:1 to i18n keys (reasonToI18nKey): a new
|
|
2
|
+
// reason code needs a bundle entry plus a line in that mapping.
|
|
10
3
|
|
|
11
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useOptionalDispatcher,
|
|
6
|
+
usePrimitives,
|
|
7
|
+
useQuery,
|
|
8
|
+
useTranslation,
|
|
9
|
+
} from "@cosmicdrift/kumiko-renderer";
|
|
12
10
|
import { type FormEvent, type ReactNode, useState } from "react";
|
|
13
11
|
import {
|
|
14
12
|
type KnownLoginFailureReason,
|
|
@@ -16,11 +14,12 @@ import {
|
|
|
16
14
|
requestEmailVerification,
|
|
17
15
|
} from "./auth-client.ts";
|
|
18
16
|
import { AuthCard } from "./auth-form-primitives.tsx";
|
|
17
|
+
import { DEFAULT_AUTH_PATHS, DEFAULT_FORGOT_PASSWORD_PATH } from "./auth-paths.ts";
|
|
19
18
|
import { followNextOrLandingAfterLogin } from "./auth-redirect.ts";
|
|
19
|
+
import { AuthQueries } from "./constants.ts";
|
|
20
20
|
import { useSession } from "./session.tsx";
|
|
21
21
|
|
|
22
|
-
// Resend
|
|
23
|
-
// reason=email_not_verified unter dem Fehler-Banner angeboten wird.
|
|
22
|
+
// Resend state for the confirmation mail offered under the error banner on reason=email_not_verified.
|
|
24
23
|
type ResendStatus =
|
|
25
24
|
| { readonly kind: "idle" }
|
|
26
25
|
| { readonly kind: "sending" }
|
|
@@ -34,32 +33,27 @@ export type AuthLegalLink = {
|
|
|
34
33
|
};
|
|
35
34
|
|
|
36
35
|
export type LoginScreenProps = {
|
|
37
|
-
/**
|
|
38
|
-
* Titel stark app-branded ist und keine Translation braucht. */
|
|
36
|
+
/** Overrides the `auth.login.title` key; only for a branded title that needs no translation. */
|
|
39
37
|
readonly title?: string;
|
|
40
38
|
readonly subtitle?: ReactNode;
|
|
41
39
|
readonly submitLabel?: string;
|
|
42
|
-
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
readonly forgotPasswordHref?: string;
|
|
40
|
+
/** Href of the "Forgot password?" link under the submit button. Unset: the
|
|
41
|
+
* default path once the server offers the reset flow (login-options);
|
|
42
|
+
* `null` hides the link deliberately. */
|
|
43
|
+
readonly forgotPasswordHref?: string | null;
|
|
47
44
|
/** Optional href to RequestAccountUnlockScreen (#1266). When set, an
|
|
48
45
|
* account_locked login error renders an "Unlock account?" link inline —
|
|
49
46
|
* the account-lockout escape hatch, since forgotPasswordHref does NOT
|
|
50
47
|
* help here: the lockout gate blocks login before password verify even
|
|
51
48
|
* runs, so it also blocks the post-reset login. Apps without
|
|
52
|
-
* accountUnlock configured just leave this unset.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* (closed-invite-only) setzen das einfach nicht. */
|
|
49
|
+
* accountUnlock configured just leave this unset. Unset: default path once
|
|
50
|
+
* the server offers the flow (login-options); `null` = deliberately no link. */
|
|
51
|
+
readonly unlockAccountHref?: string | null;
|
|
52
|
+
/** Href of the "Create account" link; invite-only apps leave it unset. */
|
|
57
53
|
readonly signupHref?: string;
|
|
58
|
-
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* Labels kommen vom Caller (typisch schon übersetzt bzw. Eigennamen
|
|
62
|
-
* wie "Impressum"). */
|
|
54
|
+
/** Imprint/privacy links under the card: the login screen is often the only
|
|
55
|
+
* public page of an admin domain, which then must reach its legal pages
|
|
56
|
+
* itself (German imprint duty). Labels come translated from the caller. */
|
|
63
57
|
readonly legalLinks?: readonly AuthLegalLink[];
|
|
64
58
|
/** Called when the server responds with an MFA challenge instead of a
|
|
65
59
|
* session. Apps typically swap this screen out for auth-mfa's
|
|
@@ -112,7 +106,7 @@ function reasonToKey(failure: LoginFailure): {
|
|
|
112
106
|
}
|
|
113
107
|
}
|
|
114
108
|
|
|
115
|
-
|
|
109
|
+
function LoginScreenBody({
|
|
116
110
|
title,
|
|
117
111
|
subtitle,
|
|
118
112
|
submitLabel,
|
|
@@ -122,7 +116,8 @@ export function LoginScreen({
|
|
|
122
116
|
legalLinks,
|
|
123
117
|
onMfaChallenge,
|
|
124
118
|
onMfaSetupRequired,
|
|
125
|
-
|
|
119
|
+
loginOptions,
|
|
120
|
+
}: LoginScreenProps & { readonly loginOptions: LoginOptions | null }): ReactNode {
|
|
126
121
|
const t = useTranslation();
|
|
127
122
|
const { Form, Field, Input, Button, Banner, Link } = usePrimitives();
|
|
128
123
|
const session = useSession();
|
|
@@ -131,10 +126,21 @@ export function LoginScreen({
|
|
|
131
126
|
const [submitting, setSubmitting] = useState(false);
|
|
132
127
|
const [error, setError] = useState<LoginFailure | null>(null);
|
|
133
128
|
const [resendStatus, setResendStatus] = useState<ResendStatus>({ kind: "idle" });
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
// (potentiell typoed) Adresse gehen ohne User-Feedback.
|
|
129
|
+
// Hides the resend button once the email input changes; otherwise resend
|
|
130
|
+
// would silently go to the edited, possibly mistyped address.
|
|
137
131
|
const [failedLoginEmail, setFailedLoginEmail] = useState<string | null>(null);
|
|
132
|
+
const effectiveForgotHref =
|
|
133
|
+
forgotPasswordHref === undefined
|
|
134
|
+
? loginOptions?.passwordReset === true
|
|
135
|
+
? DEFAULT_FORGOT_PASSWORD_PATH
|
|
136
|
+
: null
|
|
137
|
+
: forgotPasswordHref;
|
|
138
|
+
const effectiveUnlockHref =
|
|
139
|
+
unlockAccountHref === undefined
|
|
140
|
+
? loginOptions?.accountUnlock === true
|
|
141
|
+
? DEFAULT_AUTH_PATHS.unlockAccount
|
|
142
|
+
: null
|
|
143
|
+
: unlockAccountHref;
|
|
138
144
|
|
|
139
145
|
const doSubmit = async (): Promise<void> => {
|
|
140
146
|
setSubmitting(true);
|
|
@@ -173,10 +179,8 @@ export function LoginScreen({
|
|
|
173
179
|
void doSubmit();
|
|
174
180
|
};
|
|
175
181
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
// sodass kein anti-enumeration-Branching nötig ist; 429 → rate-limit-
|
|
179
|
-
// Hint inline, sonstige Fehler → generischer Inline-Hint.
|
|
182
|
+
// requestEmailVerification answers 200 even for unknown users, so no
|
|
183
|
+
// anti-enumeration branching is needed; only 429 gets its own hint.
|
|
180
184
|
const onResend = async (): Promise<void> => {
|
|
181
185
|
setResendStatus({ kind: "sending" });
|
|
182
186
|
try {
|
|
@@ -252,9 +256,9 @@ export function LoginScreen({
|
|
|
252
256
|
</Button>
|
|
253
257
|
</span>
|
|
254
258
|
)}
|
|
255
|
-
{error.reason === "account_locked" &&
|
|
259
|
+
{error.reason === "account_locked" && effectiveUnlockHref !== null && (
|
|
256
260
|
<span className="self-start">
|
|
257
|
-
<Link href={
|
|
261
|
+
<Link href={effectiveUnlockHref} variant="default">
|
|
258
262
|
{t("auth.login.unlockAccount")}
|
|
259
263
|
</Link>
|
|
260
264
|
</span>
|
|
@@ -272,8 +276,8 @@ export function LoginScreen({
|
|
|
272
276
|
{submitting ? t("auth.login.submitting") : effectiveSubmit}
|
|
273
277
|
</Button>
|
|
274
278
|
</Form>
|
|
275
|
-
{
|
|
276
|
-
<Link href={
|
|
279
|
+
{effectiveForgotHref !== null && (
|
|
280
|
+
<Link href={effectiveForgotHref} variant="muted" className="self-center">
|
|
277
281
|
{t("auth.login.forgotPassword")}
|
|
278
282
|
</Link>
|
|
279
283
|
)}
|
|
@@ -297,3 +301,25 @@ export function LoginScreen({
|
|
|
297
301
|
</AuthCard>
|
|
298
302
|
);
|
|
299
303
|
}
|
|
304
|
+
|
|
305
|
+
type LoginOptions = { readonly passwordReset: boolean; readonly accountUnlock: boolean };
|
|
306
|
+
|
|
307
|
+
function LoginScreenWithQueriedOptions(props: LoginScreenProps): ReactNode {
|
|
308
|
+
const { data } = useQuery<LoginOptions>(
|
|
309
|
+
AuthQueries.loginOptions,
|
|
310
|
+
{},
|
|
311
|
+
{ enabled: props.forgotPasswordHref === undefined || props.unlockAccountHref === undefined },
|
|
312
|
+
);
|
|
313
|
+
return <LoginScreenBody {...props} loginOptions={data} />;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Without a mounted dispatcher (standalone mounts, tests) the query is skipped
|
|
317
|
+
// and only explicit hrefs render.
|
|
318
|
+
export function LoginScreen(props: LoginScreenProps): ReactNode {
|
|
319
|
+
const dispatcher = useOptionalDispatcher();
|
|
320
|
+
return dispatcher === undefined ? (
|
|
321
|
+
<LoginScreenBody {...props} loginOptions={null} />
|
|
322
|
+
) : (
|
|
323
|
+
<LoginScreenWithQueriedOptions {...props} />
|
|
324
|
+
);
|
|
325
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.384.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Event consumers screen with restart, skip, pause and resume",
|
|
6
|
+
"detail": "SystemAdmins get the screens event-consumers and event-consumer-detail. A stopped consumer offers Restart as the primary action; Skip event (with a confirm dialog), Pause and Resume sit in the overflow menu. The write handlers no longer return the raw error text: unknown consumer, wrong status and nothing to skip have their own codes and messages, anything else is logged on the server and answers a generic message. The shared instance reads \"Shared\"."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.383.0",
|
|
4
10
|
"type": "breaking",
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
defineFeature,
|
|
3
|
-
type FeatureDefinition,
|
|
4
|
-
i18nKey,
|
|
5
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
2
|
import { catalogQuery } from "./handlers/catalog.query.ts";
|
|
3
|
+
import {
|
|
4
|
+
consumerDisableWrite,
|
|
5
|
+
consumerEnableWrite,
|
|
6
|
+
consumerRestartWrite,
|
|
7
|
+
consumerSkipWrite,
|
|
8
|
+
} from "./handlers/consumer.write.ts";
|
|
9
|
+
import { consumerDetailQuery, consumersQuery } from "./handlers/consumers.query.ts";
|
|
7
10
|
import { detailQuery } from "./handlers/detail.query.ts";
|
|
8
11
|
import { listQuery } from "./handlers/list.query.ts";
|
|
9
12
|
import {
|
|
@@ -24,14 +27,15 @@ import { tenantFailuresQuery } from "./handlers/tenant-failures.query.ts";
|
|
|
24
27
|
import { tenantRunsQuery } from "./handlers/tenant-runs.query.ts";
|
|
25
28
|
import { triggerWrite } from "./handlers/trigger.write.ts";
|
|
26
29
|
import { jobRunLogsTableMeta, jobRunsTableMeta } from "./job-run-table.ts";
|
|
30
|
+
import {
|
|
31
|
+
eventConsumerDetailScreen,
|
|
32
|
+
eventConsumersScreen,
|
|
33
|
+
jobRunDetailScreen,
|
|
34
|
+
jobRunsScreen,
|
|
35
|
+
jobTriggerScreen,
|
|
36
|
+
} from "./screens.ts";
|
|
27
37
|
import { tenantJobFailuresTableMeta } from "./tenant-job-failure-table.ts";
|
|
28
38
|
import { tenantJobRunsTableMeta } from "./tenant-job-run-table.ts";
|
|
29
|
-
import {
|
|
30
|
-
JOB_RUN_DETAIL_SCREEN_ID,
|
|
31
|
-
JOB_RUNS_SCREEN_ID,
|
|
32
|
-
JobHandlers,
|
|
33
|
-
JobQueries,
|
|
34
|
-
} from "./web/constants.ts";
|
|
35
39
|
import { JOBS_I18N } from "./web/i18n.ts";
|
|
36
40
|
|
|
37
41
|
export type JobsFeatureOptions = {
|
|
@@ -124,6 +128,10 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
|
|
|
124
128
|
const handlers = {
|
|
125
129
|
trigger: r.writeHandler(triggerWrite),
|
|
126
130
|
retry: r.writeHandler(retryWrite),
|
|
131
|
+
consumerRestart: r.writeHandler(consumerRestartWrite),
|
|
132
|
+
consumerSkip: r.writeHandler(consumerSkipWrite),
|
|
133
|
+
consumerDisable: r.writeHandler(consumerDisableWrite),
|
|
134
|
+
consumerEnable: r.writeHandler(consumerEnableWrite),
|
|
127
135
|
};
|
|
128
136
|
|
|
129
137
|
const queries = {
|
|
@@ -132,126 +140,15 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
|
|
|
132
140
|
catalog: r.queryHandler(catalogQuery),
|
|
133
141
|
failures: r.queryHandler(tenantFailuresQuery),
|
|
134
142
|
tenantRuns: r.queryHandler(tenantRunsQuery),
|
|
143
|
+
consumers: r.queryHandler(consumersQuery),
|
|
144
|
+
consumerDetails: r.queryHandler(consumerDetailQuery),
|
|
135
145
|
};
|
|
136
146
|
|
|
137
|
-
const systemAdminAccess = { roles: ["SystemAdmin"] as const };
|
|
138
|
-
|
|
139
147
|
r.translations({ keys: JOBS_I18N });
|
|
140
148
|
|
|
141
|
-
r.screen(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
query: JobQueries.list,
|
|
145
|
-
columns: [
|
|
146
|
-
{ field: "jobName", label: i18nKey("jobs.runs.col.job") },
|
|
147
|
-
{ field: "status", label: i18nKey("jobs.runs.col.status") },
|
|
148
|
-
{
|
|
149
|
-
field: "startedAt",
|
|
150
|
-
label: i18nKey("jobs.runs.col.started"),
|
|
151
|
-
renderer: { format: "timestamp" },
|
|
152
|
-
},
|
|
153
|
-
{ field: "duration", label: i18nKey("jobs.runs.col.duration") },
|
|
154
|
-
],
|
|
155
|
-
defaultSort: { field: "startedAt", dir: "desc" },
|
|
156
|
-
facets: [
|
|
157
|
-
{
|
|
158
|
-
field: "status",
|
|
159
|
-
type: "select",
|
|
160
|
-
label: i18nKey("jobs.runs.filter.status"),
|
|
161
|
-
options: [
|
|
162
|
-
{ value: "queued", label: i18nKey("jobs.runs.filter.queued") },
|
|
163
|
-
{ value: "running", label: i18nKey("jobs.runs.filter.running") },
|
|
164
|
-
{ value: "completed", label: i18nKey("jobs.runs.filter.completed") },
|
|
165
|
-
{ value: "failed", label: i18nKey("jobs.runs.filter.failed") },
|
|
166
|
-
],
|
|
167
|
-
},
|
|
168
|
-
],
|
|
169
|
-
rowActions: [
|
|
170
|
-
{
|
|
171
|
-
kind: "navigate",
|
|
172
|
-
id: "open",
|
|
173
|
-
label: i18nKey("jobs.runs.open"),
|
|
174
|
-
screen: JOB_RUN_DETAIL_SCREEN_ID,
|
|
175
|
-
entityId: "id",
|
|
176
|
-
rowClick: true,
|
|
177
|
-
},
|
|
178
|
-
],
|
|
179
|
-
toolbarActions: [
|
|
180
|
-
{
|
|
181
|
-
kind: "drawer",
|
|
182
|
-
id: "trigger",
|
|
183
|
-
label: i18nKey("jobs.trigger.title"),
|
|
184
|
-
screen: "job-trigger",
|
|
185
|
-
style: "primary",
|
|
186
|
-
},
|
|
187
|
-
],
|
|
188
|
-
pagination: "infinite",
|
|
189
|
-
access: systemAdminAccess,
|
|
190
|
-
});
|
|
191
|
-
r.screen({
|
|
192
|
-
id: "job-trigger",
|
|
193
|
-
type: "actionForm",
|
|
194
|
-
handler: JobHandlers.trigger,
|
|
195
|
-
fields: {
|
|
196
|
-
jobName: { type: "text", required: true },
|
|
197
|
-
payload: { type: "longText", multiline: true, default: "{}" },
|
|
198
|
-
},
|
|
199
|
-
layout: { sections: [{ fields: ["jobName", "payload"] }] },
|
|
200
|
-
submitLabel: i18nKey("jobs.trigger.submit"),
|
|
201
|
-
access: systemAdminAccess,
|
|
202
|
-
description: "jobs.screen.job-trigger.subtitle",
|
|
203
|
-
});
|
|
204
|
-
r.screen({
|
|
205
|
-
id: JOB_RUN_DETAIL_SCREEN_ID,
|
|
206
|
-
type: "projectionDetail",
|
|
207
|
-
query: JobQueries.details,
|
|
208
|
-
idParam: "runId",
|
|
209
|
-
fieldLabels: {
|
|
210
|
-
jobName: i18nKey("jobs.detail.field.job"),
|
|
211
|
-
status: i18nKey("jobs.detail.field.status"),
|
|
212
|
-
id: i18nKey("jobs.detail.field.id"),
|
|
213
|
-
startedAt: i18nKey("jobs.detail.field.started"),
|
|
214
|
-
finishedAt: i18nKey("jobs.detail.field.finished"),
|
|
215
|
-
duration: i18nKey("jobs.detail.field.duration"),
|
|
216
|
-
error: i18nKey("jobs.detail.field.error"),
|
|
217
|
-
logs: i18nKey("jobs.detail.logs"),
|
|
218
|
-
},
|
|
219
|
-
layout: {
|
|
220
|
-
sections: [
|
|
221
|
-
{
|
|
222
|
-
fields: [
|
|
223
|
-
"jobName",
|
|
224
|
-
"status",
|
|
225
|
-
"id",
|
|
226
|
-
{ field: "startedAt", renderer: { format: "timestamp" } },
|
|
227
|
-
{ field: "finishedAt", renderer: { format: "timestamp" } },
|
|
228
|
-
"duration",
|
|
229
|
-
"error",
|
|
230
|
-
],
|
|
231
|
-
},
|
|
232
|
-
// No section `title` here (fw#2312 label-dedup fix): the section
|
|
233
|
-
// holds exactly one field, so a title would repeat the field's own
|
|
234
|
-
// label (rendered by RenderField as the Field's heading) verbatim.
|
|
235
|
-
{
|
|
236
|
-
fields: [{ field: "logs", renderer: { format: "json" } }],
|
|
237
|
-
},
|
|
238
|
-
],
|
|
239
|
-
},
|
|
240
|
-
actions: [
|
|
241
|
-
{
|
|
242
|
-
kind: "writeHandler",
|
|
243
|
-
id: "retry",
|
|
244
|
-
label: i18nKey("jobs.detail.retry"),
|
|
245
|
-
handler: JobHandlers.retry,
|
|
246
|
-
payload: { map: { runId: "id" } },
|
|
247
|
-
visible: { field: "status", eq: "failed" },
|
|
248
|
-
style: "primary",
|
|
249
|
-
},
|
|
250
|
-
],
|
|
251
|
-
description: "jobs.screen.job-run-detail.subtitle",
|
|
252
|
-
listScreenId: JOB_RUNS_SCREEN_ID,
|
|
253
|
-
access: systemAdminAccess,
|
|
254
|
-
});
|
|
149
|
+
r.screen(jobRunsScreen);
|
|
150
|
+
r.screen(jobTriggerScreen);
|
|
151
|
+
r.screen(jobRunDetailScreen);
|
|
255
152
|
r.nav({
|
|
256
153
|
id: "job-runs",
|
|
257
154
|
label: "jobs:nav.jobRuns",
|
|
@@ -260,6 +157,16 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
|
|
|
260
157
|
order: 10,
|
|
261
158
|
});
|
|
262
159
|
|
|
160
|
+
r.screen(eventConsumersScreen);
|
|
161
|
+
r.screen(eventConsumerDetailScreen);
|
|
162
|
+
r.nav({
|
|
163
|
+
id: "event-consumers",
|
|
164
|
+
label: "jobs:nav.eventConsumers",
|
|
165
|
+
icon: "list",
|
|
166
|
+
screen: "jobs:screen:event-consumers",
|
|
167
|
+
order: 11,
|
|
168
|
+
});
|
|
169
|
+
|
|
263
170
|
return { handlers, queries };
|
|
264
171
|
});
|
|
265
172
|
}
|