hazo_auth 5.1.30 → 5.1.33

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 (81) hide show
  1. package/README.md +79 -3
  2. package/SETUP_CHECKLIST.md +76 -4
  3. package/cli-src/cli/init.ts +35 -8
  4. package/cli-src/lib/auth/auth_types.ts +1 -0
  5. package/cli-src/lib/auth/hazo_get_auth.server.ts +1 -0
  6. package/cli-src/lib/auth/nextauth_config.ts +3 -3
  7. package/cli-src/lib/config/default_config.ts +21 -0
  8. package/cli-src/lib/oauth_config.server.ts +18 -0
  9. package/cli-src/lib/relationships_config.server.ts +47 -0
  10. package/cli-src/lib/schema/sqlite_schema.ts +21 -0
  11. package/cli-src/lib/services/relationship_service.ts +563 -0
  12. package/cli-src/lib/services/session_token_service.ts +15 -8
  13. package/cli-src/lib/utils/get_origin_url.ts +61 -0
  14. package/cli-src/server/types/app_types.ts +2 -2
  15. package/config/hazo_auth_config.example.ini +52 -0
  16. package/dist/cli/init.d.ts.map +1 -1
  17. package/dist/cli/init.js +29 -3
  18. package/dist/components/layouts/dev_lock/index.d.ts.map +1 -1
  19. package/dist/components/layouts/dev_lock/index.js +9 -4
  20. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
  21. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +2 -2
  22. package/dist/components/ui/button.d.ts +1 -1
  23. package/dist/lib/auth/auth_types.d.ts +1 -0
  24. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  25. package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
  26. package/dist/lib/auth/hazo_get_auth.server.js +1 -0
  27. package/dist/lib/auth/nextauth_config.js +3 -3
  28. package/dist/lib/config/default_config.d.ts +36 -0
  29. package/dist/lib/config/default_config.d.ts.map +1 -1
  30. package/dist/lib/config/default_config.js +20 -0
  31. package/dist/lib/oauth_config.server.d.ts +4 -0
  32. package/dist/lib/oauth_config.server.d.ts.map +1 -1
  33. package/dist/lib/oauth_config.server.js +4 -0
  34. package/dist/lib/relationships_config.server.d.ts +19 -0
  35. package/dist/lib/relationships_config.server.d.ts.map +1 -0
  36. package/dist/lib/relationships_config.server.js +26 -0
  37. package/dist/lib/schema/sqlite_schema.d.ts +1 -1
  38. package/dist/lib/schema/sqlite_schema.d.ts.map +1 -1
  39. package/dist/lib/schema/sqlite_schema.js +21 -0
  40. package/dist/lib/services/relationship_service.d.ts +124 -0
  41. package/dist/lib/services/relationship_service.d.ts.map +1 -0
  42. package/dist/lib/services/relationship_service.js +431 -0
  43. package/dist/lib/services/session_token_service.d.ts +3 -1
  44. package/dist/lib/services/session_token_service.d.ts.map +1 -1
  45. package/dist/lib/services/session_token_service.js +9 -6
  46. package/dist/lib/utils/get_origin_url.d.ts +23 -0
  47. package/dist/lib/utils/get_origin_url.d.ts.map +1 -0
  48. package/dist/lib/utils/get_origin_url.js +57 -0
  49. package/dist/page_components/forgot_password.js +2 -2
  50. package/dist/page_components/login.js +2 -2
  51. package/dist/page_components/register.js +1 -1
  52. package/dist/page_components/reset_password.js +2 -2
  53. package/dist/page_components/verify_email.js +2 -2
  54. package/dist/server/routes/index.d.ts +4 -0
  55. package/dist/server/routes/index.d.ts.map +1 -1
  56. package/dist/server/routes/index.js +5 -0
  57. package/dist/server/routes/me.d.ts.map +1 -1
  58. package/dist/server/routes/me.js +10 -1
  59. package/dist/server/routes/nextauth.d.ts.map +1 -1
  60. package/dist/server/routes/nextauth.js +36 -11
  61. package/dist/server/routes/oauth_google_callback.d.ts.map +1 -1
  62. package/dist/server/routes/oauth_google_callback.js +11 -10
  63. package/dist/server/routes/pin_login.d.ts +17 -0
  64. package/dist/server/routes/pin_login.d.ts.map +1 -0
  65. package/dist/server/routes/pin_login.js +123 -0
  66. package/dist/server/routes/relationship_self.d.ts +13 -0
  67. package/dist/server/routes/relationship_self.d.ts.map +1 -0
  68. package/dist/server/routes/relationship_self.js +59 -0
  69. package/dist/server/routes/relationship_upgrade.d.ts +13 -0
  70. package/dist/server/routes/relationship_upgrade.d.ts.map +1 -0
  71. package/dist/server/routes/relationship_upgrade.js +66 -0
  72. package/dist/server/routes/relationships.d.ts +42 -0
  73. package/dist/server/routes/relationships.d.ts.map +1 -0
  74. package/dist/server/routes/relationships.js +217 -0
  75. package/dist/server/routes/remove_profile_picture.d.ts.map +1 -1
  76. package/dist/server/routes/remove_profile_picture.js +22 -1
  77. package/dist/server/routes/upload_profile_picture.d.ts.map +1 -1
  78. package/dist/server/routes/upload_profile_picture.js +25 -4
  79. package/dist/server/types/app_types.d.ts +2 -2
  80. package/dist/server/types/app_types.d.ts.map +1 -1
  81. package/package.json +121 -37
package/README.md CHANGED
@@ -150,10 +150,21 @@ See [CHANGE_LOG.md](./CHANGE_LOG.md) for detailed migration guide, rationale, an
150
150
 
151
151
  ```bash
152
152
  # Install hazo_auth and required peer dependencies
153
- npm install hazo_auth hazo_config hazo_connect hazo_logs
153
+ npm install hazo_auth hazo_config hazo_connect hazo_logs next react react-dom next-auth
154
+
155
+ # UI peer dependencies (required if using hazo_auth UI components)
156
+ npm install hazo_ui lucide-react sonner next-themes @radix-ui/react-accordion @radix-ui/react-alert-dialog @radix-ui/react-avatar @radix-ui/react-checkbox @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-hover-card @radix-ui/react-label @radix-ui/react-select @radix-ui/react-separator @radix-ui/react-slot @radix-ui/react-switch @radix-ui/react-tabs @radix-ui/react-tooltip
154
157
  ```
155
158
 
156
- **Note:** `hazo_config`, `hazo_connect`, and `hazo_logs` are required peer dependencies.
159
+ **Note:** `next`, `react`, `react-dom`, `next-auth`, and UI packages are peer dependencies. Your consuming project controls the versions, preventing duplication and type conflicts.
160
+
161
+ **Next.js config:** If using password features, add `argon2` to `serverExternalPackages` in your `next.config.mjs`:
162
+
163
+ ```javascript
164
+ const nextConfig = {
165
+ serverExternalPackages: ['argon2'],
166
+ };
167
+ ```
157
168
 
158
169
  ---
159
170
 
@@ -163,7 +174,7 @@ The fastest way to get started is using the CLI commands:
163
174
 
164
175
  ```bash
165
176
  # 1. Install the package and peer dependencies
166
- npm install hazo_auth hazo_config hazo_connect hazo_logs
177
+ npm install hazo_auth hazo_config hazo_connect hazo_logs next react react-dom next-auth
167
178
 
168
179
  # 2. Initialize your project (directories, config, database, images)
169
180
  npx hazo_auth init
@@ -1153,6 +1164,12 @@ Google OAuth adds one new dependency:
1153
1164
  - Verify `/api/hazo_auth/oauth/google/callback` route exists
1154
1165
  - Check server logs for errors during session creation
1155
1166
 
1167
+ **OAuth redirect goes to localhost behind a reverse proxy (Cloudflare Tunnel, nginx):**
1168
+ - Set `NEXTAUTH_URL` to your public domain (e.g., `https://gotimer.org`)
1169
+ - hazo_auth automatically rewrites the request URL so NextAuth constructs the correct `redirect_uri`
1170
+ - Verify `AUTH_TRUST_HOST=true` is set in your environment
1171
+ - No `next.config` changes needed — the fix is built into `hazo_auth/server/routes`
1172
+
1156
1173
  **404 after Google OAuth login (v5.1.16+ fix):**
1157
1174
  - If users get 404 after Google OAuth, the `hazo_invitations` table may be missing
1158
1175
  - **Option 1:** Run migration `009_scope_consolidation.sql` to create the table
@@ -2731,6 +2748,65 @@ For full documentation, see `CHANGE_LOG.md` and `TECHDOC.md`.
2731
2748
 
2732
2749
  ---
2733
2750
 
2751
+ ## Relationship Accounts (Managed Sub-Profiles)
2752
+
2753
+ hazo_auth supports managed sub-profiles where a parent user can create and manage child accounts that don't require email addresses. This is useful for apps like educational platforms (parent managing kids), classrooms (teacher managing students), or care facilities (caregiver managing patients).
2754
+
2755
+ ### Enabling
2756
+
2757
+ The feature is opt-in. Add to your `hazo_auth_config.ini`:
2758
+
2759
+ ```ini
2760
+ [hazo_auth__relationships]
2761
+ enabled = true
2762
+ allowed_types = parent, teacher, manager, caregiver
2763
+ default_type = parent
2764
+ max_children_per_user = 10
2765
+ pin_min_length = 4
2766
+ pin_max_length = 6
2767
+ ```
2768
+
2769
+ Run the migration: `npm run migrate migrations/013_relationships.sql`
2770
+
2771
+ ### API Endpoints
2772
+
2773
+ | Method | Endpoint | Description |
2774
+ |--------|----------|-------------|
2775
+ | GET | `/api/hazo_auth/relationships` | List managed child profiles |
2776
+ | POST | `/api/hazo_auth/relationships` | Create child profile (body: `{ name, pin?, relationship_type? }`) |
2777
+ | PATCH | `/api/hazo_auth/relationships` | Update relationship permissions |
2778
+ | DELETE | `/api/hazo_auth/relationships?relationship_id=X` | Delete relationship |
2779
+ | POST | `/api/hazo_auth/relationships/self` | Create self-relationship |
2780
+ | POST | `/api/hazo_auth/relationships/upgrade` | Upgrade managed user to standalone account |
2781
+ | POST | `/api/hazo_auth/pin-login` | PIN-based login for managed profiles |
2782
+
2783
+ ### PIN Login
2784
+
2785
+ Managed profiles can authenticate via a simple PIN on shared devices:
2786
+
2787
+ ```typescript
2788
+ const res = await fetch("/api/hazo_auth/pin-login", {
2789
+ method: "POST",
2790
+ headers: { "Content-Type": "application/json" },
2791
+ body: JSON.stringify({
2792
+ parent_user_id: "parent-uuid",
2793
+ child_user_id: "child-uuid",
2794
+ pin: "1234",
2795
+ }),
2796
+ });
2797
+ // Response includes managed_by_user_id in the session
2798
+ ```
2799
+
2800
+ ### Profile Picture Support
2801
+
2802
+ Parents can manage child profile pictures via the existing upload routes by passing `?target_user_id=child-uuid`. The relationship's `can_edit_profile` permission is checked.
2803
+
2804
+ ### Feature Gating
2805
+
2806
+ When `enabled = false` (default), all relationship routes return 404, email remains required for all users, and existing behavior is completely unchanged.
2807
+
2808
+ ---
2809
+
2734
2810
  ## User Profile Service
2735
2811
 
2736
2812
  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.
@@ -7,8 +7,8 @@ This checklist provides step-by-step instructions for setting up the `hazo_auth`
7
7
  The fastest way to set up hazo_auth:
8
8
 
9
9
  ```bash
10
- # 1. Install the package and peer dependencies
11
- npm install hazo_auth hazo_config hazo_connect hazo_logs
10
+ # 1. Install the package and required peer dependencies
11
+ npm install hazo_auth hazo_config hazo_connect hazo_logs next react react-dom next-auth
12
12
 
13
13
  # 2. Initialize project (creates directories, config files, database, images)
14
14
  npx hazo_auth init
@@ -69,10 +69,27 @@ node --version
69
69
  ### Step 1.1: Install the package and peer dependencies
70
70
 
71
71
  ```bash
72
- npm install hazo_auth hazo_config hazo_connect hazo_logs
72
+ # Required peer dependencies
73
+ npm install hazo_auth hazo_config hazo_connect hazo_logs next react react-dom next-auth
74
+
75
+ # UI peer dependencies (required if using hazo_auth UI components)
76
+ npm install hazo_ui lucide-react sonner next-themes @radix-ui/react-accordion @radix-ui/react-alert-dialog @radix-ui/react-avatar @radix-ui/react-checkbox @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-hover-card @radix-ui/react-label @radix-ui/react-select @radix-ui/react-separator @radix-ui/react-slot @radix-ui/react-switch @radix-ui/react-tabs @radix-ui/react-tooltip
77
+ ```
78
+
79
+ **Note (v5.2.0+):** `next`, `react`, `react-dom`, `next-auth`, and UI packages are peer dependencies. You must install them in your consuming project. This ensures a single copy of each package is used, preventing version conflicts and bundle duplication.
80
+
81
+ **Next.js config for server-only packages:**
82
+
83
+ If using password features (login, register, password reset), add `argon2` to your `next.config.mjs`:
84
+
85
+ ```javascript
86
+ const nextConfig = {
87
+ serverExternalPackages: ['argon2'],
88
+ };
89
+ export default nextConfig;
73
90
  ```
74
91
 
75
- **Note (v5.2.0+):** `hazo_config`, `hazo_connect`, and `hazo_logs` are now peer dependencies. You must install them in your project.
92
+ This tells Next.js to load `argon2` at runtime instead of trying to bundle it.
76
93
 
77
94
  **Verify installation:**
78
95
  ```bash
@@ -750,6 +767,11 @@ auto_link_unverified_accounts = true
750
767
  google_button_text = Continue with Google
751
768
  oauth_divider_text = or
752
769
 
770
+ # NextAuth page paths (v5.1.32+)
771
+ # Consumers should set these to their actual login page routes
772
+ # sign_in_page = /login
773
+ # error_page = /login
774
+
753
775
  # Post-Login Redirect Configuration (v5.1.16+)
754
776
  # URL for users who need to create a firm (default: /hazo_auth/create_firm)
755
777
  # create_firm_url = /hazo_auth/create_firm
@@ -1161,6 +1183,56 @@ export async function proxy(request: NextRequest) {
1161
1183
 
1162
1184
  ---
1163
1185
 
1186
+ ## Phase 5.2: Relationship Accounts Setup (Optional)
1187
+
1188
+ If your app needs managed sub-profiles (parent managing children, teacher managing students):
1189
+
1190
+ ### Step 5.2.1: Enable in config
1191
+
1192
+ Add to `config/hazo_auth_config.ini`:
1193
+ ```ini
1194
+ [hazo_auth__relationships]
1195
+ enabled = true
1196
+ allowed_types = parent, teacher, manager, caregiver
1197
+ default_type = parent
1198
+ ```
1199
+
1200
+ ### Step 5.2.2: Run migration
1201
+
1202
+ ```bash
1203
+ npm run migrate migrations/013_relationships.sql
1204
+ ```
1205
+
1206
+ ### Step 5.2.3: Create API routes
1207
+
1208
+ ```bash
1209
+ npx hazo_auth generate-routes
1210
+ ```
1211
+
1212
+ Or manually create:
1213
+
1214
+ `app/api/hazo_auth/relationships/route.ts`:
1215
+ ```typescript
1216
+ export { GET, POST, PATCH, DELETE } from "hazo_auth/server/routes/relationships";
1217
+ ```
1218
+
1219
+ `app/api/hazo_auth/relationships/self/route.ts`:
1220
+ ```typescript
1221
+ export { POST } from "hazo_auth/server/routes/relationship_self";
1222
+ ```
1223
+
1224
+ `app/api/hazo_auth/relationships/upgrade/route.ts`:
1225
+ ```typescript
1226
+ export { POST } from "hazo_auth/server/routes/relationship_upgrade";
1227
+ ```
1228
+
1229
+ `app/api/hazo_auth/pin-login/route.ts`:
1230
+ ```typescript
1231
+ export { POST } from "hazo_auth/server/routes/pin_login";
1232
+ ```
1233
+
1234
+ ---
1235
+
1164
1236
  ## Phase 6: Verification Tests
1165
1237
 
1166
1238
  Run these tests to verify your setup is working correctly.
@@ -244,11 +244,38 @@ export async function handle_init(): Promise<void> {
244
244
  result.skipped.push("auth page images (source not found)");
245
245
  }
246
246
 
247
- // Step 5: Create .gitkeep files for empty directories
247
+ // Step 5: Copy sql-wasm.wasm for SQLite WASM support
248
+ console.log("\n\x1b[1m📦 Copying sql-wasm.wasm...\x1b[0m\n");
249
+
250
+ const wasm_target = path.join(project_root, "public", "sql-wasm.wasm");
251
+ if (fs.existsSync(wasm_target)) {
252
+ console.log(`\x1b[33m[EXISTS]\x1b[0m public/sql-wasm.wasm`);
253
+ result.skipped.push("sql-wasm.wasm");
254
+ } else {
255
+ const wasm_search_paths = [
256
+ path.join(project_root, "node_modules", "sql.js", "dist", "sql-wasm.wasm"),
257
+ path.join(package_root, "..", "..", "node_modules", "sql.js", "dist", "sql-wasm.wasm"),
258
+ path.join(package_root, "node_modules", "sql.js", "dist", "sql-wasm.wasm"),
259
+ ];
260
+ const wasm_source = wasm_search_paths.find((p) => fs.existsSync(p));
261
+
262
+ if (wasm_source) {
263
+ ensure_dir(path.join(project_root, "public"));
264
+ fs.copyFileSync(wasm_source, wasm_target);
265
+ console.log(`\x1b[32m[COPY]\x1b[0m sql-wasm.wasm -> public/sql-wasm.wasm`);
266
+ result.files_copied.push("sql-wasm.wasm");
267
+ } else {
268
+ console.log(`\x1b[33m[SKIP]\x1b[0m sql-wasm.wasm not found — install sql.js or copy manually:`);
269
+ console.log(` cp node_modules/sql.js/dist/sql-wasm.wasm public/`);
270
+ result.skipped.push("sql-wasm.wasm (sql.js not found)");
271
+ }
272
+ }
273
+
274
+ // Step 6: Create .gitkeep files for empty directories
248
275
  console.log("\n\x1b[1m📌 Creating .gitkeep files...\x1b[0m\n");
249
-
276
+
250
277
  const empty_dirs = ["public/profile_pictures/uploads", "data"];
251
-
278
+
252
279
  for (const dir of empty_dirs) {
253
280
  const full_path = path.join(project_root, dir);
254
281
  if (fs.existsSync(full_path)) {
@@ -256,10 +283,10 @@ export async function handle_init(): Promise<void> {
256
283
  console.log(`\x1b[32m[CREATE]\x1b[0m ${dir}/.gitkeep`);
257
284
  }
258
285
  }
259
-
260
- // Step 6: Create .env.local.example template
286
+
287
+ // Step 7: Create .env.local.example template
261
288
  console.log("\n\x1b[1m🔑 Creating environment template...\x1b[0m\n");
262
-
289
+
263
290
  if (create_env_template(project_root)) {
264
291
  console.log(`\x1b[32m[CREATE]\x1b[0m .env.local.example`);
265
292
  result.files_copied.push(".env.local.example");
@@ -267,8 +294,8 @@ export async function handle_init(): Promise<void> {
267
294
  console.log(`\x1b[33m[EXISTS]\x1b[0m .env.local.example`);
268
295
  result.skipped.push(".env.local.example");
269
296
  }
270
-
271
- // Step 7: Create SQLite database with schema
297
+
298
+ // Step 8: Create SQLite database with schema
272
299
  console.log("\n\x1b[1m🗄️ Creating SQLite database...\x1b[0m\n");
273
300
 
274
301
  try {
@@ -10,6 +10,7 @@ export type HazoAuthUser = {
10
10
  email_address: string;
11
11
  is_active: boolean; // Derived from status column: status === 'ACTIVE'
12
12
  profile_picture_url: string | null;
13
+ managed_by_user_id?: string | null;
13
14
  // App-specific user data (JSON object stored as TEXT in database)
14
15
  app_user_data: Record<string, unknown> | null;
15
16
  };
@@ -183,6 +183,7 @@ async function fetch_user_data_from_db(user_id: string): Promise<{
183
183
  is_active: user_db.status === "ACTIVE", // Derived from status column
184
184
  profile_picture_url:
185
185
  (user_db.profile_picture_url as string | null) || null,
186
+ managed_by_user_id: (user_db.managed_by_user_id as string | undefined) || null,
186
187
  app_user_data: parse_app_user_data(user_db.app_user_data),
187
188
  };
188
189
 
@@ -70,9 +70,9 @@ export function get_nextauth_config(): AuthOptions {
70
70
  return {
71
71
  providers,
72
72
  pages: {
73
- // Use hazo_auth login page for sign-in errors
74
- signIn: "/hazo_auth/login",
75
- error: "/hazo_auth/login",
73
+ // Configurable via [hazo_auth__oauth] sign_in_page / error_page in hazo_auth_config.ini
74
+ signIn: oauth_config.sign_in_page,
75
+ error: oauth_config.error_page,
76
76
  },
77
77
  callbacks: {
78
78
  /**
@@ -165,6 +165,10 @@ export const DEFAULT_OAUTH = {
165
165
  google_button_text: "Continue with Google",
166
166
  /** Text displayed on the divider between OAuth and email/password form */
167
167
  oauth_divider_text: "or continue with email",
168
+ /** NextAuth signIn page path (default: /hazo_auth/login) */
169
+ sign_in_page: "/hazo_auth/login",
170
+ /** NextAuth error page path (default: /hazo_auth/login) */
171
+ error_page: "/hazo_auth/login",
168
172
  /** URL for users who need to create a firm (default: /hazo_auth/create_firm) */
169
173
  create_firm_url: "/hazo_auth/create_firm",
170
174
  /** Default redirect after OAuth login for users with scopes */
@@ -221,6 +225,22 @@ export const DEFAULT_MULTI_TENANCY = {
221
225
  default_user_limit: 0,
222
226
  } as const;
223
227
 
228
+ // section: relationships
229
+ export const DEFAULT_RELATIONSHIPS = {
230
+ /** Whether managed sub-profile/relationship accounts are enabled (default: false) */
231
+ enabled: false,
232
+ /** Comma-separated allowed relationship types (default: parent) */
233
+ allowed_types: "parent",
234
+ /** Default relationship type when not specified (default: parent) */
235
+ default_type: "parent",
236
+ /** Maximum number of managed profiles per user (default: 10) */
237
+ max_children_per_user: 10,
238
+ /** Minimum PIN length for managed profile auth (default: 4) */
239
+ pin_min_length: 4,
240
+ /** Maximum PIN length for managed profile auth (default: 6) */
241
+ pin_max_length: 6,
242
+ } as const;
243
+
224
244
  // section: dev_lock
225
245
  export const DEFAULT_DEV_LOCK = {
226
246
  /** Enable the development lock screen (also requires HAZO_AUTH_DEV_LOCK_ENABLED env var) */
@@ -280,6 +300,7 @@ export const HAZO_AUTH_DEFAULTS = {
280
300
  navbar: DEFAULT_NAVBAR,
281
301
  userTypes: DEFAULT_USER_TYPES,
282
302
  multiTenancy: DEFAULT_MULTI_TENANCY,
303
+ relationships: DEFAULT_RELATIONSHIPS,
283
304
  } as const;
284
305
 
285
306
  // section: types
@@ -18,6 +18,10 @@ export type OAuthConfig = {
18
18
  google_button_text: string;
19
19
  /** Text displayed on the divider between OAuth and email/password form */
20
20
  oauth_divider_text: string;
21
+ /** NextAuth signIn page path */
22
+ sign_in_page: string;
23
+ /** NextAuth error page path */
24
+ error_page: string;
21
25
  /** URL for users who need to create a firm (default: /hazo_auth/create_firm) */
22
26
  create_firm_url: string;
23
27
  /** Default redirect after OAuth login for users with scopes */
@@ -68,6 +72,18 @@ export function get_oauth_config(): OAuthConfig {
68
72
  DEFAULT_OAUTH.oauth_divider_text
69
73
  );
70
74
 
75
+ const sign_in_page = get_config_value(
76
+ SECTION_NAME,
77
+ "sign_in_page",
78
+ DEFAULT_OAUTH.sign_in_page
79
+ );
80
+
81
+ const error_page = get_config_value(
82
+ SECTION_NAME,
83
+ "error_page",
84
+ DEFAULT_OAUTH.error_page
85
+ );
86
+
71
87
  const create_firm_url = get_config_value(
72
88
  SECTION_NAME,
73
89
  "create_firm_url",
@@ -98,6 +114,8 @@ export function get_oauth_config(): OAuthConfig {
98
114
  auto_link_unverified_accounts,
99
115
  google_button_text,
100
116
  oauth_divider_text,
117
+ sign_in_page,
118
+ error_page,
101
119
  create_firm_url,
102
120
  default_redirect,
103
121
  skip_invitation_check,
@@ -0,0 +1,47 @@
1
+ // file_description: server-only helper to read relationship accounts configuration from hazo_auth_config.ini
2
+ // section: server-only-guard
3
+ import "server-only";
4
+
5
+ // section: imports
6
+ import { get_config_value, get_config_boolean, get_config_number } from "./config/config_loader.server.js";
7
+ import { DEFAULT_RELATIONSHIPS } from "./config/default_config.js";
8
+
9
+ // section: types
10
+ export type RelationshipsConfig = {
11
+ /** Whether managed sub-profile/relationship accounts are enabled */
12
+ enabled: boolean;
13
+ /** Comma-separated allowed relationship types */
14
+ allowed_types: string;
15
+ /** Default relationship type when not specified */
16
+ default_type: string;
17
+ /** Maximum number of managed profiles per user */
18
+ max_children_per_user: number;
19
+ /** Minimum PIN length for managed profile auth */
20
+ pin_min_length: number;
21
+ /** Maximum PIN length for managed profile auth */
22
+ pin_max_length: number;
23
+ };
24
+
25
+ // section: constants
26
+ const SECTION_NAME = "hazo_auth__relationships";
27
+
28
+ // section: helpers
29
+ export function get_relationships_config(): RelationshipsConfig {
30
+ return {
31
+ enabled: get_config_boolean(SECTION_NAME, "enabled", DEFAULT_RELATIONSHIPS.enabled),
32
+ allowed_types: get_config_value(SECTION_NAME, "allowed_types", DEFAULT_RELATIONSHIPS.allowed_types),
33
+ default_type: get_config_value(SECTION_NAME, "default_type", DEFAULT_RELATIONSHIPS.default_type),
34
+ max_children_per_user: get_config_number(SECTION_NAME, "max_children_per_user", DEFAULT_RELATIONSHIPS.max_children_per_user),
35
+ pin_min_length: get_config_number(SECTION_NAME, "pin_min_length", DEFAULT_RELATIONSHIPS.pin_min_length),
36
+ pin_max_length: get_config_number(SECTION_NAME, "pin_max_length", DEFAULT_RELATIONSHIPS.pin_max_length),
37
+ };
38
+ }
39
+
40
+ export function is_relationships_enabled(): boolean {
41
+ return get_config_boolean(SECTION_NAME, "enabled", DEFAULT_RELATIONSHIPS.enabled);
42
+ }
43
+
44
+ export function get_allowed_relationship_types(): string[] {
45
+ const types_str = get_config_value(SECTION_NAME, "allowed_types", DEFAULT_RELATIONSHIPS.allowed_types);
46
+ return types_str.split(",").map((t) => t.trim()).filter(Boolean);
47
+ }
@@ -24,6 +24,8 @@ CREATE TABLE IF NOT EXISTS hazo_users (
24
24
  user_type TEXT,
25
25
  app_user_data TEXT,
26
26
  status TEXT DEFAULT 'ACTIVE' CHECK(status IN ('PENDING', 'ACTIVE', 'BLOCKED')),
27
+ managed_by_user_id TEXT REFERENCES hazo_users(id) ON DELETE SET NULL,
28
+ pin_hash TEXT,
27
29
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
28
30
  changed_at TEXT NOT NULL DEFAULT (datetime('now'))
29
31
  );
@@ -135,6 +137,25 @@ CREATE INDEX IF NOT EXISTS idx_hazo_invitations_scope ON hazo_invitations(scope_
135
137
  CREATE INDEX IF NOT EXISTS idx_hazo_invitations_status ON hazo_invitations(status);
136
138
  CREATE INDEX IF NOT EXISTS idx_hazo_invitations_expires ON hazo_invitations(expires_at);
137
139
 
140
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_managed_by ON hazo_users(managed_by_user_id);
141
+
142
+ -- Relationships table
143
+ CREATE TABLE IF NOT EXISTS hazo_user_relationships (
144
+ id TEXT PRIMARY KEY,
145
+ parent_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
146
+ child_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
147
+ relationship_type TEXT NOT NULL DEFAULT 'parent',
148
+ can_view_progress INTEGER DEFAULT 1,
149
+ can_edit_profile INTEGER DEFAULT 1,
150
+ can_delete INTEGER DEFAULT 0,
151
+ is_self INTEGER DEFAULT 0,
152
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
153
+ UNIQUE(parent_user_id, child_user_id)
154
+ );
155
+
156
+ CREATE INDEX IF NOT EXISTS idx_hazo_user_relationships_parent ON hazo_user_relationships(parent_user_id);
157
+ CREATE INDEX IF NOT EXISTS idx_hazo_user_relationships_child ON hazo_user_relationships(child_user_id);
158
+
138
159
  -- Firm admin role (for firm creators)
139
160
  INSERT OR IGNORE INTO hazo_roles (id, role_name, created_at, changed_at)
140
161
  VALUES (lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab',abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6))), 'firm_admin', datetime('now'), datetime('now'));