hazo_auth 4.5.6 → 4.5.7
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 +190 -1
- package/SETUP_CHECKLIST.md +108 -21
- package/cli-src/lib/auth/auth_types.ts +2 -0
- package/cli-src/lib/auth/hazo_get_auth.server.ts +26 -0
- package/cli-src/lib/services/app_user_data_service.ts +294 -0
- package/cli-src/lib/services/index.ts +1 -0
- package/dist/app/api/hazo_auth/app_user_data/route.d.ts +64 -0
- package/dist/app/api/hazo_auth/app_user_data/route.d.ts.map +1 -0
- package/dist/app/api/hazo_auth/app_user_data/route.js +208 -0
- package/dist/app/api/hazo_auth/me/route.d.ts +2 -1
- package/dist/app/api/hazo_auth/me/route.d.ts.map +1 -1
- package/dist/app/api/hazo_auth/me/route.js +4 -1
- package/dist/components/layouts/app_user_data_test/index.d.ts +6 -0
- package/dist/components/layouts/app_user_data_test/index.d.ts.map +1 -0
- package/dist/components/layouts/app_user_data_test/index.js +145 -0
- package/dist/components/layouts/shared/components/password_field.js +1 -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/components/layouts/shared/components/two_column_auth_layout.js +1 -1
- package/dist/lib/auth/auth_types.d.ts +1 -0
- 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 +24 -0
- package/dist/lib/services/app_user_data_service.d.ts +34 -0
- package/dist/lib/services/app_user_data_service.d.ts.map +1 -0
- package/dist/lib/services/app_user_data_service.js +228 -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/server/routes/app_user_data.d.ts +2 -0
- package/dist/server/routes/app_user_data.d.ts.map +1 -0
- package/dist/server/routes/app_user_data.js +2 -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_pages/forgot_password.d.ts.map +1 -1
- package/dist/server_pages/forgot_password.js +3 -2
- package/dist/server_pages/login.d.ts.map +1 -1
- package/dist/server_pages/login.js +8 -7
- package/dist/server_pages/my_settings.d.ts.map +1 -1
- package/dist/server_pages/my_settings.js +3 -2
- package/dist/server_pages/register.d.ts.map +1 -1
- package/dist/server_pages/register.js +3 -2
- package/dist/server_pages/reset_password.d.ts.map +1 -1
- package/dist/server_pages/reset_password.js +3 -2
- package/dist/server_pages/verify_email.d.ts.map +1 -1
- package/dist/server_pages/verify_email.js +3 -2
- package/package.json +26 -1
package/README.md
CHANGED
|
@@ -94,6 +94,7 @@ export default function Page() {
|
|
|
94
94
|
- ✅ Database connection initialized server-side via hazo_connect singleton
|
|
95
95
|
- ✅ Configuration loaded from hazo_auth_config.ini (or uses sensible defaults)
|
|
96
96
|
- ✅ All props automatically configured
|
|
97
|
+
- ✅ Navbar automatically rendered based on config (no manual wrapping needed)
|
|
97
98
|
- ✅ Page renders immediately - NO loading state!
|
|
98
99
|
|
|
99
100
|
**Available zero-config pages:**
|
|
@@ -1180,6 +1181,8 @@ vertical_center = auto # 'auto' enables vertical centering when navbar is prese
|
|
|
1180
1181
|
|
|
1181
1182
|
### Authentication Page Navbar
|
|
1182
1183
|
|
|
1184
|
+
**The navbar now works automatically** - zero-config server page components include the navbar based on configuration without manual wrapping.
|
|
1185
|
+
|
|
1183
1186
|
When using `layout_mode = standalone`, you can enable a configurable navbar that appears on all auth pages:
|
|
1184
1187
|
|
|
1185
1188
|
```ini
|
|
@@ -1199,7 +1202,17 @@ height = 64 # Navbar height in pixels
|
|
|
1199
1202
|
|
|
1200
1203
|
The navbar provides consistent branding across authentication pages with your company logo, name, and optional home link. It automatically vertically centers auth content when enabled.
|
|
1201
1204
|
|
|
1202
|
-
**
|
|
1205
|
+
**Zero-config usage (recommended):**
|
|
1206
|
+
```typescript
|
|
1207
|
+
// app/hazo_auth/login/page.tsx
|
|
1208
|
+
import { LoginPage } from "hazo_auth/pages/login";
|
|
1209
|
+
|
|
1210
|
+
export default function Page() {
|
|
1211
|
+
return <LoginPage />; // Navbar appears automatically if enabled in config
|
|
1212
|
+
}
|
|
1213
|
+
```
|
|
1214
|
+
|
|
1215
|
+
**Customize via props (advanced):**
|
|
1203
1216
|
```typescript
|
|
1204
1217
|
import { LoginLayout } from "hazo_auth/components/layouts/login";
|
|
1205
1218
|
|
|
@@ -1218,6 +1231,8 @@ export default function Page() {
|
|
|
1218
1231
|
|
|
1219
1232
|
**Disable for specific pages:**
|
|
1220
1233
|
```typescript
|
|
1234
|
+
<LoginPage disableNavbar={true} />
|
|
1235
|
+
// OR for layout components:
|
|
1221
1236
|
<LoginLayout navbar={{ enable_navbar: false }} />
|
|
1222
1237
|
```
|
|
1223
1238
|
|
|
@@ -2066,6 +2081,180 @@ For full documentation, see `CLAUDE.md` or `TECHDOC.md`.
|
|
|
2066
2081
|
|
|
2067
2082
|
---
|
|
2068
2083
|
|
|
2084
|
+
## App User Data (Custom User Metadata)
|
|
2085
|
+
|
|
2086
|
+
hazo_auth provides a flexible JSON field for storing custom user-specific data without modifying the `hazo_users` table schema. This allows consuming applications to store user preferences, settings, and app-specific state.
|
|
2087
|
+
|
|
2088
|
+
### Overview
|
|
2089
|
+
|
|
2090
|
+
- **JSON Storage**: Single TEXT column stores JSON objects (no schema restrictions)
|
|
2091
|
+
- **Deep Merge Support**: PATCH endpoint merges new data with existing data
|
|
2092
|
+
- **Full Replace**: PUT endpoint replaces entire JSON object
|
|
2093
|
+
- **Clear Data**: DELETE endpoint sets field to NULL
|
|
2094
|
+
- **Type-Safe**: TypeScript service layer with validation
|
|
2095
|
+
- **Included in Auth Response**: Available in `/api/hazo_auth/me` response
|
|
2096
|
+
|
|
2097
|
+
### Quick Start
|
|
2098
|
+
|
|
2099
|
+
1. **Run database migration**:
|
|
2100
|
+
```bash
|
|
2101
|
+
npm run migrate migrations/008_add_app_user_data_to_hazo_users.sql
|
|
2102
|
+
```
|
|
2103
|
+
|
|
2104
|
+
2. **Create API route** (`app/api/hazo_auth/app_user_data/route.ts`):
|
|
2105
|
+
```typescript
|
|
2106
|
+
export {
|
|
2107
|
+
appUserDataGET as GET,
|
|
2108
|
+
appUserDataPATCH as PATCH,
|
|
2109
|
+
appUserDataPUT as PUT,
|
|
2110
|
+
appUserDataDELETE as DELETE
|
|
2111
|
+
} from "hazo_auth/server/routes";
|
|
2112
|
+
```
|
|
2113
|
+
|
|
2114
|
+
Or use CLI: `npx hazo_auth generate-routes`
|
|
2115
|
+
|
|
2116
|
+
3. **Use in your app**:
|
|
2117
|
+
```typescript
|
|
2118
|
+
// Store user preferences (deep merge)
|
|
2119
|
+
PATCH /api/hazo_auth/app_user_data
|
|
2120
|
+
{
|
|
2121
|
+
data: {
|
|
2122
|
+
theme: "dark",
|
|
2123
|
+
language: "en-US",
|
|
2124
|
+
sidebar_collapsed: true
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// Access in client components
|
|
2129
|
+
const { app_user_data } = use_hazo_auth();
|
|
2130
|
+
console.log(app_user_data?.theme); // "dark"
|
|
2131
|
+
```
|
|
2132
|
+
|
|
2133
|
+
### API Endpoints
|
|
2134
|
+
|
|
2135
|
+
**GET `/api/hazo_auth/app_user_data`** - Get current user's data
|
|
2136
|
+
```typescript
|
|
2137
|
+
Response: { data: { theme: "dark", sidebar_collapsed: true } | null }
|
|
2138
|
+
```
|
|
2139
|
+
|
|
2140
|
+
**PATCH `/api/hazo_auth/app_user_data`** - Merge with existing data (preserves other fields)
|
|
2141
|
+
```typescript
|
|
2142
|
+
Request: { data: { theme: "light" } }
|
|
2143
|
+
// If existing: { theme: "dark", sidebar_collapsed: true }
|
|
2144
|
+
// Result: { theme: "light", sidebar_collapsed: true }
|
|
2145
|
+
```
|
|
2146
|
+
|
|
2147
|
+
**PUT `/api/hazo_auth/app_user_data`** - Replace entire object
|
|
2148
|
+
```typescript
|
|
2149
|
+
Request: { data: { theme: "light" } }
|
|
2150
|
+
// Result: { theme: "light" } (sidebar_collapsed removed)
|
|
2151
|
+
```
|
|
2152
|
+
|
|
2153
|
+
**DELETE `/api/hazo_auth/app_user_data`** - Clear all data (sets to NULL)
|
|
2154
|
+
|
|
2155
|
+
### Service Functions
|
|
2156
|
+
|
|
2157
|
+
```typescript
|
|
2158
|
+
import {
|
|
2159
|
+
get_app_user_data,
|
|
2160
|
+
update_app_user_data,
|
|
2161
|
+
clear_app_user_data
|
|
2162
|
+
} from "hazo_auth";
|
|
2163
|
+
|
|
2164
|
+
// Get data
|
|
2165
|
+
const data = await get_app_user_data(adapter, user_id);
|
|
2166
|
+
|
|
2167
|
+
// Update with merge (default)
|
|
2168
|
+
await update_app_user_data(adapter, user_id, { theme: "light" }, true);
|
|
2169
|
+
|
|
2170
|
+
// Replace entirely
|
|
2171
|
+
await update_app_user_data(adapter, user_id, { theme: "light" }, false);
|
|
2172
|
+
|
|
2173
|
+
// Clear data
|
|
2174
|
+
await clear_app_user_data(adapter, user_id);
|
|
2175
|
+
```
|
|
2176
|
+
|
|
2177
|
+
### Access in `/api/hazo_auth/me`
|
|
2178
|
+
|
|
2179
|
+
The `app_user_data` field is included in the authentication response:
|
|
2180
|
+
|
|
2181
|
+
```typescript
|
|
2182
|
+
{
|
|
2183
|
+
authenticated: true,
|
|
2184
|
+
user: { ... },
|
|
2185
|
+
permissions: [...],
|
|
2186
|
+
app_user_data: { theme: "dark", sidebar_collapsed: true } | null
|
|
2187
|
+
}
|
|
2188
|
+
```
|
|
2189
|
+
|
|
2190
|
+
### Use Cases
|
|
2191
|
+
|
|
2192
|
+
**Store user preferences:**
|
|
2193
|
+
```typescript
|
|
2194
|
+
{
|
|
2195
|
+
theme: "dark",
|
|
2196
|
+
language: "en-US",
|
|
2197
|
+
timezone: "America/New_York"
|
|
2198
|
+
}
|
|
2199
|
+
```
|
|
2200
|
+
|
|
2201
|
+
**Store app-specific state:**
|
|
2202
|
+
```typescript
|
|
2203
|
+
{
|
|
2204
|
+
dashboard_layout: "grid",
|
|
2205
|
+
sidebar_collapsed: true,
|
|
2206
|
+
recent_searches: ["tax forms", "invoices"]
|
|
2207
|
+
}
|
|
2208
|
+
```
|
|
2209
|
+
|
|
2210
|
+
**Store nested configuration:**
|
|
2211
|
+
```typescript
|
|
2212
|
+
{
|
|
2213
|
+
notifications: {
|
|
2214
|
+
email: true,
|
|
2215
|
+
sms: false,
|
|
2216
|
+
push: true
|
|
2217
|
+
},
|
|
2218
|
+
privacy: {
|
|
2219
|
+
profile_public: false,
|
|
2220
|
+
show_email: false
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
```
|
|
2224
|
+
|
|
2225
|
+
### Deep Merge Behavior
|
|
2226
|
+
|
|
2227
|
+
```typescript
|
|
2228
|
+
// Existing data
|
|
2229
|
+
{ user: { name: "Alice", age: 30 }, theme: "dark" }
|
|
2230
|
+
|
|
2231
|
+
// PATCH with
|
|
2232
|
+
{ user: { age: 31 }, sidebar: true }
|
|
2233
|
+
|
|
2234
|
+
// Result (deep merge)
|
|
2235
|
+
{ user: { name: "Alice", age: 31 }, theme: "dark", sidebar: true }
|
|
2236
|
+
```
|
|
2237
|
+
|
|
2238
|
+
### Test Page
|
|
2239
|
+
|
|
2240
|
+
Visit `/hazo_auth/app_user_data_test` in your dev environment to test the API with an interactive UI:
|
|
2241
|
+
- View current data (live refresh)
|
|
2242
|
+
- Merge data (PATCH)
|
|
2243
|
+
- Replace data (PUT)
|
|
2244
|
+
- Clear data (DELETE)
|
|
2245
|
+
- JSON validation
|
|
2246
|
+
|
|
2247
|
+
### Performance & Limits
|
|
2248
|
+
|
|
2249
|
+
- **Recommended max size**: ~10KB per user (for preferences/settings)
|
|
2250
|
+
- **Storage**: JSON stored as TEXT (no compression)
|
|
2251
|
+
- **Caching**: Benefits from `hazo_get_auth()` LRU cache
|
|
2252
|
+
- **Large datasets**: Use separate tables for complex relational data
|
|
2253
|
+
|
|
2254
|
+
For full documentation, see `CHANGE_LOG.md` and `TECHDOC.md`.
|
|
2255
|
+
|
|
2256
|
+
---
|
|
2257
|
+
|
|
2069
2258
|
## User Profile Service
|
|
2070
2259
|
|
|
2071
2260
|
The `hazo_auth` package provides a batch user profile retrieval service for applications that need basic user information, such as chat applications or user lists.
|
package/SETUP_CHECKLIST.md
CHANGED
|
@@ -312,15 +312,54 @@ Run this SQL script in your PostgreSQL database:
|
|
|
312
312
|
-- Ensure we're in the public schema (or your target schema)
|
|
313
313
|
SET search_path TO public;
|
|
314
314
|
|
|
315
|
-
-- Create enum
|
|
315
|
+
-- Create enum types (drop first if they exist to avoid conflicts)
|
|
316
316
|
DROP TYPE IF EXISTS hazo_enum_profile_source_enum CASCADE;
|
|
317
317
|
CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
|
|
318
318
|
|
|
319
|
+
DROP TYPE IF EXISTS hazo_enum_scope_types CASCADE;
|
|
320
|
+
CREATE TYPE hazo_enum_scope_types AS ENUM (
|
|
321
|
+
'hazo_scopes_l1', 'hazo_scopes_l2', 'hazo_scopes_l3',
|
|
322
|
+
'hazo_scopes_l4', 'hazo_scopes_l5', 'hazo_scopes_l6', 'hazo_scopes_l7'
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
DROP TYPE IF EXISTS hazo_enum_notify_chain_status CASCADE;
|
|
326
|
+
CREATE TYPE hazo_enum_notify_chain_status AS ENUM ('draft', 'published', 'inactive');
|
|
327
|
+
|
|
328
|
+
DROP TYPE IF EXISTS hazo_enum_notify_email_type CASCADE;
|
|
329
|
+
CREATE TYPE hazo_enum_notify_email_type AS ENUM ('system', 'user');
|
|
330
|
+
|
|
331
|
+
DROP TYPE IF EXISTS hazo_enum_group_type CASCADE;
|
|
332
|
+
CREATE TYPE hazo_enum_group_type AS ENUM ('support', 'peer', 'group');
|
|
333
|
+
|
|
334
|
+
DROP TYPE IF EXISTS hazo_enum_group_role CASCADE;
|
|
335
|
+
CREATE TYPE hazo_enum_group_role AS ENUM ('client', 'staff', 'owner', 'admin', 'member');
|
|
336
|
+
|
|
337
|
+
DROP TYPE IF EXISTS hazo_enum_chat_type CASCADE;
|
|
338
|
+
CREATE TYPE hazo_enum_chat_type AS ENUM ('chat', 'field', 'project', 'support', 'general');
|
|
339
|
+
|
|
340
|
+
-- Create organization table (multi-tenancy) - MUST be created before hazo_users
|
|
341
|
+
CREATE TABLE hazo_org (
|
|
342
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
343
|
+
name TEXT NOT NULL,
|
|
344
|
+
parent_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
|
|
345
|
+
root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
|
|
346
|
+
user_limit INTEGER NOT NULL DEFAULT 0, -- 0 = unlimited
|
|
347
|
+
active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
348
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
349
|
+
created_by UUID, -- FK added after hazo_users exists
|
|
350
|
+
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
351
|
+
changed_by UUID -- FK added after hazo_users exists
|
|
352
|
+
);
|
|
353
|
+
CREATE INDEX idx_hazo_org_parent_org_id ON hazo_org(parent_org_id);
|
|
354
|
+
CREATE INDEX idx_hazo_org_root_org_id ON hazo_org(root_org_id);
|
|
355
|
+
CREATE INDEX idx_hazo_org_active ON hazo_org(active);
|
|
356
|
+
CREATE INDEX idx_hazo_org_name ON hazo_org(name);
|
|
357
|
+
|
|
319
358
|
-- Create users table
|
|
320
359
|
CREATE TABLE hazo_users (
|
|
321
360
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
322
361
|
email_address TEXT NOT NULL UNIQUE,
|
|
323
|
-
password_hash TEXT
|
|
362
|
+
password_hash TEXT, -- NULL for OAuth-only users
|
|
324
363
|
name TEXT,
|
|
325
364
|
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
326
365
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
@@ -330,10 +369,25 @@ CREATE TABLE hazo_users (
|
|
|
330
369
|
profile_source hazo_enum_profile_source_enum,
|
|
331
370
|
mfa_secret TEXT,
|
|
332
371
|
url_on_logon TEXT,
|
|
372
|
+
user_type TEXT, -- Optional user categorization
|
|
373
|
+
google_id TEXT UNIQUE, -- Google OAuth ID
|
|
374
|
+
auth_providers TEXT DEFAULT 'email', -- 'email', 'google', or 'email,google'
|
|
375
|
+
org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
|
|
376
|
+
root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
|
|
333
377
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
334
378
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
335
379
|
);
|
|
336
380
|
CREATE INDEX idx_hazo_users_email ON hazo_users(email_address);
|
|
381
|
+
CREATE INDEX idx_hazo_users_user_type ON hazo_users(user_type);
|
|
382
|
+
CREATE UNIQUE INDEX idx_hazo_users_google_id ON hazo_users(google_id);
|
|
383
|
+
CREATE INDEX idx_hazo_users_org_id ON hazo_users(org_id);
|
|
384
|
+
CREATE INDEX idx_hazo_users_root_org_id ON hazo_users(root_org_id);
|
|
385
|
+
|
|
386
|
+
-- Add FK constraints to hazo_org now that hazo_users exists
|
|
387
|
+
ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_created_by
|
|
388
|
+
FOREIGN KEY (created_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
|
|
389
|
+
ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_changed_by
|
|
390
|
+
FOREIGN KEY (changed_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
|
|
337
391
|
|
|
338
392
|
-- Create refresh tokens table
|
|
339
393
|
CREATE TABLE hazo_refresh_tokens (
|
|
@@ -453,7 +507,22 @@ GRANT USAGE ON TYPE hazo_enum_profile_source_enum TO anon, authenticated;
|
|
|
453
507
|
|
|
454
508
|
**Checklist:**
|
|
455
509
|
- [ ] Database created (SQLite file or PostgreSQL)
|
|
456
|
-
- [ ] All
|
|
510
|
+
- [ ] All enum types created (PostgreSQL only):
|
|
511
|
+
- [ ] `hazo_enum_profile_source_enum`
|
|
512
|
+
- [ ] `hazo_enum_scope_types`
|
|
513
|
+
- [ ] `hazo_enum_notify_chain_status`
|
|
514
|
+
- [ ] `hazo_enum_notify_email_type`
|
|
515
|
+
- [ ] `hazo_enum_group_type`
|
|
516
|
+
- [ ] `hazo_enum_group_role`
|
|
517
|
+
- [ ] `hazo_enum_chat_type`
|
|
518
|
+
- [ ] All core tables exist:
|
|
519
|
+
- [ ] `hazo_org` (multi-tenancy - must be created before hazo_users)
|
|
520
|
+
- [ ] `hazo_users` (with google_id, auth_providers, org_id, root_org_id, user_type fields)
|
|
521
|
+
- [ ] `hazo_refresh_tokens`
|
|
522
|
+
- [ ] `hazo_permissions`
|
|
523
|
+
- [ ] `hazo_roles`
|
|
524
|
+
- [ ] `hazo_role_permissions`
|
|
525
|
+
- [ ] `hazo_user_roles`
|
|
457
526
|
|
|
458
527
|
---
|
|
459
528
|
|
|
@@ -1244,25 +1313,29 @@ $$ LANGUAGE plpgsql;
|
|
|
1244
1313
|
CREATE TABLE hazo_scopes_l1 (
|
|
1245
1314
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1246
1315
|
seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l1'),
|
|
1247
|
-
|
|
1316
|
+
org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
|
|
1317
|
+
root_org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
|
|
1248
1318
|
name TEXT NOT NULL,
|
|
1249
1319
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
1250
1320
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
1251
1321
|
);
|
|
1252
|
-
CREATE INDEX
|
|
1322
|
+
CREATE INDEX idx_hazo_scopes_l1_org_id ON hazo_scopes_l1(org_id);
|
|
1323
|
+
CREATE INDEX idx_hazo_scopes_l1_root_org_id ON hazo_scopes_l1(root_org_id);
|
|
1253
1324
|
CREATE INDEX idx_hazo_scopes_l1_seq ON hazo_scopes_l1(seq);
|
|
1254
1325
|
|
|
1255
1326
|
-- Level 2 (parent: L1)
|
|
1256
1327
|
CREATE TABLE hazo_scopes_l2 (
|
|
1257
1328
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1258
1329
|
seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l2'),
|
|
1259
|
-
|
|
1330
|
+
org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
|
|
1331
|
+
root_org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
|
|
1260
1332
|
name TEXT NOT NULL,
|
|
1261
1333
|
parent_scope_id UUID REFERENCES hazo_scopes_l1(id) ON DELETE CASCADE,
|
|
1262
1334
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
1263
1335
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
1264
1336
|
);
|
|
1265
|
-
CREATE INDEX
|
|
1337
|
+
CREATE INDEX idx_hazo_scopes_l2_org_id ON hazo_scopes_l2(org_id);
|
|
1338
|
+
CREATE INDEX idx_hazo_scopes_l2_root_org_id ON hazo_scopes_l2(root_org_id);
|
|
1266
1339
|
CREATE INDEX idx_hazo_scopes_l2_seq ON hazo_scopes_l2(seq);
|
|
1267
1340
|
CREATE INDEX idx_hazo_scopes_l2_parent ON hazo_scopes_l2(parent_scope_id);
|
|
1268
1341
|
|
|
@@ -1270,13 +1343,15 @@ CREATE INDEX idx_hazo_scopes_l2_parent ON hazo_scopes_l2(parent_scope_id);
|
|
|
1270
1343
|
CREATE TABLE hazo_scopes_l3 (
|
|
1271
1344
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1272
1345
|
seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l3'),
|
|
1273
|
-
|
|
1346
|
+
org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
|
|
1347
|
+
root_org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
|
|
1274
1348
|
name TEXT NOT NULL,
|
|
1275
1349
|
parent_scope_id UUID REFERENCES hazo_scopes_l2(id) ON DELETE CASCADE,
|
|
1276
1350
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
1277
1351
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
1278
1352
|
);
|
|
1279
|
-
CREATE INDEX
|
|
1353
|
+
CREATE INDEX idx_hazo_scopes_l3_org_id ON hazo_scopes_l3(org_id);
|
|
1354
|
+
CREATE INDEX idx_hazo_scopes_l3_root_org_id ON hazo_scopes_l3(root_org_id);
|
|
1280
1355
|
CREATE INDEX idx_hazo_scopes_l3_seq ON hazo_scopes_l3(seq);
|
|
1281
1356
|
CREATE INDEX idx_hazo_scopes_l3_parent ON hazo_scopes_l3(parent_scope_id);
|
|
1282
1357
|
|
|
@@ -1284,13 +1359,15 @@ CREATE INDEX idx_hazo_scopes_l3_parent ON hazo_scopes_l3(parent_scope_id);
|
|
|
1284
1359
|
CREATE TABLE hazo_scopes_l4 (
|
|
1285
1360
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1286
1361
|
seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l4'),
|
|
1287
|
-
|
|
1362
|
+
org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
|
|
1363
|
+
root_org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
|
|
1288
1364
|
name TEXT NOT NULL,
|
|
1289
1365
|
parent_scope_id UUID REFERENCES hazo_scopes_l3(id) ON DELETE CASCADE,
|
|
1290
1366
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
1291
1367
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
1292
1368
|
);
|
|
1293
|
-
CREATE INDEX
|
|
1369
|
+
CREATE INDEX idx_hazo_scopes_l4_org_id ON hazo_scopes_l4(org_id);
|
|
1370
|
+
CREATE INDEX idx_hazo_scopes_l4_root_org_id ON hazo_scopes_l4(root_org_id);
|
|
1294
1371
|
CREATE INDEX idx_hazo_scopes_l4_seq ON hazo_scopes_l4(seq);
|
|
1295
1372
|
CREATE INDEX idx_hazo_scopes_l4_parent ON hazo_scopes_l4(parent_scope_id);
|
|
1296
1373
|
|
|
@@ -1298,13 +1375,15 @@ CREATE INDEX idx_hazo_scopes_l4_parent ON hazo_scopes_l4(parent_scope_id);
|
|
|
1298
1375
|
CREATE TABLE hazo_scopes_l5 (
|
|
1299
1376
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1300
1377
|
seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l5'),
|
|
1301
|
-
|
|
1378
|
+
org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
|
|
1379
|
+
root_org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
|
|
1302
1380
|
name TEXT NOT NULL,
|
|
1303
1381
|
parent_scope_id UUID REFERENCES hazo_scopes_l4(id) ON DELETE CASCADE,
|
|
1304
1382
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
1305
1383
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
1306
1384
|
);
|
|
1307
|
-
CREATE INDEX
|
|
1385
|
+
CREATE INDEX idx_hazo_scopes_l5_org_id ON hazo_scopes_l5(org_id);
|
|
1386
|
+
CREATE INDEX idx_hazo_scopes_l5_root_org_id ON hazo_scopes_l5(root_org_id);
|
|
1308
1387
|
CREATE INDEX idx_hazo_scopes_l5_seq ON hazo_scopes_l5(seq);
|
|
1309
1388
|
CREATE INDEX idx_hazo_scopes_l5_parent ON hazo_scopes_l5(parent_scope_id);
|
|
1310
1389
|
|
|
@@ -1312,13 +1391,15 @@ CREATE INDEX idx_hazo_scopes_l5_parent ON hazo_scopes_l5(parent_scope_id);
|
|
|
1312
1391
|
CREATE TABLE hazo_scopes_l6 (
|
|
1313
1392
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1314
1393
|
seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l6'),
|
|
1315
|
-
|
|
1394
|
+
org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
|
|
1395
|
+
root_org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
|
|
1316
1396
|
name TEXT NOT NULL,
|
|
1317
1397
|
parent_scope_id UUID REFERENCES hazo_scopes_l5(id) ON DELETE CASCADE,
|
|
1318
1398
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
1319
1399
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
1320
1400
|
);
|
|
1321
|
-
CREATE INDEX
|
|
1401
|
+
CREATE INDEX idx_hazo_scopes_l6_org_id ON hazo_scopes_l6(org_id);
|
|
1402
|
+
CREATE INDEX idx_hazo_scopes_l6_root_org_id ON hazo_scopes_l6(root_org_id);
|
|
1322
1403
|
CREATE INDEX idx_hazo_scopes_l6_seq ON hazo_scopes_l6(seq);
|
|
1323
1404
|
CREATE INDEX idx_hazo_scopes_l6_parent ON hazo_scopes_l6(parent_scope_id);
|
|
1324
1405
|
|
|
@@ -1326,13 +1407,15 @@ CREATE INDEX idx_hazo_scopes_l6_parent ON hazo_scopes_l6(parent_scope_id);
|
|
|
1326
1407
|
CREATE TABLE hazo_scopes_l7 (
|
|
1327
1408
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1328
1409
|
seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l7'),
|
|
1329
|
-
|
|
1410
|
+
org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
|
|
1411
|
+
root_org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
|
|
1330
1412
|
name TEXT NOT NULL,
|
|
1331
1413
|
parent_scope_id UUID REFERENCES hazo_scopes_l6(id) ON DELETE CASCADE,
|
|
1332
1414
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
1333
1415
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
1334
1416
|
);
|
|
1335
|
-
CREATE INDEX
|
|
1417
|
+
CREATE INDEX idx_hazo_scopes_l7_org_id ON hazo_scopes_l7(org_id);
|
|
1418
|
+
CREATE INDEX idx_hazo_scopes_l7_root_org_id ON hazo_scopes_l7(root_org_id);
|
|
1336
1419
|
CREATE INDEX idx_hazo_scopes_l7_seq ON hazo_scopes_l7(seq);
|
|
1337
1420
|
CREATE INDEX idx_hazo_scopes_l7_parent ON hazo_scopes_l7(parent_scope_id);
|
|
1338
1421
|
|
|
@@ -1353,14 +1436,14 @@ CREATE INDEX idx_hazo_user_scopes_scope_type ON hazo_user_scopes(scope_type);
|
|
|
1353
1436
|
-- 5. Create scope labels table
|
|
1354
1437
|
CREATE TABLE hazo_scope_labels (
|
|
1355
1438
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1356
|
-
|
|
1439
|
+
org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
|
|
1357
1440
|
scope_type hazo_enum_scope_types NOT NULL,
|
|
1358
1441
|
label TEXT NOT NULL,
|
|
1359
1442
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
1360
1443
|
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
1361
|
-
UNIQUE(
|
|
1444
|
+
UNIQUE(org_id, scope_type)
|
|
1362
1445
|
);
|
|
1363
|
-
CREATE INDEX
|
|
1446
|
+
CREATE INDEX idx_hazo_scope_labels_org_id ON hazo_scope_labels(org_id);
|
|
1364
1447
|
```
|
|
1365
1448
|
|
|
1366
1449
|
#### PostgreSQL Grant Scripts
|
|
@@ -1522,7 +1605,11 @@ sqlite3 data/hazo_auth.sqlite ".tables" | grep -E "hazo_scopes|hazo_user_scopes|
|
|
|
1522
1605
|
**HRBAC Checklist:**
|
|
1523
1606
|
- [ ] `enable_hrbac = true` in config
|
|
1524
1607
|
- [ ] HRBAC permissions added to defaults
|
|
1525
|
-
- [ ] All 9 HRBAC tables created
|
|
1608
|
+
- [ ] All 9 HRBAC tables created:
|
|
1609
|
+
- [ ] `hazo_scopes_l1` through `hazo_scopes_l7` (with org_id, root_org_id FKs)
|
|
1610
|
+
- [ ] `hazo_user_scopes` (junction table)
|
|
1611
|
+
- [ ] `hazo_scope_labels` (custom labels per org)
|
|
1612
|
+
- [ ] Scope ID generator function created (PostgreSQL)
|
|
1526
1613
|
- [ ] Grants applied (PostgreSQL)
|
|
1527
1614
|
- [ ] HRBAC tabs visible in User Management
|
|
1528
1615
|
- [ ] Scope test page works
|
|
@@ -10,6 +10,8 @@ export type HazoAuthUser = {
|
|
|
10
10
|
email_address: string;
|
|
11
11
|
is_active: boolean;
|
|
12
12
|
profile_picture_url: string | null;
|
|
13
|
+
// App-specific user data (JSON object stored as TEXT in database)
|
|
14
|
+
app_user_data: Record<string, unknown> | null;
|
|
13
15
|
// Multi-tenancy fields (only populated when multi-tenancy is enabled)
|
|
14
16
|
org_id?: string | null;
|
|
15
17
|
org_name?: string | null;
|
|
@@ -20,6 +20,31 @@ import { get_org_cache, type OrgCacheEntry } from "./org_cache.js";
|
|
|
20
20
|
|
|
21
21
|
// section: helpers
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Parse JSON string to object, returning null on failure
|
|
25
|
+
* @param json_string - JSON string to parse
|
|
26
|
+
* @returns Parsed object or null
|
|
27
|
+
*/
|
|
28
|
+
function parse_app_user_data(json_string: unknown): Record<string, unknown> | null {
|
|
29
|
+
if (json_string === null || json_string === undefined || json_string === "") {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof json_string !== "string") {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(json_string);
|
|
39
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
40
|
+
return parsed as Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
23
48
|
/**
|
|
24
49
|
* Gets client IP address from request
|
|
25
50
|
* @param request - NextRequest object
|
|
@@ -80,6 +105,7 @@ async function fetch_user_data_from_db(user_id: string): Promise<{
|
|
|
80
105
|
is_active: user_db.is_active === true,
|
|
81
106
|
profile_picture_url:
|
|
82
107
|
(user_db.profile_picture_url as string | null) || null,
|
|
108
|
+
app_user_data: parse_app_user_data(user_db.app_user_data),
|
|
83
109
|
};
|
|
84
110
|
|
|
85
111
|
// Fetch org info if multi-tenancy is enabled and user has org_id
|