hazo_auth 5.1.40 → 5.3.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 (83) hide show
  1. package/SETUP_CHECKLIST.md +46 -0
  2. package/cli-src/lib/auth/ensure_anon_id.server.ts +88 -0
  3. package/cli-src/lib/auth/index.ts +3 -0
  4. package/cli-src/lib/cookies_config.edge.ts +1 -0
  5. package/cli-src/lib/cookies_config.server.ts +1 -0
  6. package/cli-src/lib/hazo_connect_setup.server.ts +0 -8
  7. package/cli-src/lib/services/email_service.ts +136 -289
  8. package/cli-src/lib/services/email_template_manifest.ts +104 -0
  9. package/cli-src/lib/services/email_templates/email_verification.html +18 -0
  10. package/cli-src/lib/services/email_templates/email_verification.txt +9 -0
  11. package/cli-src/lib/services/email_templates/forgot_password.html +18 -0
  12. package/cli-src/lib/services/email_templates/forgot_password.txt +9 -0
  13. package/cli-src/lib/services/email_templates/password_changed.html +14 -0
  14. package/cli-src/lib/services/email_templates/password_changed.txt +9 -0
  15. package/cli-src/lib/services/session_token_service.ts +2 -2
  16. package/cli-src/lib/ui_shell_config.server.ts +6 -2
  17. package/dist/components/layouts/login/index.d.ts +16 -3
  18. package/dist/components/layouts/login/index.d.ts.map +1 -1
  19. package/dist/components/layouts/login/index.js +49 -5
  20. package/dist/components/layouts/register/index.d.ts +7 -3
  21. package/dist/components/layouts/register/index.d.ts.map +1 -1
  22. package/dist/components/layouts/register/index.js +36 -3
  23. package/dist/components/layouts/shared/components/floating_home_link.d.ts +20 -0
  24. package/dist/components/layouts/shared/components/floating_home_link.d.ts.map +1 -0
  25. package/dist/components/layouts/shared/components/floating_home_link.js +29 -0
  26. package/dist/components/layouts/shared/components/profile_pic_menu.d.ts +8 -0
  27. package/dist/components/layouts/shared/components/profile_pic_menu.d.ts.map +1 -1
  28. package/dist/components/layouts/shared/components/profile_pic_menu.js +18 -8
  29. package/dist/components/layouts/shared/index.d.ts +2 -0
  30. package/dist/components/layouts/shared/index.d.ts.map +1 -1
  31. package/dist/components/layouts/shared/index.js +1 -0
  32. package/dist/lib/auth/ensure_anon_id.server.d.ts +21 -0
  33. package/dist/lib/auth/ensure_anon_id.server.d.ts.map +1 -0
  34. package/dist/lib/auth/ensure_anon_id.server.js +73 -0
  35. package/dist/lib/auth/index.d.ts +1 -0
  36. package/dist/lib/auth/index.d.ts.map +1 -1
  37. package/dist/lib/auth/index.js +2 -0
  38. package/dist/lib/cookies_config.edge.d.ts +1 -0
  39. package/dist/lib/cookies_config.edge.d.ts.map +1 -1
  40. package/dist/lib/cookies_config.edge.js +1 -0
  41. package/dist/lib/cookies_config.server.d.ts +1 -0
  42. package/dist/lib/cookies_config.server.d.ts.map +1 -1
  43. package/dist/lib/cookies_config.server.js +1 -0
  44. package/dist/lib/hazo_connect_setup.server.d.ts.map +1 -1
  45. package/dist/lib/hazo_connect_setup.server.js +0 -8
  46. package/dist/lib/services/email_service.d.ts +17 -4
  47. package/dist/lib/services/email_service.d.ts.map +1 -1
  48. package/dist/lib/services/email_service.js +93 -221
  49. package/dist/lib/services/email_template_manifest.d.ts +11 -0
  50. package/dist/lib/services/email_template_manifest.d.ts.map +1 -0
  51. package/dist/lib/services/email_template_manifest.js +95 -0
  52. package/dist/lib/services/email_templates/email_verification.html +18 -0
  53. package/dist/lib/services/email_templates/email_verification.txt +9 -0
  54. package/dist/lib/services/email_templates/forgot_password.html +18 -0
  55. package/dist/lib/services/email_templates/forgot_password.txt +9 -0
  56. package/dist/lib/services/email_templates/password_changed.html +14 -0
  57. package/dist/lib/services/email_templates/password_changed.txt +9 -0
  58. package/dist/lib/services/session_token_service.js +2 -2
  59. package/dist/lib/ui_shell_config.server.d.ts.map +1 -1
  60. package/dist/lib/ui_shell_config.server.js +6 -2
  61. package/dist/server/routes/oauth_google_callback.d.ts.map +1 -1
  62. package/dist/server/routes/oauth_google_callback.js +33 -6
  63. package/dist/server-lib.d.ts +1 -0
  64. package/dist/server-lib.d.ts.map +1 -1
  65. package/dist/server-lib.js +1 -0
  66. package/dist/server_pages/login.d.ts +13 -1
  67. package/dist/server_pages/login.d.ts.map +1 -1
  68. package/dist/server_pages/login.js +25 -9
  69. package/dist/server_pages/login_client_wrapper.d.ts +6 -4
  70. package/dist/server_pages/login_client_wrapper.d.ts.map +1 -1
  71. package/dist/server_pages/login_client_wrapper.js +2 -2
  72. package/dist/server_pages/register.d.ts +7 -1
  73. package/dist/server_pages/register.d.ts.map +1 -1
  74. package/dist/server_pages/register.js +18 -9
  75. package/dist/server_pages/register_client_wrapper.d.ts +6 -4
  76. package/dist/server_pages/register_client_wrapper.d.ts.map +1 -1
  77. package/dist/server_pages/register_client_wrapper.js +2 -2
  78. package/package.json +26 -20
  79. package/cli-src/assets/images/new_firm_default.jpg +0 -0
  80. package/cli-src/lib/auth/org_cache.ts +0 -148
  81. package/cli-src/lib/index.ts +0 -48
  82. package/cli-src/lib/services/org_service.ts +0 -965
  83. package/cli-src/lib/services/scope_labels_service.ts +0 -348
@@ -0,0 +1,104 @@
1
+ // file_description: manifest of system email templates shipped by hazo_auth
2
+ // section: server_only_guard
3
+ import "server-only";
4
+
5
+ // section: imports
6
+ import fs from "fs";
7
+ import path from "path";
8
+ import { fileURLToPath } from "url";
9
+ import type { SystemTemplateManifest } from "hazo_notify/template_manager";
10
+
11
+ // section: constants
12
+ const TEMPLATE_DIR = path.resolve(
13
+ path.dirname(fileURLToPath(import.meta.url)),
14
+ "email_templates"
15
+ );
16
+
17
+ const SYSTEM_CATEGORY = "Auth";
18
+
19
+ // section: helpers
20
+ function read_template(template_name: string, extension: "html" | "txt"): string {
21
+ const file_path = path.join(TEMPLATE_DIR, `${template_name}.${extension}`);
22
+ return fs.readFileSync(file_path, "utf-8");
23
+ }
24
+
25
+ // section: manifest
26
+ /**
27
+ * System email templates shipped by hazo_auth.
28
+ *
29
+ * Consumers feed this into `init_template_manager({ manifests: [...hazo_auth_template_manifest] })`
30
+ * at boot. hazo_notify seeds/syncs the templates into its scope-aware database.
31
+ * Per-scope overrides are managed through the hazo_notify admin UI.
32
+ */
33
+ export const hazo_auth_template_manifest: SystemTemplateManifest[] = [
34
+ {
35
+ template_name: "email_verification",
36
+ template_label: "Email verification",
37
+ category: SYSTEM_CATEGORY,
38
+ html: read_template("email_verification", "html"),
39
+ text: read_template("email_verification", "txt"),
40
+ variables: [
41
+ {
42
+ variable_name: "user_name",
43
+ variable_description: "Recipient display name",
44
+ default_value: "",
45
+ },
46
+ {
47
+ variable_name: "user_email",
48
+ variable_description: "Recipient email",
49
+ },
50
+ {
51
+ variable_name: "verification_url",
52
+ variable_description: "Verification link with embedded token",
53
+ },
54
+ {
55
+ variable_name: "token",
56
+ variable_description: "Raw verification token",
57
+ },
58
+ ],
59
+ },
60
+ {
61
+ template_name: "forgot_password",
62
+ template_label: "Forgot password",
63
+ category: SYSTEM_CATEGORY,
64
+ html: read_template("forgot_password", "html"),
65
+ text: read_template("forgot_password", "txt"),
66
+ variables: [
67
+ {
68
+ variable_name: "user_name",
69
+ variable_description: "Recipient display name",
70
+ default_value: "",
71
+ },
72
+ {
73
+ variable_name: "user_email",
74
+ variable_description: "Recipient email",
75
+ },
76
+ {
77
+ variable_name: "reset_url",
78
+ variable_description: "Password reset link",
79
+ },
80
+ {
81
+ variable_name: "token",
82
+ variable_description: "Raw reset token",
83
+ },
84
+ ],
85
+ },
86
+ {
87
+ template_name: "password_changed",
88
+ template_label: "Password changed",
89
+ category: SYSTEM_CATEGORY,
90
+ html: read_template("password_changed", "html"),
91
+ text: read_template("password_changed", "txt"),
92
+ variables: [
93
+ {
94
+ variable_name: "user_name",
95
+ variable_description: "Recipient display name",
96
+ default_value: "",
97
+ },
98
+ {
99
+ variable_name: "user_email",
100
+ variable_description: "Recipient email",
101
+ },
102
+ ],
103
+ },
104
+ ];
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Verify Your Email</title>
6
+ </head>
7
+ <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
8
+ <h1 style="color: #0f172a;">Verify Your Email Address</h1>
9
+ <p>Thank you for registering! Please click the link below to verify your email address:</p>
10
+ <p style="margin: 20px 0;">
11
+ <a href="{{verification_url}}" style="display: inline-block; padding: 12px 24px; background-color: #0f172a; color: #ffffff; text-decoration: none; border-radius: 4px;">Verify Email Address</a>
12
+ </p>
13
+ <p>Or copy and paste this link into your browser:</p>
14
+ <p style="word-break: break-all; color: #666;">{{verification_url}}</p>
15
+ <p>This link will expire in 48 hours.</p>
16
+ <p style="margin-top: 30px; color: #666; font-size: 12px;">If you didn't create an account, you can safely ignore this email.</p>
17
+ </body>
18
+ </html>
@@ -0,0 +1,9 @@
1
+ Verify Your Email Address
2
+
3
+ Thank you for registering! Please click the link below to verify your email address:
4
+
5
+ {{verification_url}}
6
+
7
+ This link will expire in 48 hours.
8
+
9
+ If you didn't create an account, you can safely ignore this email.
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Reset Your Password</title>
6
+ </head>
7
+ <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
8
+ <h1 style="color: #0f172a;">Reset Your Password</h1>
9
+ <p>We received a request to reset your password. Click the link below to reset it:</p>
10
+ <p style="margin: 20px 0;">
11
+ <a href="{{reset_url}}" style="display: inline-block; padding: 12px 24px; background-color: #0f172a; color: #ffffff; text-decoration: none; border-radius: 4px;">Reset Password</a>
12
+ </p>
13
+ <p>Or copy and paste this link into your browser:</p>
14
+ <p style="word-break: break-all; color: #666;">{{reset_url}}</p>
15
+ <p>This link will expire in 10 minutes.</p>
16
+ <p style="margin-top: 30px; color: #666; font-size: 12px;">If you didn't request a password reset, you can safely ignore this email.</p>
17
+ </body>
18
+ </html>
@@ -0,0 +1,9 @@
1
+ Reset Your Password
2
+
3
+ We received a request to reset your password. Click the link below to reset it:
4
+
5
+ {{reset_url}}
6
+
7
+ This link will expire in 10 minutes.
8
+
9
+ If you didn't request a password reset, you can safely ignore this email.
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Password Changed</title>
6
+ </head>
7
+ <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
8
+ <h1 style="color: #0f172a;">Password Changed Successfully</h1>
9
+ <p>Hello {{user_name}},</p>
10
+ <p>This email confirms that your password has been changed successfully.</p>
11
+ <p>If you did not make this change, please contact support immediately to secure your account.</p>
12
+ <p style="margin-top: 30px; color: #666; font-size: 12px;">This is an automated notification. Please do not reply to this email.</p>
13
+ </body>
14
+ </html>
@@ -0,0 +1,9 @@
1
+ Password Changed Successfully
2
+
3
+ Hello {{user_name}},
4
+
5
+ This email confirms that your password has been changed successfully.
6
+
7
+ If you did not make this change, please contact support immediately to secure your account.
8
+
9
+ This is an automated notification. Please do not reply to this email.
@@ -89,7 +89,7 @@ export async function create_session_token(
89
89
  .setExpirationTime(exp)
90
90
  .sign(secret);
91
91
 
92
- logger.info("session_token_created", {
92
+ logger.debug("session_token_created", {
93
93
  filename: get_filename(),
94
94
  line_number: get_line_number(),
95
95
  user_id,
@@ -148,7 +148,7 @@ export async function validate_session_token(
148
148
  return { valid: false };
149
149
  }
150
150
 
151
- logger.info("session_token_validated", {
151
+ logger.debug("session_token_validated", {
152
152
  filename: get_filename(),
153
153
  line_number: get_line_number(),
154
154
  user_id,
@@ -55,15 +55,19 @@ export function get_ui_shell_config(): UiShellConfig {
55
55
  "standalone_content_class",
56
56
  "cls_standalone_shell_content w-full max-w-5xl shadow-xl rounded-2xl border bg-card"
57
57
  );
58
+ // Default to false: the package-default copy ("Welcome to hazo auth" /
59
+ // "Reuse the packaged authentication flows...") is dev/test placeholder
60
+ // text, not production-suitable. Consumers who want their own heading set
61
+ // standalone_heading + standalone_show_heading=true explicitly.
58
62
  const standalone_show_heading = get_config_value(
59
63
  section,
60
64
  "standalone_show_heading",
61
- "true"
65
+ "false"
62
66
  ).toLowerCase() === "true";
63
67
  const standalone_show_description = get_config_value(
64
68
  section,
65
69
  "standalone_show_description",
66
- "true"
70
+ "false"
67
71
  ).toLowerCase() === "true";
68
72
 
69
73
  const vertical_center = get_config_value(
@@ -12,8 +12,10 @@ export type OAuthLayoutConfig = {
12
12
  oauth_divider_text: string;
13
13
  };
14
14
  export type LoginLayoutProps<TClient = unknown> = {
15
- image_src: string | StaticImageData;
16
- image_alt: string;
15
+ /** Image source for the visual panel. Required when `layout` is `"two_column"` (the default); ignored when `"form_only"`. */
16
+ image_src?: string | StaticImageData;
17
+ /** Image alt text. Required when `layout` is `"two_column"` (the default); ignored when `"form_only"`. */
18
+ image_alt?: string;
17
19
  image_background_color?: string;
18
20
  field_overrides?: LayoutFieldMapOverrides;
19
21
  labels?: LayoutLabelOverrides;
@@ -41,6 +43,17 @@ export type LoginLayoutProps<TClient = unknown> = {
41
43
  urlOnLogon?: string;
42
44
  /** OAuth configuration */
43
45
  oauth?: OAuthLayoutConfig;
46
+ /**
47
+ * Layout mode (default: `"two_column"`).
48
+ * - `"two_column"` — renders the form inside the package's TwoColumnAuthLayout
49
+ * with an image visual on the left. Backwards-compatible default.
50
+ * - `"form_only"` — renders just the form content (header, OAuth button,
51
+ * divider, fields, action buttons, support links). For consumers who want
52
+ * to wrap the form in their own brand chrome. The "already logged in"
53
+ * guard is also skipped in this mode — handle that UX yourself if needed
54
+ * (e.g. via `use_auth_status` from `hazo_auth/components/layouts/shared`).
55
+ */
56
+ layout?: "two_column" | "form_only";
44
57
  };
45
- export default function login_layout<TClient>({ image_src, image_alt, image_background_color, field_overrides, labels, button_colors, data_client, logger, redirectRoute, successMessage, alreadyLoggedInMessage, showLogoutButton, showReturnHomeButton, returnHomeButtonLabel, returnHomePath, forgot_password_path, forgot_password_label, create_account_path, create_account_label, show_create_account_link, urlOnLogon, oauth, }: LoginLayoutProps<TClient>): import("react/jsx-runtime").JSX.Element;
58
+ export default function login_layout<TClient>({ image_src, image_alt, image_background_color, field_overrides, labels, button_colors, data_client, logger, redirectRoute, successMessage, alreadyLoggedInMessage, showLogoutButton, showReturnHomeButton, returnHomeButtonLabel, returnHomePath, forgot_password_path, forgot_password_label, create_account_path, create_account_label, show_create_account_link, urlOnLogon, oauth, layout, }: LoginLayoutProps<TClient>): import("react/jsx-runtime").JSX.Element;
46
59
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/login/index.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAWlD,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAC1B,MAAM,uCAAuC,CAAC;AAW/C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAG1E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,gCAAgC;IAChC,aAAa,EAAE,OAAO,CAAC;IACvB,8CAA8C;IAC9C,qBAAqB,EAAE,OAAO,CAAC;IAC/B,kDAAkD;IAClD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,OAAO,GAAG,OAAO,IAAI;IAChD,SAAS,EAAE,MAAM,GAAG,eAAe,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,WAAW,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,CAAC,EAAE;QACP,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QACjE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;KAClE,CAAC;IACF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,sDAAsD;IACtD,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0BAA0B;IAC1B,KAAK,CAAC,EAAE,iBAAiB,CAAC;CAC3B,CAAC;AAUF,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,OAAO,EAAE,EAC5C,SAAS,EACT,SAAS,EACT,sBAAkC,EAClC,eAAe,EACf,MAAM,EACN,aAAa,EACb,WAAW,EACX,MAAM,EACN,aAAa,EACb,cAAyC,EACzC,sBAAoD,EACpD,gBAAuB,EACvB,oBAA4B,EAC5B,qBAAqC,EACrC,cAAoB,EACpB,oBAAmD,EACnD,qBAA0C,EAC1C,mBAA2C,EAC3C,oBAAuC,EACvC,wBAA+B,EAC/B,UAAU,EACV,KAAK,GACN,EAAE,gBAAgB,CAAC,OAAO,CAAC,2CAuO3B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/login/index.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAWlD,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAC1B,MAAM,uCAAuC,CAAC;AAW/C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAG1E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,gCAAgC;IAChC,aAAa,EAAE,OAAO,CAAC;IACvB,8CAA8C;IAC9C,qBAAqB,EAAE,OAAO,CAAC;IAC/B,kDAAkD;IAClD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,OAAO,GAAG,OAAO,IAAI;IAChD,6HAA6H;IAC7H,SAAS,CAAC,EAAE,MAAM,GAAG,eAAe,CAAC;IACrC,0GAA0G;IAC1G,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,WAAW,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,CAAC,EAAE;QACP,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QACjE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;KAClE,CAAC;IACF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,sDAAsD;IACtD,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0BAA0B;IAC1B,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,YAAY,GAAG,WAAW,CAAC;CACrC,CAAC;AAUF,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,OAAO,EAAE,EAC5C,SAAS,EACT,SAAS,EACT,sBAAkC,EAClC,eAAe,EACf,MAAM,EACN,aAAa,EACb,WAAW,EACX,MAAM,EACN,aAAa,EACb,cAAyC,EACzC,sBAAoD,EACpD,gBAAuB,EACvB,oBAA4B,EAC5B,qBAAqC,EACrC,cAAoB,EACpB,oBAAmD,EACnD,qBAA0C,EAC1C,mBAA2C,EAC3C,oBAAuC,EACvC,wBAA+B,EAC/B,UAAU,EACV,KAAK,EACL,MAAqB,GACtB,EAAE,gBAAgB,CAAC,OAAO,CAAC,2CAqQ3B"}
@@ -22,7 +22,8 @@ const ORDERED_FIELDS = [
22
22
  LOGIN_FIELD_IDS.PASSWORD,
23
23
  ];
24
24
  // section: component
25
- export default function login_layout({ image_src, image_alt, image_background_color = "#f1f5f9", field_overrides, labels, button_colors, data_client, logger, redirectRoute, successMessage = "Successfully logged in", alreadyLoggedInMessage = "You are already logged in", showLogoutButton = true, showReturnHomeButton = false, returnHomeButtonLabel = "Return home", returnHomePath = "/", forgot_password_path = "/hazo_auth/forgot_password", forgot_password_label = "Forgot password?", create_account_path = "/hazo_auth/register", create_account_label = "Create account", show_create_account_link = true, urlOnLogon, oauth, }) {
25
+ export default function login_layout({ image_src, image_alt, image_background_color = "#f1f5f9", field_overrides, labels, button_colors, data_client, logger, redirectRoute, successMessage = "Successfully logged in", alreadyLoggedInMessage = "You are already logged in", showLogoutButton = true, showReturnHomeButton = false, returnHomeButtonLabel = "Return home", returnHomePath = "/", forgot_password_path = "/hazo_auth/forgot_password", forgot_password_label = "Forgot password?", create_account_path = "/hazo_auth/register", create_account_label = "Create account", show_create_account_link = true, urlOnLogon, oauth, layout = "two_column", }) {
26
+ var _a;
26
27
  // Default OAuth config: both enabled
27
28
  const oauthConfig = oauth || {
28
29
  enable_google: true,
@@ -33,6 +34,21 @@ export default function login_layout({ image_src, image_alt, image_background_co
33
34
  // Read OAuth error from URL query params (e.g., ?error=AccessDenied)
34
35
  const searchParams = useSearchParams();
35
36
  const oauthError = searchParams.get("error");
37
+ // Read post-auth redirect target from `?redirect=`. Used by both:
38
+ // - Email/password form: `redirectRoute` prop falls back to this when the
39
+ // parent didn't supply one.
40
+ // - Google OAuth: appended to the GoogleSignInButton callback URL as
41
+ // `?next=` so the OAuth callback handler can override its
42
+ // default_redirect with the per-request value.
43
+ // Validation: must be a same-origin path (starts with `/`, not `//`, no
44
+ // protocol). Anything else is dropped — prevents open-redirect attacks
45
+ // via crafted invite links.
46
+ const rawRedirect = searchParams.get("redirect");
47
+ const safeRedirect = isSafeRedirectPath(rawRedirect) ? rawRedirect : null;
48
+ const oauthCallbackUrl = safeRedirect
49
+ ? `/api/hazo_auth/oauth/google/callback?next=${encodeURIComponent(safeRedirect)}`
50
+ : "/api/hazo_auth/oauth/google/callback";
51
+ const effectiveRedirectRoute = (_a = redirectRoute !== null && redirectRoute !== void 0 ? redirectRoute : safeRedirect) !== null && _a !== void 0 ? _a : undefined;
36
52
  const getOAuthErrorMessage = (error) => {
37
53
  switch (error) {
38
54
  case "AccessDenied":
@@ -51,7 +67,7 @@ export default function login_layout({ image_src, image_alt, image_background_co
51
67
  const form = use_login_form({
52
68
  dataClient: data_client,
53
69
  logger,
54
- redirectRoute,
70
+ redirectRoute: effectiveRedirectRoute,
55
71
  successMessage,
56
72
  urlOnLogon: urlOnLogon,
57
73
  });
@@ -75,9 +91,37 @@ export default function login_layout({ image_src, image_alt, image_background_co
75
91
  return (_jsx(FormFieldWrapper, { fieldId: fieldDefinition.id, label: fieldDefinition.label, input: inputElement, errorMessage: shouldShowError }, fieldId));
76
92
  });
77
93
  };
78
- // Show success message if login was successful and no redirect route is provided
94
+ // Form content used in both layout modes. In "two_column" mode it's slotted
95
+ // into TwoColumnAuthLayout's `formContent`; in "form_only" mode it's returned
96
+ // as the entire output for the consumer to compose into their own chrome.
97
+ const successContent = (_jsxs(_Fragment, { children: [_jsx(FormHeader, { heading: resolvedLabels.heading, subHeading: resolvedLabels.subHeading }), _jsxs("div", { className: "cls_login_layout_success flex flex-col items-center justify-center gap-4 p-8 text-center", children: [_jsx(CheckCircle, { className: "cls_login_layout_success_icon h-16 w-16 text-green-600", "aria-hidden": "true" }), _jsx("p", { className: "cls_login_layout_success_message text-lg font-medium text-slate-900", children: successMessage })] })] }));
98
+ const mainContent = (_jsxs(_Fragment, { children: [_jsx(FormHeader, { heading: resolvedLabels.heading, subHeading: resolvedLabels.subHeading }), oauthError && (_jsxs("div", { className: "cls_login_layout_oauth_error flex items-center gap-2 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700", children: [_jsx(AlertCircle, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }), _jsx("span", { children: getOAuthErrorMessage(oauthError) })] })), oauthConfig.enable_google && (_jsx("div", { className: "cls_login_layout_oauth_section", children: _jsx(GoogleSignInButton, { label: oauthConfig.google_button_text, callbackUrl: oauthCallbackUrl }) })), oauthConfig.enable_google && oauthConfig.enable_email_password && (_jsx(OAuthDivider, { text: oauthConfig.oauth_divider_text })), oauthConfig.enable_email_password && (_jsxs("form", { className: "cls_login_layout_form_fields flex flex-col gap-5", onSubmit: form.handleSubmit, "aria-label": "Login form", children: [renderFields(form), _jsx(FormActionButtons, { submitLabel: resolvedLabels.submitButton, cancelLabel: resolvedLabels.cancelButton, buttonPalette: resolvedButtonPalette, isSubmitDisabled: form.isSubmitDisabled, onCancel: form.handleCancel, submitAriaLabel: "Submit login form", cancelAriaLabel: "Cancel login form" }), ((forgot_password_path && forgot_password_label) || (show_create_account_link && create_account_path && create_account_label)) && (_jsxs("div", { className: "cls_login_layout_support_links flex flex-col gap-1 text-sm text-muted-foreground", children: [forgot_password_path && forgot_password_label && (_jsx(Link, { href: forgot_password_path, className: "cls_login_layout_forgot_password_link text-primary underline-offset-4 hover:underline", "aria-label": "Go to forgot password page", children: forgot_password_label })), show_create_account_link && create_account_path && create_account_label && (_jsx(Link, { href: create_account_path, className: "cls_login_layout_create_account_link text-primary underline-offset-4 hover:underline", "aria-label": "Go to create account page", children: create_account_label }))] }))] })), show_create_account_link && create_account_path && create_account_label && !oauthConfig.enable_email_password && oauthConfig.enable_google && (_jsx("div", { className: "cls_login_layout_support_links mt-4 text-center text-sm text-muted-foreground", children: _jsx(Link, { href: create_account_path, className: "cls_login_layout_create_account_link text-primary underline-offset-4 hover:underline", "aria-label": "Go to create account page", children: create_account_label }) }))] }));
99
+ // Form-only mode: return the content directly. Consumer is expected to wrap
100
+ // it in their own page/brand chrome. We deliberately skip AlreadyLoggedInGuard
101
+ // here — its 2-col fallback would defeat the purpose of form_only. Consumer
102
+ // can wire `use_auth_status` themselves if they want that UX.
103
+ if (layout === "form_only") {
104
+ return form.isSuccess ? successContent : mainContent;
105
+ }
106
+ // Two-column mode (default): success state.
79
107
  if (form.isSuccess) {
80
- return (_jsx(TwoColumnAuthLayout, { imageSrc: image_src, imageAlt: image_alt, imageBackgroundColor: image_background_color, formContent: _jsxs(_Fragment, { children: [_jsx(FormHeader, { heading: resolvedLabels.heading, subHeading: resolvedLabels.subHeading }), _jsxs("div", { className: "cls_login_layout_success flex flex-col items-center justify-center gap-4 p-8 text-center", children: [_jsx(CheckCircle, { className: "cls_login_layout_success_icon h-16 w-16 text-green-600", "aria-hidden": "true" }), _jsx("p", { className: "cls_login_layout_success_message text-lg font-medium text-slate-900", children: successMessage })] })] }) }));
108
+ return (_jsx(TwoColumnAuthLayout, { imageSrc: image_src, imageAlt: image_alt, imageBackgroundColor: image_background_color, formContent: successContent }));
81
109
  }
82
- return (_jsx(AlreadyLoggedInGuard, { image_src: image_src, image_alt: image_alt, image_background_color: image_background_color, message: alreadyLoggedInMessage, showLogoutButton: showLogoutButton, showReturnHomeButton: showReturnHomeButton, returnHomeButtonLabel: returnHomeButtonLabel, returnHomePath: returnHomePath, children: _jsx(TwoColumnAuthLayout, { imageSrc: image_src, imageAlt: image_alt, imageBackgroundColor: image_background_color, formContent: _jsxs(_Fragment, { children: [_jsx(FormHeader, { heading: resolvedLabels.heading, subHeading: resolvedLabels.subHeading }), oauthError && (_jsxs("div", { className: "cls_login_layout_oauth_error flex items-center gap-2 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700", children: [_jsx(AlertCircle, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }), _jsx("span", { children: getOAuthErrorMessage(oauthError) })] })), oauthConfig.enable_google && (_jsx("div", { className: "cls_login_layout_oauth_section", children: _jsx(GoogleSignInButton, { label: oauthConfig.google_button_text }) })), oauthConfig.enable_google && oauthConfig.enable_email_password && (_jsx(OAuthDivider, { text: oauthConfig.oauth_divider_text })), oauthConfig.enable_email_password && (_jsxs("form", { className: "cls_login_layout_form_fields flex flex-col gap-5", onSubmit: form.handleSubmit, "aria-label": "Login form", children: [renderFields(form), _jsx(FormActionButtons, { submitLabel: resolvedLabels.submitButton, cancelLabel: resolvedLabels.cancelButton, buttonPalette: resolvedButtonPalette, isSubmitDisabled: form.isSubmitDisabled, onCancel: form.handleCancel, submitAriaLabel: "Submit login form", cancelAriaLabel: "Cancel login form" }), ((forgot_password_path && forgot_password_label) || (show_create_account_link && create_account_path && create_account_label)) && (_jsxs("div", { className: "cls_login_layout_support_links flex flex-col gap-1 text-sm text-muted-foreground", children: [forgot_password_path && forgot_password_label && (_jsx(Link, { href: forgot_password_path, className: "cls_login_layout_forgot_password_link text-primary underline-offset-4 hover:underline", "aria-label": "Go to forgot password page", children: forgot_password_label })), show_create_account_link && create_account_path && create_account_label && (_jsx(Link, { href: create_account_path, className: "cls_login_layout_create_account_link text-primary underline-offset-4 hover:underline", "aria-label": "Go to create account page", children: create_account_label }))] }))] })), show_create_account_link && create_account_path && create_account_label && !oauthConfig.enable_email_password && oauthConfig.enable_google && (_jsx("div", { className: "cls_login_layout_support_links mt-4 text-center text-sm text-muted-foreground", children: _jsx(Link, { href: create_account_path, className: "cls_login_layout_create_account_link text-primary underline-offset-4 hover:underline", "aria-label": "Go to create account page", children: create_account_label }) }))] }) }) }));
110
+ // Two-column mode (default): main flow with already-logged-in guard.
111
+ return (_jsx(AlreadyLoggedInGuard, { image_src: image_src, image_alt: image_alt, image_background_color: image_background_color, message: alreadyLoggedInMessage, showLogoutButton: showLogoutButton, showReturnHomeButton: showReturnHomeButton, returnHomeButtonLabel: returnHomeButtonLabel, returnHomePath: returnHomePath, children: _jsx(TwoColumnAuthLayout, { imageSrc: image_src, imageAlt: image_alt, imageBackgroundColor: image_background_color, formContent: mainContent }) }));
112
+ }
113
+ // Same-origin path validator for the `?redirect=` query param. Accepts
114
+ // values like "/foo" or "/foo?bar". Rejects anything that could redirect
115
+ // off-origin (`//`, schemes, javascript: URIs, fragments without leading
116
+ // slash). Used by both login and register layouts.
117
+ function isSafeRedirectPath(value) {
118
+ if (!value)
119
+ return false;
120
+ if (!value.startsWith("/"))
121
+ return false;
122
+ if (value.startsWith("//"))
123
+ return false;
124
+ if (/^[a-z][a-z0-9+.\-]*:/i.test(value))
125
+ return false;
126
+ return true;
83
127
  }
@@ -12,8 +12,10 @@ export type OAuthLayoutConfig = {
12
12
  oauth_divider_text: string;
13
13
  };
14
14
  export type RegisterLayoutProps<TClient = unknown> = {
15
- image_src: string | StaticImageData;
16
- image_alt: string;
15
+ /** Image source for the visual panel. Required when `layout` is `"two_column"` (the default); ignored when `"form_only"`. */
16
+ image_src?: string | StaticImageData;
17
+ /** Image alt text. Required when `layout` is `"two_column"` (the default); ignored when `"form_only"`. */
18
+ image_alt?: string;
17
19
  image_background_color?: string;
18
20
  field_overrides?: LayoutFieldMapOverrides;
19
21
  labels?: LayoutLabelOverrides;
@@ -31,6 +33,8 @@ export type RegisterLayoutProps<TClient = unknown> = {
31
33
  urlOnLogon?: string;
32
34
  /** OAuth configuration */
33
35
  oauth?: OAuthLayoutConfig;
36
+ /** Layout mode (default: `"two_column"`). See `LoginLayoutProps.layout` for the full description. */
37
+ layout?: "two_column" | "form_only";
34
38
  };
35
- export default function register_layout<TClient>({ image_src, image_alt, image_background_color, field_overrides, labels, button_colors, password_requirements, show_name_field, data_client, alreadyLoggedInMessage, showLogoutButton, showReturnHomeButton, returnHomeButtonLabel, returnHomePath, signInPath, signInLabel, urlOnLogon, oauth, }: RegisterLayoutProps<TClient>): import("react/jsx-runtime").JSX.Element;
39
+ export default function register_layout<TClient>({ image_src, image_alt, image_background_color, field_overrides, labels, button_colors, password_requirements, show_name_field, data_client, alreadyLoggedInMessage, showLogoutButton, showReturnHomeButton, returnHomeButtonLabel, returnHomePath, signInPath, signInLabel, urlOnLogon, oauth, layout, }: RegisterLayoutProps<TClient>): import("react/jsx-runtime").JSX.Element;
36
40
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/register/index.tsx"],"names":[],"mappings":"AAiBA,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,4BAA4B,EAClC,MAAM,uCAAuC,CAAC;AAY/C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGlD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,gCAAgC;IAChC,aAAa,EAAE,OAAO,CAAC;IACvB,8CAA8C;IAC9C,qBAAqB,EAAE,OAAO,CAAC;IAC/B,kDAAkD;IAClD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,mBAAmB,CAAC,OAAO,GAAG,OAAO,IAAI;IACnD,SAAS,EAAE,MAAM,GAAG,eAAe,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,qBAAqB,CAAC,EAAE,4BAA4B,CAAC;IACrD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0BAA0B;IAC1B,KAAK,CAAC,EAAE,iBAAiB,CAAC;CAC3B,CAAC;AAYF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,OAAO,EAAE,EAC/C,SAAS,EACT,SAAS,EACT,sBAAkC,EAClC,eAAe,EACf,MAAM,EACN,aAAa,EACb,qBAAqB,EACrB,eAAsB,EACtB,WAAW,EACX,sBAAoD,EACpD,gBAAuB,EACvB,oBAA4B,EAC5B,qBAAqC,EACrC,cAAoB,EACpB,UAA+B,EAC/B,WAAuB,EACvB,UAAU,EACV,KAAK,GACN,EAAE,mBAAmB,CAAC,OAAO,CAAC,2CAgN9B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/register/index.tsx"],"names":[],"mappings":"AAiBA,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,4BAA4B,EAClC,MAAM,uCAAuC,CAAC;AAY/C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGlD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,gCAAgC;IAChC,aAAa,EAAE,OAAO,CAAC;IACvB,8CAA8C;IAC9C,qBAAqB,EAAE,OAAO,CAAC;IAC/B,kDAAkD;IAClD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,mBAAmB,CAAC,OAAO,GAAG,OAAO,IAAI;IACnD,6HAA6H;IAC7H,SAAS,CAAC,EAAE,MAAM,GAAG,eAAe,CAAC;IACrC,0GAA0G;IAC1G,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,qBAAqB,CAAC,EAAE,4BAA4B,CAAC;IACrD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0BAA0B;IAC1B,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,qGAAqG;IACrG,MAAM,CAAC,EAAE,YAAY,GAAG,WAAW,CAAC;CACrC,CAAC;AAYF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,OAAO,EAAE,EAC/C,SAAS,EACT,SAAS,EACT,sBAAkC,EAClC,eAAe,EACf,MAAM,EACN,aAAa,EACb,qBAAqB,EACrB,eAAsB,EACtB,WAAW,EACX,sBAAoD,EACpD,gBAAuB,EACvB,oBAA4B,EAC5B,qBAAqC,EACrC,cAAoB,EACpB,UAA+B,EAC/B,WAAuB,EACvB,UAAU,EACV,KAAK,EACL,MAAqB,GACtB,EAAE,mBAAmB,CAAC,OAAO,CAAC,2CAqO9B"}
@@ -24,7 +24,8 @@ const ORDERED_FIELDS = [
24
24
  REGISTER_FIELD_IDS.CONFIRM_PASSWORD,
25
25
  ];
26
26
  // section: component
27
- export default function register_layout({ image_src, image_alt, image_background_color = "#f1f5f9", field_overrides, labels, button_colors, password_requirements, show_name_field = true, data_client, alreadyLoggedInMessage = "You are already logged in", showLogoutButton = true, showReturnHomeButton = false, returnHomeButtonLabel = "Return home", returnHomePath = "/", signInPath = "/hazo_auth/login", signInLabel = "Sign in", urlOnLogon, oauth, }) {
27
+ export default function register_layout({ image_src, image_alt, image_background_color = "#f1f5f9", field_overrides, labels, button_colors, password_requirements, show_name_field = true, data_client, alreadyLoggedInMessage = "You are already logged in", showLogoutButton = true, showReturnHomeButton = false, returnHomeButtonLabel = "Return home", returnHomePath = "/", signInPath = "/hazo_auth/login", signInLabel = "Sign in", urlOnLogon, oauth, layout = "two_column", }) {
28
+ var _a;
28
29
  // Default OAuth config: both enabled
29
30
  const oauthConfig = oauth || {
30
31
  enable_google: true,
@@ -35,6 +36,17 @@ export default function register_layout({ image_src, image_alt, image_background
35
36
  // Read OAuth error from URL query params (e.g., ?error=AccessDenied)
36
37
  const searchParams = useSearchParams();
37
38
  const oauthError = searchParams.get("error");
39
+ // `?redirect=` survives through both the email/password registration form
40
+ // (via `use_register_form`'s urlOnLogon flow) and the Google OAuth round-
41
+ // trip (encoded into the GoogleSignInButton callback URL as `?next=`).
42
+ // Same safety check as the login layout — must be a same-origin path so
43
+ // a hostile invite link can't pivot the user off-domain.
44
+ const rawRedirect = searchParams.get("redirect");
45
+ const safeRedirect = isSafeRedirectPath(rawRedirect) ? rawRedirect : null;
46
+ const oauthCallbackUrl = safeRedirect
47
+ ? `/api/hazo_auth/oauth/google/callback?next=${encodeURIComponent(safeRedirect)}`
48
+ : "/api/hazo_auth/oauth/google/callback";
49
+ const effectiveUrlOnLogon = (_a = urlOnLogon !== null && urlOnLogon !== void 0 ? urlOnLogon : safeRedirect) !== null && _a !== void 0 ? _a : undefined;
38
50
  const getOAuthErrorMessage = (error) => {
39
51
  switch (error) {
40
52
  case "AccessDenied":
@@ -55,7 +67,7 @@ export default function register_layout({ image_src, image_alt, image_background
55
67
  showNameField: show_name_field,
56
68
  passwordRequirements: resolvedPasswordRequirements,
57
69
  dataClient: data_client,
58
- urlOnLogon: urlOnLogon,
70
+ urlOnLogon: effectiveUrlOnLogon,
59
71
  });
60
72
  const renderFields = (formState) => {
61
73
  const renderOrder = ORDERED_FIELDS.filter((fieldId) => show_name_field || fieldId !== REGISTER_FIELD_IDS.NAME);
@@ -80,5 +92,26 @@ export default function register_layout({ image_src, image_alt, image_background
80
92
  return (_jsx(FormFieldWrapper, { fieldId: fieldDefinition.id, label: fieldDefinition.label, input: inputElement, errorMessage: shouldShowError }, fieldId));
81
93
  });
82
94
  };
83
- return (_jsx(AlreadyLoggedInGuard, { image_src: image_src, image_alt: image_alt, image_background_color: image_background_color, message: alreadyLoggedInMessage, showLogoutButton: showLogoutButton, showReturnHomeButton: showReturnHomeButton, returnHomeButtonLabel: returnHomeButtonLabel, returnHomePath: returnHomePath, children: _jsx(TwoColumnAuthLayout, { imageSrc: image_src, imageAlt: image_alt, imageBackgroundColor: image_background_color, formContent: _jsxs(_Fragment, { children: [_jsx(FormHeader, { heading: resolvedLabels.heading, subHeading: resolvedLabels.subHeading }), oauthError && (_jsxs("div", { className: "cls_register_layout_oauth_error flex items-center gap-2 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700", children: [_jsx(AlertCircle, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }), _jsx("span", { children: getOAuthErrorMessage(oauthError) })] })), oauthConfig.enable_google && (_jsx("div", { className: "cls_register_layout_oauth_section", children: _jsx(GoogleSignInButton, { label: oauthConfig.google_button_text }) })), oauthConfig.enable_google && oauthConfig.enable_email_password && (_jsx(OAuthDivider, { text: oauthConfig.oauth_divider_text })), oauthConfig.enable_email_password && (_jsxs("form", { className: "cls_register_layout_form_fields flex flex-col gap-5", onSubmit: form.handleSubmit, "aria-label": "Registration form", children: [renderFields(form), _jsx(FormActionButtons, { submitLabel: resolvedLabels.submitButton, cancelLabel: resolvedLabels.cancelButton, buttonPalette: resolvedButtonPalette, isSubmitDisabled: form.isSubmitDisabled, onCancel: form.handleCancel, submitAriaLabel: "Submit registration form", cancelAriaLabel: "Cancel registration form" }), _jsxs("div", { className: "cls_register_layout_sign_in_link flex items-center justify-center gap-1 text-sm text-muted-foreground", children: [_jsx("span", { children: "Already have an account?" }), _jsx(Link, { href: signInPath, className: "cls_register_layout_sign_in_link_text text-primary underline-offset-4 hover:underline", "aria-label": "Go to sign in page", children: signInLabel })] }), form.isSubmitting && (_jsx("div", { className: "cls_register_submitting_indicator text-sm text-slate-600 text-center", children: "Registering..." }))] })), !oauthConfig.enable_email_password && oauthConfig.enable_google && (_jsxs("div", { className: "cls_register_layout_sign_in_link mt-4 flex items-center justify-center gap-1 text-sm text-muted-foreground", children: [_jsx("span", { children: "Already have an account?" }), _jsx(Link, { href: signInPath, className: "cls_register_layout_sign_in_link_text text-primary underline-offset-4 hover:underline", "aria-label": "Go to sign in page", children: signInLabel })] }))] }) }) }));
95
+ // Form content used in both layout modes. See `login_layout` for the
96
+ // rationale: keeps the form independent of TwoColumnAuthLayout / AuthPageShell
97
+ // so consumers can compose it into their own brand chrome.
98
+ const mainContent = (_jsxs(_Fragment, { children: [_jsx(FormHeader, { heading: resolvedLabels.heading, subHeading: resolvedLabels.subHeading }), oauthError && (_jsxs("div", { className: "cls_register_layout_oauth_error flex items-center gap-2 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700", children: [_jsx(AlertCircle, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }), _jsx("span", { children: getOAuthErrorMessage(oauthError) })] })), oauthConfig.enable_google && (_jsx("div", { className: "cls_register_layout_oauth_section", children: _jsx(GoogleSignInButton, { label: oauthConfig.google_button_text, callbackUrl: oauthCallbackUrl }) })), oauthConfig.enable_google && oauthConfig.enable_email_password && (_jsx(OAuthDivider, { text: oauthConfig.oauth_divider_text })), oauthConfig.enable_email_password && (_jsxs("form", { className: "cls_register_layout_form_fields flex flex-col gap-5", onSubmit: form.handleSubmit, "aria-label": "Registration form", children: [renderFields(form), _jsx(FormActionButtons, { submitLabel: resolvedLabels.submitButton, cancelLabel: resolvedLabels.cancelButton, buttonPalette: resolvedButtonPalette, isSubmitDisabled: form.isSubmitDisabled, onCancel: form.handleCancel, submitAriaLabel: "Submit registration form", cancelAriaLabel: "Cancel registration form" }), _jsxs("div", { className: "cls_register_layout_sign_in_link flex items-center justify-center gap-1 text-sm text-muted-foreground", children: [_jsx("span", { children: "Already have an account?" }), _jsx(Link, { href: signInPath, className: "cls_register_layout_sign_in_link_text text-primary underline-offset-4 hover:underline", "aria-label": "Go to sign in page", children: signInLabel })] }), form.isSubmitting && (_jsx("div", { className: "cls_register_submitting_indicator text-sm text-slate-600 text-center", children: "Registering..." }))] })), !oauthConfig.enable_email_password && oauthConfig.enable_google && (_jsxs("div", { className: "cls_register_layout_sign_in_link mt-4 flex items-center justify-center gap-1 text-sm text-muted-foreground", children: [_jsx("span", { children: "Already have an account?" }), _jsx(Link, { href: signInPath, className: "cls_register_layout_sign_in_link_text text-primary underline-offset-4 hover:underline", "aria-label": "Go to sign in page", children: signInLabel })] }))] }));
99
+ // form_only mode: emit just the form content. See login_layout for the
100
+ // rationale; AlreadyLoggedInGuard is intentionally skipped here.
101
+ if (layout === "form_only") {
102
+ return mainContent;
103
+ }
104
+ return (_jsx(AlreadyLoggedInGuard, { image_src: image_src, image_alt: image_alt, image_background_color: image_background_color, message: alreadyLoggedInMessage, showLogoutButton: showLogoutButton, showReturnHomeButton: showReturnHomeButton, returnHomeButtonLabel: returnHomeButtonLabel, returnHomePath: returnHomePath, children: _jsx(TwoColumnAuthLayout, { imageSrc: image_src, imageAlt: image_alt, imageBackgroundColor: image_background_color, formContent: mainContent }) }));
105
+ }
106
+ // Same-origin path validator. See login/index.tsx for the rationale.
107
+ function isSafeRedirectPath(value) {
108
+ if (!value)
109
+ return false;
110
+ if (!value.startsWith("/"))
111
+ return false;
112
+ if (value.startsWith("//"))
113
+ return false;
114
+ if (/^[a-z][a-z0-9+.\-]*:/i.test(value))
115
+ return false;
116
+ return true;
84
117
  }
@@ -0,0 +1,20 @@
1
+ export type FloatingHomeLinkProps = {
2
+ /** Path to navigate to. Defaults to "/" — the consuming app's root. */
3
+ path?: string;
4
+ /** Label shown next to the back arrow. */
5
+ label?: string;
6
+ /** Override classes on the anchor (positioning, colours, etc.). */
7
+ className?: string;
8
+ };
9
+ /**
10
+ * Floating "← Back to home" link. Renders fixed top-left of the viewport so
11
+ * users mid-auth-flow always have a way out. Use on auth pages that don't
12
+ * include the full `<AuthNavbar>` (e.g. when consumer wraps `LoginPage` or
13
+ * `RegisterPage` with `layout="form_only"` and their own brand chrome).
14
+ *
15
+ * Reads label/path from props so consumers can localise — the navbar config
16
+ * helper `get_navbar_config()` is the typical source on the server side.
17
+ */
18
+ export declare function FloatingHomeLink({ path, label, className, }: FloatingHomeLinkProps): import("react/jsx-runtime").JSX.Element;
19
+ export default FloatingHomeLink;
20
+ //# sourceMappingURL=floating_home_link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floating_home_link.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/shared/components/floating_home_link.tsx"],"names":[],"mappings":"AAUA,MAAM,MAAM,qBAAqB,GAAG;IAClC,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAGF;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,IAAU,EACV,KAAsB,EACtB,SAAS,GACV,EAAE,qBAAqB,2CAoBvB;AAED,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,29 @@
1
+ // file_description: small "back to home" affordance pinned to the top-left of auth pages
2
+ // section: client_directive
3
+ "use client";
4
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
+ // section: imports
6
+ import Link from "next/link";
7
+ import { ArrowLeft } from "lucide-react";
8
+ import { cn } from "../../../../lib/utils.js";
9
+ // section: component
10
+ /**
11
+ * Floating "← Back to home" link. Renders fixed top-left of the viewport so
12
+ * users mid-auth-flow always have a way out. Use on auth pages that don't
13
+ * include the full `<AuthNavbar>` (e.g. when consumer wraps `LoginPage` or
14
+ * `RegisterPage` with `layout="form_only"` and their own brand chrome).
15
+ *
16
+ * Reads label/path from props so consumers can localise — the navbar config
17
+ * helper `get_navbar_config()` is the typical source on the server side.
18
+ */
19
+ export function FloatingHomeLink({ path = "/", label = "Back to home", className, }) {
20
+ return (_jsxs(Link, { href: path, "aria-label": label, className: cn(
21
+ // Positioning: fixed top-left, above auth content, with a healthy
22
+ // safe-area margin so it never collides with brand-panel chrome.
23
+ "cls_floating_home_link fixed left-4 top-4 z-50 inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm font-medium",
24
+ // Visuals: subtle pill that adapts to whatever brand backdrop the
25
+ // consumer has rendered. Uses neutral text so it works on both light
26
+ // brand panels and dark ones.
27
+ "bg-background/70 text-foreground/80 shadow-sm backdrop-blur-sm transition-colors hover:bg-background hover:text-foreground", className), children: [_jsx(ArrowLeft, { className: "h-4 w-4", "aria-hidden": "true" }), _jsx("span", { children: label })] }));
28
+ }
29
+ export default FloatingHomeLink;
@@ -6,6 +6,14 @@ export type ProfilePicMenuProps = {
6
6
  register_path?: string;
7
7
  login_path?: string;
8
8
  settings_path?: string;
9
+ /**
10
+ * Where to navigate AFTER a successful logout (matches the navigation
11
+ * semantic of `login_path`/`register_path`/`settings_path`). Defaults to
12
+ * `router.refresh()` so the current page re-renders unauthenticated.
13
+ *
14
+ * Note: the logout API endpoint itself is always `${apiBasePath}/logout`
15
+ * — `logout_path` is purely a post-logout destination.
16
+ */
9
17
  logout_path?: string;
10
18
  custom_menu_items?: ProfilePicMenuMenuItem[];
11
19
  className?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"profile_pic_menu.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/shared/components/profile_pic_menu.tsx"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gDAAgD,CAAC;AAI7F,MAAM,MAAM,mBAAmB,GAAG;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,sBAAsB,EAAE,CAAC;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,SAAS,GAAG,IAAI,GAAG,IAAI,CAAC;IACtC,OAAO,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IACjC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAGF;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,EAC7B,kBAA0B,EAC1B,aAAyB,EACzB,aAAyB,EACzB,aAAqC,EACrC,UAA+B,EAC/B,aAAwC,EACxC,WAAW,EACX,iBAAsB,EACtB,SAAS,EACT,WAAuB,EACvB,OAAoB,EACpB,mBAA+B,GAChC,EAAE,mBAAmB,2CA6erB"}
1
+ {"version":3,"file":"profile_pic_menu.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/shared/components/profile_pic_menu.tsx"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gDAAgD,CAAC;AAI7F,MAAM,MAAM,mBAAmB,GAAG;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,sBAAsB,EAAE,CAAC;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,SAAS,GAAG,IAAI,GAAG,IAAI,CAAC;IACtC,OAAO,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IACjC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAGF;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,EAC7B,kBAA0B,EAC1B,aAAyB,EACzB,aAAyB,EACzB,aAAqC,EACrC,UAA+B,EAC/B,aAAwC,EACxC,WAAW,EACX,iBAAsB,EACtB,SAAS,EACT,WAAuB,EACvB,OAAoB,EACpB,mBAA+B,GAChC,EAAE,mBAAmB,2CAsfrB"}