hazo_auth 9.1.1 → 10.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.
- package/README.md +124 -6
- package/SETUP_CHECKLIST.md +24 -16
- package/cli-src/cli/init_users.ts +40 -48
- package/cli-src/lib/auth/auth_types.ts +0 -2
- package/cli-src/lib/auth/hazo_get_auth.server.ts +31 -25
- package/cli-src/lib/auth/hazo_get_tenant_auth.server.ts +9 -13
- package/cli-src/lib/auth/nextauth_config.ts +41 -0
- package/cli-src/lib/auth/request_google_scopes.ts +23 -0
- package/cli-src/lib/constants.ts +2 -0
- package/cli-src/lib/profile_pic_menu_config.server.ts +4 -3
- package/cli-src/lib/schema/sqlite_schema.ts +16 -4
- package/cli-src/lib/scope_hierarchy_config.server.ts +1 -9
- package/cli-src/lib/services/google_token_service.ts +408 -0
- package/cli-src/lib/services/index.ts +1 -1
- package/cli-src/lib/services/invitation_service.ts +1 -1
- package/cli-src/lib/services/scope_service.ts +2 -76
- package/cli-src/lib/services/user_scope_service.ts +7 -61
- package/dist/cli/init_users.d.ts.map +1 -1
- package/dist/cli/init_users.js +42 -42
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -1
- package/dist/components/layouts/google_token_test/index.d.ts +6 -0
- package/dist/components/layouts/google_token_test/index.d.ts.map +1 -0
- package/dist/components/layouts/google_token_test/index.js +74 -0
- package/dist/components/layouts/shared/components/profile_pic_menu.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/profile_pic_menu.js +7 -1
- package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/lib/auth/auth_types.d.ts +0 -2
- package/dist/lib/auth/auth_types.d.ts.map +1 -1
- package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
- package/dist/lib/auth/hazo_get_auth.server.js +27 -19
- package/dist/lib/auth/hazo_get_tenant_auth.server.d.ts.map +1 -1
- package/dist/lib/auth/hazo_get_tenant_auth.server.js +10 -10
- package/dist/lib/auth/nextauth_config.d.ts +2 -0
- package/dist/lib/auth/nextauth_config.d.ts.map +1 -1
- package/dist/lib/auth/nextauth_config.js +39 -1
- package/dist/lib/auth/request_google_scopes.d.ts +10 -0
- package/dist/lib/auth/request_google_scopes.d.ts.map +1 -0
- package/dist/lib/auth/request_google_scopes.js +13 -0
- package/dist/lib/constants.d.ts +1 -0
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +1 -0
- package/dist/lib/profile_pic_menu_config.server.d.ts +2 -1
- package/dist/lib/profile_pic_menu_config.server.d.ts.map +1 -1
- package/dist/lib/profile_pic_menu_config.server.js +1 -1
- package/dist/lib/schema/sqlite_schema.d.ts +1 -1
- package/dist/lib/schema/sqlite_schema.d.ts.map +1 -1
- package/dist/lib/schema/sqlite_schema.js +16 -4
- package/dist/lib/scope_hierarchy_config.server.d.ts +0 -2
- package/dist/lib/scope_hierarchy_config.server.d.ts.map +1 -1
- package/dist/lib/scope_hierarchy_config.server.js +1 -3
- package/dist/lib/services/google_token_service.d.ts +48 -0
- package/dist/lib/services/google_token_service.d.ts.map +1 -0
- package/dist/lib/services/google_token_service.js +319 -0
- package/dist/lib/services/index.d.ts +1 -0
- package/dist/lib/services/index.d.ts.map +1 -1
- package/dist/lib/services/index.js +1 -0
- package/dist/lib/services/invitation_service.d.ts +1 -1
- package/dist/lib/services/invitation_service.js +1 -1
- package/dist/lib/services/scope_service.d.ts +1 -14
- package/dist/lib/services/scope_service.d.ts.map +1 -1
- package/dist/lib/services/scope_service.js +2 -67
- package/dist/lib/services/user_scope_service.d.ts +5 -12
- package/dist/lib/services/user_scope_service.d.ts.map +1 -1
- package/dist/lib/services/user_scope_service.js +8 -45
- package/dist/server/routes/google_token.d.ts +13 -0
- package/dist/server/routes/google_token.d.ts.map +1 -0
- package/dist/server/routes/google_token.js +66 -0
- package/dist/server/routes/index.d.ts +1 -0
- package/dist/server/routes/index.d.ts.map +1 -1
- package/dist/server/routes/index.js +2 -0
- package/dist/server/routes/invitations.d.ts +1 -1
- package/dist/server/routes/invitations.d.ts.map +1 -1
- package/dist/server/routes/invitations.js +12 -11
- package/dist/server/routes/user_management_users.d.ts +1 -1
- package/package.json +17 -13
package/README.md
CHANGED
|
@@ -2,6 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
A reusable authentication UI component package powered by Next.js, TailwindCSS, and shadcn. It integrates `hazo_config` for configuration management and `hazo_connect` for data access, enabling future components to stay aligned with platform conventions.
|
|
4
4
|
|
|
5
|
+
### What's New in v10.1.0 🚀
|
|
6
|
+
|
|
7
|
+
**Incremental Google OAuth Scopes & Server-Side Token Access** — Grant additional Google API scopes (Analytics, Sheets, Drive, etc.) without creating a separate OAuth app. Store encrypted tokens server-side and access them programmatically.
|
|
8
|
+
|
|
9
|
+
**New Features:**
|
|
10
|
+
- **`hazo_google_oauth_tokens` table** (migration 021) — Encrypted AES-256-GCM storage for Google OAuth tokens with scope tracking
|
|
11
|
+
- **Token Service Exports** from `hazo_auth/server-lib`:
|
|
12
|
+
- `store_google_oauth_token(user_id, tokens, scopes)` — Save OAuth tokens after user grants new scopes
|
|
13
|
+
- `getGoogleToken(user_id, opts?)` — Get a fresh access token (refreshes if expired)
|
|
14
|
+
- `revoke_google_oauth_token(user_id)` — Permanently revoke stored token and forget scopes
|
|
15
|
+
- `get_google_token_status(user_id)` — Check connection status, scopes, and expiry
|
|
16
|
+
- **Client Helper** — `requestGoogleScopes(scopes, opts?)` from `hazo_auth/client` triggers Google consent prompt for incremental scopes
|
|
17
|
+
- **HTTP Routes:**
|
|
18
|
+
- `GET /api/hazo_auth/google/token` — Returns `{ connected, scopes, expires_at }`
|
|
19
|
+
- `DELETE /api/hazo_auth/google/token` — Revoke stored token without signing user out
|
|
20
|
+
- **NextAuth Config Updates** — Captures `refresh_token` and adds `include_granted_scopes: true` parameter for incremental scope flow
|
|
21
|
+
- **Route Handler Exports** from `hazo_auth/server/routes`:
|
|
22
|
+
- `googleTokenGET` — implements GET status endpoint
|
|
23
|
+
- `googleTokenDELETE` — implements DELETE revoke endpoint
|
|
24
|
+
|
|
25
|
+
**Setup:**
|
|
26
|
+
```bash
|
|
27
|
+
# 1. Run the migration
|
|
28
|
+
npm run migrate -- migrations/021_hazo_google_oauth_tokens.sql
|
|
29
|
+
|
|
30
|
+
# 2. Set encryption environment variables
|
|
31
|
+
HAZO_AUTH_OAUTH_KEY_CURRENT=v1
|
|
32
|
+
HAZO_AUTH_OAUTH_KEY_V1=$(openssl rand -base64 32)
|
|
33
|
+
|
|
34
|
+
# 3. Install optional peer (for token encryption)
|
|
35
|
+
npm install hazo_secure
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Client Usage:**
|
|
39
|
+
```tsx
|
|
40
|
+
import { requestGoogleScopes } from "hazo_auth/client";
|
|
41
|
+
|
|
42
|
+
<button onClick={() => requestGoogleScopes([
|
|
43
|
+
"https://www.googleapis.com/auth/analytics.readonly"
|
|
44
|
+
])}>
|
|
45
|
+
Connect Google Analytics
|
|
46
|
+
</button>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Server Usage:**
|
|
50
|
+
```ts
|
|
51
|
+
import { getGoogleToken } from "hazo_auth/server-lib";
|
|
52
|
+
|
|
53
|
+
const result = await getGoogleToken(userId, {
|
|
54
|
+
scopes: ["https://www.googleapis.com/auth/analytics.readonly"]
|
|
55
|
+
});
|
|
56
|
+
if (result.ok) {
|
|
57
|
+
// use result.access_token to call Google APIs
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
See [Google API Access (Incremental Scopes)](#google-api-access-incremental-scopes) below for full details.
|
|
62
|
+
|
|
5
63
|
### What's New in v9.1.1 🔧
|
|
6
64
|
|
|
7
65
|
**Dev-server noise fixes for Next.js 16 + Turbopack**
|
|
@@ -742,9 +800,7 @@ CREATE INDEX idx_hazo_scopes_level ON hazo_scopes(level);
|
|
|
742
800
|
CREATE INDEX idx_hazo_scopes_slug ON hazo_scopes(slug);
|
|
743
801
|
|
|
744
802
|
-- 7a. Reserved system scopes
|
|
745
|
-
|
|
746
|
-
VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system')
|
|
747
|
-
ON CONFLICT (id) DO NOTHING;
|
|
803
|
+
-- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
|
|
748
804
|
INSERT INTO hazo_scopes (id, parent_id, name, level)
|
|
749
805
|
VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default')
|
|
750
806
|
ON CONFLICT (id) DO NOTHING;
|
|
@@ -900,8 +956,7 @@ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
|
|
|
900
956
|
CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
|
|
901
957
|
|
|
902
958
|
-- Reserved system scopes
|
|
903
|
-
|
|
904
|
-
VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', datetime('now'), datetime('now'));
|
|
959
|
+
-- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
|
|
905
960
|
INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
|
|
906
961
|
VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', datetime('now'), datetime('now'));
|
|
907
962
|
|
|
@@ -1254,6 +1309,48 @@ Google OAuth adds one new dependency:
|
|
|
1254
1309
|
- Check logs for `invitation_table_missing` warnings
|
|
1255
1310
|
- If using custom paths, set `create_firm_url` to your app's create firm page URL
|
|
1256
1311
|
|
|
1312
|
+
### Google API Access (Incremental Scopes)
|
|
1313
|
+
|
|
1314
|
+
`hazo_auth` v10.1+ supports granting additional Google API scopes (Analytics, Sheets, etc.) without a separate OAuth app.
|
|
1315
|
+
|
|
1316
|
+
**Setup:**
|
|
1317
|
+
1. Run the migration: `npm run migrate -- migrations/021_hazo_google_oauth_tokens.sql`
|
|
1318
|
+
2. Set encryption env vars:
|
|
1319
|
+
```bash
|
|
1320
|
+
HAZO_AUTH_OAUTH_KEY_CURRENT=v1
|
|
1321
|
+
HAZO_AUTH_OAUTH_KEY_V1=$(openssl rand -base64 32)
|
|
1322
|
+
```
|
|
1323
|
+
3. Install the optional peer: `npm install hazo_secure`
|
|
1324
|
+
|
|
1325
|
+
**Usage:**
|
|
1326
|
+
|
|
1327
|
+
```tsx
|
|
1328
|
+
// Client component — triggers Google consent for extra scopes
|
|
1329
|
+
import { requestGoogleScopes } from "hazo_auth/client";
|
|
1330
|
+
|
|
1331
|
+
<button onClick={() => requestGoogleScopes([
|
|
1332
|
+
"https://www.googleapis.com/auth/analytics.readonly"
|
|
1333
|
+
])}>
|
|
1334
|
+
Connect Analytics
|
|
1335
|
+
</button>
|
|
1336
|
+
```
|
|
1337
|
+
|
|
1338
|
+
```ts
|
|
1339
|
+
// Server action / API route — get a fresh access token
|
|
1340
|
+
import { getGoogleToken } from "hazo_auth/server-lib";
|
|
1341
|
+
|
|
1342
|
+
const result = await getGoogleToken(userId, {
|
|
1343
|
+
scopes: ["https://www.googleapis.com/auth/analytics.readonly"]
|
|
1344
|
+
});
|
|
1345
|
+
if (result.ok) {
|
|
1346
|
+
// use result.access_token to call Google APIs
|
|
1347
|
+
}
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
The incremental scope token is stored and revoked independently of the user's sign-in session. `DELETE /api/hazo_auth/google/token` removes the stored token without signing the user out.
|
|
1351
|
+
|
|
1352
|
+
**Status endpoint:** `GET /api/hazo_auth/google/token` returns `{ connected, scopes, expires_at }`.
|
|
1353
|
+
|
|
1257
1354
|
---
|
|
1258
1355
|
|
|
1259
1356
|
## Using Components
|
|
@@ -1743,7 +1840,6 @@ type TenantOrganization = {
|
|
|
1743
1840
|
slug: string | null; // URL-friendly identifier
|
|
1744
1841
|
level: string; // "Company", "Division", etc.
|
|
1745
1842
|
role_id: string; // User's role in this scope
|
|
1746
|
-
is_super_admin: boolean;
|
|
1747
1843
|
branding?: {
|
|
1748
1844
|
logo_url: string | null;
|
|
1749
1845
|
primary_color: string | null;
|
|
@@ -2500,6 +2596,28 @@ import { ProfilePicMenu } from "hazo_auth/components/layouts/shared";
|
|
|
2500
2596
|
/>
|
|
2501
2597
|
```
|
|
2502
2598
|
|
|
2599
|
+
#### Menu item types
|
|
2600
|
+
|
|
2601
|
+
`custom_menu_items` accepts four `type` values:
|
|
2602
|
+
|
|
2603
|
+
| Type | Renders as | Fields used |
|
|
2604
|
+
|------|------------|-------------|
|
|
2605
|
+
| `info` | Read-only label/value row | `label`, `value` |
|
|
2606
|
+
| `link` | Navigation link | `label`, `href` |
|
|
2607
|
+
| `separator` | Divider | — |
|
|
2608
|
+
| `action` | Clickable item that fires a client-side callback | `label`, `onSelect` |
|
|
2609
|
+
|
|
2610
|
+
```typescript
|
|
2611
|
+
<ProfilePicMenu
|
|
2612
|
+
variant="dropdown"
|
|
2613
|
+
custom_menu_items={[
|
|
2614
|
+
{ type: "action", label: "Switch workspace", onSelect: () => openSwitcher(), order: 1, id: "switch" }
|
|
2615
|
+
]}
|
|
2616
|
+
/>
|
|
2617
|
+
```
|
|
2618
|
+
|
|
2619
|
+
> **`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.
|
|
2620
|
+
|
|
2503
2621
|
### Configuration
|
|
2504
2622
|
|
|
2505
2623
|
```ini
|
package/SETUP_CHECKLIST.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
---
|
|
@@ -1138,6 +1133,24 @@ ls app/api/hazo_auth/set_password/route.ts
|
|
|
1138
1133
|
- [ ] OAuth API routes created (`[...nextauth]`, `oauth/google/callback`, `set_password`)
|
|
1139
1134
|
- [ ] Post-login redirect configured (if not using invitations, set `skip_invitation_check = true`)
|
|
1140
1135
|
|
|
1136
|
+
#### Google API Token Storage (optional — required for `getGoogleToken`)
|
|
1137
|
+
|
|
1138
|
+
Only needed if you use `requestGoogleScopes` / `getGoogleToken` for Google API access beyond sign-in:
|
|
1139
|
+
|
|
1140
|
+
1. Install optional peer: `npm install hazo_secure`
|
|
1141
|
+
2. Run migration: `npm run migrate -- migrations/021_hazo_google_oauth_tokens.sql`
|
|
1142
|
+
3. Set env vars:
|
|
1143
|
+
```env
|
|
1144
|
+
HAZO_AUTH_OAUTH_KEY_CURRENT=v1
|
|
1145
|
+
HAZO_AUTH_OAUTH_KEY_V1=<base64-32-bytes from: openssl rand -base64 32>
|
|
1146
|
+
```
|
|
1147
|
+
4. Wire the new routes in your Next.js app (same pattern as other hazo_auth routes):
|
|
1148
|
+
```ts
|
|
1149
|
+
// app/api/hazo_auth/google/token/route.ts
|
|
1150
|
+
export { GET as googleTokenGET, DELETE as googleTokenDELETE } from "hazo_auth/server/routes";
|
|
1151
|
+
export const dynamic = "force-dynamic";
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1141
1154
|
---
|
|
1142
1155
|
|
|
1143
1156
|
## Phase 4: API Routes
|
|
@@ -1768,10 +1781,7 @@ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
|
|
|
1768
1781
|
CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
|
|
1769
1782
|
|
|
1770
1783
|
-- 3. Create system scopes
|
|
1771
|
-
|
|
1772
|
-
VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', NOW(), NOW())
|
|
1773
|
-
ON CONFLICT (id) DO NOTHING;
|
|
1774
|
-
|
|
1784
|
+
-- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
|
|
1775
1785
|
INSERT INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
|
|
1776
1786
|
VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', NOW(), NOW())
|
|
1777
1787
|
ON CONFLICT (id) DO NOTHING;
|
|
@@ -1861,9 +1871,7 @@ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
|
|
|
1861
1871
|
CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
|
|
1862
1872
|
|
|
1863
1873
|
-- 3. Create system scopes
|
|
1864
|
-
|
|
1865
|
-
VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', datetime('now'), datetime('now'));
|
|
1866
|
-
|
|
1874
|
+
-- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
|
|
1867
1875
|
INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
|
|
1868
1876
|
VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', datetime('now'), datetime('now'));
|
|
1869
1877
|
|
|
@@ -1959,8 +1967,8 @@ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
|
|
|
1959
1967
|
- [ ] `hazo_user_scopes` (user-scope-role assignments)
|
|
1960
1968
|
- [ ] `hazo_invitations` (user invitation flow)
|
|
1961
1969
|
- [ ] System scopes exist:
|
|
1962
|
-
- [ ] Super Admin scope (00000000-0000-0000-0000-000000000000)
|
|
1963
1970
|
- [ ] Default System scope (00000000-0000-0000-0000-000000000001)
|
|
1971
|
+
- [ ] Migration 020 run: Super Admin scope retired, `hazo_org_global_admin` permission granted
|
|
1964
1972
|
- [ ] Grants applied (PostgreSQL)
|
|
1965
1973
|
- [ ] HRBAC tabs visible in User Management
|
|
1966
1974
|
- [ ] 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 {
|
|
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
|
|
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
|
|
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
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
338
|
-
|
|
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
|
-
//
|
|
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:
|
|
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
|
|
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:
|
|
356
|
-
root_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
|
|
353
|
+
console.log(`✓ Assigned user to default system scope`);
|
|
363
354
|
}
|
|
364
355
|
|
|
365
356
|
console.log();
|
|
366
357
|
|
|
367
|
-
//
|
|
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,
|
|
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.
|
|
413
|
-
6. Assigns the user to the
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
170
|
-
|
|
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: "", //
|
|
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,
|
|
@@ -11,6 +11,7 @@ import { get_oauth_config } from "../oauth_config.server.js";
|
|
|
11
11
|
import { handle_google_oauth_login } from "../services/oauth_service.js";
|
|
12
12
|
import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
|
|
13
13
|
import { create_app_logger } from "../app_logger.js";
|
|
14
|
+
import { store_google_oauth_token, GoogleTokenStorageUnconfigured } from "../services/google_token_service.js";
|
|
14
15
|
|
|
15
16
|
// section: types
|
|
16
17
|
export type NextAuthCallbackUser = {
|
|
@@ -27,6 +28,8 @@ export type NextAuthCallbackAccount = {
|
|
|
27
28
|
access_token?: string;
|
|
28
29
|
id_token?: string;
|
|
29
30
|
expires_at?: number;
|
|
31
|
+
refresh_token?: string;
|
|
32
|
+
scope?: string;
|
|
30
33
|
};
|
|
31
34
|
|
|
32
35
|
export type NextAuthCallbackProfile = {
|
|
@@ -62,6 +65,7 @@ export function get_nextauth_config(): AuthOptions {
|
|
|
62
65
|
prompt: "consent",
|
|
63
66
|
access_type: "offline",
|
|
64
67
|
response_type: "code",
|
|
68
|
+
include_granted_scopes: "true",
|
|
65
69
|
},
|
|
66
70
|
},
|
|
67
71
|
})
|
|
@@ -170,6 +174,43 @@ export function get_nextauth_config(): AuthOptions {
|
|
|
170
174
|
// Store user_id in account for the JWT callback to pick up
|
|
171
175
|
(account as Record<string, unknown>).hazo_user_id = result.user_id;
|
|
172
176
|
|
|
177
|
+
// Capture refresh token when extra scopes were granted
|
|
178
|
+
const BASE_SCOPES = ["openid", "email", "profile"];
|
|
179
|
+
const granted_scopes = (account.scope ?? "").split(" ").filter(Boolean);
|
|
180
|
+
const has_extra_scopes = granted_scopes.some(s => !BASE_SCOPES.includes(s));
|
|
181
|
+
|
|
182
|
+
if (account.refresh_token && has_extra_scopes) {
|
|
183
|
+
try {
|
|
184
|
+
await store_google_oauth_token({
|
|
185
|
+
user_id: result.user_id!,
|
|
186
|
+
refresh_token: account.refresh_token,
|
|
187
|
+
access_token: account.access_token,
|
|
188
|
+
scopes: account.scope ?? "",
|
|
189
|
+
expires_at: account.expires_at
|
|
190
|
+
? new Date(account.expires_at * 1000).toISOString()
|
|
191
|
+
: undefined,
|
|
192
|
+
});
|
|
193
|
+
logger.info("nextauth_google_token_stored", {
|
|
194
|
+
user_id: result.user_id,
|
|
195
|
+
scopes: account.scope,
|
|
196
|
+
});
|
|
197
|
+
} catch (tokenError) {
|
|
198
|
+
if (tokenError instanceof GoogleTokenStorageUnconfigured) {
|
|
199
|
+
logger.error("nextauth_google_token_storage_unconfigured", {
|
|
200
|
+
user_id: result.user_id,
|
|
201
|
+
error: tokenError.message,
|
|
202
|
+
});
|
|
203
|
+
return "/api/auth/error?error=GoogleTokenStorageUnconfigured";
|
|
204
|
+
}
|
|
205
|
+
const errorMsg = tokenError instanceof Error ? tokenError.message : String(tokenError);
|
|
206
|
+
logger.error("nextauth_google_token_store_failed", {
|
|
207
|
+
user_id: result.user_id,
|
|
208
|
+
error: errorMsg,
|
|
209
|
+
});
|
|
210
|
+
// Non-fatal: continue sign-in even if token storage fails
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
173
214
|
return true;
|
|
174
215
|
} catch (error) {
|
|
175
216
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|