@objectstack/platform-objects 4.0.5 → 4.1.1
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/apps/index.d.mts +66 -48
- package/dist/apps/index.d.ts +66 -48
- package/dist/apps/index.js +12158 -217
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/index.mjs +12154 -212
- package/dist/apps/index.mjs.map +1 -1
- package/dist/audit/index.d.mts +39010 -55
- package/dist/audit/index.d.ts +39010 -55
- package/dist/audit/index.js +1428 -0
- package/dist/audit/index.js.map +1 -1
- package/dist/audit/index.mjs +1417 -1
- package/dist/audit/index.mjs.map +1 -1
- package/dist/identity/index.d.mts +14904 -2817
- package/dist/identity/index.d.ts +14904 -2817
- package/dist/identity/index.js +1090 -6
- package/dist/identity/index.js.map +1 -1
- package/dist/identity/index.mjs +1089 -7
- package/dist/identity/index.mjs.map +1 -1
- package/dist/index.d.mts +9 -7
- package/dist/index.d.ts +9 -7
- package/dist/index.js +15839 -1650
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +15815 -1633
- package/dist/index.mjs.map +1 -1
- package/dist/integration/index.d.mts +2906 -0
- package/dist/integration/index.d.ts +2906 -0
- package/dist/integration/index.js +140 -0
- package/dist/integration/index.js.map +1 -0
- package/dist/integration/index.mjs +138 -0
- package/dist/integration/index.mjs.map +1 -0
- package/dist/metadata/index.d.mts +581 -21183
- package/dist/metadata/index.d.ts +581 -21183
- package/dist/metadata/index.js +29 -619
- package/dist/metadata/index.js.map +1 -1
- package/dist/metadata/index.mjs +30 -615
- package/dist/metadata/index.mjs.map +1 -1
- package/dist/security/index.d.mts +7288 -50
- package/dist/security/index.d.ts +7288 -50
- package/dist/security/index.js +540 -0
- package/dist/security/index.js.map +1 -1
- package/dist/security/index.mjs +539 -1
- package/dist/security/index.mjs.map +1 -1
- package/dist/system/index.d.mts +8412 -0
- package/dist/system/index.d.ts +8412 -0
- package/dist/system/index.js +395 -0
- package/dist/system/index.js.map +1 -0
- package/dist/system/index.mjs +391 -0
- package/dist/system/index.mjs.map +1 -0
- package/package.json +13 -8
- package/dist/tenant/index.d.mts +0 -18464
- package/dist/tenant/index.d.ts +0 -18464
- package/dist/tenant/index.js +0 -741
- package/dist/tenant/index.js.map +0 -1
- package/dist/tenant/index.mjs +0 -733
- package/dist/tenant/index.mjs.map +0 -1
- /package/dist/{state-machine.zod-BFg-VE0M.d-Ek3_yo9P.d.mts → state-machine.zod-BNanU03M.d-Ek3_yo9P.d.mts} +0 -0
- /package/dist/{state-machine.zod-BFg-VE0M.d-Ek3_yo9P.d.ts → state-machine.zod-BNanU03M.d-Ek3_yo9P.d.ts} +0 -0
package/dist/identity/index.js
CHANGED
|
@@ -14,6 +14,149 @@ var SysUser = data.ObjectSchema.create({
|
|
|
14
14
|
displayNameField: "name",
|
|
15
15
|
titleFormat: "{name}",
|
|
16
16
|
compactLayout: ["name", "email", "email_verified"],
|
|
17
|
+
// Custom actions — generic CRUD is suppressed because user accounts are
|
|
18
|
+
// managed by better-auth, but we still need first-class affordances for
|
|
19
|
+
// common operations. Each action delegates to a Console-side named script
|
|
20
|
+
// registered on the ActionRunner (see objectui `AppContent.tsx`). Adding
|
|
21
|
+
// new affordances (reset password, revoke session, …) is now a pure
|
|
22
|
+
// schema + script-registration change — no per-view code.
|
|
23
|
+
actions: [
|
|
24
|
+
{
|
|
25
|
+
name: "invite_user",
|
|
26
|
+
label: "Invite User",
|
|
27
|
+
icon: "user-plus",
|
|
28
|
+
variant: "primary",
|
|
29
|
+
locations: ["list_toolbar"],
|
|
30
|
+
type: "api",
|
|
31
|
+
target: "/api/v1/auth/organization/invite-member",
|
|
32
|
+
successMessage: "Invitation sent",
|
|
33
|
+
refreshAfter: true,
|
|
34
|
+
params: [
|
|
35
|
+
{ field: "email", required: true },
|
|
36
|
+
{ field: "role", objectOverride: "sys_member", required: true }
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
// ── Platform admin operations (require better-auth `admin` plugin) ─
|
|
40
|
+
//
|
|
41
|
+
// These actions hit /api/v1/auth/admin/* endpoints that are only
|
|
42
|
+
// wired when `auth.plugins.admin` is enabled. When the plugin is
|
|
43
|
+
// disabled the actions still render (schema is static) but server
|
|
44
|
+
// returns 404. UI surfaces them under the row menu so platform
|
|
45
|
+
// admins can manage accounts without dropping to SQL or
|
|
46
|
+
// a custom Setup wizard.
|
|
47
|
+
{
|
|
48
|
+
name: "ban_user",
|
|
49
|
+
label: "Ban User",
|
|
50
|
+
icon: "ban",
|
|
51
|
+
variant: "danger",
|
|
52
|
+
locations: ["list_item"],
|
|
53
|
+
type: "api",
|
|
54
|
+
target: "/api/v1/auth/admin/ban-user",
|
|
55
|
+
recordIdParam: "userId",
|
|
56
|
+
successMessage: "User banned",
|
|
57
|
+
refreshAfter: true,
|
|
58
|
+
confirmText: "Ban this user? They will be signed out and unable to sign in until unbanned.",
|
|
59
|
+
params: [
|
|
60
|
+
{ name: "banReason", label: "Ban Reason", type: "text", required: false }
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "unban_user",
|
|
65
|
+
label: "Unban User",
|
|
66
|
+
icon: "check-circle-2",
|
|
67
|
+
variant: "secondary",
|
|
68
|
+
locations: ["list_item"],
|
|
69
|
+
type: "api",
|
|
70
|
+
target: "/api/v1/auth/admin/unban-user",
|
|
71
|
+
recordIdParam: "userId",
|
|
72
|
+
successMessage: "User unbanned",
|
|
73
|
+
refreshAfter: true
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "set_user_password",
|
|
77
|
+
label: "Set Password",
|
|
78
|
+
icon: "key-round",
|
|
79
|
+
variant: "secondary",
|
|
80
|
+
locations: ["list_item"],
|
|
81
|
+
type: "api",
|
|
82
|
+
target: "/api/v1/auth/admin/set-user-password",
|
|
83
|
+
recordIdParam: "userId",
|
|
84
|
+
successMessage: "Password updated",
|
|
85
|
+
refreshAfter: false,
|
|
86
|
+
params: [
|
|
87
|
+
{ name: "newPassword", label: "New Password", type: "text", required: true }
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "set_user_role",
|
|
92
|
+
label: "Set Platform Role",
|
|
93
|
+
icon: "shield-check",
|
|
94
|
+
variant: "secondary",
|
|
95
|
+
locations: ["list_item"],
|
|
96
|
+
type: "api",
|
|
97
|
+
target: "/api/v1/auth/admin/set-role",
|
|
98
|
+
recordIdParam: "userId",
|
|
99
|
+
successMessage: "Role updated",
|
|
100
|
+
refreshAfter: true,
|
|
101
|
+
params: [
|
|
102
|
+
{ name: "role", label: "Platform Role", type: "text", required: true }
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "impersonate_user",
|
|
107
|
+
label: "Impersonate User",
|
|
108
|
+
icon: "user-cog",
|
|
109
|
+
variant: "secondary",
|
|
110
|
+
locations: ["list_item"],
|
|
111
|
+
type: "api",
|
|
112
|
+
target: "/api/v1/auth/admin/impersonate-user",
|
|
113
|
+
recordIdParam: "userId",
|
|
114
|
+
successMessage: "Now impersonating user",
|
|
115
|
+
refreshAfter: true,
|
|
116
|
+
confirmText: "Start an impersonation session for this user? Use only for legitimate support cases \u2014 actions will be logged."
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
listViews: {
|
|
120
|
+
all_users: {
|
|
121
|
+
type: "grid",
|
|
122
|
+
name: "all_users",
|
|
123
|
+
label: "All Users",
|
|
124
|
+
data: { provider: "object", object: "sys_user" },
|
|
125
|
+
columns: ["name", "email", "email_verified", "two_factor_enabled", "created_at"],
|
|
126
|
+
sort: [{ field: "name", order: "asc" }],
|
|
127
|
+
pagination: { pageSize: 50 }
|
|
128
|
+
},
|
|
129
|
+
unverified: {
|
|
130
|
+
type: "grid",
|
|
131
|
+
name: "unverified",
|
|
132
|
+
label: "Unverified",
|
|
133
|
+
data: { provider: "object", object: "sys_user" },
|
|
134
|
+
columns: ["name", "email", "created_at"],
|
|
135
|
+
filter: [{ field: "email_verified", operator: "equals", value: false }],
|
|
136
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
137
|
+
pagination: { pageSize: 50 }
|
|
138
|
+
},
|
|
139
|
+
two_factor: {
|
|
140
|
+
type: "grid",
|
|
141
|
+
name: "two_factor",
|
|
142
|
+
label: "2FA Enabled",
|
|
143
|
+
data: { provider: "object", object: "sys_user" },
|
|
144
|
+
columns: ["name", "email", "two_factor_enabled", "updated_at"],
|
|
145
|
+
filter: [{ field: "two_factor_enabled", operator: "equals", value: true }],
|
|
146
|
+
sort: [{ field: "name", order: "asc" }],
|
|
147
|
+
pagination: { pageSize: 50 }
|
|
148
|
+
},
|
|
149
|
+
banned: {
|
|
150
|
+
type: "grid",
|
|
151
|
+
name: "banned",
|
|
152
|
+
label: "Banned",
|
|
153
|
+
data: { provider: "object", object: "sys_user" },
|
|
154
|
+
columns: ["name", "email", "banned", "ban_reason", "ban_expires"],
|
|
155
|
+
filter: [{ field: "banned", operator: "equals", value: true }],
|
|
156
|
+
sort: [{ field: "updated_at", order: "desc" }],
|
|
157
|
+
pagination: { pageSize: 50 }
|
|
158
|
+
}
|
|
159
|
+
},
|
|
17
160
|
fields: {
|
|
18
161
|
// ── Identity (primary business fields) ───────────────────────
|
|
19
162
|
name: data.Field.text({
|
|
@@ -40,6 +183,32 @@ var SysUser = data.ObjectSchema.create({
|
|
|
40
183
|
group: "Identity",
|
|
41
184
|
description: "Whether two-factor authentication is enabled for this user. Maintained by the better-auth `twoFactor` plugin."
|
|
42
185
|
}),
|
|
186
|
+
// ── Admin (managed by better-auth `admin` plugin when enabled) ───
|
|
187
|
+
role: data.Field.text({
|
|
188
|
+
label: "Platform Role",
|
|
189
|
+
required: false,
|
|
190
|
+
maxLength: 64,
|
|
191
|
+
group: "Admin",
|
|
192
|
+
description: "Platform-level role (admin, user, \u2026). Set via the Set Platform Role action."
|
|
193
|
+
}),
|
|
194
|
+
banned: data.Field.boolean({
|
|
195
|
+
label: "Banned",
|
|
196
|
+
defaultValue: false,
|
|
197
|
+
group: "Admin",
|
|
198
|
+
description: "When true, the user cannot sign in. Toggle via Ban User / Unban User actions."
|
|
199
|
+
}),
|
|
200
|
+
ban_reason: data.Field.text({
|
|
201
|
+
label: "Ban Reason",
|
|
202
|
+
required: false,
|
|
203
|
+
maxLength: 255,
|
|
204
|
+
group: "Admin"
|
|
205
|
+
}),
|
|
206
|
+
ban_expires: data.Field.datetime({
|
|
207
|
+
label: "Ban Expires",
|
|
208
|
+
required: false,
|
|
209
|
+
group: "Admin",
|
|
210
|
+
description: "When set, the ban auto-clears at this time."
|
|
211
|
+
}),
|
|
43
212
|
// ── Profile ──────────────────────────────────────────────────
|
|
44
213
|
image: data.Field.url({
|
|
45
214
|
label: "Profile Image",
|
|
@@ -100,6 +269,62 @@ var SysSession = data.ObjectSchema.create({
|
|
|
100
269
|
displayNameField: "user_id",
|
|
101
270
|
titleFormat: "Session \u2014 {user_id}",
|
|
102
271
|
compactLayout: ["user_id", "ip_address", "expires_at"],
|
|
272
|
+
// Custom actions — sessions are managed by better-auth (generic CRUD
|
|
273
|
+
// suppressed). "Sign out other devices" is the high-value self-service
|
|
274
|
+
// affordance every IdP exposes. Maps to better-auth's
|
|
275
|
+
// `revoke-other-sessions` endpoint which terminates every session for
|
|
276
|
+
// the current user except the one making the request.
|
|
277
|
+
actions: [
|
|
278
|
+
{
|
|
279
|
+
name: "revoke_my_other_sessions",
|
|
280
|
+
label: "Sign out other devices",
|
|
281
|
+
icon: "log-out",
|
|
282
|
+
variant: "danger",
|
|
283
|
+
locations: ["list_toolbar"],
|
|
284
|
+
type: "api",
|
|
285
|
+
target: "/api/v1/auth/revoke-other-sessions",
|
|
286
|
+
confirmText: "Sign out of every other device where you're currently logged in? Your current session will remain active.",
|
|
287
|
+
successMessage: "All other sessions revoked",
|
|
288
|
+
refreshAfter: true
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: "revoke_session",
|
|
292
|
+
label: "Revoke Session",
|
|
293
|
+
icon: "log-out",
|
|
294
|
+
variant: "danger",
|
|
295
|
+
mode: "delete",
|
|
296
|
+
locations: ["list_item"],
|
|
297
|
+
type: "api",
|
|
298
|
+
target: "/api/v1/auth/revoke-session",
|
|
299
|
+
// better-auth `revoke-session` keys off the session token, not the id.
|
|
300
|
+
recordIdParam: "token",
|
|
301
|
+
recordIdField: "token",
|
|
302
|
+
confirmText: "Revoke this session? The user will be signed out from that device.",
|
|
303
|
+
successMessage: "Session revoked",
|
|
304
|
+
refreshAfter: true
|
|
305
|
+
}
|
|
306
|
+
],
|
|
307
|
+
listViews: {
|
|
308
|
+
mine: {
|
|
309
|
+
type: "grid",
|
|
310
|
+
name: "mine",
|
|
311
|
+
label: "My Sessions",
|
|
312
|
+
data: { provider: "object", object: "sys_session" },
|
|
313
|
+
columns: ["ip_address", "active_organization_id", "created_at", "expires_at"],
|
|
314
|
+
filter: [{ field: "user_id", operator: "equals", value: "{current_user_id}" }],
|
|
315
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
316
|
+
pagination: { pageSize: 50 }
|
|
317
|
+
},
|
|
318
|
+
all_sessions: {
|
|
319
|
+
type: "grid",
|
|
320
|
+
name: "all_sessions",
|
|
321
|
+
label: "All",
|
|
322
|
+
data: { provider: "object", object: "sys_session" },
|
|
323
|
+
columns: ["user_id", "ip_address", "active_organization_id", "created_at", "expires_at"],
|
|
324
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
325
|
+
pagination: { pageSize: 50 }
|
|
326
|
+
}
|
|
327
|
+
},
|
|
103
328
|
fields: {
|
|
104
329
|
// ── Session owner & expiry ──────────────────────────────────
|
|
105
330
|
user_id: data.Field.lookup("sys_user", {
|
|
@@ -137,6 +362,13 @@ var SysSession = data.ObjectSchema.create({
|
|
|
137
362
|
required: false,
|
|
138
363
|
group: "Client"
|
|
139
364
|
}),
|
|
365
|
+
// ── Admin (managed by better-auth `admin` plugin) ────────────
|
|
366
|
+
impersonated_by: data.Field.lookup("sys_user", {
|
|
367
|
+
label: "Impersonated By",
|
|
368
|
+
required: false,
|
|
369
|
+
group: "Admin",
|
|
370
|
+
description: "User id of the admin that started this impersonation session, if any."
|
|
371
|
+
}),
|
|
140
372
|
// ── Secret (hidden by default) ──────────────────────────────
|
|
141
373
|
token: data.Field.text({
|
|
142
374
|
label: "Session Token",
|
|
@@ -191,6 +423,37 @@ var SysAccount = data.ObjectSchema.create({
|
|
|
191
423
|
description: "OAuth and authentication provider accounts",
|
|
192
424
|
titleFormat: "{provider_id} - {account_id}",
|
|
193
425
|
compactLayout: ["provider_id", "user_id", "account_id"],
|
|
426
|
+
listViews: {
|
|
427
|
+
mine: {
|
|
428
|
+
type: "grid",
|
|
429
|
+
name: "mine",
|
|
430
|
+
label: "My Links",
|
|
431
|
+
data: { provider: "object", object: "sys_account" },
|
|
432
|
+
columns: ["provider_id", "account_id", "created_at", "updated_at"],
|
|
433
|
+
filter: [{ field: "user_id", operator: "equals", value: "{current_user_id}" }],
|
|
434
|
+
sort: [{ field: "provider_id", order: "asc" }],
|
|
435
|
+
pagination: { pageSize: 50 }
|
|
436
|
+
},
|
|
437
|
+
by_provider: {
|
|
438
|
+
type: "grid",
|
|
439
|
+
name: "by_provider",
|
|
440
|
+
label: "By Provider",
|
|
441
|
+
data: { provider: "object", object: "sys_account" },
|
|
442
|
+
columns: ["provider_id", "user_id", "account_id", "created_at"],
|
|
443
|
+
sort: [{ field: "provider_id", order: "asc" }, { field: "created_at", order: "desc" }],
|
|
444
|
+
grouping: { fields: [{ field: "provider_id", order: "asc", collapsed: false }] },
|
|
445
|
+
pagination: { pageSize: 100 }
|
|
446
|
+
},
|
|
447
|
+
all_links: {
|
|
448
|
+
type: "grid",
|
|
449
|
+
name: "all_links",
|
|
450
|
+
label: "All",
|
|
451
|
+
data: { provider: "object", object: "sys_account" },
|
|
452
|
+
columns: ["provider_id", "user_id", "account_id", "created_at", "updated_at"],
|
|
453
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
454
|
+
pagination: { pageSize: 100 }
|
|
455
|
+
}
|
|
456
|
+
},
|
|
194
457
|
fields: {
|
|
195
458
|
id: data.Field.text({
|
|
196
459
|
label: "Account ID",
|
|
@@ -331,6 +594,104 @@ var SysOrganization = data.ObjectSchema.create({
|
|
|
331
594
|
displayNameField: "name",
|
|
332
595
|
titleFormat: "{name}",
|
|
333
596
|
compactLayout: ["name", "slug"],
|
|
597
|
+
// Custom actions — generic CRUD is suppressed (better-auth-managed),
|
|
598
|
+
// but admins still need to create new orgs from the Setup app.
|
|
599
|
+
actions: [
|
|
600
|
+
{
|
|
601
|
+
name: "create_organization",
|
|
602
|
+
label: "Create Organization",
|
|
603
|
+
icon: "plus",
|
|
604
|
+
variant: "primary",
|
|
605
|
+
locations: ["list_toolbar"],
|
|
606
|
+
type: "api",
|
|
607
|
+
target: "/api/v1/auth/organization/create",
|
|
608
|
+
successMessage: "Organization created",
|
|
609
|
+
refreshAfter: true,
|
|
610
|
+
params: [
|
|
611
|
+
{ field: "name", required: true },
|
|
612
|
+
{ field: "slug", required: true },
|
|
613
|
+
{ field: "logo" }
|
|
614
|
+
]
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
name: "update_organization",
|
|
618
|
+
label: "Edit Organization",
|
|
619
|
+
icon: "pencil",
|
|
620
|
+
mode: "edit",
|
|
621
|
+
locations: ["list_item"],
|
|
622
|
+
type: "api",
|
|
623
|
+
target: "/api/v1/auth/organization/update",
|
|
624
|
+
recordIdParam: "organizationId",
|
|
625
|
+
// better-auth `organization/update` nests editable fields under `data`.
|
|
626
|
+
bodyShape: { wrap: "data" },
|
|
627
|
+
successMessage: "Organization updated",
|
|
628
|
+
refreshAfter: true,
|
|
629
|
+
params: [
|
|
630
|
+
{ field: "name", required: true, defaultFromRow: true },
|
|
631
|
+
{ field: "slug", required: true, defaultFromRow: true },
|
|
632
|
+
{ field: "logo", defaultFromRow: true }
|
|
633
|
+
]
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: "delete_organization",
|
|
637
|
+
label: "Delete Organization",
|
|
638
|
+
icon: "trash-2",
|
|
639
|
+
variant: "danger",
|
|
640
|
+
mode: "delete",
|
|
641
|
+
locations: ["list_item"],
|
|
642
|
+
type: "api",
|
|
643
|
+
target: "/api/v1/auth/organization/delete",
|
|
644
|
+
recordIdParam: "organizationId",
|
|
645
|
+
confirmText: "Delete this organization? All members will lose access immediately. This cannot be undone.",
|
|
646
|
+
successMessage: "Organization deleted",
|
|
647
|
+
refreshAfter: true
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
// Switch the caller's active organization context. Standard
|
|
651
|
+
// better-auth endpoint; no extra params needed (org id ships as
|
|
652
|
+
// the row id). Used from the Setup list and the record header so
|
|
653
|
+
// admins can context-switch without leaving the page.
|
|
654
|
+
name: "set_active_organization",
|
|
655
|
+
label: "Set Active",
|
|
656
|
+
icon: "check-circle-2",
|
|
657
|
+
variant: "secondary",
|
|
658
|
+
mode: "custom",
|
|
659
|
+
locations: ["list_item", "record_header"],
|
|
660
|
+
type: "api",
|
|
661
|
+
target: "/api/v1/auth/organization/set-active",
|
|
662
|
+
recordIdParam: "organizationId",
|
|
663
|
+
successMessage: "Active organization switched",
|
|
664
|
+
refreshAfter: true
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
// Current user leaves the org. Distinct from `delete_organization`
|
|
668
|
+
// (admin-only, destroys the org) — `leave` only removes the caller's
|
|
669
|
+
// own membership. Better-auth: `organization/leave { organizationId }`.
|
|
670
|
+
name: "leave_organization",
|
|
671
|
+
label: "Leave Organization",
|
|
672
|
+
icon: "log-out",
|
|
673
|
+
variant: "danger",
|
|
674
|
+
mode: "custom",
|
|
675
|
+
locations: ["list_item", "record_header"],
|
|
676
|
+
type: "api",
|
|
677
|
+
target: "/api/v1/auth/organization/leave",
|
|
678
|
+
recordIdParam: "organizationId",
|
|
679
|
+
confirmText: "Leave this organization? You will lose access to all of its resources.",
|
|
680
|
+
successMessage: "You have left the organization",
|
|
681
|
+
refreshAfter: true
|
|
682
|
+
}
|
|
683
|
+
],
|
|
684
|
+
listViews: {
|
|
685
|
+
all_orgs: {
|
|
686
|
+
type: "grid",
|
|
687
|
+
name: "all_orgs",
|
|
688
|
+
label: "All",
|
|
689
|
+
data: { provider: "object", object: "sys_organization" },
|
|
690
|
+
columns: ["name", "slug", "created_at", "updated_at"],
|
|
691
|
+
sort: [{ field: "name", order: "asc" }],
|
|
692
|
+
pagination: { pageSize: 50 }
|
|
693
|
+
}
|
|
694
|
+
},
|
|
334
695
|
fields: {
|
|
335
696
|
// ── Identity ─────────────────────────────────────────────────
|
|
336
697
|
name: data.Field.text({
|
|
@@ -404,6 +765,63 @@ var SysMember = data.ObjectSchema.create({
|
|
|
404
765
|
description: "Organization membership records",
|
|
405
766
|
titleFormat: "{user_id} in {organization_id}",
|
|
406
767
|
compactLayout: ["user_id", "organization_id", "role"],
|
|
768
|
+
// Row-level actions: better-auth `organization/update-member-role` and
|
|
769
|
+
// `organization/remove-member`. Generic CRUD is suppressed on better-auth
|
|
770
|
+
// managed tables, so these are the canonical edit/delete entry points.
|
|
771
|
+
// The `add_member` toolbar action covers the admin "attach an existing
|
|
772
|
+
// user directly without sending an invitation" flow.
|
|
773
|
+
actions: [
|
|
774
|
+
{
|
|
775
|
+
// Admin-only: directly attach an existing user to the active org,
|
|
776
|
+
// bypassing the invite-accept flow. Better-auth:
|
|
777
|
+
// `organization/add-member { userId, role, organizationId?, teamId? }`.
|
|
778
|
+
// organizationId/teamId default to the caller's active org/team when
|
|
779
|
+
// omitted, so we leave them as optional params.
|
|
780
|
+
name: "add_member",
|
|
781
|
+
label: "Add Member",
|
|
782
|
+
icon: "user-plus",
|
|
783
|
+
variant: "primary",
|
|
784
|
+
locations: ["list_toolbar"],
|
|
785
|
+
type: "api",
|
|
786
|
+
target: "/api/v1/auth/organization/add-member",
|
|
787
|
+
successMessage: "Member added",
|
|
788
|
+
refreshAfter: true,
|
|
789
|
+
params: [
|
|
790
|
+
{ name: "userId", field: "user_id", required: true },
|
|
791
|
+
{ field: "role", required: true },
|
|
792
|
+
{ name: "organizationId", field: "organization_id" }
|
|
793
|
+
]
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
name: "update_member_role",
|
|
797
|
+
label: "Change Role",
|
|
798
|
+
icon: "shield",
|
|
799
|
+
mode: "edit",
|
|
800
|
+
locations: ["list_item"],
|
|
801
|
+
type: "api",
|
|
802
|
+
target: "/api/v1/auth/organization/update-member-role",
|
|
803
|
+
recordIdParam: "memberId",
|
|
804
|
+
successMessage: "Member role updated",
|
|
805
|
+
refreshAfter: true,
|
|
806
|
+
params: [
|
|
807
|
+
{ field: "role", required: true, defaultFromRow: true }
|
|
808
|
+
]
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: "remove_member",
|
|
812
|
+
label: "Remove Member",
|
|
813
|
+
icon: "user-minus",
|
|
814
|
+
variant: "danger",
|
|
815
|
+
mode: "delete",
|
|
816
|
+
locations: ["list_item"],
|
|
817
|
+
type: "api",
|
|
818
|
+
target: "/api/v1/auth/organization/remove-member",
|
|
819
|
+
recordIdParam: "memberIdOrEmail",
|
|
820
|
+
confirmText: "Remove this member from the organization? They will lose access to all org resources.",
|
|
821
|
+
successMessage: "Member removed",
|
|
822
|
+
refreshAfter: true
|
|
823
|
+
}
|
|
824
|
+
],
|
|
407
825
|
fields: {
|
|
408
826
|
id: data.Field.text({
|
|
409
827
|
label: "Member ID",
|
|
@@ -423,11 +841,16 @@ var SysMember = data.ObjectSchema.create({
|
|
|
423
841
|
label: "User",
|
|
424
842
|
required: true
|
|
425
843
|
}),
|
|
426
|
-
role: data.Field.
|
|
844
|
+
role: data.Field.select({
|
|
427
845
|
label: "Role",
|
|
428
846
|
required: false,
|
|
429
|
-
description: "Member role within the organization
|
|
430
|
-
|
|
847
|
+
description: "Member role within the organization",
|
|
848
|
+
options: [
|
|
849
|
+
{ label: "Owner", value: "owner" },
|
|
850
|
+
{ label: "Admin", value: "admin" },
|
|
851
|
+
{ label: "Member", value: "member" }
|
|
852
|
+
],
|
|
853
|
+
defaultValue: "member"
|
|
431
854
|
})
|
|
432
855
|
},
|
|
433
856
|
indexes: [
|
|
@@ -453,6 +876,97 @@ var SysInvitation = data.ObjectSchema.create({
|
|
|
453
876
|
description: "Organization invitations for user onboarding",
|
|
454
877
|
titleFormat: "Invitation to {organization_id}",
|
|
455
878
|
compactLayout: ["email", "organization_id", "status"],
|
|
879
|
+
// Custom actions — generic CRUD is suppressed (better-auth-managed).
|
|
880
|
+
// Mirror the `invite_user` toolbar action from sys_user here so admins
|
|
881
|
+
// landing on the Invitations page get an obvious entry point.
|
|
882
|
+
actions: [
|
|
883
|
+
{
|
|
884
|
+
name: "invite_user",
|
|
885
|
+
label: "Invite User",
|
|
886
|
+
icon: "user-plus",
|
|
887
|
+
variant: "primary",
|
|
888
|
+
locations: ["list_toolbar"],
|
|
889
|
+
type: "api",
|
|
890
|
+
target: "/api/v1/auth/organization/invite-member",
|
|
891
|
+
successMessage: "Invitation sent",
|
|
892
|
+
refreshAfter: true,
|
|
893
|
+
params: [
|
|
894
|
+
{ field: "email", required: true },
|
|
895
|
+
{ field: "role", required: true }
|
|
896
|
+
]
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
name: "cancel_invitation",
|
|
900
|
+
label: "Cancel Invitation",
|
|
901
|
+
icon: "x-circle",
|
|
902
|
+
variant: "danger",
|
|
903
|
+
mode: "delete",
|
|
904
|
+
locations: ["list_item"],
|
|
905
|
+
type: "api",
|
|
906
|
+
target: "/api/v1/auth/organization/cancel-invitation",
|
|
907
|
+
recordIdParam: "invitationId",
|
|
908
|
+
confirmText: "Cancel this invitation? The recipient will no longer be able to accept it.",
|
|
909
|
+
successMessage: "Invitation canceled",
|
|
910
|
+
refreshAfter: true
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
name: "resend_invitation",
|
|
914
|
+
label: "Resend Invitation",
|
|
915
|
+
icon: "send",
|
|
916
|
+
variant: "secondary",
|
|
917
|
+
locations: ["list_item"],
|
|
918
|
+
type: "api",
|
|
919
|
+
target: "/api/v1/auth/organization/invite-member",
|
|
920
|
+
bodyExtra: { resend: true },
|
|
921
|
+
successMessage: "Invitation resent",
|
|
922
|
+
refreshAfter: true,
|
|
923
|
+
params: [
|
|
924
|
+
{ field: "email", required: true, defaultFromRow: true },
|
|
925
|
+
{ field: "role", required: true, defaultFromRow: true }
|
|
926
|
+
]
|
|
927
|
+
}
|
|
928
|
+
],
|
|
929
|
+
listViews: {
|
|
930
|
+
pending: {
|
|
931
|
+
type: "grid",
|
|
932
|
+
name: "pending",
|
|
933
|
+
label: "Pending",
|
|
934
|
+
data: { provider: "object", object: "sys_invitation" },
|
|
935
|
+
columns: ["email", "role", "organization_id", "inviter_id", "expires_at"],
|
|
936
|
+
filter: [{ field: "status", operator: "equals", value: "pending" }],
|
|
937
|
+
sort: [{ field: "expires_at", order: "asc" }],
|
|
938
|
+
pagination: { pageSize: 50 }
|
|
939
|
+
},
|
|
940
|
+
accepted: {
|
|
941
|
+
type: "grid",
|
|
942
|
+
name: "accepted",
|
|
943
|
+
label: "Accepted",
|
|
944
|
+
data: { provider: "object", object: "sys_invitation" },
|
|
945
|
+
columns: ["email", "role", "organization_id", "inviter_id", "created_at"],
|
|
946
|
+
filter: [{ field: "status", operator: "equals", value: "accepted" }],
|
|
947
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
948
|
+
pagination: { pageSize: 50 }
|
|
949
|
+
},
|
|
950
|
+
expired: {
|
|
951
|
+
type: "grid",
|
|
952
|
+
name: "expired",
|
|
953
|
+
label: "Expired / Canceled",
|
|
954
|
+
data: { provider: "object", object: "sys_invitation" },
|
|
955
|
+
columns: ["email", "status", "organization_id", "expires_at"],
|
|
956
|
+
filter: [{ field: "status", operator: "in", value: ["expired", "rejected", "canceled"] }],
|
|
957
|
+
sort: [{ field: "expires_at", order: "desc" }],
|
|
958
|
+
pagination: { pageSize: 50 }
|
|
959
|
+
},
|
|
960
|
+
all_invitations: {
|
|
961
|
+
type: "grid",
|
|
962
|
+
name: "all_invitations",
|
|
963
|
+
label: "All",
|
|
964
|
+
data: { provider: "object", object: "sys_invitation" },
|
|
965
|
+
columns: ["email", "status", "role", "organization_id", "inviter_id", "created_at"],
|
|
966
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
967
|
+
pagination: { pageSize: 50 }
|
|
968
|
+
}
|
|
969
|
+
},
|
|
456
970
|
fields: {
|
|
457
971
|
id: data.Field.text({
|
|
458
972
|
label: "Invitation ID",
|
|
@@ -473,11 +987,16 @@ var SysInvitation = data.ObjectSchema.create({
|
|
|
473
987
|
required: true,
|
|
474
988
|
description: "Email address of the invited user"
|
|
475
989
|
}),
|
|
476
|
-
role: data.Field.
|
|
990
|
+
role: data.Field.select({
|
|
477
991
|
label: "Role",
|
|
478
992
|
required: false,
|
|
479
|
-
|
|
480
|
-
|
|
993
|
+
description: "Role to assign upon acceptance",
|
|
994
|
+
options: [
|
|
995
|
+
{ label: "Owner", value: "owner" },
|
|
996
|
+
{ label: "Admin", value: "admin" },
|
|
997
|
+
{ label: "Member", value: "member" }
|
|
998
|
+
],
|
|
999
|
+
defaultValue: "member"
|
|
481
1000
|
}),
|
|
482
1001
|
status: data.Field.select(["pending", "accepted", "rejected", "expired", "canceled"], {
|
|
483
1002
|
label: "Status",
|
|
@@ -524,6 +1043,84 @@ var SysTeam = data.ObjectSchema.create({
|
|
|
524
1043
|
displayNameField: "name",
|
|
525
1044
|
titleFormat: "{name}",
|
|
526
1045
|
compactLayout: ["name", "organization_id"],
|
|
1046
|
+
// Custom actions calling better-auth's team endpoints. Generic CRUD is
|
|
1047
|
+
// suppressed (managedBy: 'better-auth'), so these are the canonical
|
|
1048
|
+
// entry points for create/update/delete.
|
|
1049
|
+
actions: [
|
|
1050
|
+
{
|
|
1051
|
+
// Better-auth: `organization/create-team { name, organizationId? }`.
|
|
1052
|
+
// organizationId defaults to the caller's active org when omitted.
|
|
1053
|
+
name: "create_team",
|
|
1054
|
+
label: "Create Team",
|
|
1055
|
+
icon: "plus",
|
|
1056
|
+
variant: "primary",
|
|
1057
|
+
locations: ["list_toolbar"],
|
|
1058
|
+
type: "api",
|
|
1059
|
+
target: "/api/v1/auth/organization/create-team",
|
|
1060
|
+
successMessage: "Team created",
|
|
1061
|
+
refreshAfter: true,
|
|
1062
|
+
params: [
|
|
1063
|
+
{ field: "name", required: true },
|
|
1064
|
+
{ name: "organizationId", field: "organization_id" }
|
|
1065
|
+
]
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
// Better-auth: `organization/update-team { teamId, data: { name } }`.
|
|
1069
|
+
// teamId stays flat (top-level); the user-editable params nest under
|
|
1070
|
+
// `data` via bodyShape.
|
|
1071
|
+
name: "update_team",
|
|
1072
|
+
label: "Edit Team",
|
|
1073
|
+
icon: "pencil",
|
|
1074
|
+
mode: "edit",
|
|
1075
|
+
locations: ["list_item"],
|
|
1076
|
+
type: "api",
|
|
1077
|
+
target: "/api/v1/auth/organization/update-team",
|
|
1078
|
+
recordIdParam: "teamId",
|
|
1079
|
+
bodyShape: { wrap: "data" },
|
|
1080
|
+
successMessage: "Team updated",
|
|
1081
|
+
refreshAfter: true,
|
|
1082
|
+
params: [
|
|
1083
|
+
{ field: "name", required: true, defaultFromRow: true }
|
|
1084
|
+
]
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
// Better-auth: `organization/remove-team { teamId, organizationId? }`.
|
|
1088
|
+
// organizationId defaults to the caller's active org when omitted.
|
|
1089
|
+
name: "remove_team",
|
|
1090
|
+
label: "Delete Team",
|
|
1091
|
+
icon: "trash-2",
|
|
1092
|
+
variant: "danger",
|
|
1093
|
+
mode: "delete",
|
|
1094
|
+
locations: ["list_item"],
|
|
1095
|
+
type: "api",
|
|
1096
|
+
target: "/api/v1/auth/organization/remove-team",
|
|
1097
|
+
recordIdParam: "teamId",
|
|
1098
|
+
confirmText: "Delete this team? Members will lose any team-scoped access. This cannot be undone.",
|
|
1099
|
+
successMessage: "Team deleted",
|
|
1100
|
+
refreshAfter: true
|
|
1101
|
+
}
|
|
1102
|
+
],
|
|
1103
|
+
listViews: {
|
|
1104
|
+
by_org: {
|
|
1105
|
+
type: "grid",
|
|
1106
|
+
name: "by_org",
|
|
1107
|
+
label: "By Organization",
|
|
1108
|
+
data: { provider: "object", object: "sys_team" },
|
|
1109
|
+
columns: ["organization_id", "name", "created_at", "updated_at"],
|
|
1110
|
+
sort: [{ field: "organization_id", order: "asc" }, { field: "name", order: "asc" }],
|
|
1111
|
+
grouping: { fields: [{ field: "organization_id", order: "asc", collapsed: false }] },
|
|
1112
|
+
pagination: { pageSize: 100 }
|
|
1113
|
+
},
|
|
1114
|
+
all_teams: {
|
|
1115
|
+
type: "grid",
|
|
1116
|
+
name: "all_teams",
|
|
1117
|
+
label: "All",
|
|
1118
|
+
data: { provider: "object", object: "sys_team" },
|
|
1119
|
+
columns: ["name", "organization_id", "created_at", "updated_at"],
|
|
1120
|
+
sort: [{ field: "name", order: "asc" }],
|
|
1121
|
+
pagination: { pageSize: 50 }
|
|
1122
|
+
}
|
|
1123
|
+
},
|
|
527
1124
|
fields: {
|
|
528
1125
|
// ── Identity ─────────────────────────────────────────────────
|
|
529
1126
|
name: data.Field.text({
|
|
@@ -582,6 +1179,48 @@ var SysTeamMember = data.ObjectSchema.create({
|
|
|
582
1179
|
description: "Team membership records linking users to teams",
|
|
583
1180
|
titleFormat: "{user_id} in {team_id}",
|
|
584
1181
|
compactLayout: ["user_id", "team_id", "created_at"],
|
|
1182
|
+
// Custom actions calling better-auth's team-member endpoints. Generic
|
|
1183
|
+
// CRUD is suppressed (managedBy: 'better-auth') so these are the
|
|
1184
|
+
// canonical add/remove entry points.
|
|
1185
|
+
actions: [
|
|
1186
|
+
{
|
|
1187
|
+
// Better-auth: `organization/add-team-member { teamId, userId }`.
|
|
1188
|
+
name: "add_team_member",
|
|
1189
|
+
label: "Add Member",
|
|
1190
|
+
icon: "user-plus",
|
|
1191
|
+
variant: "primary",
|
|
1192
|
+
locations: ["list_toolbar"],
|
|
1193
|
+
type: "api",
|
|
1194
|
+
target: "/api/v1/auth/organization/add-team-member",
|
|
1195
|
+
successMessage: "Team member added",
|
|
1196
|
+
refreshAfter: true,
|
|
1197
|
+
params: [
|
|
1198
|
+
{ name: "teamId", field: "team_id", required: true },
|
|
1199
|
+
{ name: "userId", field: "user_id", required: true }
|
|
1200
|
+
]
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
// Better-auth: `organization/remove-team-member { teamId, userId }`.
|
|
1204
|
+
// The endpoint identifies the membership by the (teamId, userId)
|
|
1205
|
+
// pair rather than the join-row id, so we pull both from the row
|
|
1206
|
+
// via `defaultFromRow` instead of using `recordIdParam`.
|
|
1207
|
+
name: "remove_team_member",
|
|
1208
|
+
label: "Remove from Team",
|
|
1209
|
+
icon: "user-minus",
|
|
1210
|
+
variant: "danger",
|
|
1211
|
+
mode: "delete",
|
|
1212
|
+
locations: ["list_item"],
|
|
1213
|
+
type: "api",
|
|
1214
|
+
target: "/api/v1/auth/organization/remove-team-member",
|
|
1215
|
+
confirmText: "Remove this user from the team? They will lose any team-scoped access.",
|
|
1216
|
+
successMessage: "Team member removed",
|
|
1217
|
+
refreshAfter: true,
|
|
1218
|
+
params: [
|
|
1219
|
+
{ name: "teamId", field: "team_id", required: true, defaultFromRow: true },
|
|
1220
|
+
{ name: "userId", field: "user_id", required: true, defaultFromRow: true }
|
|
1221
|
+
]
|
|
1222
|
+
}
|
|
1223
|
+
],
|
|
585
1224
|
fields: {
|
|
586
1225
|
id: data.Field.text({
|
|
587
1226
|
label: "Team Member ID",
|
|
@@ -615,6 +1254,248 @@ var SysTeamMember = data.ObjectSchema.create({
|
|
|
615
1254
|
mru: false
|
|
616
1255
|
}
|
|
617
1256
|
});
|
|
1257
|
+
var SysDepartment = data.ObjectSchema.create({
|
|
1258
|
+
name: "sys_department",
|
|
1259
|
+
label: "Department",
|
|
1260
|
+
pluralLabel: "Departments",
|
|
1261
|
+
icon: "building",
|
|
1262
|
+
isSystem: true,
|
|
1263
|
+
managedBy: "platform",
|
|
1264
|
+
description: "Hierarchical org-skeleton node (department / division / business unit / office).",
|
|
1265
|
+
displayNameField: "name",
|
|
1266
|
+
titleFormat: "{name}",
|
|
1267
|
+
compactLayout: ["name", "kind", "parent_department_id", "manager_user_id"],
|
|
1268
|
+
listViews: {
|
|
1269
|
+
active: {
|
|
1270
|
+
type: "grid",
|
|
1271
|
+
name: "active",
|
|
1272
|
+
label: "Active",
|
|
1273
|
+
data: { provider: "object", object: "sys_department" },
|
|
1274
|
+
columns: ["name", "code", "kind", "parent_department_id", "manager_user_id", "effective_from"],
|
|
1275
|
+
filter: [{ field: "active", operator: "equals", value: true }],
|
|
1276
|
+
sort: [{ field: "name", order: "asc" }],
|
|
1277
|
+
pagination: { pageSize: 100 }
|
|
1278
|
+
},
|
|
1279
|
+
inactive: {
|
|
1280
|
+
type: "grid",
|
|
1281
|
+
name: "inactive",
|
|
1282
|
+
label: "Inactive",
|
|
1283
|
+
data: { provider: "object", object: "sys_department" },
|
|
1284
|
+
columns: ["name", "code", "kind", "effective_to"],
|
|
1285
|
+
filter: [{ field: "active", operator: "equals", value: false }],
|
|
1286
|
+
sort: [{ field: "effective_to", order: "desc" }],
|
|
1287
|
+
pagination: { pageSize: 50 }
|
|
1288
|
+
},
|
|
1289
|
+
by_kind: {
|
|
1290
|
+
type: "grid",
|
|
1291
|
+
name: "by_kind",
|
|
1292
|
+
label: "By Kind",
|
|
1293
|
+
data: { provider: "object", object: "sys_department" },
|
|
1294
|
+
columns: ["kind", "name", "code", "parent_department_id", "manager_user_id", "active"],
|
|
1295
|
+
sort: [{ field: "kind", order: "asc" }, { field: "name", order: "asc" }],
|
|
1296
|
+
grouping: { fields: [{ field: "kind", order: "asc", collapsed: false }] },
|
|
1297
|
+
pagination: { pageSize: 100 }
|
|
1298
|
+
},
|
|
1299
|
+
all_departments: {
|
|
1300
|
+
type: "grid",
|
|
1301
|
+
name: "all_departments",
|
|
1302
|
+
label: "All",
|
|
1303
|
+
data: { provider: "object", object: "sys_department" },
|
|
1304
|
+
columns: ["name", "code", "kind", "parent_department_id", "manager_user_id", "active"],
|
|
1305
|
+
sort: [{ field: "name", order: "asc" }],
|
|
1306
|
+
pagination: { pageSize: 100 }
|
|
1307
|
+
}
|
|
1308
|
+
},
|
|
1309
|
+
fields: {
|
|
1310
|
+
// ── Identity ─────────────────────────────────────────────────
|
|
1311
|
+
name: data.Field.text({
|
|
1312
|
+
label: "Name",
|
|
1313
|
+
required: true,
|
|
1314
|
+
searchable: true,
|
|
1315
|
+
maxLength: 255,
|
|
1316
|
+
group: "Identity"
|
|
1317
|
+
}),
|
|
1318
|
+
code: data.Field.text({
|
|
1319
|
+
label: "Code",
|
|
1320
|
+
required: false,
|
|
1321
|
+
searchable: true,
|
|
1322
|
+
maxLength: 64,
|
|
1323
|
+
description: "Short stable code (e.g. EMEA-SALES). Unique within tenant.",
|
|
1324
|
+
group: "Identity"
|
|
1325
|
+
}),
|
|
1326
|
+
kind: data.Field.select(
|
|
1327
|
+
["company", "division", "department", "team", "office", "cost_center"],
|
|
1328
|
+
{
|
|
1329
|
+
label: "Kind",
|
|
1330
|
+
required: true,
|
|
1331
|
+
defaultValue: "department",
|
|
1332
|
+
description: "Categorisation hint \u2014 does not change graph semantics.",
|
|
1333
|
+
group: "Identity"
|
|
1334
|
+
}
|
|
1335
|
+
),
|
|
1336
|
+
// ── Hierarchy ────────────────────────────────────────────────
|
|
1337
|
+
parent_department_id: data.Field.lookup("sys_department", {
|
|
1338
|
+
label: "Parent Department",
|
|
1339
|
+
required: false,
|
|
1340
|
+
description: "Self-reference for the org tree. Null = root of tenant.",
|
|
1341
|
+
group: "Hierarchy"
|
|
1342
|
+
}),
|
|
1343
|
+
organization_id: data.Field.lookup("sys_organization", {
|
|
1344
|
+
label: "Organization",
|
|
1345
|
+
required: true,
|
|
1346
|
+
description: "Tenant scope.",
|
|
1347
|
+
group: "Hierarchy"
|
|
1348
|
+
}),
|
|
1349
|
+
// ── Leadership ───────────────────────────────────────────────
|
|
1350
|
+
manager_user_id: data.Field.lookup("sys_user", {
|
|
1351
|
+
label: "Department Head",
|
|
1352
|
+
required: false,
|
|
1353
|
+
description: "User responsible for this org unit (department head / lead).",
|
|
1354
|
+
group: "Leadership"
|
|
1355
|
+
}),
|
|
1356
|
+
// ── Lifecycle ────────────────────────────────────────────────
|
|
1357
|
+
active: data.Field.boolean({
|
|
1358
|
+
label: "Active",
|
|
1359
|
+
required: false,
|
|
1360
|
+
defaultValue: true,
|
|
1361
|
+
description: "When false, members are not expanded by graph queries.",
|
|
1362
|
+
group: "Lifecycle"
|
|
1363
|
+
}),
|
|
1364
|
+
effective_from: data.Field.datetime({
|
|
1365
|
+
label: "Effective From",
|
|
1366
|
+
required: false,
|
|
1367
|
+
description: "When this department came into existence (HRIS sync).",
|
|
1368
|
+
group: "Lifecycle"
|
|
1369
|
+
}),
|
|
1370
|
+
effective_to: data.Field.datetime({
|
|
1371
|
+
label: "Effective To",
|
|
1372
|
+
required: false,
|
|
1373
|
+
description: "When this department was retired (HRIS sync).",
|
|
1374
|
+
group: "Lifecycle"
|
|
1375
|
+
}),
|
|
1376
|
+
external_ref: data.Field.text({
|
|
1377
|
+
label: "External Reference",
|
|
1378
|
+
required: false,
|
|
1379
|
+
maxLength: 200,
|
|
1380
|
+
description: "ID in upstream HRIS (Workday / SAP HR / \u5317\u68EE).",
|
|
1381
|
+
group: "Lifecycle"
|
|
1382
|
+
}),
|
|
1383
|
+
// ── System ───────────────────────────────────────────────────
|
|
1384
|
+
id: data.Field.text({
|
|
1385
|
+
label: "Department ID",
|
|
1386
|
+
required: true,
|
|
1387
|
+
readonly: true,
|
|
1388
|
+
group: "System"
|
|
1389
|
+
}),
|
|
1390
|
+
created_at: data.Field.datetime({
|
|
1391
|
+
label: "Created At",
|
|
1392
|
+
defaultValue: "NOW()",
|
|
1393
|
+
readonly: true,
|
|
1394
|
+
group: "System"
|
|
1395
|
+
}),
|
|
1396
|
+
updated_at: data.Field.datetime({
|
|
1397
|
+
label: "Updated At",
|
|
1398
|
+
defaultValue: "NOW()",
|
|
1399
|
+
readonly: true,
|
|
1400
|
+
group: "System"
|
|
1401
|
+
})
|
|
1402
|
+
},
|
|
1403
|
+
indexes: [
|
|
1404
|
+
{ fields: ["organization_id"] },
|
|
1405
|
+
{ fields: ["parent_department_id"] },
|
|
1406
|
+
{ fields: ["code", "organization_id"], unique: true },
|
|
1407
|
+
{ fields: ["active"] }
|
|
1408
|
+
],
|
|
1409
|
+
enable: {
|
|
1410
|
+
trackHistory: true,
|
|
1411
|
+
searchable: true,
|
|
1412
|
+
apiEnabled: true,
|
|
1413
|
+
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
1414
|
+
trash: true,
|
|
1415
|
+
mru: false
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
var SysDepartmentMember = data.ObjectSchema.create({
|
|
1419
|
+
name: "sys_department_member",
|
|
1420
|
+
label: "Department Member",
|
|
1421
|
+
pluralLabel: "Department Members",
|
|
1422
|
+
icon: "user-cog",
|
|
1423
|
+
isSystem: true,
|
|
1424
|
+
managedBy: "platform",
|
|
1425
|
+
description: "User assignment to a department (matrix-org friendly, effective-dated).",
|
|
1426
|
+
titleFormat: "{user_id} in {department_id}",
|
|
1427
|
+
compactLayout: ["user_id", "department_id", "role_in_department", "is_primary"],
|
|
1428
|
+
fields: {
|
|
1429
|
+
id: data.Field.text({
|
|
1430
|
+
label: "Member ID",
|
|
1431
|
+
required: true,
|
|
1432
|
+
readonly: true,
|
|
1433
|
+
group: "System"
|
|
1434
|
+
}),
|
|
1435
|
+
department_id: data.Field.lookup("sys_department", {
|
|
1436
|
+
label: "Department",
|
|
1437
|
+
required: true,
|
|
1438
|
+
group: "Assignment"
|
|
1439
|
+
}),
|
|
1440
|
+
user_id: data.Field.lookup("sys_user", {
|
|
1441
|
+
label: "User",
|
|
1442
|
+
required: true,
|
|
1443
|
+
group: "Assignment"
|
|
1444
|
+
}),
|
|
1445
|
+
role_in_department: data.Field.select(
|
|
1446
|
+
["member", "lead", "deputy"],
|
|
1447
|
+
{
|
|
1448
|
+
label: "Role in Department",
|
|
1449
|
+
required: false,
|
|
1450
|
+
defaultValue: "member",
|
|
1451
|
+
description: "`lead` is the day-to-day head; `deputy` may stand in for the lead in approval routing.",
|
|
1452
|
+
group: "Assignment"
|
|
1453
|
+
}
|
|
1454
|
+
),
|
|
1455
|
+
is_primary: data.Field.boolean({
|
|
1456
|
+
label: "Primary Assignment",
|
|
1457
|
+
required: false,
|
|
1458
|
+
defaultValue: true,
|
|
1459
|
+
description: "When the user is in multiple departments, this marks the canonical one for reporting.",
|
|
1460
|
+
group: "Assignment"
|
|
1461
|
+
}),
|
|
1462
|
+
effective_from: data.Field.datetime({
|
|
1463
|
+
label: "Effective From",
|
|
1464
|
+
required: false,
|
|
1465
|
+
group: "Lifecycle"
|
|
1466
|
+
}),
|
|
1467
|
+
effective_to: data.Field.datetime({
|
|
1468
|
+
label: "Effective To",
|
|
1469
|
+
required: false,
|
|
1470
|
+
group: "Lifecycle"
|
|
1471
|
+
}),
|
|
1472
|
+
created_at: data.Field.datetime({
|
|
1473
|
+
label: "Created At",
|
|
1474
|
+
defaultValue: "NOW()",
|
|
1475
|
+
readonly: true,
|
|
1476
|
+
group: "System"
|
|
1477
|
+
}),
|
|
1478
|
+
updated_at: data.Field.datetime({
|
|
1479
|
+
label: "Updated At",
|
|
1480
|
+
defaultValue: "NOW()",
|
|
1481
|
+
readonly: true,
|
|
1482
|
+
group: "System"
|
|
1483
|
+
})
|
|
1484
|
+
},
|
|
1485
|
+
indexes: [
|
|
1486
|
+
{ fields: ["department_id", "user_id"], unique: true },
|
|
1487
|
+
{ fields: ["user_id"] },
|
|
1488
|
+
{ fields: ["is_primary"] }
|
|
1489
|
+
],
|
|
1490
|
+
enable: {
|
|
1491
|
+
trackHistory: true,
|
|
1492
|
+
searchable: true,
|
|
1493
|
+
apiEnabled: true,
|
|
1494
|
+
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
1495
|
+
trash: true,
|
|
1496
|
+
mru: false
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
618
1499
|
var SysApiKey = data.ObjectSchema.create({
|
|
619
1500
|
name: "sys_api_key",
|
|
620
1501
|
label: "API Key",
|
|
@@ -626,6 +1507,85 @@ var SysApiKey = data.ObjectSchema.create({
|
|
|
626
1507
|
displayNameField: "name",
|
|
627
1508
|
titleFormat: "{name}",
|
|
628
1509
|
compactLayout: ["name", "prefix", "user_id", "expires_at", "revoked"],
|
|
1510
|
+
// Custom actions — sys_api_key is managed-by 'better-auth' but the
|
|
1511
|
+
// `revoked` boolean is a column we control via the data API. These row
|
|
1512
|
+
// actions use the generic PATCH /api/v1/sys_api_key/{id} endpoint with
|
|
1513
|
+
// `bodyExtra` to set the `revoked` flag explicitly.
|
|
1514
|
+
actions: [
|
|
1515
|
+
{
|
|
1516
|
+
name: "revoke_api_key",
|
|
1517
|
+
label: "Revoke API Key",
|
|
1518
|
+
icon: "shield-off",
|
|
1519
|
+
variant: "danger",
|
|
1520
|
+
mode: "custom",
|
|
1521
|
+
locations: ["list_item"],
|
|
1522
|
+
type: "api",
|
|
1523
|
+
method: "PATCH",
|
|
1524
|
+
target: "/api/v1/data/sys_api_key/{id}",
|
|
1525
|
+
bodyExtra: { revoked: true },
|
|
1526
|
+
confirmText: "Revoke this API key? Any clients using it will immediately lose access.",
|
|
1527
|
+
successMessage: "API key revoked",
|
|
1528
|
+
refreshAfter: true
|
|
1529
|
+
},
|
|
1530
|
+
{
|
|
1531
|
+
name: "restore_api_key",
|
|
1532
|
+
label: "Restore API Key",
|
|
1533
|
+
icon: "shield-check",
|
|
1534
|
+
variant: "secondary",
|
|
1535
|
+
mode: "custom",
|
|
1536
|
+
locations: ["list_item"],
|
|
1537
|
+
type: "api",
|
|
1538
|
+
method: "PATCH",
|
|
1539
|
+
target: "/api/v1/data/sys_api_key/{id}",
|
|
1540
|
+
bodyExtra: { revoked: false },
|
|
1541
|
+
confirmText: "Restore this revoked API key? Existing clients holding the key will regain access.",
|
|
1542
|
+
successMessage: "API key restored",
|
|
1543
|
+
refreshAfter: true
|
|
1544
|
+
}
|
|
1545
|
+
],
|
|
1546
|
+
listViews: {
|
|
1547
|
+
mine: {
|
|
1548
|
+
type: "grid",
|
|
1549
|
+
name: "mine",
|
|
1550
|
+
label: "My Keys",
|
|
1551
|
+
data: { provider: "object", object: "sys_api_key" },
|
|
1552
|
+
columns: ["name", "prefix", "expires_at", "last_used_at", "revoked"],
|
|
1553
|
+
filter: [
|
|
1554
|
+
{ field: "user_id", operator: "equals", value: "{current_user_id}" }
|
|
1555
|
+
],
|
|
1556
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
1557
|
+
pagination: { pageSize: 50 }
|
|
1558
|
+
},
|
|
1559
|
+
active: {
|
|
1560
|
+
type: "grid",
|
|
1561
|
+
name: "active",
|
|
1562
|
+
label: "Active",
|
|
1563
|
+
data: { provider: "object", object: "sys_api_key" },
|
|
1564
|
+
columns: ["name", "prefix", "user_id", "expires_at", "last_used_at"],
|
|
1565
|
+
filter: [{ field: "revoked", operator: "equals", value: false }],
|
|
1566
|
+
sort: [{ field: "last_used_at", order: "desc" }],
|
|
1567
|
+
pagination: { pageSize: 50 }
|
|
1568
|
+
},
|
|
1569
|
+
revoked: {
|
|
1570
|
+
type: "grid",
|
|
1571
|
+
name: "revoked",
|
|
1572
|
+
label: "Revoked",
|
|
1573
|
+
data: { provider: "object", object: "sys_api_key" },
|
|
1574
|
+
columns: ["name", "prefix", "user_id", "expires_at", "updated_at"],
|
|
1575
|
+
filter: [{ field: "revoked", operator: "equals", value: true }],
|
|
1576
|
+
sort: [{ field: "updated_at", order: "desc" }],
|
|
1577
|
+
pagination: { pageSize: 50 }
|
|
1578
|
+
},
|
|
1579
|
+
all_keys: {
|
|
1580
|
+
type: "grid",
|
|
1581
|
+
name: "all_keys",
|
|
1582
|
+
label: "All",
|
|
1583
|
+
data: { provider: "object", object: "sys_api_key" },
|
|
1584
|
+
columns: ["name", "prefix", "user_id", "expires_at", "last_used_at", "revoked"],
|
|
1585
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
1586
|
+
pagination: { pageSize: 50 }
|
|
1587
|
+
}
|
|
1588
|
+
},
|
|
629
1589
|
fields: {
|
|
630
1590
|
// ── Identity ─────────────────────────────────────────────────
|
|
631
1591
|
name: data.Field.text({
|
|
@@ -728,6 +1688,62 @@ var SysTwoFactor = data.ObjectSchema.create({
|
|
|
728
1688
|
description: "Two-factor authentication credentials",
|
|
729
1689
|
titleFormat: "Two-factor for {user_id}",
|
|
730
1690
|
compactLayout: ["user_id", "created_at"],
|
|
1691
|
+
listViews: {
|
|
1692
|
+
mine: {
|
|
1693
|
+
type: "grid",
|
|
1694
|
+
name: "mine",
|
|
1695
|
+
label: "My Enrollment",
|
|
1696
|
+
data: { provider: "object", object: "sys_two_factor" },
|
|
1697
|
+
columns: ["created_at", "updated_at"],
|
|
1698
|
+
filter: [{ field: "user_id", operator: "equals", value: "{current_user_id}" }],
|
|
1699
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
1700
|
+
pagination: { pageSize: 50 }
|
|
1701
|
+
},
|
|
1702
|
+
all_enrollments: {
|
|
1703
|
+
type: "grid",
|
|
1704
|
+
name: "all_enrollments",
|
|
1705
|
+
label: "All",
|
|
1706
|
+
data: { provider: "object", object: "sys_two_factor" },
|
|
1707
|
+
columns: ["user_id", "created_at", "updated_at"],
|
|
1708
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
1709
|
+
pagination: { pageSize: 50 }
|
|
1710
|
+
}
|
|
1711
|
+
},
|
|
1712
|
+
// Toolbar actions for self-service 2FA enrollment. The actual TOTP secret
|
|
1713
|
+
// and backup codes returned by better-auth must be shown in the response
|
|
1714
|
+
// toast / dialog — the action runner surfaces successMessage; the raw
|
|
1715
|
+
// payload is logged client-side for now (TODO: dedicated 2FA setup wizard).
|
|
1716
|
+
actions: [
|
|
1717
|
+
{
|
|
1718
|
+
name: "enable_two_factor",
|
|
1719
|
+
label: "Enable 2FA",
|
|
1720
|
+
icon: "shield-check",
|
|
1721
|
+
variant: "primary",
|
|
1722
|
+
locations: ["list_toolbar"],
|
|
1723
|
+
type: "api",
|
|
1724
|
+
target: "/api/v1/auth/two-factor/enable",
|
|
1725
|
+
successMessage: "2FA enrollment started \u2014 check response for TOTP URI and backup codes",
|
|
1726
|
+
refreshAfter: true,
|
|
1727
|
+
params: [
|
|
1728
|
+
{ name: "password", label: "Current Password", type: "text", required: true }
|
|
1729
|
+
]
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
name: "disable_two_factor",
|
|
1733
|
+
label: "Disable 2FA",
|
|
1734
|
+
icon: "shield-off",
|
|
1735
|
+
variant: "danger",
|
|
1736
|
+
locations: ["list_toolbar"],
|
|
1737
|
+
type: "api",
|
|
1738
|
+
target: "/api/v1/auth/two-factor/disable",
|
|
1739
|
+
confirmText: "Disable two-factor authentication on your account?",
|
|
1740
|
+
successMessage: "2FA disabled",
|
|
1741
|
+
refreshAfter: true,
|
|
1742
|
+
params: [
|
|
1743
|
+
{ name: "password", label: "Current Password", type: "text", required: true }
|
|
1744
|
+
]
|
|
1745
|
+
}
|
|
1746
|
+
],
|
|
731
1747
|
fields: {
|
|
732
1748
|
id: data.Field.text({
|
|
733
1749
|
label: "Two Factor ID",
|
|
@@ -867,9 +1883,44 @@ var SysUserPreference = data.ObjectSchema.create({
|
|
|
867
1883
|
pluralLabel: "User Preferences",
|
|
868
1884
|
icon: "settings",
|
|
869
1885
|
isSystem: true,
|
|
1886
|
+
// managedBy: 'system' — preferences are per-user state authored from
|
|
1887
|
+
// the user's own settings page, never created by an admin. The list
|
|
1888
|
+
// surface in Setup is a support/diagnostic view only.
|
|
1889
|
+
managedBy: "system",
|
|
870
1890
|
description: "Per-user key-value preferences (theme, locale, etc.)",
|
|
871
1891
|
titleFormat: "{key}",
|
|
872
1892
|
compactLayout: ["user_id", "key"],
|
|
1893
|
+
listViews: {
|
|
1894
|
+
mine: {
|
|
1895
|
+
type: "grid",
|
|
1896
|
+
name: "mine",
|
|
1897
|
+
label: "My Preferences",
|
|
1898
|
+
data: { provider: "object", object: "sys_user_preference" },
|
|
1899
|
+
columns: ["key", "updated_at"],
|
|
1900
|
+
filter: [{ field: "user_id", operator: "equals", value: "{current_user_id}" }],
|
|
1901
|
+
sort: [{ field: "key", order: "asc" }],
|
|
1902
|
+
pagination: { pageSize: 100 }
|
|
1903
|
+
},
|
|
1904
|
+
by_user: {
|
|
1905
|
+
type: "grid",
|
|
1906
|
+
name: "by_user",
|
|
1907
|
+
label: "By User",
|
|
1908
|
+
data: { provider: "object", object: "sys_user_preference" },
|
|
1909
|
+
columns: ["user_id", "key", "updated_at"],
|
|
1910
|
+
sort: [{ field: "user_id", order: "asc" }, { field: "key", order: "asc" }],
|
|
1911
|
+
grouping: { fields: [{ field: "user_id", order: "asc", collapsed: true }] },
|
|
1912
|
+
pagination: { pageSize: 200 }
|
|
1913
|
+
},
|
|
1914
|
+
all_preferences: {
|
|
1915
|
+
type: "grid",
|
|
1916
|
+
name: "all_preferences",
|
|
1917
|
+
label: "All",
|
|
1918
|
+
data: { provider: "object", object: "sys_user_preference" },
|
|
1919
|
+
columns: ["user_id", "key", "created_at", "updated_at"],
|
|
1920
|
+
sort: [{ field: "updated_at", order: "desc" }],
|
|
1921
|
+
pagination: { pageSize: 100 }
|
|
1922
|
+
}
|
|
1923
|
+
},
|
|
873
1924
|
fields: {
|
|
874
1925
|
id: data.Field.text({
|
|
875
1926
|
label: "Preference ID",
|
|
@@ -926,6 +1977,37 @@ var SysOauthApplication = data.ObjectSchema.create({
|
|
|
926
1977
|
displayNameField: "name",
|
|
927
1978
|
titleFormat: "{name}",
|
|
928
1979
|
compactLayout: ["name", "client_id", "type", "disabled"],
|
|
1980
|
+
listViews: {
|
|
1981
|
+
active: {
|
|
1982
|
+
type: "grid",
|
|
1983
|
+
name: "active",
|
|
1984
|
+
label: "Active",
|
|
1985
|
+
data: { provider: "object", object: "sys_oauth_application" },
|
|
1986
|
+
columns: ["name", "client_id", "type", "updated_at"],
|
|
1987
|
+
filter: [{ field: "disabled", operator: "equals", value: false }],
|
|
1988
|
+
sort: [{ field: "name", order: "asc" }],
|
|
1989
|
+
pagination: { pageSize: 50 }
|
|
1990
|
+
},
|
|
1991
|
+
disabled_apps: {
|
|
1992
|
+
type: "grid",
|
|
1993
|
+
name: "disabled_apps",
|
|
1994
|
+
label: "Disabled",
|
|
1995
|
+
data: { provider: "object", object: "sys_oauth_application" },
|
|
1996
|
+
columns: ["name", "client_id", "type", "updated_at"],
|
|
1997
|
+
filter: [{ field: "disabled", operator: "equals", value: true }],
|
|
1998
|
+
sort: [{ field: "updated_at", order: "desc" }],
|
|
1999
|
+
pagination: { pageSize: 50 }
|
|
2000
|
+
},
|
|
2001
|
+
all_apps: {
|
|
2002
|
+
type: "grid",
|
|
2003
|
+
name: "all_apps",
|
|
2004
|
+
label: "All",
|
|
2005
|
+
data: { provider: "object", object: "sys_oauth_application" },
|
|
2006
|
+
columns: ["name", "client_id", "type", "disabled", "created_at"],
|
|
2007
|
+
sort: [{ field: "name", order: "asc" }],
|
|
2008
|
+
pagination: { pageSize: 50 }
|
|
2009
|
+
}
|
|
2010
|
+
},
|
|
929
2011
|
fields: {
|
|
930
2012
|
// ── Identity ─────────────────────────────────────────────────
|
|
931
2013
|
id: data.Field.text({
|
|
@@ -1401,6 +2483,8 @@ var SysJwks = data.ObjectSchema.create({
|
|
|
1401
2483
|
|
|
1402
2484
|
exports.SysAccount = SysAccount;
|
|
1403
2485
|
exports.SysApiKey = SysApiKey;
|
|
2486
|
+
exports.SysDepartment = SysDepartment;
|
|
2487
|
+
exports.SysDepartmentMember = SysDepartmentMember;
|
|
1404
2488
|
exports.SysDeviceCode = SysDeviceCode;
|
|
1405
2489
|
exports.SysInvitation = SysInvitation;
|
|
1406
2490
|
exports.SysJwks = SysJwks;
|