@cosmicdrift/kumiko-bundled-features 0.217.0 → 0.218.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -9
- package/src/auth-email-password/__tests__/signup-flow.integration.test.ts +2 -2
- package/src/auth-email-password/changes.json +14 -8
- package/src/auth-email-password/handlers/signup-confirm.write.ts +2 -2
- package/src/auth-email-password/seeding.ts +3 -3
- package/src/user/changes.json +6 -0
- package/src/user/handlers/list.query.ts +10 -3
- package/src/user/i18n.ts +7 -1
- package/src/user/schema/user.ts +3 -0
- package/src/user/screens.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.218.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>",
|
|
@@ -126,12 +126,12 @@
|
|
|
126
126
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
127
127
|
},
|
|
128
128
|
"dependencies": {
|
|
129
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
130
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
131
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
132
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
133
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
134
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
129
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.218.0",
|
|
130
|
+
"@cosmicdrift/kumiko-framework": "0.218.0",
|
|
131
|
+
"@cosmicdrift/kumiko-headless": "0.218.0",
|
|
132
|
+
"@cosmicdrift/kumiko-renderer": "0.218.0",
|
|
133
|
+
"@cosmicdrift/kumiko-renderer-web": "0.218.0",
|
|
134
|
+
"@cosmicdrift/kumiko-types": "0.218.0",
|
|
135
135
|
"@mollie/api-client": "^4.5.0",
|
|
136
136
|
"@node-rs/argon2": "^2.0.2",
|
|
137
137
|
"@types/mailparser": "^3.4.6",
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"devDependencies": {
|
|
161
161
|
"@testing-library/user-event": "^14.6.1",
|
|
162
162
|
"@types/qrcode": "^1.5.5",
|
|
163
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
164
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
163
|
+
"@cosmicdrift/kumiko-locale-de": "0.218.0",
|
|
164
|
+
"@cosmicdrift/kumiko-locale-es": "0.218.0"
|
|
165
165
|
}
|
|
166
166
|
}
|
|
@@ -275,7 +275,7 @@ describe("POST /api/auth/signup-confirm", () => {
|
|
|
275
275
|
expect(body.token).toBeTruthy();
|
|
276
276
|
expect(body.user?.id).toBeTruthy();
|
|
277
277
|
expect(body.user?.tenantId).toBeTruthy();
|
|
278
|
-
expect(body.user?.roles).toContain("
|
|
278
|
+
expect(body.user?.roles).toContain("TenantAdmin");
|
|
279
279
|
expect(body.tenantKey).toBeTruthy();
|
|
280
280
|
|
|
281
281
|
// Cookies gesetzt (auth + csrf)
|
|
@@ -299,7 +299,7 @@ describe("POST /api/auth/signup-confirm", () => {
|
|
|
299
299
|
expect(memberships).toHaveLength(1);
|
|
300
300
|
const rolesRaw = memberships[0]?.["roles"];
|
|
301
301
|
if (typeof rolesRaw === "string") {
|
|
302
|
-
expect(JSON.parse(rolesRaw) as string[]).toContain("
|
|
302
|
+
expect(JSON.parse(rolesRaw) as string[]).toContain("TenantAdmin");
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
// #1463 regression: seedTenant fires the tenant entity's postSave
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.218.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Self-signup provisions TenantAdmin on the new tenant",
|
|
6
|
+
"detail": "INITIAL_SIGNUP_ROLES is TenantAdmin (was Admin) so the first user on a new tenant can invite peers and manage memberships without elevation dead-ends."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.165.1",
|
|
4
10
|
"type": "breaking",
|
|
@@ -10,8 +16,8 @@
|
|
|
10
16
|
"version": "0.167.1",
|
|
11
17
|
"type": "improvement",
|
|
12
18
|
"title": "accountLockout is reachable through runProdApp/runDevApp (fw#1627).",
|
|
13
|
-
"detail": "accountLockout was an option on createAuthEmailPasswordFeature, but both wrappers mount auth-email-password themselves and their auth options only knew accountUnlock
|
|
14
|
-
"migration": "Opt-in, nothing breaks without it. Two things to know before relying on it: without ctx.redis the lockout is silently skipped in login.write.ts
|
|
19
|
+
"detail": "accountLockout was an option on createAuthEmailPasswordFeature, but both wrappers mount auth-email-password themselves and their auth options only knew accountUnlock \u2014 an app on either entrypoint had no way to turn brute-force protection on. RunProdAppAuthOptions and RunDevAppAuthOptions now carry accountLockout and composeFeatures passes it through.",
|
|
20
|
+
"migration": "Opt-in, nothing breaks without it. Two things to know before relying on it: without ctx.redis the lockout is silently skipped in login.write.ts \u2014 no error, no warning, no protection \u2014 so treat Redis as a deploy invariant, and prove the lockout with a behavioural test instead of assuming the mount is enough."
|
|
15
21
|
},
|
|
16
22
|
{
|
|
17
23
|
"version": "0.162.0",
|
|
@@ -22,19 +28,19 @@
|
|
|
22
28
|
{
|
|
23
29
|
"version": "0.154.1",
|
|
24
30
|
"type": "fix",
|
|
25
|
-
"title": "Export `LoginScreen`/`LoginScreenProps`/`AuthLegalLink` from `@cosmicdrift/kumiko-bundled
|
|
26
|
-
"detail": "Export `LoginScreen`/`LoginScreenProps`/`AuthLegalLink` from `@cosmicdrift/kumiko-bundled-features/auth-email-password/web`. Every other auth screen (ForgotPasswordScreen, SignupScreen, ResetPasswordScreen,
|
|
31
|
+
"title": "Export `LoginScreen`/`LoginScreenProps`/`AuthLegalLink` from `@cosmicdrift/kumiko-bundled\u2026",
|
|
32
|
+
"detail": "Export `LoginScreen`/`LoginScreenProps`/`AuthLegalLink` from `@cosmicdrift/kumiko-bundled-features/auth-email-password/web`. Every other auth screen (ForgotPasswordScreen, SignupScreen, ResetPasswordScreen, \u2026) was already exported from the barrel with its props type; `LoginScreen` was missed. `makeAuthGate`'s second parameter is typed `LoginScreenProps`, so consumers passing a typed `loginScreenProps` override (e.g. `@cosmicdriftgamestudio/kumiko-designer`) couldn't import the type at all. - @cosmicdrift/kumiko-framework@0.154.1 - @cosmicdrift/kumiko-dispatcher-live@0.154.1 - @cosmicdrift/kumiko-headless@0.154.1 - @cosmicdrift/kumiko-renderer@0.154.1 - @cosmicdrift/kumiko-renderer-web@0.154.1"
|
|
27
33
|
},
|
|
28
34
|
{
|
|
29
35
|
"version": "0.151.0",
|
|
30
36
|
"type": "improvement",
|
|
31
|
-
"title": "`auth-email-password/web` no longer exports `LoginScreen`/`LoginScreenProps` directly
|
|
32
|
-
"detail": "`auth-email-password/web` no longer exports `LoginScreen`/`LoginScreenProps` directly
|
|
37
|
+
"title": "`auth-email-password/web` no longer exports `LoginScreen`/`LoginScreenProps` directly \u2014 u\u2026",
|
|
38
|
+
"detail": "`auth-email-password/web` no longer exports `LoginScreen`/`LoginScreenProps` directly \u2014 use the new `createLoginRoute({ loginScreenProps, mfaVerifyScreen, unaffected (they already build on the same logic internally now). Why: a raw `<LoginScreen />` render has no MFA-challenge handling unless the caller remembers to hand-wire `onMfaChallenge` + swap in a verify screen itself \u2014 exactly how kumiko-framework#266's login-time MFA step went missing in a real app's standalone apex/marketing login route (it renders outside `emailPasswordClient`'s own gate, which already handled this correctly). `createLoginRoute` is the one place this logic lives now, for both the gated and standalone cases \u2014 there's no lower-level piece left to misuse. The `apex-surface-auth` recipe is updated to match."
|
|
33
39
|
},
|
|
34
40
|
{
|
|
35
41
|
"version": "0.131.0",
|
|
36
42
|
"type": "improvement",
|
|
37
|
-
"title": "App-Mounting 2.0
|
|
38
|
-
"detail": "App-Mounting 2.0
|
|
43
|
+
"title": "App-Mounting 2.0 S\u00e4ule A: Mid-Level-Widget-Kit in renderer-web (StatCard, MiniStat, Secti\u2026",
|
|
44
|
+
"detail": "App-Mounting 2.0 S\u00e4ule A: Mid-Level-Widget-Kit in renderer-web (StatCard, MiniStat, SectionCard, StatusBadge, ProgressBar, CollapsibleSection, DetailList, ModeSwitch, StatusBarChart, TimeseriesChart, EmptyState/LoadingState/ErrorState, QueryTable) + Status-Farb-Tokens (--color-status-\\*). Neue Hooks useMutation + useDisclosure. Neues Core-Primitive Link (default/button/muted), Button-Variant \"link\", Text-Variant \"muted\"; auth-email-password nutzt sie (authButtonClass/authMutedLinkClass entfernt)."
|
|
39
45
|
}
|
|
40
46
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Magic-Link-Signup, Step 2 (confirm).
|
|
2
2
|
//
|
|
3
3
|
// Token aus URL + Password vom User → wir lösen den Token in Redis ein
|
|
4
|
-
// und legen Tenant + User +
|
|
4
|
+
// und legen Tenant + User + TenantAdmin membership atomically. emailVerified
|
|
5
5
|
// wird sofort auf true gesetzt — der Magic-Link IST der Beweis.
|
|
6
6
|
//
|
|
7
7
|
// Pipeline:
|
|
@@ -141,7 +141,7 @@ export function createSignupConfirmHandler() {
|
|
|
141
141
|
|
|
142
142
|
// SessionUser für JWT-Mint. Roles aus INITIAL_SIGNUP_ROLES
|
|
143
143
|
// damit DB-write (provisionSignupAccount) und Session-claim
|
|
144
|
-
// dieselbe Quelle teilen — sonst hätten zwei Stellen "
|
|
144
|
+
// dieselbe Quelle teilen — sonst hätten zwei Stellen "TenantAdmin"
|
|
145
145
|
// hardcoded und ein Refactor würde role-mismatch zwischen DB
|
|
146
146
|
// und JWT erzeugen.
|
|
147
147
|
const session: SessionUser = {
|
|
@@ -103,12 +103,12 @@ export async function seedUserWithPassword(
|
|
|
103
103
|
* minten (Account-Takeover, #365). `seedUser` selbst bleibt idempotent
|
|
104
104
|
* add-only (für Bootstrap/Tests); die create-only-Garantie sitzt hier.
|
|
105
105
|
* tenantKey-Kollisionen löst der Caller via generateUniqueName. */
|
|
106
|
-
/** Default
|
|
106
|
+
/** Default membership roles for self-signup tenant owners. Shared between
|
|
107
107
|
* provisionSignupAccount (DB-write) und signup-confirm-handler
|
|
108
108
|
* (SessionUser-Konstruktion für JWT-Mint) — sonst hätten zwei
|
|
109
|
-
* Stellen unabhängig "
|
|
109
|
+
* Stellen unabhängig "TenantAdmin" hardcoded und würden bei einem
|
|
110
110
|
* Refactor zu role-mismatch zwischen DB und Session leiden. */
|
|
111
|
-
export const INITIAL_SIGNUP_ROLES = ["
|
|
111
|
+
export const INITIAL_SIGNUP_ROLES = ["TenantAdmin"] as const;
|
|
112
112
|
|
|
113
113
|
export type ProvisionSignupAccountOptions = {
|
|
114
114
|
readonly email: string;
|
package/src/user/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.218.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "user-list: system-scope search; membership roles on tenants; emailVerified sort",
|
|
6
|
+
"detail": "systemScope entityList search uses SYSTEM_TENANT_ID (was session tenant → Meili miss/500). Tenants column shows membership roles (Offlot Demo (TenantAdmin)). emailVerified sortable; email also searchable for roster lookup. Global roles stay SystemAdmin-only — TenantAdmin/Admin live on membership (Team → Members)."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.217.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
defineEntityListHandler,
|
|
6
6
|
type HandlerContext,
|
|
7
7
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
8
9
|
// kumiko-lint-ignore cross-feature-import SystemAdmin user-list joins memberships for tenants column
|
|
9
10
|
import { tenantMembershipsTable, tenantTable } from "../../tenant";
|
|
10
11
|
import { userEntity } from "../schema/user";
|
|
@@ -13,7 +14,7 @@ const baseList = defineEntityListHandler("user", userEntity, {
|
|
|
13
14
|
access: { roles: access.systemAdmin },
|
|
14
15
|
});
|
|
15
16
|
|
|
16
|
-
type MembershipRow = { userId: unknown; tenantId: unknown };
|
|
17
|
+
type MembershipRow = { userId: unknown; tenantId: unknown; roles?: unknown };
|
|
17
18
|
type TenantRow = { id: unknown; name?: unknown; key?: unknown };
|
|
18
19
|
|
|
19
20
|
function isMissingRelation(err: unknown): boolean {
|
|
@@ -30,7 +31,6 @@ async function loadMemberships(
|
|
|
30
31
|
userId: { in: [...userIds] },
|
|
31
32
|
});
|
|
32
33
|
} catch (err) {
|
|
33
|
-
// User-only test stacks (and mid-migration DBs) may lack membership tables.
|
|
34
34
|
if (isMissingRelation(err)) return null;
|
|
35
35
|
throw err;
|
|
36
36
|
}
|
|
@@ -46,6 +46,12 @@ function tenantLabelById(tenants: readonly TenantRow[]): Map<string, string> {
|
|
|
46
46
|
return map;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** "Offlot Demo (TenantAdmin)" — membership roles live here, not on user.roles. */
|
|
50
|
+
function membershipLabel(tenantLabel: string, membershipRoles: readonly string[]): string {
|
|
51
|
+
if (membershipRoles.length === 0) return tenantLabel;
|
|
52
|
+
return `${tenantLabel} (${membershipRoles.join("+")})`;
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
function labelsByUserId(
|
|
50
56
|
memberships: readonly MembershipRow[],
|
|
51
57
|
labelByTenantId: Map<string, string>,
|
|
@@ -55,7 +61,8 @@ function labelsByUserId(
|
|
|
55
61
|
const userId = String(m.userId ?? "");
|
|
56
62
|
const tenantId = String(m.tenantId ?? "");
|
|
57
63
|
if (userId === "" || tenantId === "") continue;
|
|
58
|
-
const
|
|
64
|
+
const tenantLabel = labelByTenantId.get(tenantId) ?? tenantId;
|
|
65
|
+
const label = membershipLabel(tenantLabel, parseRoles(m.roles ?? null));
|
|
59
66
|
const list = map.get(userId) ?? [];
|
|
60
67
|
list.push(label);
|
|
61
68
|
map.set(userId, list);
|
package/src/user/i18n.ts
CHANGED
|
@@ -5,8 +5,14 @@ export const USER_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
|
5
5
|
"screen:user-edit.title": { en: "Edit user" },
|
|
6
6
|
"user:entity:user:field:email": { en: "Email" },
|
|
7
7
|
"user:entity:user:field:displayName": { en: "Display name" },
|
|
8
|
-
"user:entity:user:field:roles": { en: "
|
|
8
|
+
"user:entity:user:field:roles": { en: "Platform roles" },
|
|
9
9
|
"user:entity:user:field:tenants": { en: "Tenants" },
|
|
10
|
+
"user:entity:user:field:tenants:description": {
|
|
11
|
+
en: "Membership tenants with roles, e.g. Offlot Demo (TenantAdmin). Edit under Team → Members.",
|
|
12
|
+
},
|
|
13
|
+
"user:entity:user:field:roles:description": {
|
|
14
|
+
en: "Global platform roles only (SystemAdmin). TenantAdmin/Admin are membership roles under Team → Members.",
|
|
15
|
+
},
|
|
10
16
|
"user:entity:user:field:roles:option:SystemAdmin": { en: "SystemAdmin" },
|
|
11
17
|
"user:entity:user:field:status": { en: "Status" },
|
|
12
18
|
"user:entity:user:field:emailVerified": { en: "Email verified" },
|
package/src/user/schema/user.ts
CHANGED
|
@@ -72,6 +72,8 @@ export const userEntity = createEntity({
|
|
|
72
72
|
maxLength: 320,
|
|
73
73
|
personal: "self",
|
|
74
74
|
find: "exact",
|
|
75
|
+
// Roster search needs email hits; find:"exact" alone is blind-index only.
|
|
76
|
+
searchable: true,
|
|
75
77
|
access: { write: access.privileged },
|
|
76
78
|
}),
|
|
77
79
|
|
|
@@ -112,6 +114,7 @@ export const userEntity = createEntity({
|
|
|
112
114
|
// refuse a session while this is false (strict mode).
|
|
113
115
|
emailVerified: createBooleanField({
|
|
114
116
|
default: false,
|
|
117
|
+
sortable: true,
|
|
115
118
|
access: { write: access.privileged },
|
|
116
119
|
}),
|
|
117
120
|
|
package/src/user/screens.ts
CHANGED
|
@@ -27,7 +27,8 @@ export const userListScreen: EntityListScreenDefinition = {
|
|
|
27
27
|
},
|
|
28
28
|
],
|
|
29
29
|
defaultSort: { field: "status", dir: "asc" },
|
|
30
|
-
//
|
|
30
|
+
// Search uses SYSTEM_TENANT_ID for systemScope lists (see event-store-executor-read).
|
|
31
|
+
// Apps must boot a user search index on that tenant (offlot: bootWaitlistSearch).
|
|
31
32
|
searchable: true,
|
|
32
33
|
access: { roles: ["SystemAdmin"] },
|
|
33
34
|
};
|