hazo_auth 9.1.1 → 10.0.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 (55) hide show
  1. package/README.md +24 -6
  2. package/SETUP_CHECKLIST.md +6 -16
  3. package/cli-src/cli/init_users.ts +40 -48
  4. package/cli-src/lib/auth/auth_types.ts +0 -2
  5. package/cli-src/lib/auth/hazo_get_auth.server.ts +31 -25
  6. package/cli-src/lib/auth/hazo_get_tenant_auth.server.ts +9 -13
  7. package/cli-src/lib/constants.ts +2 -0
  8. package/cli-src/lib/profile_pic_menu_config.server.ts +4 -3
  9. package/cli-src/lib/schema/sqlite_schema.ts +0 -4
  10. package/cli-src/lib/scope_hierarchy_config.server.ts +1 -9
  11. package/cli-src/lib/services/invitation_service.ts +1 -1
  12. package/cli-src/lib/services/scope_service.ts +2 -76
  13. package/cli-src/lib/services/user_scope_service.ts +7 -61
  14. package/dist/cli/init_users.d.ts.map +1 -1
  15. package/dist/cli/init_users.js +42 -42
  16. package/dist/client.d.ts +1 -1
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +1 -1
  19. package/dist/components/layouts/shared/components/profile_pic_menu.d.ts.map +1 -1
  20. package/dist/components/layouts/shared/components/profile_pic_menu.js +7 -1
  21. package/dist/components/ui/button.d.ts +1 -1
  22. package/dist/components/ui/input-otp.d.ts +2 -2
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/lib/auth/auth_types.d.ts +0 -2
  27. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  28. package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
  29. package/dist/lib/auth/hazo_get_auth.server.js +27 -19
  30. package/dist/lib/auth/hazo_get_tenant_auth.server.d.ts.map +1 -1
  31. package/dist/lib/auth/hazo_get_tenant_auth.server.js +10 -10
  32. package/dist/lib/constants.d.ts +1 -0
  33. package/dist/lib/constants.d.ts.map +1 -1
  34. package/dist/lib/constants.js +1 -0
  35. package/dist/lib/profile_pic_menu_config.server.d.ts +2 -1
  36. package/dist/lib/profile_pic_menu_config.server.d.ts.map +1 -1
  37. package/dist/lib/profile_pic_menu_config.server.js +1 -1
  38. package/dist/lib/schema/sqlite_schema.d.ts +1 -1
  39. package/dist/lib/schema/sqlite_schema.d.ts.map +1 -1
  40. package/dist/lib/schema/sqlite_schema.js +0 -4
  41. package/dist/lib/scope_hierarchy_config.server.d.ts +0 -2
  42. package/dist/lib/scope_hierarchy_config.server.d.ts.map +1 -1
  43. package/dist/lib/scope_hierarchy_config.server.js +1 -3
  44. package/dist/lib/services/invitation_service.d.ts +1 -1
  45. package/dist/lib/services/invitation_service.js +1 -1
  46. package/dist/lib/services/scope_service.d.ts +1 -14
  47. package/dist/lib/services/scope_service.d.ts.map +1 -1
  48. package/dist/lib/services/scope_service.js +2 -67
  49. package/dist/lib/services/user_scope_service.d.ts +5 -12
  50. package/dist/lib/services/user_scope_service.d.ts.map +1 -1
  51. package/dist/lib/services/user_scope_service.js +8 -45
  52. package/dist/server/routes/invitations.d.ts +1 -1
  53. package/dist/server/routes/invitations.d.ts.map +1 -1
  54. package/dist/server/routes/invitations.js +12 -11
  55. package/package.json +12 -12
package/README.md CHANGED
@@ -742,9 +742,7 @@ CREATE INDEX idx_hazo_scopes_level ON hazo_scopes(level);
742
742
  CREATE INDEX idx_hazo_scopes_slug ON hazo_scopes(slug);
743
743
 
744
744
  -- 7a. Reserved system scopes
745
- INSERT INTO hazo_scopes (id, parent_id, name, level)
746
- VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system')
747
- ON CONFLICT (id) DO NOTHING;
745
+ -- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
748
746
  INSERT INTO hazo_scopes (id, parent_id, name, level)
749
747
  VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default')
750
748
  ON CONFLICT (id) DO NOTHING;
@@ -900,8 +898,7 @@ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
900
898
  CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
901
899
 
902
900
  -- Reserved system scopes
903
- INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
904
- VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', datetime('now'), datetime('now'));
901
+ -- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
905
902
  INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
906
903
  VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', datetime('now'), datetime('now'));
907
904
 
@@ -1743,7 +1740,6 @@ type TenantOrganization = {
1743
1740
  slug: string | null; // URL-friendly identifier
1744
1741
  level: string; // "Company", "Division", etc.
1745
1742
  role_id: string; // User's role in this scope
1746
- is_super_admin: boolean;
1747
1743
  branding?: {
1748
1744
  logo_url: string | null;
1749
1745
  primary_color: string | null;
@@ -2500,6 +2496,28 @@ import { ProfilePicMenu } from "hazo_auth/components/layouts/shared";
2500
2496
  />
2501
2497
  ```
2502
2498
 
2499
+ #### Menu item types
2500
+
2501
+ `custom_menu_items` accepts four `type` values:
2502
+
2503
+ | Type | Renders as | Fields used |
2504
+ |------|------------|-------------|
2505
+ | `info` | Read-only label/value row | `label`, `value` |
2506
+ | `link` | Navigation link | `label`, `href` |
2507
+ | `separator` | Divider | — |
2508
+ | `action` | Clickable item that fires a client-side callback | `label`, `onSelect` |
2509
+
2510
+ ```typescript
2511
+ <ProfilePicMenu
2512
+ variant="dropdown"
2513
+ custom_menu_items={[
2514
+ { type: "action", label: "Switch workspace", onSelect: () => openSwitcher(), order: 1, id: "switch" }
2515
+ ]}
2516
+ />
2517
+ ```
2518
+
2519
+ > **`action` items are React-only.** Because `onSelect` is a function, action items cannot be expressed via the INI `custom_menu_items` config (which only supports the serialisable `info` / `link` / `separator` types) — pass them through the `custom_menu_items` prop instead.
2520
+
2503
2521
  ### Configuration
2504
2522
 
2505
2523
  ```ini
@@ -544,9 +544,7 @@ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
544
544
  CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
545
545
 
546
546
  -- Reserved system scopes
547
- INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
548
- VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', datetime('now'), datetime('now'));
549
-
547
+ -- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
550
548
  INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
551
549
  VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', datetime('now'), datetime('now'));
552
550
 
@@ -733,10 +731,7 @@ CREATE INDEX idx_hazo_scopes_level ON hazo_scopes(level);
733
731
  CREATE INDEX idx_hazo_scopes_slug ON hazo_scopes(slug);
734
732
 
735
733
  -- 7a. Reserved system scopes
736
- INSERT INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
737
- VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', NOW(), NOW())
738
- ON CONFLICT (id) DO NOTHING;
739
-
734
+ -- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
740
735
  INSERT INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
741
736
  VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', NOW(), NOW())
742
737
  ON CONFLICT (id) DO NOTHING;
@@ -897,8 +892,8 @@ GRANT USAGE ON TYPE hazo_enum_invitation_status TO anon, authenticated;
897
892
  - [ ] `hazo_invitations`
898
893
  - [ ] `hazo_user_relationships` (managed sub-profile parent/child links)
899
894
  - [ ] Reserved system scopes inserted:
900
- - [ ] `00000000-0000-0000-0000-000000000000` (Super Admin)
901
895
  - [ ] `00000000-0000-0000-0000-000000000001` (System / non-multi-tenancy default)
896
+ - [ ] Migration 020 run: Super Admin scope retired, `hazo_org_global_admin` permission granted to affected roles
902
897
  - [ ] `firm_admin` role inserted into `hazo_roles`
903
898
 
904
899
  ---
@@ -1768,10 +1763,7 @@ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
1768
1763
  CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
1769
1764
 
1770
1765
  -- 3. Create system scopes
1771
- INSERT INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
1772
- VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', NOW(), NOW())
1773
- ON CONFLICT (id) DO NOTHING;
1774
-
1766
+ -- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
1775
1767
  INSERT INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
1776
1768
  VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', NOW(), NOW())
1777
1769
  ON CONFLICT (id) DO NOTHING;
@@ -1861,9 +1853,7 @@ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
1861
1853
  CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
1862
1854
 
1863
1855
  -- 3. Create system scopes
1864
- INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
1865
- VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', datetime('now'), datetime('now'));
1866
-
1856
+ -- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
1867
1857
  INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
1868
1858
  VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', datetime('now'), datetime('now'));
1869
1859
 
@@ -1959,8 +1949,8 @@ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
1959
1949
  - [ ] `hazo_user_scopes` (user-scope-role assignments)
1960
1950
  - [ ] `hazo_invitations` (user invitation flow)
1961
1951
  - [ ] System scopes exist:
1962
- - [ ] Super Admin scope (00000000-0000-0000-0000-000000000000)
1963
1952
  - [ ] Default System scope (00000000-0000-0000-0000-000000000001)
1953
+ - [ ] Migration 020 run: Super Admin scope retired, `hazo_org_global_admin` permission granted
1964
1954
  - [ ] Grants applied (PostgreSQL)
1965
1955
  - [ ] HRBAC tabs visible in User Management
1966
1956
  - [ ] Scope test page works
@@ -5,7 +5,8 @@ import { createCrudService } from "hazo_connect/server";
5
5
  import { get_user_management_config } from "../lib/user_management_config.server.js";
6
6
  import { get_config_value } from "../lib/config/config_loader.server.js";
7
7
  import { create_app_logger } from "../lib/app_logger.js";
8
- import { SUPER_ADMIN_SCOPE_ID } from "../lib/services/scope_service.js";
8
+ import { DEFAULT_SYSTEM_SCOPE_ID } from "../lib/services/scope_service.js";
9
+ import { GLOBAL_ADMIN_PERMISSION } from "../lib/constants.js";
9
10
 
10
11
  // section: types
11
12
  type InitSummary = {
@@ -23,10 +24,6 @@ type InitSummary = {
23
24
  existing: number;
24
25
  };
25
26
  // v5.x: Removed user_role - roles are now assigned via hazo_user_scopes
26
- super_admin_scope: {
27
- inserted: boolean;
28
- existing: boolean;
29
- };
30
27
  user_scope: {
31
28
  inserted: boolean;
32
29
  existing: boolean;
@@ -77,23 +74,13 @@ function print_summary(summary: InitSummary): void {
77
74
 
78
75
  // v5.x: User-Role assignments are now handled via User-Scope assignments (see below)
79
76
 
80
- // Super admin scope summary
81
- console.log("Super Admin Scope:");
82
- if (summary.super_admin_scope.inserted) {
83
- console.log(` ✓ Inserted: Super Admin scope (ID: ${SUPER_ADMIN_SCOPE_ID})`);
84
- }
85
- if (summary.super_admin_scope.existing) {
86
- console.log(` ⊙ Already existed: Super Admin scope (ID: ${SUPER_ADMIN_SCOPE_ID})`);
87
- }
88
- console.log();
89
-
90
77
  // User scope summary
91
78
  console.log("User-Scope Assignment:");
92
79
  if (summary.user_scope.inserted) {
93
- console.log(` ✓ Inserted: User assigned to Super Admin scope`);
80
+ console.log(` ✓ Inserted: User assigned to default system scope`);
94
81
  }
95
82
  if (summary.user_scope.existing) {
96
- console.log(` ⊙ Already existed: User already in Super Admin scope`);
83
+ console.log(` ⊙ Already existed: User already in default system scope`);
97
84
  }
98
85
  console.log();
99
86
 
@@ -131,10 +118,6 @@ export async function handle_init_users(options: InitUsersOptions = {}): Promise
131
118
  existing: 0,
132
119
  },
133
120
  // v5.x: Removed user_role - roles are now assigned via hazo_user_scopes
134
- super_admin_scope: {
135
- inserted: false,
136
- existing: false,
137
- },
138
121
  user_scope: {
139
122
  inserted: false,
140
123
  existing: false,
@@ -155,7 +138,6 @@ export async function handle_init_users(options: InitUsersOptions = {}): Promise
155
138
  });
156
139
  const users_service = createCrudService(hazoConnect, "hazo_users");
157
140
  // v5.x: Removed hazo_user_roles - roles are now assigned via hazo_user_scopes
158
- const scopes_service = createCrudService(hazoConnect, "hazo_scopes");
159
141
  // hazo_user_scopes uses composite primary key (user_id, scope_id), no 'id' column
160
142
  const user_scopes_service = createCrudService(hazoConnect, "hazo_user_scopes", {
161
143
  primaryKeys: ["user_id", "scope_id"],
@@ -317,54 +299,63 @@ export async function handle_init_users(options: InitUsersOptions = {}): Promise
317
299
  console.log(`✓ Found user: ${super_user_email} (ID: ${user_id})`);
318
300
  console.log();
319
301
 
320
- // v5.x: Step 7 removed - role assignment now happens via hazo_user_scopes (see step 9)
321
-
322
- // 8. Ensure super admin scope exists
323
- const existing_scopes = await scopes_service.findBy({ id: SUPER_ADMIN_SCOPE_ID });
324
-
325
- if (Array.isArray(existing_scopes) && existing_scopes.length > 0) {
326
- summary.super_admin_scope.existing = true;
327
- console.log(`✓ Super Admin scope already exists (ID: ${SUPER_ADMIN_SCOPE_ID})`);
328
- } else {
329
- await scopes_service.insert({
330
- id: SUPER_ADMIN_SCOPE_ID,
331
- parent_id: null,
332
- name: "Super Admin",
333
- level: "system",
302
+ // 7. Ensure hazo_org_global_admin is in the permission catalog
303
+ const global_admin_perms = await permissions_service.findBy({
304
+ permission_name: GLOBAL_ADMIN_PERMISSION,
305
+ });
306
+ if (!Array.isArray(global_admin_perms) || global_admin_perms.length === 0) {
307
+ await permissions_service.insert({
308
+ permission_name: GLOBAL_ADMIN_PERMISSION,
309
+ description: "Global admin access to all scopes and operations",
334
310
  created_at: now,
335
311
  changed_at: now,
336
312
  });
337
- summary.super_admin_scope.inserted = true;
338
- console.log(`✓ Created Super Admin scope (ID: ${SUPER_ADMIN_SCOPE_ID})`);
313
+ console.log(`✓ Created permission: ${GLOBAL_ADMIN_PERMISSION}`);
314
+ } else {
315
+ console.log(`✓ Permission already exists: ${GLOBAL_ADMIN_PERMISSION}`);
339
316
  }
317
+ console.log();
340
318
 
319
+ // 9. Ensure hazo_org_global_admin is assigned to the super user role
320
+ // (The role already has all configured permissions; this ensures the global admin perm is included)
321
+ const perm_row = await permissions_service.findBy({ permission_name: GLOBAL_ADMIN_PERMISSION });
322
+ const perm_id = Array.isArray(perm_row) && perm_row.length > 0 ? (perm_row[0].id as string) : null;
323
+ if (perm_id && role_id) {
324
+ const existing_rp = await role_permissions_service.findBy({ role_id, permission_id: perm_id });
325
+ if (!Array.isArray(existing_rp) || existing_rp.length === 0) {
326
+ await role_permissions_service.insert({ role_id, permission_id: perm_id });
327
+ console.log(`✓ Assigned ${GLOBAL_ADMIN_PERMISSION} to super user role`);
328
+ } else {
329
+ console.log(`✓ Super user role already has ${GLOBAL_ADMIN_PERMISSION}`);
330
+ }
331
+ }
341
332
  console.log();
342
333
 
343
- // 9. Assign user to super admin scope
334
+ // 10. Assign user to DEFAULT_SYSTEM_SCOPE_ID (global access comes from the permission, not the scope)
344
335
  const existing_user_scopes = await user_scopes_service.findBy({
345
336
  user_id,
346
- scope_id: SUPER_ADMIN_SCOPE_ID,
337
+ scope_id: DEFAULT_SYSTEM_SCOPE_ID,
347
338
  });
348
339
 
349
340
  if (Array.isArray(existing_user_scopes) && existing_user_scopes.length > 0) {
350
341
  summary.user_scope.existing = true;
351
- console.log(`✓ User already assigned to Super Admin scope`);
342
+ console.log(`✓ User already assigned to default system scope`);
352
343
  } else {
353
344
  await user_scopes_service.insert({
354
345
  user_id,
355
- scope_id: SUPER_ADMIN_SCOPE_ID,
356
- root_scope_id: SUPER_ADMIN_SCOPE_ID,
346
+ scope_id: DEFAULT_SYSTEM_SCOPE_ID,
347
+ root_scope_id: DEFAULT_SYSTEM_SCOPE_ID,
357
348
  role_id,
358
349
  created_at: now,
359
350
  changed_at: now,
360
351
  });
361
352
  summary.user_scope.inserted = true;
362
- console.log(`✓ Assigned user to Super Admin scope`);
353
+ console.log(`✓ Assigned user to default system scope`);
363
354
  }
364
355
 
365
356
  console.log();
366
357
 
367
- // 10. Print summary
358
+ // 11. Print summary
368
359
  print_summary(summary);
369
360
 
370
361
  logger.info("init_users_completed", {
@@ -402,15 +393,16 @@ export function show_init_users_help(): void {
402
393
  console.log(`
403
394
  hazo_auth init-users
404
395
 
405
- Initialize users, roles, permissions, and super admin scope from configuration.
396
+ Initialize users, roles, and permissions from configuration.
406
397
 
407
398
  This command reads from hazo_auth_config.ini and:
408
399
  1. Creates permissions from [hazo_auth__user_management] application_permission_list_defaults
409
400
  2. Creates a 'default_super_user_role' role
410
401
  3. Assigns all permissions to the super user role
411
402
  4. Finds user by email (from --email parameter or config)
412
- 5. Creates the Super Admin scope (${SUPER_ADMIN_SCOPE_ID})
413
- 6. Assigns the user to the Super Admin scope with the super user role
403
+ 5. Ensures the '${GLOBAL_ADMIN_PERMISSION}' permission exists and is assigned to the super user role
404
+ 6. Assigns the user to the default system scope (${DEFAULT_SYSTEM_SCOPE_ID})
405
+ Global admin access is granted via the '${GLOBAL_ADMIN_PERMISSION}' permission, not by scope
414
406
  (v5.x: Roles are assigned per-scope via hazo_user_scopes table)
415
407
 
416
408
  Options:
@@ -24,7 +24,6 @@ export type HazoAuthUser = {
24
24
  export type ScopeAccessInfo = {
25
25
  scope_id: string;
26
26
  scope_name?: string;
27
- is_super_admin?: boolean;
28
27
  };
29
28
 
30
29
  /**
@@ -135,7 +134,6 @@ export type TenantOrganization = {
135
134
  slug: string | null;
136
135
  level: string;
137
136
  role_id: string;
138
- is_super_admin: boolean;
139
137
  branding?: {
140
138
  logo_url: string | null;
141
139
  primary_color: string | null;
@@ -30,6 +30,7 @@ import {
30
30
  } from "../services/user_scope_service.js";
31
31
  import { get_cookie_name, BASE_COOKIE_NAMES } from "../cookies_config.server.js";
32
32
  import { get_app_permission_descriptions } from "../app_permissions_config.server.js";
33
+ import { GLOBAL_ADMIN_PERMISSION } from "../constants.js";
33
34
 
34
35
  // section: helpers
35
36
 
@@ -383,7 +384,6 @@ async function check_scope_access_internal(
383
384
  scope_access_via: {
384
385
  scope_id: result.access_via.scope_id,
385
386
  scope_name: result.access_via.scope_name,
386
- is_super_admin: result.is_super_admin,
387
387
  },
388
388
  user_scopes,
389
389
  };
@@ -587,31 +587,37 @@ export async function hazo_get_auth(
587
587
  const hrbac_enabled = is_hrbac_enabled();
588
588
 
589
589
  if (hrbac_enabled && options?.scope_id) {
590
- const scope_result = await check_scope_access_internal(
591
- user.id,
592
- options.scope_id,
593
- );
594
-
595
- scope_ok = scope_result.scope_ok;
596
- scope_access_via = scope_result.scope_access_via;
597
-
598
- // Log scope denial if permission logging is enabled
599
- if (!scope_ok && config.log_permission_denials) {
600
- const client_ip = get_client_ip(request);
601
- logger.warn("auth_utility_scope_access_denied", {
602
- filename: get_filename(),
603
- line_number: get_line_number(),
604
- user_id: user.id,
605
- scope_id: options.scope_id,
606
- user_scopes: scope_result.user_scopes,
607
- ip: client_ip,
608
- correlation_id: getCorrelationId(),
609
- });
610
- }
590
+ // Global admin permission grants access to all scopes
591
+ const has_global_admin = permissions.includes(GLOBAL_ADMIN_PERMISSION);
592
+ if (has_global_admin) {
593
+ scope_ok = true;
594
+ scope_access_via = { scope_id: options.scope_id };
595
+ } else {
596
+ const scope_result = await check_scope_access_internal(
597
+ user.id,
598
+ options.scope_id,
599
+ );
600
+ scope_ok = scope_result.scope_ok;
601
+ scope_access_via = scope_result.scope_access_via;
602
+
603
+ // Log scope denial if permission logging is enabled
604
+ if (!scope_ok && config.log_permission_denials) {
605
+ const client_ip = get_client_ip(request);
606
+ logger.warn("auth_utility_scope_access_denied", {
607
+ filename: get_filename(),
608
+ line_number: get_line_number(),
609
+ user_id: user.id,
610
+ scope_id: options.scope_id,
611
+ user_scopes: scope_result.user_scopes,
612
+ ip: client_ip,
613
+ correlation_id: getCorrelationId(),
614
+ });
615
+ }
611
616
 
612
- // Throw error if strict mode and scope access denied
613
- if (!scope_ok && options.strict) {
614
- throw new ScopeAccessError(options.scope_id, scope_result.user_scopes);
617
+ // Throw error if strict mode and scope access denied
618
+ if (!scope_ok && options.strict) {
619
+ throw new ScopeAccessError(options.scope_id, scope_result.user_scopes);
620
+ }
615
621
  }
616
622
  }
617
623
 
@@ -7,6 +7,7 @@ import { NextRequest } from "next/server";
7
7
  import { hazo_get_auth } from "./hazo_get_auth.server.js";
8
8
  import { get_auth_cache } from "./auth_cache.js";
9
9
  import { get_scope_by_id } from "../services/scope_service.js";
10
+ import { GLOBAL_ADMIN_PERMISSION } from "../constants.js";
10
11
  import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
11
12
  import { get_cookie_name } from "../cookies_config.server.js";
12
13
  import { get_auth_utility_config } from "../auth_utility_config.server.js";
@@ -62,14 +63,12 @@ export function extract_scope_id_from_request(
62
63
  }
63
64
 
64
65
  /**
65
- * Builds TenantOrganization from scope details and access info
66
+ * Builds TenantOrganization from scope details
66
67
  * @param scope_details - Full scope details from cache
67
- * @param is_super_admin - Whether user is accessing as super admin
68
68
  * @returns TenantOrganization object
69
69
  */
70
70
  function build_tenant_organization(
71
71
  scope_details: ScopeDetails,
72
- is_super_admin: boolean,
73
72
  ): TenantOrganization {
74
73
  return {
75
74
  id: scope_details.id,
@@ -77,7 +76,6 @@ function build_tenant_organization(
77
76
  slug: scope_details.slug,
78
77
  level: scope_details.level,
79
78
  role_id: scope_details.role_id,
80
- is_super_admin,
81
79
  branding:
82
80
  scope_details.logo_url || scope_details.primary_color
83
81
  ? {
@@ -159,18 +157,17 @@ export async function hazo_get_tenant_auth(
159
157
  let organization: TenantOrganization | null = null;
160
158
 
161
159
  if (scope_id && auth_result.scope_ok && auth_result.scope_access_via) {
162
- // Find the scope in user's scopes that matches the access_via scope
160
+ // Try to find the scope in user's cached scope assignments first.
161
+ // For global admins the scope may not be in their cache (they can access any scope),
162
+ // in which case we fall through to the permission-based fetch below.
163
163
  const access_scope = user_scopes.find(
164
164
  (s) => s.id === auth_result.scope_access_via?.scope_id,
165
165
  );
166
166
 
167
167
  if (access_scope) {
168
- organization = build_tenant_organization(
169
- access_scope,
170
- auth_result.scope_access_via.is_super_admin || false,
171
- );
172
- } else if (auth_result.scope_access_via.is_super_admin) {
173
- // Super admin accessing scope they're not assigned to - fetch scope details
168
+ organization = build_tenant_organization(access_scope);
169
+ } else if (auth_result.permissions.includes(GLOBAL_ADMIN_PERMISSION)) {
170
+ // Global admin accessing a scope they aren't directly assigned to — fetch scope details
174
171
  const hazoConnect = get_hazo_connect_instance();
175
172
  const scope_result = await get_scope_by_id(hazoConnect, scope_id);
176
173
  if (scope_result.success && scope_result.scope) {
@@ -179,8 +176,7 @@ export async function hazo_get_tenant_auth(
179
176
  name: scope_result.scope.name,
180
177
  slug: null, // Could fetch from scope if slug column exists
181
178
  level: scope_result.scope.level,
182
- role_id: "", // Super admin doesn't have a role in the scope
183
- is_super_admin: true,
179
+ role_id: "", // Global admin doesn't have a role assignment in the scope
184
180
  branding: scope_result.scope.logo_url
185
181
  ? {
186
182
  logo_url: scope_result.scope.logo_url,
@@ -13,3 +13,5 @@ export const HAZO_AUTH_PERMISSIONS = {
13
13
  } as const;
14
14
 
15
15
  export const ALL_ADMIN_PERMISSIONS: string[] = Object.values(HAZO_AUTH_PERMISSIONS);
16
+
17
+ export const GLOBAL_ADMIN_PERMISSION = "hazo_org_global_admin";
@@ -8,13 +8,14 @@ import { get_config_value, get_config_boolean, get_config_array } from "./config
8
8
  // section: types
9
9
  // Note: These types are also used in client components, but TypeScript types are erased at runtime
10
10
  // so importing from a server file is safe for type-only imports
11
- export type MenuItemType = "info" | "link" | "separator";
11
+ export type MenuItemType = "info" | "link" | "separator" | "action";
12
12
 
13
13
  export type ProfilePicMenuMenuItem = {
14
14
  type: MenuItemType;
15
- label?: string; // For info and link types
15
+ label?: string; // For info, link, and action types
16
16
  value?: string; // For info type (e.g., user name, email)
17
17
  href?: string; // For link type
18
+ onSelect?: () => void; // For action type (client-side callback, not serialisable via INI config)
18
19
  order: number; // Ordering within type group
19
20
  id: string; // Unique identifier for the item
20
21
  };
@@ -50,7 +51,7 @@ function parse_custom_menu_items(items_string: string[]): ProfilePicMenuMenuItem
50
51
 
51
52
  const type = parts[0] as MenuItemType;
52
53
  if (type !== "info" && type !== "link" && type !== "separator") {
53
- return; // Invalid type, skip
54
+ return; // Invalid type or action (action items carry callbacks, not expressible in INI)
54
55
  }
55
56
 
56
57
  if (type === "separator") {
@@ -97,10 +97,6 @@ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_parent ON hazo_scopes(parent_id);
97
97
  CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
98
98
  CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
99
99
 
100
- -- Super admin scope
101
- INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
102
- VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', datetime('now'), datetime('now'));
103
-
104
100
  -- Default system scope (for non-multi-tenancy mode)
105
101
  INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
106
102
  VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', datetime('now'), datetime('now'));
@@ -8,7 +8,7 @@ import {
8
8
  get_config_number,
9
9
  get_config_boolean,
10
10
  } from "./config/config_loader.server.js";
11
- import { SUPER_ADMIN_SCOPE_ID, DEFAULT_SYSTEM_SCOPE_ID } from "./services/scope_service.js";
11
+ import { DEFAULT_SYSTEM_SCOPE_ID } from "./services/scope_service.js";
12
12
 
13
13
  // section: types
14
14
 
@@ -23,8 +23,6 @@ export type ScopeHierarchyConfig = {
23
23
  scope_cache_ttl_minutes: number;
24
24
  /** Maximum entries in scope cache (default: 5000) */
25
25
  scope_cache_max_entries: number;
26
- /** Super admin scope ID */
27
- super_admin_scope_id: string;
28
26
  /** Default system scope ID (for non-multi-tenancy mode) */
29
27
  default_system_scope_id: string;
30
28
  };
@@ -57,11 +55,6 @@ export function get_scope_hierarchy_config(): ScopeHierarchyConfig {
57
55
  );
58
56
 
59
57
  // Scope IDs (with defaults)
60
- const super_admin_scope_id = get_config_value(
61
- SECTION_NAME,
62
- "super_admin_scope_id",
63
- SUPER_ADMIN_SCOPE_ID,
64
- );
65
58
  const default_system_scope_id = get_config_value(
66
59
  SECTION_NAME,
67
60
  "default_system_scope_id",
@@ -72,7 +65,6 @@ export function get_scope_hierarchy_config(): ScopeHierarchyConfig {
72
65
  enable_hrbac,
73
66
  scope_cache_ttl_minutes,
74
67
  scope_cache_max_entries,
75
- super_admin_scope_id,
76
68
  default_system_scope_id,
77
69
  };
78
70
  }
@@ -478,7 +478,7 @@ export async function list_invitations_by_scope(
478
478
  }
479
479
 
480
480
  /**
481
- * Lists all invitations (for super admin)
481
+ * Lists all invitations (for global admins)
482
482
  */
483
483
  export async function list_all_invitations(
484
484
  adapter: HazoConnectAdapter,
@@ -7,12 +7,6 @@ import { sanitize_error_for_user } from "../utils/error_sanitizer.js";
7
7
 
8
8
  // section: constants
9
9
 
10
- /**
11
- * Super admin scope ID - special UUID for system-level administrators
12
- * Users assigned to this scope have global access
13
- */
14
- export const SUPER_ADMIN_SCOPE_ID = "00000000-0000-0000-0000-000000000000";
15
-
16
10
  /**
17
11
  * Default system scope ID - for non-multi-tenancy mode
18
12
  * All users are assigned to this scope when multi-tenancy is disabled
@@ -119,13 +113,6 @@ export function has_branding(scope: ScopeRecord): boolean {
119
113
  return !!(scope.logo_url || scope.primary_color || scope.secondary_color || scope.tagline);
120
114
  }
121
115
 
122
- /**
123
- * Checks if the given scope_id is the super admin scope
124
- */
125
- export function is_super_admin_scope(scope_id: string): boolean {
126
- return scope_id === SUPER_ADMIN_SCOPE_ID;
127
- }
128
-
129
116
  /**
130
117
  * Checks if the given scope_id is the default system scope
131
118
  */
@@ -134,10 +121,10 @@ export function is_default_system_scope(scope_id: string): boolean {
134
121
  }
135
122
 
136
123
  /**
137
- * Checks if the given scope_id is a system scope (super admin or default system)
124
+ * Checks if the given scope_id is a system scope (default system)
138
125
  */
139
126
  export function is_system_scope(scope_id: string): boolean {
140
- return is_super_admin_scope(scope_id) || is_default_system_scope(scope_id);
127
+ return is_default_system_scope(scope_id);
141
128
  }
142
129
 
143
130
  // section: crud operations
@@ -781,67 +768,6 @@ export async function get_scope_tree(
781
768
  }
782
769
  }
783
770
 
784
- /**
785
- * Ensures the super admin scope exists
786
- */
787
- export async function ensure_super_admin_scope(
788
- adapter: HazoConnectAdapter,
789
- ): Promise<ScopeServiceResult> {
790
- try {
791
- // Check if already exists
792
- const existing = await get_scope_by_id(adapter, SUPER_ADMIN_SCOPE_ID);
793
- if (existing.success && existing.scope) {
794
- return existing;
795
- }
796
-
797
- // Create it
798
- const scope_service = createCrudService(adapter, "hazo_scopes");
799
- const now = new Date().toISOString();
800
-
801
- const inserted = await scope_service.insert({
802
- id: SUPER_ADMIN_SCOPE_ID,
803
- name: "Super Admin",
804
- level: "system",
805
- parent_id: null,
806
- logo_url: null,
807
- primary_color: null,
808
- secondary_color: null,
809
- tagline: null,
810
- created_at: now,
811
- changed_at: now,
812
- });
813
-
814
- if (!Array.isArray(inserted) || inserted.length === 0) {
815
- return {
816
- success: false,
817
- error: "Failed to create super admin scope",
818
- };
819
- }
820
-
821
- return {
822
- success: true,
823
- scope: normalize_scope_record(inserted[0] as Record<string, unknown>),
824
- };
825
- } catch (error) {
826
- const logger = create_app_logger();
827
- const error_message = sanitize_error_for_user(error, {
828
- logToConsole: true,
829
- logToLogger: true,
830
- logger,
831
- context: {
832
- filename: "scope_service.ts",
833
- line_number: 0,
834
- operation: "ensure_super_admin_scope",
835
- },
836
- });
837
-
838
- return {
839
- success: false,
840
- error: error_message,
841
- };
842
- }
843
- }
844
-
845
771
  /**
846
772
  * Ensures the default system scope exists
847
773
  */