hazo_auth 4.5.6 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +206 -1
  2. package/SETUP_CHECKLIST.md +124 -21
  3. package/cli-src/lib/auth/auth_types.ts +2 -0
  4. package/cli-src/lib/auth/auth_utils.server.ts +9 -10
  5. package/cli-src/lib/auth/dev_lock_validator.edge.ts +5 -4
  6. package/cli-src/lib/auth/hazo_get_auth.server.ts +32 -5
  7. package/cli-src/lib/auth/server_auth.ts +3 -2
  8. package/cli-src/lib/auth/session_token_validator.edge.ts +4 -2
  9. package/cli-src/lib/cookies_config.edge.ts +55 -0
  10. package/cli-src/lib/cookies_config.server.ts +93 -0
  11. package/cli-src/lib/services/app_user_data_service.ts +294 -0
  12. package/cli-src/lib/services/index.ts +1 -0
  13. package/dist/app/api/hazo_auth/app_user_data/route.d.ts +64 -0
  14. package/dist/app/api/hazo_auth/app_user_data/route.d.ts.map +1 -0
  15. package/dist/app/api/hazo_auth/app_user_data/route.js +208 -0
  16. package/dist/app/api/hazo_auth/login/route.d.ts.map +1 -1
  17. package/dist/app/api/hazo_auth/login/route.js +8 -17
  18. package/dist/app/api/hazo_auth/logout/route.d.ts.map +1 -1
  19. package/dist/app/api/hazo_auth/logout/route.js +9 -13
  20. package/dist/app/api/hazo_auth/me/route.d.ts +2 -1
  21. package/dist/app/api/hazo_auth/me/route.d.ts.map +1 -1
  22. package/dist/app/api/hazo_auth/me/route.js +4 -1
  23. package/dist/app/api/hazo_auth/update_user/route.d.ts.map +1 -1
  24. package/dist/app/api/hazo_auth/update_user/route.js +6 -3
  25. package/dist/components/layouts/app_user_data_test/index.d.ts +6 -0
  26. package/dist/components/layouts/app_user_data_test/index.d.ts.map +1 -0
  27. package/dist/components/layouts/app_user_data_test/index.js +145 -0
  28. package/dist/components/layouts/shared/components/password_field.js +1 -1
  29. package/dist/components/layouts/shared/components/profile_pic_menu.d.ts.map +1 -1
  30. package/dist/components/layouts/shared/components/profile_pic_menu.js +49 -43
  31. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
  32. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +2 -2
  33. package/dist/components/layouts/shared/components/two_column_auth_layout.js +1 -1
  34. package/dist/lib/auth/auth_types.d.ts +1 -0
  35. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  36. package/dist/lib/auth/auth_utils.server.d.ts.map +1 -1
  37. package/dist/lib/auth/auth_utils.server.js +9 -10
  38. package/dist/lib/auth/dev_lock_validator.edge.d.ts +2 -1
  39. package/dist/lib/auth/dev_lock_validator.edge.d.ts.map +1 -1
  40. package/dist/lib/auth/dev_lock_validator.edge.js +5 -4
  41. package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
  42. package/dist/lib/auth/hazo_get_auth.server.js +29 -4
  43. package/dist/lib/auth/server_auth.d.ts.map +1 -1
  44. package/dist/lib/auth/server_auth.js +3 -2
  45. package/dist/lib/auth/session_token_validator.edge.d.ts +1 -0
  46. package/dist/lib/auth/session_token_validator.edge.d.ts.map +1 -1
  47. package/dist/lib/auth/session_token_validator.edge.js +4 -2
  48. package/dist/lib/cookies_config.edge.d.ts +33 -0
  49. package/dist/lib/cookies_config.edge.d.ts.map +1 -0
  50. package/dist/lib/cookies_config.edge.js +48 -0
  51. package/dist/lib/cookies_config.server.d.ts +42 -0
  52. package/dist/lib/cookies_config.server.d.ts.map +1 -0
  53. package/dist/lib/cookies_config.server.js +71 -0
  54. package/dist/lib/services/app_user_data_service.d.ts +34 -0
  55. package/dist/lib/services/app_user_data_service.d.ts.map +1 -0
  56. package/dist/lib/services/app_user_data_service.js +228 -0
  57. package/dist/lib/services/index.d.ts +1 -0
  58. package/dist/lib/services/index.d.ts.map +1 -1
  59. package/dist/lib/services/index.js +1 -0
  60. package/dist/server/routes/app_user_data.d.ts +2 -0
  61. package/dist/server/routes/app_user_data.d.ts.map +1 -0
  62. package/dist/server/routes/app_user_data.js +2 -0
  63. package/dist/server/routes/index.d.ts +1 -0
  64. package/dist/server/routes/index.d.ts.map +1 -1
  65. package/dist/server/routes/index.js +2 -0
  66. package/dist/server_pages/forgot_password.d.ts.map +1 -1
  67. package/dist/server_pages/forgot_password.js +3 -2
  68. package/dist/server_pages/login.d.ts.map +1 -1
  69. package/dist/server_pages/login.js +8 -7
  70. package/dist/server_pages/my_settings.d.ts.map +1 -1
  71. package/dist/server_pages/my_settings.js +3 -2
  72. package/dist/server_pages/register.d.ts.map +1 -1
  73. package/dist/server_pages/register.js +3 -2
  74. package/dist/server_pages/reset_password.d.ts.map +1 -1
  75. package/dist/server_pages/reset_password.js +3 -2
  76. package/dist/server_pages/verify_email.d.ts.map +1 -1
  77. package/dist/server_pages/verify_email.js +3 -2
  78. package/hazo_auth_config.example.ini +15 -0
  79. 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:**
@@ -381,6 +382,22 @@ HAZO_AUTH_GOOGLE_CLIENT_SECRET=your_google_client_secret
381
382
 
382
383
  See [Google OAuth Setup](#google-oauth-setup) for detailed instructions.
383
384
 
385
+ **For Cookie Customization (optional):**
386
+ ```env
387
+ # Cookie prefix (prevents conflicts when running multiple apps on localhost)
388
+ HAZO_AUTH_COOKIE_PREFIX=myapp_
389
+
390
+ # Cookie domain (optional, for cross-subdomain sharing)
391
+ HAZO_AUTH_COOKIE_DOMAIN=.example.com
392
+ ```
393
+
394
+ These environment variables are required for Edge Runtime (middleware) when using cookie customization. Also set in `hazo_auth_config.ini`:
395
+ ```ini
396
+ [hazo_auth__cookies]
397
+ cookie_prefix = myapp_
398
+ cookie_domain = .example.com
399
+ ```
400
+
384
401
  **Important:** The configuration files must be located in your project root directory (where `process.cwd()` points to), not inside `node_modules`. The package reads configuration from `process.cwd()` at runtime, so storing them elsewhere (including `node_modules/hazo_auth`) will break runtime access.
385
402
 
386
403
  ---
@@ -1180,6 +1197,8 @@ vertical_center = auto # 'auto' enables vertical centering when navbar is prese
1180
1197
 
1181
1198
  ### Authentication Page Navbar
1182
1199
 
1200
+ **The navbar now works automatically** - zero-config server page components include the navbar based on configuration without manual wrapping.
1201
+
1183
1202
  When using `layout_mode = standalone`, you can enable a configurable navbar that appears on all auth pages:
1184
1203
 
1185
1204
  ```ini
@@ -1199,7 +1218,17 @@ height = 64 # Navbar height in pixels
1199
1218
 
1200
1219
  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
1220
 
1202
- **Customize via props:**
1221
+ **Zero-config usage (recommended):**
1222
+ ```typescript
1223
+ // app/hazo_auth/login/page.tsx
1224
+ import { LoginPage } from "hazo_auth/pages/login";
1225
+
1226
+ export default function Page() {
1227
+ return <LoginPage />; // Navbar appears automatically if enabled in config
1228
+ }
1229
+ ```
1230
+
1231
+ **Customize via props (advanced):**
1203
1232
  ```typescript
1204
1233
  import { LoginLayout } from "hazo_auth/components/layouts/login";
1205
1234
 
@@ -1218,6 +1247,8 @@ export default function Page() {
1218
1247
 
1219
1248
  **Disable for specific pages:**
1220
1249
  ```typescript
1250
+ <LoginPage disableNavbar={true} />
1251
+ // OR for layout components:
1221
1252
  <LoginLayout navbar={{ enable_navbar: false }} />
1222
1253
  ```
1223
1254
 
@@ -2066,6 +2097,180 @@ For full documentation, see `CLAUDE.md` or `TECHDOC.md`.
2066
2097
 
2067
2098
  ---
2068
2099
 
2100
+ ## App User Data (Custom User Metadata)
2101
+
2102
+ 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.
2103
+
2104
+ ### Overview
2105
+
2106
+ - **JSON Storage**: Single TEXT column stores JSON objects (no schema restrictions)
2107
+ - **Deep Merge Support**: PATCH endpoint merges new data with existing data
2108
+ - **Full Replace**: PUT endpoint replaces entire JSON object
2109
+ - **Clear Data**: DELETE endpoint sets field to NULL
2110
+ - **Type-Safe**: TypeScript service layer with validation
2111
+ - **Included in Auth Response**: Available in `/api/hazo_auth/me` response
2112
+
2113
+ ### Quick Start
2114
+
2115
+ 1. **Run database migration**:
2116
+ ```bash
2117
+ npm run migrate migrations/008_add_app_user_data_to_hazo_users.sql
2118
+ ```
2119
+
2120
+ 2. **Create API route** (`app/api/hazo_auth/app_user_data/route.ts`):
2121
+ ```typescript
2122
+ export {
2123
+ appUserDataGET as GET,
2124
+ appUserDataPATCH as PATCH,
2125
+ appUserDataPUT as PUT,
2126
+ appUserDataDELETE as DELETE
2127
+ } from "hazo_auth/server/routes";
2128
+ ```
2129
+
2130
+ Or use CLI: `npx hazo_auth generate-routes`
2131
+
2132
+ 3. **Use in your app**:
2133
+ ```typescript
2134
+ // Store user preferences (deep merge)
2135
+ PATCH /api/hazo_auth/app_user_data
2136
+ {
2137
+ data: {
2138
+ theme: "dark",
2139
+ language: "en-US",
2140
+ sidebar_collapsed: true
2141
+ }
2142
+ }
2143
+
2144
+ // Access in client components
2145
+ const { app_user_data } = use_hazo_auth();
2146
+ console.log(app_user_data?.theme); // "dark"
2147
+ ```
2148
+
2149
+ ### API Endpoints
2150
+
2151
+ **GET `/api/hazo_auth/app_user_data`** - Get current user's data
2152
+ ```typescript
2153
+ Response: { data: { theme: "dark", sidebar_collapsed: true } | null }
2154
+ ```
2155
+
2156
+ **PATCH `/api/hazo_auth/app_user_data`** - Merge with existing data (preserves other fields)
2157
+ ```typescript
2158
+ Request: { data: { theme: "light" } }
2159
+ // If existing: { theme: "dark", sidebar_collapsed: true }
2160
+ // Result: { theme: "light", sidebar_collapsed: true }
2161
+ ```
2162
+
2163
+ **PUT `/api/hazo_auth/app_user_data`** - Replace entire object
2164
+ ```typescript
2165
+ Request: { data: { theme: "light" } }
2166
+ // Result: { theme: "light" } (sidebar_collapsed removed)
2167
+ ```
2168
+
2169
+ **DELETE `/api/hazo_auth/app_user_data`** - Clear all data (sets to NULL)
2170
+
2171
+ ### Service Functions
2172
+
2173
+ ```typescript
2174
+ import {
2175
+ get_app_user_data,
2176
+ update_app_user_data,
2177
+ clear_app_user_data
2178
+ } from "hazo_auth";
2179
+
2180
+ // Get data
2181
+ const data = await get_app_user_data(adapter, user_id);
2182
+
2183
+ // Update with merge (default)
2184
+ await update_app_user_data(adapter, user_id, { theme: "light" }, true);
2185
+
2186
+ // Replace entirely
2187
+ await update_app_user_data(adapter, user_id, { theme: "light" }, false);
2188
+
2189
+ // Clear data
2190
+ await clear_app_user_data(adapter, user_id);
2191
+ ```
2192
+
2193
+ ### Access in `/api/hazo_auth/me`
2194
+
2195
+ The `app_user_data` field is included in the authentication response:
2196
+
2197
+ ```typescript
2198
+ {
2199
+ authenticated: true,
2200
+ user: { ... },
2201
+ permissions: [...],
2202
+ app_user_data: { theme: "dark", sidebar_collapsed: true } | null
2203
+ }
2204
+ ```
2205
+
2206
+ ### Use Cases
2207
+
2208
+ **Store user preferences:**
2209
+ ```typescript
2210
+ {
2211
+ theme: "dark",
2212
+ language: "en-US",
2213
+ timezone: "America/New_York"
2214
+ }
2215
+ ```
2216
+
2217
+ **Store app-specific state:**
2218
+ ```typescript
2219
+ {
2220
+ dashboard_layout: "grid",
2221
+ sidebar_collapsed: true,
2222
+ recent_searches: ["tax forms", "invoices"]
2223
+ }
2224
+ ```
2225
+
2226
+ **Store nested configuration:**
2227
+ ```typescript
2228
+ {
2229
+ notifications: {
2230
+ email: true,
2231
+ sms: false,
2232
+ push: true
2233
+ },
2234
+ privacy: {
2235
+ profile_public: false,
2236
+ show_email: false
2237
+ }
2238
+ }
2239
+ ```
2240
+
2241
+ ### Deep Merge Behavior
2242
+
2243
+ ```typescript
2244
+ // Existing data
2245
+ { user: { name: "Alice", age: 30 }, theme: "dark" }
2246
+
2247
+ // PATCH with
2248
+ { user: { age: 31 }, sidebar: true }
2249
+
2250
+ // Result (deep merge)
2251
+ { user: { name: "Alice", age: 31 }, theme: "dark", sidebar: true }
2252
+ ```
2253
+
2254
+ ### Test Page
2255
+
2256
+ Visit `/hazo_auth/app_user_data_test` in your dev environment to test the API with an interactive UI:
2257
+ - View current data (live refresh)
2258
+ - Merge data (PATCH)
2259
+ - Replace data (PUT)
2260
+ - Clear data (DELETE)
2261
+ - JSON validation
2262
+
2263
+ ### Performance & Limits
2264
+
2265
+ - **Recommended max size**: ~10KB per user (for preferences/settings)
2266
+ - **Storage**: JSON stored as TEXT (no compression)
2267
+ - **Caching**: Benefits from `hazo_get_auth()` LRU cache
2268
+ - **Large datasets**: Use separate tables for complex relational data
2269
+
2270
+ For full documentation, see `CHANGE_LOG.md` and `TECHDOC.md`.
2271
+
2272
+ ---
2273
+
2069
2274
  ## User Profile Service
2070
2275
 
2071
2276
  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.
@@ -197,6 +197,10 @@ HAZO_CONNECT_POSTGREST_API_KEY=your_postgrest_api_key_here
197
197
  # Required for JWT authentication
198
198
  JWT_SECRET=your_secure_random_string_at_least_32_characters
199
199
  # Note: JWT_SECRET is required for JWT session token functionality (Edge-compatible proxy/middleware authentication)
200
+
201
+ # Optional: Cookie customization (prevents conflicts when running multiple apps)
202
+ HAZO_AUTH_COOKIE_PREFIX=myapp_
203
+ HAZO_AUTH_COOKIE_DOMAIN=
200
204
  ```
201
205
 
202
206
  **Generate a secure JWT secret:**
@@ -204,6 +208,18 @@ JWT_SECRET=your_secure_random_string_at_least_32_characters
204
208
  openssl rand -base64 32
205
209
  ```
206
210
 
211
+ **Cookie Customization (Optional):**
212
+ If you're running multiple apps that use hazo_auth on localhost (different ports), set `HAZO_AUTH_COOKIE_PREFIX` to prevent cookie conflicts. For example:
213
+ - App 1 (port 3000): `HAZO_AUTH_COOKIE_PREFIX=app1_`
214
+ - App 2 (port 3001): `HAZO_AUTH_COOKIE_PREFIX=app2_`
215
+
216
+ Also configure in `hazo_auth_config.ini`:
217
+ ```ini
218
+ [hazo_auth__cookies]
219
+ cookie_prefix = myapp_
220
+ cookie_domain =
221
+ ```
222
+
207
223
  ### Step 2.2: Configure email settings
208
224
 
209
225
  Edit `hazo_notify_config.ini`:
@@ -312,15 +328,54 @@ Run this SQL script in your PostgreSQL database:
312
328
  -- Ensure we're in the public schema (or your target schema)
313
329
  SET search_path TO public;
314
330
 
315
- -- Create enum type (drop first if it exists to avoid conflicts)
331
+ -- Create enum types (drop first if they exist to avoid conflicts)
316
332
  DROP TYPE IF EXISTS hazo_enum_profile_source_enum CASCADE;
317
333
  CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
318
334
 
335
+ DROP TYPE IF EXISTS hazo_enum_scope_types CASCADE;
336
+ CREATE TYPE hazo_enum_scope_types AS ENUM (
337
+ 'hazo_scopes_l1', 'hazo_scopes_l2', 'hazo_scopes_l3',
338
+ 'hazo_scopes_l4', 'hazo_scopes_l5', 'hazo_scopes_l6', 'hazo_scopes_l7'
339
+ );
340
+
341
+ DROP TYPE IF EXISTS hazo_enum_notify_chain_status CASCADE;
342
+ CREATE TYPE hazo_enum_notify_chain_status AS ENUM ('draft', 'published', 'inactive');
343
+
344
+ DROP TYPE IF EXISTS hazo_enum_notify_email_type CASCADE;
345
+ CREATE TYPE hazo_enum_notify_email_type AS ENUM ('system', 'user');
346
+
347
+ DROP TYPE IF EXISTS hazo_enum_group_type CASCADE;
348
+ CREATE TYPE hazo_enum_group_type AS ENUM ('support', 'peer', 'group');
349
+
350
+ DROP TYPE IF EXISTS hazo_enum_group_role CASCADE;
351
+ CREATE TYPE hazo_enum_group_role AS ENUM ('client', 'staff', 'owner', 'admin', 'member');
352
+
353
+ DROP TYPE IF EXISTS hazo_enum_chat_type CASCADE;
354
+ CREATE TYPE hazo_enum_chat_type AS ENUM ('chat', 'field', 'project', 'support', 'general');
355
+
356
+ -- Create organization table (multi-tenancy) - MUST be created before hazo_users
357
+ CREATE TABLE hazo_org (
358
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
359
+ name TEXT NOT NULL,
360
+ parent_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
361
+ root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
362
+ user_limit INTEGER NOT NULL DEFAULT 0, -- 0 = unlimited
363
+ active BOOLEAN NOT NULL DEFAULT TRUE,
364
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
365
+ created_by UUID, -- FK added after hazo_users exists
366
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
367
+ changed_by UUID -- FK added after hazo_users exists
368
+ );
369
+ CREATE INDEX idx_hazo_org_parent_org_id ON hazo_org(parent_org_id);
370
+ CREATE INDEX idx_hazo_org_root_org_id ON hazo_org(root_org_id);
371
+ CREATE INDEX idx_hazo_org_active ON hazo_org(active);
372
+ CREATE INDEX idx_hazo_org_name ON hazo_org(name);
373
+
319
374
  -- Create users table
320
375
  CREATE TABLE hazo_users (
321
376
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
322
377
  email_address TEXT NOT NULL UNIQUE,
323
- password_hash TEXT NOT NULL,
378
+ password_hash TEXT, -- NULL for OAuth-only users
324
379
  name TEXT,
325
380
  email_verified BOOLEAN NOT NULL DEFAULT FALSE,
326
381
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
@@ -330,10 +385,25 @@ CREATE TABLE hazo_users (
330
385
  profile_source hazo_enum_profile_source_enum,
331
386
  mfa_secret TEXT,
332
387
  url_on_logon TEXT,
388
+ user_type TEXT, -- Optional user categorization
389
+ google_id TEXT UNIQUE, -- Google OAuth ID
390
+ auth_providers TEXT DEFAULT 'email', -- 'email', 'google', or 'email,google'
391
+ org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
392
+ root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
333
393
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
334
394
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
335
395
  );
336
396
  CREATE INDEX idx_hazo_users_email ON hazo_users(email_address);
397
+ CREATE INDEX idx_hazo_users_user_type ON hazo_users(user_type);
398
+ CREATE UNIQUE INDEX idx_hazo_users_google_id ON hazo_users(google_id);
399
+ CREATE INDEX idx_hazo_users_org_id ON hazo_users(org_id);
400
+ CREATE INDEX idx_hazo_users_root_org_id ON hazo_users(root_org_id);
401
+
402
+ -- Add FK constraints to hazo_org now that hazo_users exists
403
+ ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_created_by
404
+ FOREIGN KEY (created_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
405
+ ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_changed_by
406
+ FOREIGN KEY (changed_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
337
407
 
338
408
  -- Create refresh tokens table
339
409
  CREATE TABLE hazo_refresh_tokens (
@@ -453,7 +523,22 @@ GRANT USAGE ON TYPE hazo_enum_profile_source_enum TO anon, authenticated;
453
523
 
454
524
  **Checklist:**
455
525
  - [ ] 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`
526
+ - [ ] All enum types created (PostgreSQL only):
527
+ - [ ] `hazo_enum_profile_source_enum`
528
+ - [ ] `hazo_enum_scope_types`
529
+ - [ ] `hazo_enum_notify_chain_status`
530
+ - [ ] `hazo_enum_notify_email_type`
531
+ - [ ] `hazo_enum_group_type`
532
+ - [ ] `hazo_enum_group_role`
533
+ - [ ] `hazo_enum_chat_type`
534
+ - [ ] All core tables exist:
535
+ - [ ] `hazo_org` (multi-tenancy - must be created before hazo_users)
536
+ - [ ] `hazo_users` (with google_id, auth_providers, org_id, root_org_id, user_type fields)
537
+ - [ ] `hazo_refresh_tokens`
538
+ - [ ] `hazo_permissions`
539
+ - [ ] `hazo_roles`
540
+ - [ ] `hazo_role_permissions`
541
+ - [ ] `hazo_user_roles`
457
542
 
458
543
  ---
459
544
 
@@ -1244,25 +1329,29 @@ $$ LANGUAGE plpgsql;
1244
1329
  CREATE TABLE hazo_scopes_l1 (
1245
1330
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1246
1331
  seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l1'),
1247
- org TEXT NOT NULL,
1332
+ org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
1333
+ root_org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
1248
1334
  name TEXT NOT NULL,
1249
1335
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1250
1336
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
1251
1337
  );
1252
- CREATE INDEX idx_hazo_scopes_l1_org ON hazo_scopes_l1(org);
1338
+ CREATE INDEX idx_hazo_scopes_l1_org_id ON hazo_scopes_l1(org_id);
1339
+ CREATE INDEX idx_hazo_scopes_l1_root_org_id ON hazo_scopes_l1(root_org_id);
1253
1340
  CREATE INDEX idx_hazo_scopes_l1_seq ON hazo_scopes_l1(seq);
1254
1341
 
1255
1342
  -- Level 2 (parent: L1)
1256
1343
  CREATE TABLE hazo_scopes_l2 (
1257
1344
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1258
1345
  seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l2'),
1259
- 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,
1260
1348
  name TEXT NOT NULL,
1261
1349
  parent_scope_id UUID REFERENCES hazo_scopes_l1(id) ON DELETE CASCADE,
1262
1350
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1263
1351
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
1264
1352
  );
1265
- CREATE INDEX idx_hazo_scopes_l2_org ON hazo_scopes_l2(org);
1353
+ CREATE INDEX idx_hazo_scopes_l2_org_id ON hazo_scopes_l2(org_id);
1354
+ CREATE INDEX idx_hazo_scopes_l2_root_org_id ON hazo_scopes_l2(root_org_id);
1266
1355
  CREATE INDEX idx_hazo_scopes_l2_seq ON hazo_scopes_l2(seq);
1267
1356
  CREATE INDEX idx_hazo_scopes_l2_parent ON hazo_scopes_l2(parent_scope_id);
1268
1357
 
@@ -1270,13 +1359,15 @@ CREATE INDEX idx_hazo_scopes_l2_parent ON hazo_scopes_l2(parent_scope_id);
1270
1359
  CREATE TABLE hazo_scopes_l3 (
1271
1360
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1272
1361
  seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l3'),
1273
- 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,
1274
1364
  name TEXT NOT NULL,
1275
1365
  parent_scope_id UUID REFERENCES hazo_scopes_l2(id) ON DELETE CASCADE,
1276
1366
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1277
1367
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
1278
1368
  );
1279
- CREATE INDEX idx_hazo_scopes_l3_org ON hazo_scopes_l3(org);
1369
+ CREATE INDEX idx_hazo_scopes_l3_org_id ON hazo_scopes_l3(org_id);
1370
+ CREATE INDEX idx_hazo_scopes_l3_root_org_id ON hazo_scopes_l3(root_org_id);
1280
1371
  CREATE INDEX idx_hazo_scopes_l3_seq ON hazo_scopes_l3(seq);
1281
1372
  CREATE INDEX idx_hazo_scopes_l3_parent ON hazo_scopes_l3(parent_scope_id);
1282
1373
 
@@ -1284,13 +1375,15 @@ CREATE INDEX idx_hazo_scopes_l3_parent ON hazo_scopes_l3(parent_scope_id);
1284
1375
  CREATE TABLE hazo_scopes_l4 (
1285
1376
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1286
1377
  seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l4'),
1287
- 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,
1288
1380
  name TEXT NOT NULL,
1289
1381
  parent_scope_id UUID REFERENCES hazo_scopes_l3(id) ON DELETE CASCADE,
1290
1382
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1291
1383
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
1292
1384
  );
1293
- CREATE INDEX idx_hazo_scopes_l4_org ON hazo_scopes_l4(org);
1385
+ CREATE INDEX idx_hazo_scopes_l4_org_id ON hazo_scopes_l4(org_id);
1386
+ CREATE INDEX idx_hazo_scopes_l4_root_org_id ON hazo_scopes_l4(root_org_id);
1294
1387
  CREATE INDEX idx_hazo_scopes_l4_seq ON hazo_scopes_l4(seq);
1295
1388
  CREATE INDEX idx_hazo_scopes_l4_parent ON hazo_scopes_l4(parent_scope_id);
1296
1389
 
@@ -1298,13 +1391,15 @@ CREATE INDEX idx_hazo_scopes_l4_parent ON hazo_scopes_l4(parent_scope_id);
1298
1391
  CREATE TABLE hazo_scopes_l5 (
1299
1392
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1300
1393
  seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l5'),
1301
- 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,
1302
1396
  name TEXT NOT NULL,
1303
1397
  parent_scope_id UUID REFERENCES hazo_scopes_l4(id) ON DELETE CASCADE,
1304
1398
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1305
1399
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
1306
1400
  );
1307
- CREATE INDEX idx_hazo_scopes_l5_org ON hazo_scopes_l5(org);
1401
+ CREATE INDEX idx_hazo_scopes_l5_org_id ON hazo_scopes_l5(org_id);
1402
+ CREATE INDEX idx_hazo_scopes_l5_root_org_id ON hazo_scopes_l5(root_org_id);
1308
1403
  CREATE INDEX idx_hazo_scopes_l5_seq ON hazo_scopes_l5(seq);
1309
1404
  CREATE INDEX idx_hazo_scopes_l5_parent ON hazo_scopes_l5(parent_scope_id);
1310
1405
 
@@ -1312,13 +1407,15 @@ CREATE INDEX idx_hazo_scopes_l5_parent ON hazo_scopes_l5(parent_scope_id);
1312
1407
  CREATE TABLE hazo_scopes_l6 (
1313
1408
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1314
1409
  seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l6'),
1315
- 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,
1316
1412
  name TEXT NOT NULL,
1317
1413
  parent_scope_id UUID REFERENCES hazo_scopes_l5(id) ON DELETE CASCADE,
1318
1414
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1319
1415
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
1320
1416
  );
1321
- CREATE INDEX idx_hazo_scopes_l6_org ON hazo_scopes_l6(org);
1417
+ CREATE INDEX idx_hazo_scopes_l6_org_id ON hazo_scopes_l6(org_id);
1418
+ CREATE INDEX idx_hazo_scopes_l6_root_org_id ON hazo_scopes_l6(root_org_id);
1322
1419
  CREATE INDEX idx_hazo_scopes_l6_seq ON hazo_scopes_l6(seq);
1323
1420
  CREATE INDEX idx_hazo_scopes_l6_parent ON hazo_scopes_l6(parent_scope_id);
1324
1421
 
@@ -1326,13 +1423,15 @@ CREATE INDEX idx_hazo_scopes_l6_parent ON hazo_scopes_l6(parent_scope_id);
1326
1423
  CREATE TABLE hazo_scopes_l7 (
1327
1424
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1328
1425
  seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l7'),
1329
- org TEXT NOT NULL,
1426
+ org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
1427
+ root_org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
1330
1428
  name TEXT NOT NULL,
1331
1429
  parent_scope_id UUID REFERENCES hazo_scopes_l6(id) ON DELETE CASCADE,
1332
1430
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1333
1431
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
1334
1432
  );
1335
- CREATE INDEX idx_hazo_scopes_l7_org ON hazo_scopes_l7(org);
1433
+ CREATE INDEX idx_hazo_scopes_l7_org_id ON hazo_scopes_l7(org_id);
1434
+ CREATE INDEX idx_hazo_scopes_l7_root_org_id ON hazo_scopes_l7(root_org_id);
1336
1435
  CREATE INDEX idx_hazo_scopes_l7_seq ON hazo_scopes_l7(seq);
1337
1436
  CREATE INDEX idx_hazo_scopes_l7_parent ON hazo_scopes_l7(parent_scope_id);
1338
1437
 
@@ -1353,14 +1452,14 @@ CREATE INDEX idx_hazo_user_scopes_scope_type ON hazo_user_scopes(scope_type);
1353
1452
  -- 5. Create scope labels table
1354
1453
  CREATE TABLE hazo_scope_labels (
1355
1454
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1356
- org TEXT NOT NULL,
1455
+ org_id UUID NOT NULL REFERENCES hazo_org(id) ON DELETE CASCADE,
1357
1456
  scope_type hazo_enum_scope_types NOT NULL,
1358
1457
  label TEXT NOT NULL,
1359
1458
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1360
1459
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1361
- UNIQUE(org, scope_type)
1460
+ UNIQUE(org_id, scope_type)
1362
1461
  );
1363
- CREATE INDEX idx_hazo_scope_labels_org ON hazo_scope_labels(org);
1462
+ CREATE INDEX idx_hazo_scope_labels_org_id ON hazo_scope_labels(org_id);
1364
1463
  ```
1365
1464
 
1366
1465
  #### PostgreSQL Grant Scripts
@@ -1522,7 +1621,11 @@ sqlite3 data/hazo_auth.sqlite ".tables" | grep -E "hazo_scopes|hazo_user_scopes|
1522
1621
  **HRBAC Checklist:**
1523
1622
  - [ ] `enable_hrbac = true` in config
1524
1623
  - [ ] HRBAC permissions added to defaults
1525
- - [ ] All 9 HRBAC tables created
1624
+ - [ ] All 9 HRBAC tables created:
1625
+ - [ ] `hazo_scopes_l1` through `hazo_scopes_l7` (with org_id, root_org_id FKs)
1626
+ - [ ] `hazo_user_scopes` (junction table)
1627
+ - [ ] `hazo_scope_labels` (custom labels per org)
1628
+ - [ ] Scope ID generator function created (PostgreSQL)
1526
1629
  - [ ] Grants applied (PostgreSQL)
1527
1630
  - [ ] HRBAC tabs visible in User Management
1528
1631
  - [ ] 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;
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from "next/server";
4
4
  import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
5
5
  import { createCrudService } from "hazo_connect/server";
6
6
  import { map_db_source_to_ui } from "../services/profile_picture_source_mapper.js";
7
+ import { get_cookie_name, get_cookie_options, BASE_COOKIE_NAMES } from "../cookies_config.server.js";
7
8
 
8
9
  // section: types
9
10
  export type AuthUser = {
@@ -24,19 +25,17 @@ export type AuthResult =
24
25
 
25
26
  // section: helpers
26
27
  /**
27
- * Clears authentication cookies from response
28
+ * Clears authentication cookies from response (with configurable prefix and domain)
28
29
  * @param response - NextResponse object to clear cookies from
29
30
  * @returns The response with cleared cookies
30
31
  */
31
32
  function clear_auth_cookies(response: NextResponse): NextResponse {
32
- response.cookies.set("hazo_auth_user_email", "", {
33
- expires: new Date(0),
34
- path: "/",
35
- });
36
- response.cookies.set("hazo_auth_user_id", "", {
33
+ const clear_cookie_options = get_cookie_options({
37
34
  expires: new Date(0),
38
35
  path: "/",
39
36
  });
37
+ response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.USER_EMAIL), "", clear_cookie_options);
38
+ response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.USER_ID), "", clear_cookie_options);
40
39
  return response;
41
40
  }
42
41
 
@@ -48,8 +47,8 @@ function clear_auth_cookies(response: NextResponse): NextResponse {
48
47
  * @returns AuthResult with user info or authenticated: false
49
48
  */
50
49
  export async function get_authenticated_user(request: NextRequest): Promise<AuthResult> {
51
- const user_id = request.cookies.get("hazo_auth_user_id")?.value;
52
- const user_email = request.cookies.get("hazo_auth_user_email")?.value;
50
+ const user_id = request.cookies.get(get_cookie_name(BASE_COOKIE_NAMES.USER_ID))?.value;
51
+ const user_email = request.cookies.get(get_cookie_name(BASE_COOKIE_NAMES.USER_EMAIL))?.value;
53
52
 
54
53
  if (!user_id || !user_email) {
55
54
  return { authenticated: false };
@@ -132,8 +131,8 @@ export async function get_authenticated_user_with_response(request: NextRequest)
132
131
  auth_result: AuthResult;
133
132
  response?: NextResponse;
134
133
  }> {
135
- const user_id = request.cookies.get("hazo_auth_user_id")?.value;
136
- const user_email = request.cookies.get("hazo_auth_user_email")?.value;
134
+ const user_id = request.cookies.get(get_cookie_name(BASE_COOKIE_NAMES.USER_ID))?.value;
135
+ const user_email = request.cookies.get(get_cookie_name(BASE_COOKIE_NAMES.USER_EMAIL))?.value;
137
136
 
138
137
  if (!user_id || !user_email) {
139
138
  return { auth_result: { authenticated: false } };
@@ -2,9 +2,9 @@
2
2
  // Uses Web Crypto API which works in Edge Runtime (no Node.js crypto module)
3
3
  // section: imports
4
4
  import type { NextRequest } from "next/server";
5
+ import { get_cookie_name_edge, BASE_COOKIE_NAMES } from "../cookies_config.edge.js";
5
6
 
6
7
  // section: constants
7
- const COOKIE_NAME = "hazo_auth_dev_lock";
8
8
  const SEPARATOR = "|";
9
9
 
10
10
  // section: types
@@ -105,7 +105,7 @@ export async function validate_dev_lock_cookie(
105
105
  return { valid: false };
106
106
  }
107
107
 
108
- const cookie = request.cookies.get(COOKIE_NAME)?.value;
108
+ const cookie = request.cookies.get(get_cookie_name_edge(BASE_COOKIE_NAMES.DEV_LOCK))?.value;
109
109
 
110
110
  if (!cookie) {
111
111
  return { valid: false };
@@ -162,10 +162,11 @@ export function validate_dev_lock_password(password: string): boolean {
162
162
  }
163
163
 
164
164
  /**
165
- * Gets the dev lock cookie name
165
+ * Gets the dev lock cookie name (with configurable prefix)
166
166
  * Exported for use in API routes when setting the cookie
167
+ * Uses HAZO_AUTH_COOKIE_PREFIX env var for configurable prefix
167
168
  * @returns Cookie name string
168
169
  */
169
170
  export function get_dev_lock_cookie_name(): string {
170
- return COOKIE_NAME;
171
+ return get_cookie_name_edge(BASE_COOKIE_NAMES.DEV_LOCK);
171
172
  }