@objectstack/platform-objects 4.0.5 → 4.1.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.
Files changed (57) hide show
  1. package/dist/apps/index.d.mts +16 -48
  2. package/dist/apps/index.d.ts +16 -48
  3. package/dist/apps/index.js +139 -217
  4. package/dist/apps/index.js.map +1 -1
  5. package/dist/apps/index.mjs +140 -212
  6. package/dist/apps/index.mjs.map +1 -1
  7. package/dist/audit/index.d.mts +38990 -51
  8. package/dist/audit/index.d.ts +38990 -51
  9. package/dist/audit/index.js +1428 -0
  10. package/dist/audit/index.js.map +1 -1
  11. package/dist/audit/index.mjs +1417 -1
  12. package/dist/audit/index.mjs.map +1 -1
  13. package/dist/identity/index.d.mts +14869 -2802
  14. package/dist/identity/index.d.ts +14869 -2802
  15. package/dist/identity/index.js +1090 -6
  16. package/dist/identity/index.js.map +1 -1
  17. package/dist/identity/index.mjs +1089 -7
  18. package/dist/identity/index.mjs.map +1 -1
  19. package/dist/index.d.mts +8 -7
  20. package/dist/index.d.ts +8 -7
  21. package/dist/index.js +3652 -1482
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.mjs +3633 -1465
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/integration/index.d.mts +2905 -0
  26. package/dist/integration/index.d.ts +2905 -0
  27. package/dist/integration/index.js +140 -0
  28. package/dist/integration/index.js.map +1 -0
  29. package/dist/integration/index.mjs +138 -0
  30. package/dist/integration/index.mjs.map +1 -0
  31. package/dist/metadata/index.d.mts +577 -21181
  32. package/dist/metadata/index.d.ts +577 -21181
  33. package/dist/metadata/index.js +29 -619
  34. package/dist/metadata/index.js.map +1 -1
  35. package/dist/metadata/index.mjs +30 -615
  36. package/dist/metadata/index.mjs.map +1 -1
  37. package/dist/security/index.d.mts +7278 -46
  38. package/dist/security/index.d.ts +7278 -46
  39. package/dist/security/index.js +540 -0
  40. package/dist/security/index.js.map +1 -1
  41. package/dist/security/index.mjs +539 -1
  42. package/dist/security/index.mjs.map +1 -1
  43. package/dist/system/index.d.mts +8409 -0
  44. package/dist/system/index.d.ts +8409 -0
  45. package/dist/system/index.js +395 -0
  46. package/dist/system/index.js.map +1 -0
  47. package/dist/system/index.mjs +391 -0
  48. package/dist/system/index.mjs.map +1 -0
  49. package/package.json +13 -8
  50. package/dist/tenant/index.d.mts +0 -18464
  51. package/dist/tenant/index.d.ts +0 -18464
  52. package/dist/tenant/index.js +0 -741
  53. package/dist/tenant/index.js.map +0 -1
  54. package/dist/tenant/index.mjs +0 -733
  55. package/dist/tenant/index.mjs.map +0 -1
  56. /package/dist/{state-machine.zod-BFg-VE0M.d-Ek3_yo9P.d.mts → state-machine.zod-BNanU03M.d-Ek3_yo9P.d.mts} +0 -0
  57. /package/dist/{state-machine.zod-BFg-VE0M.d-Ek3_yo9P.d.ts → state-machine.zod-BNanU03M.d-Ek3_yo9P.d.ts} +0 -0
@@ -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.text({
844
+ role: data.Field.select({
427
845
  label: "Role",
428
846
  required: false,
429
- description: "Member role within the organization (e.g. admin, member)",
430
- maxLength: 100
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.text({
990
+ role: data.Field.select({
477
991
  label: "Role",
478
992
  required: false,
479
- maxLength: 100,
480
- description: "Role to assign upon acceptance"
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;