@cosmicdrift/kumiko-bundled-features 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/dist/audit/event-classification.d.ts +8 -0
- package/dist/audit/event-classification.js +69 -0
- package/dist/audit/feature.js +3 -3
- package/dist/audit/handlers/audit-events.d.ts +25 -3
- package/dist/audit/handlers/audit-events.js +41 -7
- package/dist/audit/handlers/details-all-tenants.query.d.ts +3 -0
- package/dist/audit/handlers/details-all-tenants.query.js +2 -5
- package/dist/audit/handlers/details.query.d.ts +3 -0
- package/dist/audit/handlers/details.query.js +5 -3
- package/dist/audit/handlers/list-all-tenants.query.d.ts +9 -2
- package/dist/audit/handlers/list-all-tenants.query.js +10 -8
- package/dist/audit/handlers/list.query.d.ts +8 -0
- package/dist/audit/handlers/list.query.js +12 -4
- package/dist/audit/screens.d.ts +37 -0
- package/dist/audit/screens.js +26 -4
- package/dist/audit/web/audit-event-cell.d.ts +4 -0
- package/dist/audit/web/audit-event-cell.js +20 -0
- package/dist/audit/web/audit-event-labels.d.ts +7 -0
- package/dist/audit/web/audit-event-labels.js +23 -0
- package/dist/audit/web/client-plugin.js +6 -1
- package/dist/audit/web/constants.d.ts +8 -0
- package/dist/audit/web/constants.js +7 -0
- package/dist/audit/web/i18n.js +48 -1
- package/dist/auth-email-password/feature.js +3 -2
- package/dist/auth-email-password/handlers/login-options.query.d.ts +5 -0
- package/dist/auth-email-password/handlers/login-options.query.js +17 -0
- package/dist/auth-email-password/index.d.ts +1 -1
- package/dist/auth-email-password/index.js +1 -1
- package/dist/auth-email-password/{auth-paths.d.ts → web/auth-paths.d.ts} +5 -3
- package/dist/auth-email-password/{auth-paths.js → web/auth-paths.js} +5 -3
- package/dist/auth-email-password/web/client-plugin.d.ts +5 -0
- package/dist/auth-email-password/web/client-plugin.js +2 -0
- package/dist/auth-email-password/web/constants.d.ts +1 -0
- package/dist/auth-email-password/web/constants.js +1 -0
- package/dist/auth-email-password/web/default-auth-pages-gate.d.ts +4 -0
- package/dist/auth-email-password/web/default-auth-pages-gate.js +28 -0
- package/dist/auth-email-password/web/login-screen.d.ts +13 -18
- package/dist/auth-email-password/web/login-screen.js +31 -19
- package/dist/auth-mfa/schema/user-mfa.d.ts +6 -126
- package/dist/billing-foundation/entities.d.ts +8 -168
- package/dist/billing-foundation/projection.d.ts +8 -168
- package/dist/cap-counter/entity.d.ts +1 -21
- package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -28
- package/dist/config/table.d.ts +6 -126
- package/dist/custom-fields/entity.d.ts +4 -84
- package/dist/data-retention/schema/tenant-retention-override.d.ts +6 -112
- package/dist/delivery/tables.d.ts +6 -126
- package/dist/document-ingest-foundation/entity.d.ts +6 -112
- package/dist/folders/entity.d.ts +8 -168
- package/dist/form-draft/entity.d.ts +2 -42
- package/dist/inbound-mail-foundation/entities.d.ts +25 -525
- package/dist/inbound-mail-foundation/projection.d.ts +20 -420
- package/dist/jobs/feature.js +22 -117
- package/dist/jobs/handlers/consumer.write.d.ts +37 -0
- package/dist/jobs/handlers/consumer.write.js +59 -0
- package/dist/jobs/handlers/consumers.query.d.ts +52 -0
- package/dist/jobs/handlers/consumers.query.js +114 -0
- package/dist/jobs/job-run-table.d.ts +6 -126
- package/dist/jobs/screens.d.ts +6 -0
- package/dist/jobs/screens.js +241 -0
- package/dist/jobs/web/constants.d.ts +20 -0
- package/dist/jobs/web/constants.js +21 -0
- package/dist/jobs/web/i18n.js +41 -0
- package/dist/ledger/entity.d.ts +13 -273
- package/dist/managed-pages/table.d.ts +12 -252
- package/dist/notes-history/entity.d.ts +14 -280
- package/dist/personal-access-tokens/schema/api-token.d.ts +6 -126
- package/dist/secrets/i18n.d.ts +0 -3
- package/dist/secrets/i18n.js +0 -3
- package/dist/secrets/table.d.ts +1 -21
- package/dist/sessions/constants.d.ts +2 -1
- package/dist/sessions/constants.js +2 -1
- package/dist/sessions/handlers/detail.query.d.ts +5 -0
- package/dist/sessions/handlers/detail.query.js +7 -3
- package/dist/sessions/handlers/list.query.d.ts +5 -0
- package/dist/sessions/handlers/list.query.js +16 -10
- package/dist/sessions/handlers/mine.query.d.ts +5 -0
- package/dist/sessions/handlers/mine.query.js +15 -9
- package/dist/sessions/schema/user-session.d.ts +4 -84
- package/dist/sessions/screens.js +22 -8
- package/dist/sessions/web/client-plugin.d.ts +6 -0
- package/dist/sessions/web/client-plugin.js +11 -0
- package/dist/sessions/web/constants.d.ts +3 -0
- package/dist/sessions/web/constants.js +3 -0
- package/dist/sessions/web/default-translations.d.ts +2 -0
- package/dist/sessions/web/default-translations.js +3 -0
- package/dist/sessions/web/i18n.js +5 -1
- package/dist/sessions/web/index.d.ts +1 -0
- package/dist/sessions/web/index.js +1 -0
- package/dist/sessions/web/session-device-cell.d.ts +3 -0
- package/dist/sessions/web/session-device-cell.js +16 -0
- package/dist/sessions/web/user-agent.d.ts +26 -0
- package/dist/sessions/web/user-agent.js +58 -0
- package/dist/tags/entity.d.ts +9 -189
- package/dist/template-resolver/feature.d.ts +1 -0
- package/dist/template-resolver/feature.js +2 -0
- package/dist/template-resolver/handlers/overview.query.d.ts +5 -0
- package/dist/template-resolver/handlers/overview.query.js +19 -2
- package/dist/template-resolver/handlers/tenant-override-counts.query.d.ts +17 -0
- package/dist/template-resolver/handlers/tenant-override-counts.query.js +35 -0
- package/dist/template-resolver/handlers/tenant-override-selection.d.ts +12 -0
- package/dist/template-resolver/handlers/tenant-override-selection.js +34 -0
- package/dist/template-resolver/handlers/tenant-overrides.query.js +3 -29
- package/dist/template-resolver/table.d.ts +20 -350
- package/dist/template-resolver/user-content-table.d.ts +12 -238
- package/dist/template-resolver/web/qualified-names.d.ts +1 -0
- package/dist/template-resolver/web/qualified-names.js +1 -0
- package/dist/tenant/invitation-table.d.ts +6 -126
- package/dist/tenant/membership-table.d.ts +4 -84
- package/dist/tenant/schema/tenant.d.ts +6 -126
- package/dist/tier-engine/entity.d.ts +1 -21
- package/dist/tier-engine/tier-resolver.d.ts +1 -21
- package/dist/user/schema/user.d.ts +12 -252
- package/dist/user-data-rights/feature.js +33 -33
- package/dist/user-data-rights/schema/download-attempt.d.ts +14 -294
- package/dist/user-data-rights/schema/download-token.d.ts +8 -168
- package/dist/user-data-rights/schema/export-job.d.ts +8 -154
- package/dist/user-data-rights/web/i18n.js +4 -1
- package/dist/waitlist/entity.d.ts +16 -322
- package/package.json +17 -9
- package/src/audit/changes.json +6 -0
- package/src/auth-email-password/changes.json +6 -0
- package/src/jobs/changes.json +6 -0
- package/src/sessions/changes.json +6 -0
- package/src/template-resolver/changes.json +6 -0
- package/src/user-data-rights/changes.json +18 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AUDIT_EVENT_ACTIONS } from "./constants.js";
|
|
2
|
+
export function isAuditEventAction(value) {
|
|
3
|
+
return AUDIT_EVENT_ACTIONS.some((action) => action === value);
|
|
4
|
+
}
|
|
5
|
+
function humanizeEntityName(aggregateType) {
|
|
6
|
+
const spaced = aggregateType.replaceAll("-", " ").replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
7
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
/** Singular entity noun: the entity's own `:noun` key, then the audit log's label for bundled entities, then the humanized aggregate type. */
|
|
10
|
+
export function auditEntityLabel(t, row) {
|
|
11
|
+
const aggregateType = row["aggregateType"];
|
|
12
|
+
if (typeof aggregateType !== "string")
|
|
13
|
+
return null;
|
|
14
|
+
const nounKey = row["entityNounKey"];
|
|
15
|
+
if (typeof nounKey === "string") {
|
|
16
|
+
const noun = t(nounKey, { count: 1 });
|
|
17
|
+
if (noun !== nounKey)
|
|
18
|
+
return noun;
|
|
19
|
+
}
|
|
20
|
+
const bundledKey = `audit.log.entity.${aggregateType}`;
|
|
21
|
+
const bundledLabel = t(bundledKey);
|
|
22
|
+
return bundledLabel === bundledKey ? humanizeEntityName(aggregateType) : bundledLabel;
|
|
23
|
+
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { mergeTranslations } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
-
import {
|
|
2
|
+
import { AuditEventCell, AuditObjectCell } from "./audit-event-cell.js";
|
|
3
|
+
import { AUDIT_EVENT_CELL_COMPONENT, AUDIT_FEATURE, AUDIT_OBJECT_CELL_COMPONENT, } from "./constants.js";
|
|
3
4
|
import { defaultTranslations } from "./default-translations.js";
|
|
4
5
|
export function auditClient(options) {
|
|
5
6
|
return {
|
|
6
7
|
name: AUDIT_FEATURE,
|
|
7
8
|
translations: mergeTranslations(defaultTranslations, options?.translations ?? {}),
|
|
9
|
+
columnRenderers: {
|
|
10
|
+
[AUDIT_EVENT_CELL_COMPONENT]: AuditEventCell,
|
|
11
|
+
[AUDIT_OBJECT_CELL_COMPONENT]: AuditObjectCell,
|
|
12
|
+
},
|
|
8
13
|
};
|
|
9
14
|
}
|
|
@@ -5,6 +5,14 @@ export declare const SYSTEM_ACTOR_ID: "system";
|
|
|
5
5
|
export declare const ANONYMOUS_USER_ID: "anonymous";
|
|
6
6
|
/** All `createdBy` values that mean "system" in the audit UI (nil-UUID from createSystemUser + literal). */
|
|
7
7
|
export declare const SYSTEM_ACTOR_IDS: ReadonlySet<string>;
|
|
8
|
+
/** Column-renderer names shared by screens.ts and `auditClient()`. */
|
|
9
|
+
export declare const AUDIT_EVENT_CELL_COMPONENT: "audit-event-cell";
|
|
10
|
+
export declare const AUDIT_OBJECT_CELL_COMPONENT: "audit-object-cell";
|
|
11
|
+
/** Action suffix of entity events (`<entity>.<action>`); other event types have none. */
|
|
12
|
+
export declare const AUDIT_EVENT_ACTIONS: readonly ["created", "updated", "deleted", "restored"];
|
|
13
|
+
export type AuditEventAction = (typeof AUDIT_EVENT_ACTIONS)[number];
|
|
14
|
+
/** Value of the facet that brings technical entries (escape-hatch use) back into the log. */
|
|
15
|
+
export declare const INCLUDE_TECHNICAL_FILTER: "include-technical";
|
|
8
16
|
export declare const AuditQueries: {
|
|
9
17
|
readonly list: "audit:query:list";
|
|
10
18
|
readonly details: "audit:query:details";
|
|
@@ -9,6 +9,13 @@ export const SYSTEM_ACTOR_IDS = new Set([
|
|
|
9
9
|
SYSTEM_ACTOR_ID,
|
|
10
10
|
"00000000-0000-0000-0000-000000000000",
|
|
11
11
|
]);
|
|
12
|
+
/** Column-renderer names shared by screens.ts and `auditClient()`. */
|
|
13
|
+
export const AUDIT_EVENT_CELL_COMPONENT = "audit-event-cell";
|
|
14
|
+
export const AUDIT_OBJECT_CELL_COMPONENT = "audit-object-cell";
|
|
15
|
+
/** Action suffix of entity events (`<entity>.<action>`); other event types have none. */
|
|
16
|
+
export const AUDIT_EVENT_ACTIONS = ["created", "updated", "deleted", "restored"];
|
|
17
|
+
/** Value of the facet that brings technical entries (escape-hatch use) back into the log. */
|
|
18
|
+
export const INCLUDE_TECHNICAL_FILTER = "include-technical";
|
|
12
19
|
export const AuditQueries = {
|
|
13
20
|
list: "audit:query:list",
|
|
14
21
|
details: "audit:query:details",
|
package/dist/audit/web/i18n.js
CHANGED
|
@@ -21,8 +21,55 @@ export const AUDIT_I18N = {
|
|
|
21
21
|
"audit.log.col.tenant": { en: "Tenant" },
|
|
22
22
|
"audit.log.col.when": { en: "When" },
|
|
23
23
|
"audit.log.col.type": { en: "Event" },
|
|
24
|
-
"audit.log.col.aggregateType": { en: "Aggregate type" },
|
|
25
24
|
"audit.log.col.aggregateId": { en: "Aggregate ID" },
|
|
25
|
+
"audit.log.col.aggregateType": { en: "Object" },
|
|
26
|
+
"audit.log.event": { en: "{entity} {action}" },
|
|
27
|
+
"audit.log.action.created": { en: "created" },
|
|
28
|
+
"audit.log.action.updated": { en: "updated" },
|
|
29
|
+
"audit.log.action.deleted": { en: "deleted" },
|
|
30
|
+
"audit.log.action.restored": { en: "restored" },
|
|
31
|
+
"audit.log.facet.technical": { en: "Technical entries" },
|
|
32
|
+
"audit.log.facet.technical.include": { en: "Show technical entries" },
|
|
33
|
+
"audit.log.event.custom-field-set": { en: "Custom field value set" },
|
|
34
|
+
"audit.log.event.custom-field-cleared": { en: "Custom field value cleared" },
|
|
35
|
+
"audit.log.event.field-definition-deleted": { en: "Custom field deleted" },
|
|
36
|
+
"audit.log.event.secret-read": { en: "Secret read" },
|
|
37
|
+
"audit.log.event.session-revoked": { en: "Session ended" },
|
|
38
|
+
"audit.log.event.subject-forgotten": { en: "Person forgotten (data erased)" },
|
|
39
|
+
"audit.log.event.forget-denied": { en: "Erasure request denied" },
|
|
40
|
+
"audit.log.event.pat-revoked": { en: "Access token revoked" },
|
|
41
|
+
"audit.log.event.mail-account-connected": { en: "Mail account connected" },
|
|
42
|
+
"audit.log.event.mail-account-updated": { en: "Mail account updated" },
|
|
43
|
+
"audit.log.event.mail-account-disconnected": { en: "Mail account disconnected" },
|
|
44
|
+
"audit.log.event.subscription-created": { en: "Subscription created" },
|
|
45
|
+
"audit.log.event.subscription-updated": { en: "Subscription updated" },
|
|
46
|
+
"audit.log.event.subscription-canceled": { en: "Subscription canceled" },
|
|
47
|
+
"audit.log.event.invoice-paid": { en: "Invoice paid" },
|
|
48
|
+
"audit.log.event.invoice-payment-failed": { en: "Invoice payment failed" },
|
|
49
|
+
"audit.log.event.payment-received": { en: "Payment received" },
|
|
50
|
+
"audit.log.event.checkout-consent-recorded": { en: "Checkout consent recorded" },
|
|
51
|
+
"audit.log.event.contract-confirmation-issued": { en: "Contract confirmation issued" },
|
|
52
|
+
"audit.log.event.contract-termination-revoked": { en: "Contract termination revoked" },
|
|
53
|
+
"audit.log.event.destruction-requested": { en: "Tenant deletion requested" },
|
|
54
|
+
"audit.log.event.destruction-cancelled": { en: "Tenant deletion cancelled" },
|
|
55
|
+
"audit.log.event.tenant-destruction-started": { en: "Tenant deletion started" },
|
|
56
|
+
"audit.log.event.tenant-destruction-completed": { en: "Tenant deletion completed" },
|
|
57
|
+
"audit.log.event.tenant-destruction-failed": { en: "Tenant deletion failed" },
|
|
58
|
+
"audit.log.event.tenant-handover-claimed": { en: "Tenant handover claimed" },
|
|
59
|
+
"audit.log.event.waitlist-exported": { en: "Waitlist exported" },
|
|
60
|
+
"audit.log.event.waitlist-purged": { en: "Waitlist purged" },
|
|
61
|
+
"audit.log.event.feature-toggle-set": { en: "Feature toggle changed" },
|
|
62
|
+
"audit.log.event.contract-termination-requested": { en: "Contract termination requested" },
|
|
63
|
+
"audit.log.event.contract-termination-declared": { en: "Contract termination declared" },
|
|
64
|
+
"audit.log.event.contract-termination-unmatched": { en: "Contract termination not matched" },
|
|
65
|
+
"audit.log.entity.tenant": { en: "Tenant" },
|
|
66
|
+
"audit.log.entity.tenant-membership": { en: "Membership" },
|
|
67
|
+
"audit.log.entity.tenant-invitation": { en: "Invitation" },
|
|
68
|
+
"audit.log.entity.user": { en: "User" },
|
|
69
|
+
"audit.log.entity.folder": { en: "Folder" },
|
|
70
|
+
"audit.log.entity.folder-assignment": { en: "Folder assignment" },
|
|
71
|
+
"audit.log.entity.field-definition": { en: "Custom field" },
|
|
72
|
+
"audit.log.entity.template-resource": { en: "Template" },
|
|
26
73
|
"audit.log.col.actor": { en: "Actor" },
|
|
27
74
|
"audit.log.details": { en: "Details" },
|
|
28
75
|
"audit.log.detail.payload": { en: "Event payload" },
|
|
@@ -9,6 +9,7 @@ import { createInviteCreateHandler, createSystemInviteCreateHandler, } from "./h
|
|
|
9
9
|
import { inviteInfoQuery } from "./handlers/invite-info.query.js";
|
|
10
10
|
import { createInviteSignupCompleteHandler } from "./handlers/invite-signup-complete.write.js";
|
|
11
11
|
import { createLoginHandler } from "./handlers/login.write.js";
|
|
12
|
+
import { loginOptionsQuery } from "./handlers/login-options.query.js";
|
|
12
13
|
import { logoutWrite } from "./handlers/logout.write.js";
|
|
13
14
|
import { createRequestAccountUnlockHandler, } from "./handlers/request-account-unlock.write.js";
|
|
14
15
|
import { createRequestEmailVerificationHandler } from "./handlers/request-email-verification.write.js";
|
|
@@ -83,8 +84,7 @@ export function createAuthEmailPasswordFeature(opts = {}) {
|
|
|
83
84
|
{ key: "accountUnlock", label: "Account-Unlock-Flow", type: "boolean", default: false },
|
|
84
85
|
],
|
|
85
86
|
});
|
|
86
|
-
r.requires({ features: ["user"] });
|
|
87
|
-
r.requires({ features: ["tenant"] });
|
|
87
|
+
r.requires({ features: ["user", "tenant"] });
|
|
88
88
|
// All five magic-link flows (reset/verify/signup/invite/unlock) dispatch
|
|
89
89
|
// via ctx.notify → delivery must be mounted. Fail closed at boot instead
|
|
90
90
|
// of silently dropping the mail.
|
|
@@ -135,6 +135,7 @@ export function createAuthEmailPasswordFeature(opts = {}) {
|
|
|
135
135
|
r.writeHandler(createVerifyEmailHandler(opts.emailVerification));
|
|
136
136
|
}
|
|
137
137
|
const queries = {
|
|
138
|
+
loginOptions: r.queryHandler(loginOptionsQuery),
|
|
138
139
|
...(opts.signup && {
|
|
139
140
|
signupRegistrationStatus: r.queryHandler(selfRegistrationStatusQuery),
|
|
140
141
|
}),
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
export declare const loginOptionsQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"login-options", z.ZodObject<{}, z.core.$strip>, {
|
|
3
|
+
passwordReset: boolean;
|
|
4
|
+
accountUnlock: boolean;
|
|
5
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { AuthHandlers } from "../web/constants.js";
|
|
4
|
+
// Anonymous-readable so the (unauthenticated) login screen can show the
|
|
5
|
+
// forgot-password and unlock links only when the matching mail flows are
|
|
6
|
+
// actually mounted, without every app having to pass hrefs by hand.
|
|
7
|
+
export const loginOptionsQuery = defineQueryHandler({
|
|
8
|
+
name: "login-options",
|
|
9
|
+
schema: z.object({}),
|
|
10
|
+
access: { roles: ["anonymous", "User", "TenantAdmin", "SystemAdmin"] },
|
|
11
|
+
rateLimit: { per: "ip+handler", limit: 30, windowSeconds: 60 },
|
|
12
|
+
description: "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.",
|
|
13
|
+
handler: async (_query, ctx) => ({
|
|
14
|
+
passwordReset: ctx.registry.getWriteHandler(AuthHandlers.requestPasswordReset) !== undefined,
|
|
15
|
+
accountUnlock: ctx.registry.getWriteHandler(AuthHandlers.requestAccountUnlock) !== undefined,
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { hashPassword, verifyPassword } from "../shared/password-hashing.ts";
|
|
2
|
-
export { type AuthPath, type AuthPaths, DEFAULT_AUTH_PATHS, makeAuthPaths } from "./auth-paths.ts";
|
|
3
2
|
export type { BootstrapConfigValue, BootstrapDeps, BootstrapInvite, BootstrapInviteOutcome, BootstrapPlan, BootstrapReport, BootstrapSeedDeps, BootstrapSystemAdmin, BootstrapTenant, BootstrapTenantOutcome, } from "./bootstrap.ts";
|
|
4
3
|
export { BootstrapPlanError, BootstrapWriteError, bootstrapTenants } from "./bootstrap.ts";
|
|
5
4
|
export type { AuthMailLocale } from "./email-templates.ts";
|
|
@@ -10,4 +9,5 @@ export { AUTH_SELF_REGISTRATION_FEATURE, createAuthSelfRegistrationToggleFeature
|
|
|
10
9
|
export { signToken, TokenPurpose, type VerifyResult, verifyToken } from "./signed-token.ts";
|
|
11
10
|
export { type IssuedSignupActivation, type IssueSignupActivationArgs, issueSignupActivation, SIGNUP_ACTIVATION_NOTIFICATION_TYPE, } from "./signup-activation.ts";
|
|
12
11
|
export { invalidateExistingSignupToken, normalizeEmail, SIGNUP_TOKEN_KEY_PREFIXES, storeSignupToken, } from "./signup-token-store.ts";
|
|
12
|
+
export { type AuthPath, type AuthPaths, DEFAULT_AUTH_PATHS, DEFAULT_FORGOT_PASSWORD_PATH, makeAuthPaths, } from "./web/auth-paths.ts";
|
|
13
13
|
export { AUTH_EMAIL_PASSWORD_FEATURE, AuthErrors, AuthHandlers, AuthQueries, } from "./web/constants.ts";
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
// hashPassword/verifyPassword; removing it is a breaking change that needs
|
|
6
6
|
// its own changeset + deliberate deprecation window, not a silent drop.
|
|
7
7
|
export { hashPassword, verifyPassword } from "../shared/password-hashing.js";
|
|
8
|
-
export { DEFAULT_AUTH_PATHS, makeAuthPaths } from "./auth-paths.js";
|
|
9
8
|
export { BootstrapPlanError, BootstrapWriteError, bootstrapTenants } from "./bootstrap.js";
|
|
10
9
|
export { authEmailPasswordEnvSchema, createAuthEmailPasswordFeature } from "./feature.js";
|
|
11
10
|
export { AuthMailNotificationTypes } from "./mail-notifications.js";
|
|
@@ -18,4 +17,5 @@ export { AUTH_SELF_REGISTRATION_FEATURE, createAuthSelfRegistrationToggleFeature
|
|
|
18
17
|
export { signToken, TokenPurpose, verifyToken } from "./signed-token.js";
|
|
19
18
|
export { issueSignupActivation, SIGNUP_ACTIVATION_NOTIFICATION_TYPE, } from "./signup-activation.js";
|
|
20
19
|
export { invalidateExistingSignupToken, normalizeEmail, SIGNUP_TOKEN_KEY_PREFIXES, storeSignupToken, } from "./signup-token-store.js";
|
|
20
|
+
export { DEFAULT_AUTH_PATHS, DEFAULT_FORGOT_PASSWORD_PATH, makeAuthPaths, } from "./web/auth-paths.js";
|
|
21
21
|
export { AUTH_EMAIL_PASSWORD_FEATURE, AuthErrors, AuthHandlers, AuthQueries, } from "./web/constants.js";
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
/** A plain path, or a function picking the path per resolved mail locale
|
|
2
2
|
* (e.g. `/en/reset-password`, `/de/reset-password`). */
|
|
3
3
|
export type AuthPath = string | ((locale: string) => string);
|
|
4
|
-
/** Pfad-Konstanten der 4 Auth-Seiten (relativ zur App-baseUrl). */
|
|
5
4
|
export type AuthPaths = {
|
|
6
5
|
readonly resetPassword: AuthPath;
|
|
7
6
|
readonly verifyEmail: AuthPath;
|
|
8
7
|
readonly signupComplete: AuthPath;
|
|
9
8
|
readonly inviteAccept: AuthPath;
|
|
9
|
+
readonly unlockAccount: AuthPath;
|
|
10
10
|
};
|
|
11
|
-
/**
|
|
12
|
-
* nur die Ausnahme via `makeAuthPaths({ ... })`. */
|
|
11
|
+
/** Shared by every Kumiko app; apps override only the exceptions via `makeAuthPaths({ ... })`. */
|
|
13
12
|
export declare const DEFAULT_AUTH_PATHS: {
|
|
14
13
|
readonly resetPassword: "/reset-password";
|
|
15
14
|
readonly verifyEmail: "/verify-email";
|
|
16
15
|
readonly signupComplete: "/signup/complete";
|
|
17
16
|
readonly inviteAccept: "/invite/accept";
|
|
17
|
+
readonly unlockAccount: "/unlock-account";
|
|
18
18
|
};
|
|
19
|
+
/** Client page that requests a reset mail; no mail links to it, so it is not part of AuthPaths. */
|
|
20
|
+
export declare const DEFAULT_FORGOT_PASSWORD_PATH = "/forgot-password";
|
|
19
21
|
export declare function makeAuthPaths(overrides?: Partial<AuthPaths>): AuthPaths;
|
|
@@ -1,14 +1,16 @@
|
|
|
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
|
-
/**
|
|
5
|
-
* nur die Ausnahme via `makeAuthPaths({ ... })`. */
|
|
4
|
+
/** Shared by every Kumiko app; apps override only the exceptions via `makeAuthPaths({ ... })`. */
|
|
6
5
|
export const DEFAULT_AUTH_PATHS = {
|
|
7
6
|
resetPassword: "/reset-password",
|
|
8
7
|
verifyEmail: "/verify-email",
|
|
9
8
|
signupComplete: "/signup/complete",
|
|
10
9
|
inviteAccept: "/invite/accept",
|
|
10
|
+
unlockAccount: "/unlock-account",
|
|
11
11
|
};
|
|
12
|
+
/** Client page that requests a reset mail; no mail links to it, so it is not part of AuthPaths. */
|
|
13
|
+
export const DEFAULT_FORGOT_PASSWORD_PATH = "/forgot-password";
|
|
12
14
|
export function makeAuthPaths(overrides = {}) {
|
|
13
15
|
return { ...DEFAULT_AUTH_PATHS, ...overrides };
|
|
14
16
|
}
|
|
@@ -31,6 +31,11 @@ export type EmailPasswordClientOptions = {
|
|
|
31
31
|
* Für Branding ("Sign in" → "Login to Acme") oder weitere Sprachen
|
|
32
32
|
* (`fr`, `es`, …) zusätzlich. */
|
|
33
33
|
readonly translations?: TranslationsByLocale;
|
|
34
|
+
/** Serve the forgot-password, reset-password and unlock-account pages at
|
|
35
|
+
* their convention paths for signed-out visitors. Apps with their own
|
|
36
|
+
* screens at those paths either register an earlier gate (it wins) or
|
|
37
|
+
* set this to false. Default: true. */
|
|
38
|
+
readonly defaultAuthPages?: boolean;
|
|
34
39
|
};
|
|
35
40
|
export type EmailPasswordClientFeature = {
|
|
36
41
|
readonly name: "auth-email-password";
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// Strings pro Locale, pro Key.
|
|
6
6
|
import { makeSessionAuthGate, } from "./auth-gate.js";
|
|
7
7
|
import { emailPasswordClientActions } from "./client-actions.js";
|
|
8
|
+
import { DefaultAuthPagesGate } from "./default-auth-pages-gate.js";
|
|
8
9
|
import { defaultTranslations, mergeTranslations } from "./i18n.js";
|
|
9
10
|
export function emailPasswordClient(options = {}) {
|
|
10
11
|
const translations = mergeTranslations(defaultTranslations, options.translations ?? {});
|
|
@@ -12,6 +13,7 @@ export function emailPasswordClient(options = {}) {
|
|
|
12
13
|
name: "auth-email-password",
|
|
13
14
|
providers: [],
|
|
14
15
|
gates: [
|
|
16
|
+
...(options.defaultAuthPages === false ? [] : [DefaultAuthPagesGate]),
|
|
15
17
|
makeSessionAuthGate({
|
|
16
18
|
loginScreen: options.loginScreen,
|
|
17
19
|
loginScreenProps: options.loginScreenProps,
|
|
@@ -20,6 +20,7 @@ export declare const AuthHandlers: {
|
|
|
20
20
|
readonly switchTenantMfaGate: "auth-email-password:write:switch-tenant-mfa-gate";
|
|
21
21
|
};
|
|
22
22
|
export declare const AuthQueries: {
|
|
23
|
+
readonly loginOptions: "auth-email-password:query:login-options";
|
|
23
24
|
readonly signupRegistrationStatus: "auth-email-password:query:signup-registration-status";
|
|
24
25
|
readonly inviteInfo: "auth-email-password:query:invite-info";
|
|
25
26
|
};
|
|
@@ -45,6 +45,7 @@ export const AuthHandlers = {
|
|
|
45
45
|
// Qualified query names. Anonymous-readable status so the (unauthenticated)
|
|
46
46
|
// signup page can decide whether to show its own link/form.
|
|
47
47
|
export const AuthQueries = {
|
|
48
|
+
loginOptions: "auth-email-password:query:login-options",
|
|
48
49
|
signupRegistrationStatus: "auth-email-password:query:signup-registration-status",
|
|
49
50
|
// Anonymous, read-only invite lookup for the invite-acceptance page —
|
|
50
51
|
// see invite-info.query.ts. Does not consume the token.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { DEFAULT_AUTH_PATHS, DEFAULT_FORGOT_PASSWORD_PATH } from "./auth-paths.js";
|
|
3
|
+
import { ConfirmAccountUnlockScreen } from "./confirm-account-unlock-screen.js";
|
|
4
|
+
import { ForgotPasswordScreen } from "./forgot-password-screen.js";
|
|
5
|
+
import { RequestAccountUnlockScreen } from "./request-account-unlock-screen.js";
|
|
6
|
+
import { ResetPasswordScreen } from "./reset-password-screen.js";
|
|
7
|
+
function hasToken(search) {
|
|
8
|
+
return (new URLSearchParams(search).get("token") ?? "") !== "";
|
|
9
|
+
}
|
|
10
|
+
function pickDefaultAuthPage(pathname, search) {
|
|
11
|
+
const path = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
|
|
12
|
+
switch (path) {
|
|
13
|
+
case DEFAULT_FORGOT_PASSWORD_PATH:
|
|
14
|
+
return _jsx(ForgotPasswordScreen, {});
|
|
15
|
+
case DEFAULT_AUTH_PATHS.resetPassword:
|
|
16
|
+
return hasToken(search) ? _jsx(ResetPasswordScreen, {}) : null;
|
|
17
|
+
case DEFAULT_AUTH_PATHS.unlockAccount:
|
|
18
|
+
return hasToken(search) ? _jsx(ConfirmAccountUnlockScreen, {}) : _jsx(RequestAccountUnlockScreen, {});
|
|
19
|
+
default:
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Sits inside emailPasswordClient's gate list, so app gates registered earlier
|
|
24
|
+
// (outer) still win on the same path. The pages are public: no session needed.
|
|
25
|
+
export function DefaultAuthPagesGate({ children }) {
|
|
26
|
+
const { pathname, search } = window.location;
|
|
27
|
+
return pickDefaultAuthPage(pathname, search) ?? _jsx(_Fragment, { children: children });
|
|
28
|
+
}
|
|
@@ -4,32 +4,27 @@ export type AuthLegalLink = {
|
|
|
4
4
|
readonly href: string;
|
|
5
5
|
};
|
|
6
6
|
export type LoginScreenProps = {
|
|
7
|
-
/**
|
|
8
|
-
* Titel stark app-branded ist und keine Translation braucht. */
|
|
7
|
+
/** Overrides the `auth.login.title` key; only for a branded title that needs no translation. */
|
|
9
8
|
readonly title?: string;
|
|
10
9
|
readonly subtitle?: ReactNode;
|
|
11
10
|
readonly submitLabel?: string;
|
|
12
|
-
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
readonly forgotPasswordHref?: string;
|
|
11
|
+
/** Href of the "Forgot password?" link under the submit button. Unset: the
|
|
12
|
+
* default path once the server offers the reset flow (login-options);
|
|
13
|
+
* `null` hides the link deliberately. */
|
|
14
|
+
readonly forgotPasswordHref?: string | null;
|
|
17
15
|
/** Optional href to RequestAccountUnlockScreen (#1266). When set, an
|
|
18
16
|
* account_locked login error renders an "Unlock account?" link inline —
|
|
19
17
|
* the account-lockout escape hatch, since forgotPasswordHref does NOT
|
|
20
18
|
* help here: the lockout gate blocks login before password verify even
|
|
21
19
|
* runs, so it also blocks the post-reset login. Apps without
|
|
22
|
-
* accountUnlock configured just leave this unset.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* (closed-invite-only) setzen das einfach nicht. */
|
|
20
|
+
* accountUnlock configured just leave this unset. Unset: default path once
|
|
21
|
+
* the server offers the flow (login-options); `null` = deliberately no link. */
|
|
22
|
+
readonly unlockAccountHref?: string | null;
|
|
23
|
+
/** Href of the "Create account" link; invite-only apps leave it unset. */
|
|
27
24
|
readonly signupHref?: string;
|
|
28
|
-
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* Labels kommen vom Caller (typisch schon übersetzt bzw. Eigennamen
|
|
32
|
-
* wie "Impressum"). */
|
|
25
|
+
/** Imprint/privacy links under the card: the login screen is often the only
|
|
26
|
+
* public page of an admin domain, which then must reach its legal pages
|
|
27
|
+
* itself (German imprint duty). Labels come translated from the caller. */
|
|
33
28
|
readonly legalLinks?: readonly AuthLegalLink[];
|
|
34
29
|
/** Called when the server responds with an MFA challenge instead of a
|
|
35
30
|
* session. Apps typically swap this screen out for auth-mfa's
|
|
@@ -46,4 +41,4 @@ export type LoginScreenProps = {
|
|
|
46
41
|
* error. */
|
|
47
42
|
readonly onMfaSetupRequired?: (preauthSetupToken: string, accountLabel: string) => void;
|
|
48
43
|
};
|
|
49
|
-
export declare function LoginScreen(
|
|
44
|
+
export declare function LoginScreen(props: LoginScreenProps): ReactNode;
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
// was anderes wollen, setzen entweder einen eigenen LocaleResolver auf
|
|
6
|
-
// App-Level oder reichen Key-Overrides an `emailPasswordClient()`.
|
|
7
|
-
//
|
|
8
|
-
// Die Reason-Codes aus dem Login-Handler werden 1:1 auf i18n-Keys
|
|
9
|
-
// gemappt (reasonToI18nKey) — neue Reason-Codes brauchen nur eine
|
|
10
|
-
// neue Zeile im Bundle + hier im Mapping.
|
|
11
|
-
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
// Login handler reason codes map 1:1 to i18n keys (reasonToI18nKey): a new
|
|
3
|
+
// reason code needs a bundle entry plus a line in that mapping.
|
|
4
|
+
import { useOptionalDispatcher, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
12
5
|
import { useState } from "react";
|
|
13
6
|
import { requestEmailVerification, } from "./auth-client.js";
|
|
14
7
|
import { AuthCard } from "./auth-form-primitives.js";
|
|
8
|
+
import { DEFAULT_AUTH_PATHS, DEFAULT_FORGOT_PASSWORD_PATH } from "./auth-paths.js";
|
|
15
9
|
import { followNextOrLandingAfterLogin } from "./auth-redirect.js";
|
|
10
|
+
import { AuthQueries } from "./constants.js";
|
|
16
11
|
import { useSession } from "./session.js";
|
|
17
12
|
// Map vom Reason-Code des Login-Handlers auf einen i18n-Key plus
|
|
18
13
|
// optional extrahierte Interpolations-Parameter. Ungekannte Codes
|
|
@@ -45,7 +40,7 @@ function reasonToKey(failure) {
|
|
|
45
40
|
return { key: "auth.errors.loginFailed" };
|
|
46
41
|
}
|
|
47
42
|
}
|
|
48
|
-
|
|
43
|
+
function LoginScreenBody({ title, subtitle, submitLabel, forgotPasswordHref, unlockAccountHref, signupHref, legalLinks, onMfaChallenge, onMfaSetupRequired, loginOptions, }) {
|
|
49
44
|
const t = useTranslation();
|
|
50
45
|
const { Form, Field, Input, Button, Banner, Link } = usePrimitives();
|
|
51
46
|
const session = useSession();
|
|
@@ -54,10 +49,19 @@ export function LoginScreen({ title, subtitle, submitLabel, forgotPasswordHref,
|
|
|
54
49
|
const [submitting, setSubmitting] = useState(false);
|
|
55
50
|
const [error, setError] = useState(null);
|
|
56
51
|
const [resendStatus, setResendStatus] = useState({ kind: "idle" });
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// (potentiell typoed) Adresse gehen ohne User-Feedback.
|
|
52
|
+
// Hides the resend button once the email input changes; otherwise resend
|
|
53
|
+
// would silently go to the edited, possibly mistyped address.
|
|
60
54
|
const [failedLoginEmail, setFailedLoginEmail] = useState(null);
|
|
55
|
+
const effectiveForgotHref = forgotPasswordHref === undefined
|
|
56
|
+
? loginOptions?.passwordReset === true
|
|
57
|
+
? DEFAULT_FORGOT_PASSWORD_PATH
|
|
58
|
+
: null
|
|
59
|
+
: forgotPasswordHref;
|
|
60
|
+
const effectiveUnlockHref = unlockAccountHref === undefined
|
|
61
|
+
? loginOptions?.accountUnlock === true
|
|
62
|
+
? DEFAULT_AUTH_PATHS.unlockAccount
|
|
63
|
+
: null
|
|
64
|
+
: unlockAccountHref;
|
|
61
65
|
const doSubmit = async () => {
|
|
62
66
|
setSubmitting(true);
|
|
63
67
|
setError(null);
|
|
@@ -93,10 +97,8 @@ export function LoginScreen({ title, subtitle, submitLabel, forgotPasswordHref,
|
|
|
93
97
|
e?.preventDefault();
|
|
94
98
|
void doSubmit();
|
|
95
99
|
};
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// sodass kein anti-enumeration-Branching nötig ist; 429 → rate-limit-
|
|
99
|
-
// Hint inline, sonstige Fehler → generischer Inline-Hint.
|
|
100
|
+
// requestEmailVerification answers 200 even for unknown users, so no
|
|
101
|
+
// anti-enumeration branching is needed; only 429 gets its own hint.
|
|
100
102
|
const onResend = async () => {
|
|
101
103
|
setResendStatus({ kind: "sending" });
|
|
102
104
|
try {
|
|
@@ -122,5 +124,15 @@ export function LoginScreen({ title, subtitle, submitLabel, forgotPasswordHref,
|
|
|
122
124
|
email.trim().length > 0 &&
|
|
123
125
|
email === failedLoginEmail && (_jsx("span", { className: "self-start", children: _jsx(Button, { variant: "link", onClick: () => void onResend(), disabled: resendStatus.kind === "sending", children: resendStatus.kind === "sending"
|
|
124
126
|
? t("auth.login.submitting")
|
|
125
|
-
: t("auth.login.resendVerification") }) })), error.reason === "account_locked" &&
|
|
127
|
+
: t("auth.login.resendVerification") }) })), error.reason === "account_locked" && effectiveUnlockHref !== null && (_jsx("span", { className: "self-start", children: _jsx(Link, { href: effectiveUnlockHref, variant: "default", children: t("auth.login.unlockAccount") }) })), resendStatus.kind === "rateLimited" && (_jsx("span", { className: "text-xs", children: t("auth.login.resendRateLimited") })), resendStatus.kind === "error" && (_jsx("span", { className: "text-xs", children: t("auth.login.resendError") }))] }) })) : null, _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting ? t("auth.login.submitting") : effectiveSubmit })] }), effectiveForgotHref !== null && (_jsx(Link, { href: effectiveForgotHref, variant: "muted", className: "self-center", children: t("auth.login.forgotPassword") })), signupHref !== undefined && (_jsx(Link, { href: signupHref, variant: "muted", className: "self-center", children: t("auth.signup.title") })), legalLinks !== undefined && legalLinks.length > 0 && (_jsx("nav", { "data-testid": "login-legal-links", className: "flex items-center justify-center gap-3 pt-2 border-t border-border/50", children: legalLinks.map((link) => (_jsx(Link, { href: link.href, variant: "muted", className: "text-xs", children: link.label }, link.href))) }))] }));
|
|
128
|
+
}
|
|
129
|
+
function LoginScreenWithQueriedOptions(props) {
|
|
130
|
+
const { data } = useQuery(AuthQueries.loginOptions, {}, { enabled: props.forgotPasswordHref === undefined || props.unlockAccountHref === undefined });
|
|
131
|
+
return _jsx(LoginScreenBody, { ...props, loginOptions: data });
|
|
132
|
+
}
|
|
133
|
+
// Without a mounted dispatcher (standalone mounts, tests) the query is skipped
|
|
134
|
+
// and only explicit hrefs render.
|
|
135
|
+
export function LoginScreen(props) {
|
|
136
|
+
const dispatcher = useOptionalDispatcher();
|
|
137
|
+
return dispatcher === undefined ? (_jsx(LoginScreenBody, { ...props, loginOptions: null })) : (_jsx(LoginScreenWithQueriedOptions, { ...props }));
|
|
126
138
|
}
|