hazo_auth 1.0.5 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +50 -0
  2. package/hazo_auth_config.example.ini +41 -0
  3. package/instrumentation.ts +2 -2
  4. package/package.json +2 -1
  5. package/scripts/init_users.ts +378 -0
  6. package/src/app/api/hazo_auth/login/route.ts +27 -1
  7. package/src/app/api/hazo_auth/register/route.ts +13 -10
  8. package/src/app/hazo_auth/forgot_password/page.tsx +3 -3
  9. package/src/app/hazo_auth/login/login_page_client.tsx +15 -0
  10. package/src/app/hazo_auth/login/page.tsx +16 -4
  11. package/src/app/hazo_auth/my_settings/page.tsx +3 -3
  12. package/src/app/hazo_auth/register/page.tsx +14 -4
  13. package/src/app/hazo_auth/register/register_page_client.tsx +9 -0
  14. package/src/app/hazo_auth/reset_password/page.tsx +3 -3
  15. package/src/app/hazo_auth/user_management/page.tsx +3 -3
  16. package/src/app/hazo_auth/verify_email/page.tsx +3 -3
  17. package/src/components/layouts/login/hooks/use_login_form.ts +13 -8
  18. package/src/components/layouts/login/index.tsx +28 -0
  19. package/src/components/layouts/register/hooks/use_register_form.ts +4 -1
  20. package/src/components/layouts/register/index.tsx +18 -0
  21. package/src/components/layouts/shared/components/auth_page_shell.tsx +36 -0
  22. package/src/components/layouts/shared/components/standalone_layout_wrapper.tsx +53 -0
  23. package/src/lib/config/config_loader.server.ts +20 -5
  24. package/src/lib/login_config.server.ts +25 -0
  25. package/src/lib/register_config.server.ts +17 -1
  26. package/src/lib/services/login_service.ts +19 -3
  27. package/src/lib/services/registration_service.ts +25 -4
  28. package/src/lib/services/user_update_service.ts +16 -3
  29. package/src/lib/ui_shell_config.server.ts +73 -0
  30. package/src/lib/utils/error_sanitizer.ts +75 -0
package/README.md CHANGED
@@ -50,6 +50,56 @@ Because `src/app/hazo_auth` (pages) and `src/app/api/hazo_auth` (API routes) nee
50
50
 
51
51
  > The package expects these routes to live at `src/app/api/hazo_auth` and `src/app/hazo_auth` inside the consumer project. Without copying or linking them, Next.js won’t mount the auth pages or APIs.
52
52
 
53
+ ### Choose the UI Shell (Test Sidebar vs Standalone)
54
+
55
+ By default, the pages render inside the “test workspace” sidebar so you can quickly preview every flow. When you reuse the routes inside another project you’ll usually want a clean, standalone wrapper instead. Set this in `hazo_auth_config.ini`:
56
+
57
+ ```ini
58
+ [hazo_auth__ui_shell]
59
+ # Options: test_sidebar | standalone
60
+ layout_mode = standalone
61
+ # Optional tweaks for the standalone header wrapper/classes:
62
+ # standalone_heading = Welcome back
63
+ # standalone_description = Your description here
64
+ # standalone_wrapper_class = min-h-screen bg-background py-8
65
+ # standalone_content_class = mx-auto w-full max-w-4xl rounded-2xl border bg-card
66
+ ```
67
+
68
+ - `test_sidebar`: keeps the developer sidebar (perfect for the demo workspace or Storybook screenshots).
69
+ - `standalone`: renders the page body directly so it inherits your own app shell, layout, and theme tokens.
70
+ - The wrapper and content class overrides let you align spacing/borders with your design system without editing package code.
71
+
72
+ Every route (`/hazo_auth/login`, `/hazo_auth/register`, etc.) automatically looks at this config, so switching modes is instant.
73
+
74
+ ### Using Just the Layout Components
75
+
76
+ Prefer to drop the forms into your own routes without copying the provided pages? Import the layouts directly and feed them a `data_client` plus any label/button overrides:
77
+
78
+ ```tsx
79
+ // app/(auth)/login/page.tsx in your project
80
+ import login_layout from "hazo_auth/components/layouts/login";
81
+ import { createLayoutDataClient } from "hazo_auth/components/layouts/shared/data/layout_data_client";
82
+ import { create_sqlite_hazo_connect } from "hazo_auth/lib/hazo_connect_setup";
83
+
84
+ export default async function LoginPage() {
85
+ const hazoConnect = create_sqlite_hazo_connect();
86
+ const dataClient = createLayoutDataClient(hazoConnect);
87
+ const LoginLayout = login_layout;
88
+
89
+ return (
90
+ <div className="my-app-shell">
91
+ <LoginLayout
92
+ image_src="/marketing/login-hero.svg"
93
+ data_client={dataClient}
94
+ redirectRoute="/dashboard"
95
+ />
96
+ </div>
97
+ );
98
+ }
99
+ ```
100
+
101
+ The same import pattern works for every layout under `components/layouts/*`, so you can mix-and-match pieces (profile picture dialog, password field, etc.) wherever you need them.
102
+
53
103
  ## Authentication Service
54
104
 
55
105
  The `hazo_auth` package provides a comprehensive authentication and authorization system with role-based access control (RBAC). The main authentication utility is `hazo_get_auth`, which provides user details, permissions, and permission checking with built-in caching and rate limiting.
@@ -7,6 +7,31 @@
7
7
  # Database type: sqlite, postgrest, supabase, or file
8
8
  type = sqlite
9
9
 
10
+ # UI shell controls whether pages render inside the developer sidebar or a minimal wrapper.
11
+ [hazo_auth__ui_shell]
12
+ # layout_mode = test_sidebar
13
+ # Options:
14
+ # - test_sidebar: keeps the demo sidebar for quick testing in the package workspace.
15
+ # - standalone: renders pages in a minimal wrapper so consumers inherit their own shell/theme.
16
+ #
17
+ # Heading shown above standalone layouts
18
+ # standalone_heading = Welcome to hazo auth
19
+ #
20
+ # Description beneath the heading for standalone layouts
21
+ # standalone_description = Reuse the packaged authentication flows while inheriting your existing app shell styles.
22
+ #
23
+ # Tailwind utility classes applied to the standalone wrapper div
24
+ # standalone_wrapper_class = cls_standalone_shell flex min-h-screen w-full items-center justify-center bg-background px-4 py-10
25
+ #
26
+ # Tailwind utility classes applied to the standalone inner content div
27
+ # standalone_content_class = cls_standalone_shell_content w-full max-w-5xl shadow-xl rounded-2xl border bg-card
28
+ #
29
+ # Show/hide heading in standalone mode (true/false, default: true)
30
+ # standalone_show_heading = true
31
+ #
32
+ # Show/hide description in standalone mode (true/false, default: true)
33
+ # standalone_show_description = true
34
+
10
35
  # SQLite database configuration
11
36
  # Path to SQLite database file (relative to process.cwd() or absolute)
12
37
  sqlite_path = ./data/hazo_auth.sqlite
@@ -57,6 +82,10 @@ enable_admin_ui = true
57
82
  # Show name field (true/false)
58
83
  # Note: This is now deprecated, use hazo_auth__user_fields section instead
59
84
  # show_name_field = false
85
+ #
86
+ # Sign in link shown below register button
87
+ # sign_in_path = /hazo_auth/login
88
+ # sign_in_label = Sign in
60
89
 
61
90
  [hazo_auth__user_fields]
62
91
  # Shared user fields configuration used by register and my_settings layouts
@@ -104,6 +133,12 @@ enable_admin_ui = true
104
133
  # Redirect on successful login (leave empty to show success message instead)
105
134
  # redirect_route_on_successful_login = /
106
135
 
136
+ # Forgot password + sign up links shown under login button
137
+ # forgot_password_path = /hazo_auth/forgot_password
138
+ # forgot_password_label = Forgot password?
139
+ # create_account_path = /hazo_auth/register
140
+ # create_account_label = Create account
141
+
107
142
  # Success message (shown when no redirect route is provided)
108
143
  # success_message = Successfully logged in
109
144
 
@@ -263,6 +298,12 @@ enable_admin_ui = true
263
298
  # Example: application_permission_list_defaults = PERM_ONE,PERM_TWO,PERM_THREE
264
299
  # application_permission_list_defaults =
265
300
 
301
+ [hazo_auth__initial_setup]
302
+ # Initial setup configuration for initializing users, roles, and permissions
303
+ # Email address of the super user to assign the default_super_user_role
304
+ # This user will receive all permissions from application_permission_list_defaults
305
+ # default_super_user_email = admin@example.com
306
+
266
307
  [hazo_auth__auth_utility]
267
308
  # Authentication utility configuration
268
309
 
@@ -9,13 +9,13 @@ export async function register() {
9
9
  const hazo_notify_module = await import("hazo_notify");
10
10
 
11
11
  // Step 2: Load hazo_notify emailer configuration
12
- // This reads from hazo_notify_config.ini in the project root (same location as hazo_auth_config.ini)
12
+ // This reads from hazo_notify_config.ini in the ui_component directory (same location as hazo_auth_config.ini)
13
13
  const { load_emailer_config } = hazo_notify_module;
14
14
  const notify_config = load_emailer_config();
15
15
 
16
16
  // Step 3: Pass the initialized configuration to hazo_auth email service
17
17
  // This allows the email service to reuse the same configuration instance
18
- const { set_hazo_notify_instance } = await import("@/lib/services/email_service");
18
+ const { set_hazo_notify_instance } = await import("./src/lib/services/email_service");
19
19
  set_hazo_notify_instance(notify_config);
20
20
 
21
21
  // Log successful initialization
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_auth",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "files": [
5
5
  "src/**/*",
6
6
  "public/file.svg",
@@ -31,6 +31,7 @@
31
31
  "build-storybook": "storybook build",
32
32
  "dev:server": "tsx src/server/index.ts",
33
33
  "migrate": "tsx scripts/apply_migration.ts",
34
+ "init-users": "tsx scripts/init_users.ts init_users",
34
35
  "test": "cross-env NODE_ENV=test POSTGREST_URL=http://209.38.26.241:4402 POSTGREST_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYXBpX3VzZXIifQ.zBoUGymrxTUk1DNYIGUCtQU4HFaWEHlbE9_8Y3hUaTw jest --runInBand",
35
36
  "test:watch": "cross-env NODE_ENV=test POSTGREST_URL=http://209.38.26.241:4402 POSTGREST_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYXBpX3VzZXIifQ.zBoUGymrxTUk1DNYIGUCtQU4HFaWEHlbE9_8Y3hUaTw jest --watch"
36
37
  },
@@ -0,0 +1,378 @@
1
+ // file_description: script to initialize users, roles, and permissions from configuration
2
+ // Run with: npx tsx scripts/init_users.ts init_users
3
+ // section: imports
4
+ import { get_hazo_connect_instance } from "../src/lib/hazo_connect_instance.server";
5
+ import { createCrudService } from "hazo_connect/server";
6
+ import { get_user_management_config } from "../src/lib/user_management_config.server";
7
+ import { get_config_value } from "../src/lib/config/config_loader.server";
8
+ import { create_app_logger } from "../src/lib/app_logger";
9
+
10
+ // section: types
11
+ type InitSummary = {
12
+ permissions: {
13
+ inserted: string[];
14
+ existing: string[];
15
+ };
16
+ role: {
17
+ inserted: boolean;
18
+ existing: boolean;
19
+ role_id: number | null;
20
+ };
21
+ role_permissions: {
22
+ inserted: number;
23
+ existing: number;
24
+ };
25
+ user_role: {
26
+ inserted: boolean;
27
+ existing: boolean;
28
+ };
29
+ };
30
+
31
+ // section: helpers
32
+ /**
33
+ * Displays help information for available commands
34
+ */
35
+ function show_help(): void {
36
+ console.log(`
37
+ hazo_auth CLI - User and Permission Management
38
+
39
+ Usage: npx tsx scripts/init_users.ts <command>
40
+
41
+ Available Commands:
42
+ init_users Initialize users, roles, and permissions from configuration
43
+ - Reads permissions from hazo_auth_config.ini [hazo_auth__user_management] application_permission_list_defaults
44
+ - Creates default_super_user_role in hazo_roles
45
+ - Assigns all permissions to the super user role
46
+ - Finds user by email from hazo_auth_config.ini [hazo_auth__initial_setup] default_super_user_email
47
+ - Assigns super user role to the user
48
+ - Provides summary of what was inserted vs what already existed
49
+
50
+ help Show this help message
51
+
52
+ Configuration:
53
+ Add the following to hazo_auth_config.ini:
54
+
55
+ [hazo_auth__user_management]
56
+ application_permission_list_defaults = admin_user_management,admin_role_management,admin_permission_management
57
+
58
+ [hazo_auth__initial_setup]
59
+ default_super_user_email = admin@example.com
60
+
61
+ Examples:
62
+ npx tsx scripts/init_users.ts init_users
63
+ npx tsx scripts/init_users.ts help
64
+ `);
65
+ }
66
+
67
+ /**
68
+ * Initializes users, roles, and permissions from configuration
69
+ */
70
+ async function init_users(): Promise<void> {
71
+ const logger = create_app_logger();
72
+ const summary: InitSummary = {
73
+ permissions: {
74
+ inserted: [],
75
+ existing: [],
76
+ },
77
+ role: {
78
+ inserted: false,
79
+ existing: false,
80
+ role_id: null,
81
+ },
82
+ role_permissions: {
83
+ inserted: 0,
84
+ existing: 0,
85
+ },
86
+ user_role: {
87
+ inserted: false,
88
+ existing: false,
89
+ },
90
+ };
91
+
92
+ try {
93
+ console.log("Initializing users, roles, and permissions from configuration...\n");
94
+
95
+ // Get hazo_connect instance
96
+ const hazoConnect = get_hazo_connect_instance();
97
+ const permissions_service = createCrudService(hazoConnect, "hazo_permissions");
98
+ const roles_service = createCrudService(hazoConnect, "hazo_roles");
99
+ const role_permissions_service = createCrudService(hazoConnect, "hazo_role_permissions");
100
+ const users_service = createCrudService(hazoConnect, "hazo_users");
101
+ const user_roles_service = createCrudService(hazoConnect, "hazo_user_roles");
102
+
103
+ // 1. Get permissions from config
104
+ const config = get_user_management_config();
105
+ const permission_names = config.application_permission_list_defaults || [];
106
+
107
+ if (permission_names.length === 0) {
108
+ console.log("⚠ No permissions found in configuration.");
109
+ console.log(" Add permissions to [hazo_auth__user_management] application_permission_list_defaults\n");
110
+ return;
111
+ }
112
+
113
+ console.log(`Found ${permission_names.length} permission(s) in configuration:`);
114
+ permission_names.forEach((name) => console.log(` - ${name}`));
115
+ console.log();
116
+
117
+ // 2. Add permissions to hazo_permissions table
118
+ const permission_id_map: Record<string, number> = {};
119
+ const now = new Date().toISOString();
120
+
121
+ for (const permission_name of permission_names) {
122
+ const trimmed_name = permission_name.trim();
123
+ if (!trimmed_name) continue;
124
+
125
+ // Check if permission already exists
126
+ const existing_permissions = await permissions_service.findBy({
127
+ permission_name: trimmed_name,
128
+ });
129
+
130
+ if (Array.isArray(existing_permissions) && existing_permissions.length > 0) {
131
+ const existing_permission = existing_permissions[0];
132
+ const perm_id = existing_permission.id as number;
133
+ permission_id_map[trimmed_name] = perm_id;
134
+ summary.permissions.existing.push(trimmed_name);
135
+ console.log(`✓ Permission already exists: ${trimmed_name} (ID: ${perm_id})`);
136
+ } else {
137
+ // Insert new permission
138
+ const new_permission = await permissions_service.insert({
139
+ permission_name: trimmed_name,
140
+ description: `Permission for ${trimmed_name}`,
141
+ created_at: now,
142
+ changed_at: now,
143
+ });
144
+
145
+ const perm_id = Array.isArray(new_permission)
146
+ ? (new_permission[0] as { id: number }).id
147
+ : (new_permission as { id: number }).id;
148
+ permission_id_map[trimmed_name] = perm_id;
149
+ summary.permissions.inserted.push(trimmed_name);
150
+ console.log(`✓ Inserted permission: ${trimmed_name} (ID: ${perm_id})`);
151
+ }
152
+ }
153
+
154
+ console.log();
155
+
156
+ // 3. Create or get default_super_user_role
157
+ const role_name = "default_super_user_role";
158
+ const existing_roles = await roles_service.findBy({
159
+ role_name,
160
+ });
161
+
162
+ let role_id: number;
163
+ if (Array.isArray(existing_roles) && existing_roles.length > 0) {
164
+ role_id = existing_roles[0].id as number;
165
+ summary.role.existing = true;
166
+ summary.role.role_id = role_id;
167
+ console.log(`✓ Role already exists: ${role_name} (ID: ${role_id})`);
168
+ } else {
169
+ const new_role = await roles_service.insert({
170
+ role_name,
171
+ created_at: now,
172
+ changed_at: now,
173
+ });
174
+
175
+ role_id = Array.isArray(new_role)
176
+ ? (new_role[0] as { id: number }).id
177
+ : (new_role as { id: number }).id;
178
+ summary.role.inserted = true;
179
+ summary.role.role_id = role_id;
180
+ console.log(`✓ Created role: ${role_name} (ID: ${role_id})`);
181
+ }
182
+
183
+ console.log();
184
+
185
+ // 4. Assign all permissions to the role
186
+ const permission_ids = Object.values(permission_id_map);
187
+
188
+ for (const permission_id of permission_ids) {
189
+ // Check if role-permission assignment already exists
190
+ const existing_assignments = await role_permissions_service.findBy({
191
+ role_id,
192
+ permission_id,
193
+ });
194
+
195
+ if (Array.isArray(existing_assignments) && existing_assignments.length > 0) {
196
+ summary.role_permissions.existing++;
197
+ const perm_name = Object.keys(permission_id_map).find(
198
+ (key) => permission_id_map[key] === permission_id,
199
+ );
200
+ console.log(`✓ Role-permission already exists: ${role_name} -> ${perm_name}`);
201
+ } else {
202
+ await role_permissions_service.insert({
203
+ role_id,
204
+ permission_id,
205
+ created_at: now,
206
+ changed_at: now,
207
+ });
208
+ summary.role_permissions.inserted++;
209
+ const perm_name = Object.keys(permission_id_map).find(
210
+ (key) => permission_id_map[key] === permission_id,
211
+ );
212
+ console.log(`✓ Assigned permission to role: ${role_name} -> ${perm_name}`);
213
+ }
214
+ }
215
+
216
+ console.log();
217
+
218
+ // 5. Get super user email from config
219
+ const super_user_email = get_config_value(
220
+ "hazo_auth__initial_setup",
221
+ "default_super_user_email",
222
+ "",
223
+ ).trim();
224
+
225
+ if (!super_user_email) {
226
+ console.log("⚠ No super user email found in configuration.");
227
+ console.log(" Add [hazo_auth__initial_setup] default_super_user_email to config\n");
228
+ print_summary(summary);
229
+ return;
230
+ }
231
+
232
+ console.log(`Looking up user with email: ${super_user_email}`);
233
+
234
+ // 6. Find user by email
235
+ const users = await users_service.findBy({
236
+ email_address: super_user_email,
237
+ });
238
+
239
+ if (!Array.isArray(users) || users.length === 0) {
240
+ console.log(`✗ User not found with email: ${super_user_email}`);
241
+ console.log(" Please ensure the user exists in the database before running this script.\n");
242
+ print_summary(summary);
243
+ return;
244
+ }
245
+
246
+ const user = users[0];
247
+ const user_id = user.id as string;
248
+ console.log(`✓ Found user: ${super_user_email} (ID: ${user_id})`);
249
+ console.log();
250
+
251
+ // 7. Assign role to user
252
+ const existing_user_roles = await user_roles_service.findBy({
253
+ user_id,
254
+ role_id,
255
+ });
256
+
257
+ if (Array.isArray(existing_user_roles) && existing_user_roles.length > 0) {
258
+ summary.user_role.existing = true;
259
+ console.log(`✓ User already has role assigned: ${user_id} -> ${role_name}`);
260
+ } else {
261
+ await user_roles_service.insert({
262
+ user_id,
263
+ role_id,
264
+ created_at: now,
265
+ changed_at: now,
266
+ });
267
+ summary.user_role.inserted = true;
268
+ console.log(`✓ Assigned role to user: ${user_id} -> ${role_name}`);
269
+ }
270
+
271
+ console.log();
272
+
273
+ // 8. Print summary
274
+ print_summary(summary);
275
+
276
+ logger.info("init_users_completed", {
277
+ filename: "init_users.ts",
278
+ line_number: 0,
279
+ summary,
280
+ });
281
+ } catch (error) {
282
+ const error_message = error instanceof Error ? error.message : "Unknown error";
283
+ const error_stack = error instanceof Error ? error.stack : undefined;
284
+
285
+ console.error("\n✗ Error initializing users:");
286
+ console.error(` ${error_message}`);
287
+ if (error_stack) {
288
+ console.error("\nStack trace:");
289
+ console.error(error_stack);
290
+ }
291
+
292
+ const logger = create_app_logger();
293
+ logger.error("init_users_failed", {
294
+ filename: "init_users.ts",
295
+ line_number: 0,
296
+ error_message,
297
+ error_stack,
298
+ });
299
+
300
+ process.exit(1);
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Prints a summary of what was inserted vs what already existed
306
+ */
307
+ function print_summary(summary: InitSummary): void {
308
+ console.log("=".repeat(60));
309
+ console.log("INITIALIZATION SUMMARY");
310
+ console.log("=".repeat(60));
311
+ console.log();
312
+
313
+ // Permissions summary
314
+ console.log("Permissions:");
315
+ if (summary.permissions.inserted.length > 0) {
316
+ console.log(` ✓ Inserted (${summary.permissions.inserted.length}):`);
317
+ summary.permissions.inserted.forEach((name) => console.log(` - ${name}`));
318
+ }
319
+ if (summary.permissions.existing.length > 0) {
320
+ console.log(` ⊙ Already existed (${summary.permissions.existing.length}):`);
321
+ summary.permissions.existing.forEach((name) => console.log(` - ${name}`));
322
+ }
323
+ console.log();
324
+
325
+ // Role summary
326
+ console.log("Role:");
327
+ if (summary.role.inserted) {
328
+ console.log(` ✓ Inserted: default_super_user_role (ID: ${summary.role.role_id})`);
329
+ }
330
+ if (summary.role.existing) {
331
+ console.log(` ⊙ Already existed: default_super_user_role (ID: ${summary.role.role_id})`);
332
+ }
333
+ console.log();
334
+
335
+ // Role permissions summary
336
+ console.log("Role-Permission Assignments:");
337
+ if (summary.role_permissions.inserted > 0) {
338
+ console.log(` ✓ Inserted: ${summary.role_permissions.inserted} assignment(s)`);
339
+ }
340
+ if (summary.role_permissions.existing > 0) {
341
+ console.log(` ⊙ Already existed: ${summary.role_permissions.existing} assignment(s)`);
342
+ }
343
+ console.log();
344
+
345
+ // User role summary
346
+ console.log("User-Role Assignment:");
347
+ if (summary.user_role.inserted) {
348
+ console.log(` ✓ Inserted: Super user role assigned to user`);
349
+ }
350
+ if (summary.user_role.existing) {
351
+ console.log(` ⊙ Already existed: User already has super user role`);
352
+ }
353
+ console.log();
354
+
355
+ console.log("=".repeat(60));
356
+ }
357
+
358
+ // section: main
359
+ function main(): void {
360
+ const command = process.argv[2];
361
+
362
+ if (!command || command === "help" || command === "--help" || command === "-h") {
363
+ show_help();
364
+ return;
365
+ }
366
+
367
+ if (command === "init_users") {
368
+ void init_users();
369
+ } else {
370
+ console.error(`Unknown command: ${command}\n`);
371
+ show_help();
372
+ process.exit(1);
373
+ }
374
+ }
375
+
376
+ main();
377
+
378
+
@@ -6,6 +6,7 @@ import { create_app_logger } from "../../../../lib/app_logger";
6
6
  import { authenticate_user } from "../../../../lib/services/login_service";
7
7
  import { createCrudService } from "hazo_connect/server";
8
8
  import { get_filename, get_line_number } from "../../../../lib/utils/api_route_helpers";
9
+ import { get_login_config } from "../../../../lib/login_config.server";
9
10
 
10
11
  // section: api_handler
11
12
  export async function POST(request: NextRequest) {
@@ -13,7 +14,7 @@ export async function POST(request: NextRequest) {
13
14
 
14
15
  try {
15
16
  const body = await request.json();
16
- const { email, password } = body;
17
+ const { email, password, url_on_logon } = body;
17
18
 
18
19
  // Validate input
19
20
  if (!email || !password) {
@@ -106,6 +107,30 @@ export async function POST(request: NextRequest) {
106
107
  const user = users && users.length > 0 ? users[0] : null;
107
108
  const user_name = user?.name as string | undefined;
108
109
 
110
+ // Determine redirect URL priority:
111
+ // 1. url_on_logon from request body (if valid)
112
+ // 2. stored_url_on_logon from database (if available)
113
+ // 3. redirect_route_on_successful_login from config
114
+ // 4. Default to "/"
115
+
116
+ let redirectUrl = "/";
117
+
118
+ // Check priority 1: Request body
119
+ if (url_on_logon && typeof url_on_logon === "string" && url_on_logon.startsWith("/") && !url_on_logon.startsWith("//")) {
120
+ redirectUrl = url_on_logon;
121
+ }
122
+ // Check priority 2: Stored URL from DB
123
+ else if (result.stored_url_on_logon && typeof result.stored_url_on_logon === "string") {
124
+ redirectUrl = result.stored_url_on_logon;
125
+ }
126
+ // Check priority 3: Config
127
+ else {
128
+ const loginConfig = get_login_config();
129
+ if (loginConfig.redirectRoute) {
130
+ redirectUrl = loginConfig.redirectRoute;
131
+ }
132
+ }
133
+
109
134
  // Create response with cookies
110
135
  const response = NextResponse.json(
111
136
  {
@@ -114,6 +139,7 @@ export async function POST(request: NextRequest) {
114
139
  user_id: user_id,
115
140
  email,
116
141
  name: user_name,
142
+ redirectUrl,
117
143
  },
118
144
  { status: 200 }
119
145
  );
@@ -5,6 +5,7 @@ import { get_hazo_connect_instance } from "../../../../lib/hazo_connect_instance
5
5
  import { create_app_logger } from "../../../../lib/app_logger";
6
6
  import { register_user } from "../../../../lib/services/registration_service";
7
7
  import { get_filename, get_line_number } from "../../../../lib/utils/api_route_helpers";
8
+ import { sanitize_error_for_user } from "../../../../lib/utils/error_sanitizer";
8
9
 
9
10
  // section: api_handler
10
11
  export async function POST(request: NextRequest) {
@@ -12,7 +13,7 @@ export async function POST(request: NextRequest) {
12
13
 
13
14
  try {
14
15
  const body = await request.json();
15
- const { name, email, password } = body;
16
+ const { name, email, password, url_on_logon } = body;
16
17
 
17
18
  // Validate input
18
19
  if (!email || !password) {
@@ -52,6 +53,7 @@ export async function POST(request: NextRequest) {
52
53
  email,
53
54
  password,
54
55
  name,
56
+ url_on_logon,
55
57
  });
56
58
 
57
59
  if (!result.success) {
@@ -87,18 +89,19 @@ export async function POST(request: NextRequest) {
87
89
  { status: 201 }
88
90
  );
89
91
  } catch (error) {
90
- const error_message = error instanceof Error ? error.message : "Unknown error";
91
- const error_stack = error instanceof Error ? error.stack : undefined;
92
-
93
- logger.error("registration_error", {
94
- filename: get_filename(),
95
- line_number: get_line_number(),
96
- error_message,
97
- error_stack,
92
+ const user_friendly_error = sanitize_error_for_user(error, {
93
+ logToConsole: true,
94
+ logToLogger: true,
95
+ logger,
96
+ context: {
97
+ filename: get_filename(),
98
+ line_number: get_line_number(),
99
+ operation: "register_api_route",
100
+ },
98
101
  });
99
102
 
100
103
  return NextResponse.json(
101
- { error: "Registration failed. Please try again." },
104
+ { error: user_friendly_error },
102
105
  { status: 500 }
103
106
  );
104
107
  }
@@ -1,6 +1,6 @@
1
1
  // file_description: render the forgot password page shell and mount the forgot password layout component within sidebar
2
2
  // section: imports
3
- import { SidebarLayoutWrapper } from "../../../components/layouts/shared/components/sidebar_layout_wrapper";
3
+ import { AuthPageShell } from "../../../components/layouts/shared/components/auth_page_shell";
4
4
  import { ForgotPasswordPageClient } from "./forgot_password_page_client";
5
5
  import { get_forgot_password_config } from "../../../lib/forgot_password_config.server";
6
6
 
@@ -10,7 +10,7 @@ export default function forgot_password_page() {
10
10
  const forgotPasswordConfig = get_forgot_password_config();
11
11
 
12
12
  return (
13
- <SidebarLayoutWrapper>
13
+ <AuthPageShell>
14
14
  <ForgotPasswordPageClient
15
15
  alreadyLoggedInMessage={forgotPasswordConfig.alreadyLoggedInMessage}
16
16
  showLogoutButton={forgotPasswordConfig.showLogoutButton}
@@ -18,7 +18,7 @@ export default function forgot_password_page() {
18
18
  returnHomeButtonLabel={forgotPasswordConfig.returnHomeButtonLabel}
19
19
  returnHomePath={forgotPasswordConfig.returnHomePath}
20
20
  />
21
- </SidebarLayoutWrapper>
21
+ </AuthPageShell>
22
22
  );
23
23
  }
24
24