hazo_auth 4.5.5 → 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.
Files changed (48) hide show
  1. package/README.md +282 -14
  2. package/SETUP_CHECKLIST.md +108 -21
  3. package/cli-src/lib/auth/auth_types.ts +2 -0
  4. package/cli-src/lib/auth/hazo_get_auth.server.ts +26 -0
  5. package/cli-src/lib/services/app_user_data_service.ts +294 -0
  6. package/cli-src/lib/services/index.ts +1 -0
  7. package/dist/app/api/hazo_auth/app_user_data/route.d.ts +64 -0
  8. package/dist/app/api/hazo_auth/app_user_data/route.d.ts.map +1 -0
  9. package/dist/app/api/hazo_auth/app_user_data/route.js +208 -0
  10. package/dist/app/api/hazo_auth/me/route.d.ts +2 -1
  11. package/dist/app/api/hazo_auth/me/route.d.ts.map +1 -1
  12. package/dist/app/api/hazo_auth/me/route.js +4 -1
  13. package/dist/components/layouts/app_user_data_test/index.d.ts +6 -0
  14. package/dist/components/layouts/app_user_data_test/index.d.ts.map +1 -0
  15. package/dist/components/layouts/app_user_data_test/index.js +145 -0
  16. package/dist/components/layouts/shared/components/password_field.js +1 -1
  17. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
  18. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +2 -2
  19. package/dist/components/layouts/shared/components/two_column_auth_layout.js +1 -1
  20. package/dist/lib/auth/auth_types.d.ts +1 -0
  21. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  22. package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
  23. package/dist/lib/auth/hazo_get_auth.server.js +24 -0
  24. package/dist/lib/services/app_user_data_service.d.ts +34 -0
  25. package/dist/lib/services/app_user_data_service.d.ts.map +1 -0
  26. package/dist/lib/services/app_user_data_service.js +228 -0
  27. package/dist/lib/services/index.d.ts +1 -0
  28. package/dist/lib/services/index.d.ts.map +1 -1
  29. package/dist/lib/services/index.js +1 -0
  30. package/dist/server/routes/app_user_data.d.ts +2 -0
  31. package/dist/server/routes/app_user_data.d.ts.map +1 -0
  32. package/dist/server/routes/app_user_data.js +2 -0
  33. package/dist/server/routes/index.d.ts +1 -0
  34. package/dist/server/routes/index.d.ts.map +1 -1
  35. package/dist/server/routes/index.js +2 -0
  36. package/dist/server_pages/forgot_password.d.ts.map +1 -1
  37. package/dist/server_pages/forgot_password.js +3 -2
  38. package/dist/server_pages/login.d.ts.map +1 -1
  39. package/dist/server_pages/login.js +8 -7
  40. package/dist/server_pages/my_settings.d.ts.map +1 -1
  41. package/dist/server_pages/my_settings.js +3 -2
  42. package/dist/server_pages/register.d.ts.map +1 -1
  43. package/dist/server_pages/register.js +3 -2
  44. package/dist/server_pages/reset_password.d.ts.map +1 -1
  45. package/dist/server_pages/reset_password.js +3 -2
  46. package/dist/server_pages/verify_email.d.ts.map +1 -1
  47. package/dist/server_pages/verify_email.js +3 -2
  48. 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:**
@@ -398,16 +399,44 @@ Run the following SQL scripts in your PostgreSQL database:
398
399
  ```sql
399
400
  -- Enum type for profile picture source
400
401
  CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
402
+
403
+ -- Scope types enum (for HRBAC)
404
+ CREATE TYPE hazo_enum_scope_types AS ENUM (
405
+ 'hazo_scopes_l1', 'hazo_scopes_l2', 'hazo_scopes_l3',
406
+ 'hazo_scopes_l4', 'hazo_scopes_l5', 'hazo_scopes_l6', 'hazo_scopes_l7'
407
+ );
408
+ ```
409
+
410
+ #### 2. Create the Organization Table (Multi-Tenancy)
411
+
412
+ ```sql
413
+ -- Organization table for multi-tenancy (create before hazo_users)
414
+ CREATE TABLE hazo_org (
415
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
416
+ name TEXT NOT NULL,
417
+ parent_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
418
+ root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
419
+ user_limit INTEGER NOT NULL DEFAULT 0,
420
+ active BOOLEAN NOT NULL DEFAULT TRUE,
421
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
422
+ created_by UUID, -- Will reference hazo_users after it's created
423
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
424
+ changed_by UUID
425
+ );
426
+
427
+ CREATE INDEX idx_hazo_org_parent_org_id ON hazo_org(parent_org_id);
428
+ CREATE INDEX idx_hazo_org_root_org_id ON hazo_org(root_org_id);
429
+ CREATE INDEX idx_hazo_org_active ON hazo_org(active);
401
430
  ```
402
431
 
403
- #### 2. Create the Users Table
432
+ #### 3. Create the Users Table
404
433
 
405
434
  ```sql
406
435
  -- Main users table
407
436
  CREATE TABLE hazo_users (
408
437
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
409
438
  email_address TEXT NOT NULL UNIQUE,
410
- password_hash TEXT NOT NULL,
439
+ password_hash TEXT, -- NULL for OAuth-only users
411
440
  name TEXT,
412
441
  email_verified BOOLEAN NOT NULL DEFAULT FALSE,
413
442
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
@@ -417,17 +446,32 @@ CREATE TABLE hazo_users (
417
446
  profile_source hazo_enum_profile_source_enum,
418
447
  mfa_secret TEXT,
419
448
  url_on_logon TEXT,
449
+ user_type TEXT, -- Optional user categorization
450
+ google_id TEXT UNIQUE, -- Google OAuth ID
451
+ auth_providers TEXT DEFAULT 'email', -- 'email', 'google', or 'email,google'
452
+ org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
453
+ root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
420
454
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
421
455
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
422
456
  );
423
457
 
424
- -- Index for email lookups
458
+ -- Indexes
425
459
  CREATE INDEX idx_hazo_users_email ON hazo_users(email_address);
460
+ CREATE INDEX idx_hazo_users_user_type ON hazo_users(user_type);
461
+ CREATE UNIQUE INDEX idx_hazo_users_google_id ON hazo_users(google_id);
462
+ CREATE INDEX idx_hazo_users_org_id ON hazo_users(org_id);
463
+ CREATE INDEX idx_hazo_users_root_org_id ON hazo_users(root_org_id);
464
+
465
+ -- Add FK constraints to hazo_org after hazo_users exists
466
+ ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_created_by
467
+ FOREIGN KEY (created_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
468
+ ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_changed_by
469
+ FOREIGN KEY (changed_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
426
470
  ```
427
471
 
428
472
  **Note:** The `url_on_logon` field is used to store a custom redirect URL for users after successful login. This allows per-user customization of post-login navigation.
429
473
 
430
- #### 3. Create the Refresh Tokens Table
474
+ #### 4. Create the Refresh Tokens Table
431
475
 
432
476
  ```sql
433
477
  -- Refresh tokens table (used for password reset, email verification, etc.)
@@ -445,7 +489,7 @@ CREATE INDEX idx_hazo_refresh_tokens_user_id ON hazo_refresh_tokens(user_id);
445
489
  CREATE INDEX idx_hazo_refresh_tokens_token_type ON hazo_refresh_tokens(token_type);
446
490
  ```
447
491
 
448
- #### 4. Create the Permissions Table
492
+ #### 5. Create the Permissions Table
449
493
 
450
494
  ```sql
451
495
  -- Permissions table for RBAC
@@ -458,7 +502,7 @@ CREATE TABLE hazo_permissions (
458
502
  );
459
503
  ```
460
504
 
461
- #### 5. Create the Roles Table
505
+ #### 6. Create the Roles Table
462
506
 
463
507
  ```sql
464
508
  -- Roles table for RBAC
@@ -470,7 +514,7 @@ CREATE TABLE hazo_roles (
470
514
  );
471
515
  ```
472
516
 
473
- #### 6. Create the Role-Permissions Junction Table
517
+ #### 7. Create the Role-Permissions Junction Table
474
518
 
475
519
  ```sql
476
520
  -- Junction table linking roles to permissions
@@ -487,7 +531,7 @@ CREATE INDEX idx_hazo_role_permissions_role_id ON hazo_role_permissions(role_id)
487
531
  CREATE INDEX idx_hazo_role_permissions_permission_id ON hazo_role_permissions(permission_id);
488
532
  ```
489
533
 
490
- #### 7. Create the User-Roles Junction Table
534
+ #### 8. Create the User-Roles Junction Table
491
535
 
492
536
  ```sql
493
537
  -- Junction table linking users to roles
@@ -513,14 +557,35 @@ For convenience, here's the complete SQL script to create all tables at once:
513
557
  -- hazo_auth Database Setup Script (PostgreSQL)
514
558
  -- ============================================
515
559
 
516
- -- 1. Create enum type
560
+ -- 1. Create enum types
517
561
  CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
562
+ CREATE TYPE hazo_enum_scope_types AS ENUM (
563
+ 'hazo_scopes_l1', 'hazo_scopes_l2', 'hazo_scopes_l3',
564
+ 'hazo_scopes_l4', 'hazo_scopes_l5', 'hazo_scopes_l6', 'hazo_scopes_l7'
565
+ );
518
566
 
519
- -- 2. Create users table
567
+ -- 2. Create organization table (multi-tenancy)
568
+ CREATE TABLE hazo_org (
569
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
570
+ name TEXT NOT NULL,
571
+ parent_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
572
+ root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
573
+ user_limit INTEGER NOT NULL DEFAULT 0,
574
+ active BOOLEAN NOT NULL DEFAULT TRUE,
575
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
576
+ created_by UUID,
577
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
578
+ changed_by UUID
579
+ );
580
+ CREATE INDEX idx_hazo_org_parent_org_id ON hazo_org(parent_org_id);
581
+ CREATE INDEX idx_hazo_org_root_org_id ON hazo_org(root_org_id);
582
+ CREATE INDEX idx_hazo_org_active ON hazo_org(active);
583
+
584
+ -- 3. Create users table
520
585
  CREATE TABLE hazo_users (
521
586
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
522
587
  email_address TEXT NOT NULL UNIQUE,
523
- password_hash TEXT NOT NULL,
588
+ password_hash TEXT,
524
589
  name TEXT,
525
590
  email_verified BOOLEAN NOT NULL DEFAULT FALSE,
526
591
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
@@ -530,12 +595,27 @@ CREATE TABLE hazo_users (
530
595
  profile_source hazo_enum_profile_source_enum,
531
596
  mfa_secret TEXT,
532
597
  url_on_logon TEXT,
598
+ user_type TEXT,
599
+ google_id TEXT UNIQUE,
600
+ auth_providers TEXT DEFAULT 'email',
601
+ org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
602
+ root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
533
603
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
534
604
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
535
605
  );
536
606
  CREATE INDEX idx_hazo_users_email ON hazo_users(email_address);
537
-
538
- -- 3. Create refresh tokens table
607
+ CREATE INDEX idx_hazo_users_user_type ON hazo_users(user_type);
608
+ CREATE UNIQUE INDEX idx_hazo_users_google_id ON hazo_users(google_id);
609
+ CREATE INDEX idx_hazo_users_org_id ON hazo_users(org_id);
610
+ CREATE INDEX idx_hazo_users_root_org_id ON hazo_users(root_org_id);
611
+
612
+ -- Add FK constraints to hazo_org after hazo_users exists
613
+ ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_created_by
614
+ FOREIGN KEY (created_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
615
+ ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_changed_by
616
+ FOREIGN KEY (changed_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
617
+
618
+ -- 4. Create refresh tokens table
539
619
  CREATE TABLE hazo_refresh_tokens (
540
620
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
541
621
  user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
@@ -1101,6 +1181,8 @@ vertical_center = auto # 'auto' enables vertical centering when navbar is prese
1101
1181
 
1102
1182
  ### Authentication Page Navbar
1103
1183
 
1184
+ **The navbar now works automatically** - zero-config server page components include the navbar based on configuration without manual wrapping.
1185
+
1104
1186
  When using `layout_mode = standalone`, you can enable a configurable navbar that appears on all auth pages:
1105
1187
 
1106
1188
  ```ini
@@ -1120,7 +1202,17 @@ height = 64 # Navbar height in pixels
1120
1202
 
1121
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.
1122
1204
 
1123
- **Customize via props:**
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):**
1124
1216
  ```typescript
1125
1217
  import { LoginLayout } from "hazo_auth/components/layouts/login";
1126
1218
 
@@ -1139,6 +1231,8 @@ export default function Page() {
1139
1231
 
1140
1232
  **Disable for specific pages:**
1141
1233
  ```typescript
1234
+ <LoginPage disableNavbar={true} />
1235
+ // OR for layout components:
1142
1236
  <LoginLayout navbar={{ enable_navbar: false }} />
1143
1237
  ```
1144
1238
 
@@ -1987,6 +2081,180 @@ For full documentation, see `CLAUDE.md` or `TECHDOC.md`.
1987
2081
 
1988
2082
  ---
1989
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
+
1990
2258
  ## User Profile Service
1991
2259
 
1992
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.
@@ -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 type (drop first if it exists to avoid conflicts)
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 NOT NULL,
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 6 tables exist: `hazo_users`, `hazo_refresh_tokens`, `hazo_permissions`, `hazo_roles`, `hazo_role_permissions`, `hazo_user_roles`
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
- org TEXT NOT NULL,
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 idx_hazo_scopes_l1_org ON hazo_scopes_l1(org);
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
- org TEXT NOT NULL,
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 idx_hazo_scopes_l2_org ON hazo_scopes_l2(org);
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
- org TEXT NOT NULL,
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 idx_hazo_scopes_l3_org ON hazo_scopes_l3(org);
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
- org TEXT NOT NULL,
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 idx_hazo_scopes_l4_org ON hazo_scopes_l4(org);
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
- org TEXT NOT NULL,
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 idx_hazo_scopes_l5_org ON hazo_scopes_l5(org);
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
- org TEXT NOT NULL,
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 idx_hazo_scopes_l6_org ON hazo_scopes_l6(org);
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
- org TEXT NOT NULL,
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 idx_hazo_scopes_l7_org ON hazo_scopes_l7(org);
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
- org TEXT NOT NULL,
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(org, scope_type)
1444
+ UNIQUE(org_id, scope_type)
1362
1445
  );
1363
- CREATE INDEX idx_hazo_scope_labels_org ON hazo_scope_labels(org);
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;