hazo_auth 5.1.40 → 5.2.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 (55) hide show
  1. package/cli-src/lib/auth/ensure_anon_id.server.ts +88 -0
  2. package/cli-src/lib/auth/index.ts +3 -0
  3. package/cli-src/lib/cookies_config.edge.ts +1 -0
  4. package/cli-src/lib/cookies_config.server.ts +1 -0
  5. package/cli-src/lib/hazo_connect_setup.server.ts +0 -8
  6. package/cli-src/lib/services/session_token_service.ts +2 -2
  7. package/cli-src/lib/ui_shell_config.server.ts +6 -2
  8. package/dist/components/layouts/login/index.d.ts +16 -3
  9. package/dist/components/layouts/login/index.d.ts.map +1 -1
  10. package/dist/components/layouts/login/index.js +49 -5
  11. package/dist/components/layouts/register/index.d.ts +7 -3
  12. package/dist/components/layouts/register/index.d.ts.map +1 -1
  13. package/dist/components/layouts/register/index.js +36 -3
  14. package/dist/components/layouts/shared/components/floating_home_link.d.ts +20 -0
  15. package/dist/components/layouts/shared/components/floating_home_link.d.ts.map +1 -0
  16. package/dist/components/layouts/shared/components/floating_home_link.js +29 -0
  17. package/dist/components/layouts/shared/components/profile_pic_menu.d.ts +8 -0
  18. package/dist/components/layouts/shared/components/profile_pic_menu.d.ts.map +1 -1
  19. package/dist/components/layouts/shared/components/profile_pic_menu.js +18 -8
  20. package/dist/components/layouts/shared/index.d.ts +2 -0
  21. package/dist/components/layouts/shared/index.d.ts.map +1 -1
  22. package/dist/components/layouts/shared/index.js +1 -0
  23. package/dist/components/ui/button.d.ts +2 -2
  24. package/dist/lib/auth/ensure_anon_id.server.d.ts +21 -0
  25. package/dist/lib/auth/ensure_anon_id.server.d.ts.map +1 -0
  26. package/dist/lib/auth/ensure_anon_id.server.js +73 -0
  27. package/dist/lib/auth/index.d.ts +1 -0
  28. package/dist/lib/auth/index.d.ts.map +1 -1
  29. package/dist/lib/auth/index.js +2 -0
  30. package/dist/lib/cookies_config.edge.d.ts +1 -0
  31. package/dist/lib/cookies_config.edge.d.ts.map +1 -1
  32. package/dist/lib/cookies_config.edge.js +1 -0
  33. package/dist/lib/cookies_config.server.d.ts +1 -0
  34. package/dist/lib/cookies_config.server.d.ts.map +1 -1
  35. package/dist/lib/cookies_config.server.js +1 -0
  36. package/dist/lib/hazo_connect_setup.server.d.ts.map +1 -1
  37. package/dist/lib/hazo_connect_setup.server.js +0 -8
  38. package/dist/lib/services/session_token_service.js +2 -2
  39. package/dist/lib/ui_shell_config.server.d.ts.map +1 -1
  40. package/dist/lib/ui_shell_config.server.js +6 -2
  41. package/dist/server/routes/oauth_google_callback.d.ts.map +1 -1
  42. package/dist/server/routes/oauth_google_callback.js +33 -6
  43. package/dist/server_pages/login.d.ts +13 -1
  44. package/dist/server_pages/login.d.ts.map +1 -1
  45. package/dist/server_pages/login.js +25 -9
  46. package/dist/server_pages/login_client_wrapper.d.ts +6 -4
  47. package/dist/server_pages/login_client_wrapper.d.ts.map +1 -1
  48. package/dist/server_pages/login_client_wrapper.js +2 -2
  49. package/dist/server_pages/register.d.ts +7 -1
  50. package/dist/server_pages/register.d.ts.map +1 -1
  51. package/dist/server_pages/register.js +18 -9
  52. package/dist/server_pages/register_client_wrapper.d.ts +6 -4
  53. package/dist/server_pages/register_client_wrapper.d.ts.map +1 -1
  54. package/dist/server_pages/register_client_wrapper.js +2 -2
  55. package/package.json +25 -20
@@ -0,0 +1,88 @@
1
+ // file_description: server-only helper to issue/read a stable opaque anonymous visitor ID via httpOnly cookie
2
+ //
3
+ // Used by hazo_feedback (and any future caller) to group submissions from the
4
+ // same anonymous visitor across page loads / weeks without requiring login.
5
+ //
6
+ // Signature choice (v5.2.0): async, request-only — Option A in the design doc.
7
+ // - Reads the existing cookie from the incoming `request.cookies`.
8
+ // - Writes a freshly-issued cookie via the async `cookies()` API from
9
+ // `next/headers`, which is the supported write surface in Server Components,
10
+ // Server Actions, and Route Handlers in Next.js 15+.
11
+ // - Tradeoff: NOT valid in Edge middleware (where `next/headers` throws). If a
12
+ // middleware caller ever needs this, add a sibling `ensure_anon_id_edge`
13
+ // that takes (request, response) and writes via `response.cookies.set`.
14
+ //
15
+ // Idempotent per-request: a per-request WeakMap remembers which id was issued
16
+ // for a given NextRequest instance, so calling `ensure_anon_id(request)` twice
17
+ // in the same request returns the same id and queues only one Set-Cookie. This
18
+ // matters because `request.cookies` reflects the *incoming* state and won't
19
+ // see an id that the same request just issued.
20
+
21
+ // section: server-only-guard
22
+ import "server-only";
23
+
24
+ // section: imports
25
+ import { NextRequest } from "next/server";
26
+ import { cookies } from "next/headers";
27
+ import {
28
+ BASE_COOKIE_NAMES,
29
+ get_cookie_name,
30
+ get_cookie_options,
31
+ } from "../cookies_config.server.js";
32
+
33
+ // section: constants
34
+ const TWO_YEARS_SECONDS = 60 * 60 * 24 * 365 * 2;
35
+
36
+ // section: state
37
+ // WeakMap keyed by NextRequest so entries are GC'd with the request.
38
+ const issued_per_request: WeakMap<NextRequest, string> = new WeakMap();
39
+
40
+ // section: main_function
41
+ /**
42
+ * Returns a stable opaque anonymous visitor ID for the caller, reading an
43
+ * existing httpOnly cookie or issuing a fresh UUID v4 if absent.
44
+ *
45
+ * - Cookie name: `hazo_auth_anon_id` (prefixed via `[hazo_auth__cookies] cookie_prefix`)
46
+ * - Cookie attributes: httpOnly, sameSite=lax, path=/, secure in production,
47
+ * maxAge = 2 years.
48
+ * - Idempotent per-request: calling twice with the same NextRequest returns
49
+ * the same id and only queues one Set-Cookie.
50
+ *
51
+ * Independent from authenticated identity: an authenticated user can still
52
+ * have an anon id cookie. Whether to consult it for a logged-in caller is up
53
+ * to the caller (hazo_feedback, for example, doesn't bother).
54
+ *
55
+ * @param request - The incoming NextRequest.
56
+ * @returns The existing or freshly-issued anonymous visitor id (UUID v4 string).
57
+ */
58
+ export async function ensure_anon_id(request: NextRequest): Promise<string> {
59
+ const cookie_name = get_cookie_name(BASE_COOKIE_NAMES.ANON_ID);
60
+
61
+ // Per-request idempotency: if we already issued for this NextRequest, reuse it.
62
+ const already_issued = issued_per_request.get(request);
63
+ if (already_issued) {
64
+ return already_issued;
65
+ }
66
+
67
+ // Read the existing cookie from the incoming request.
68
+ const existing = request.cookies.get(cookie_name)?.value;
69
+ if (existing) {
70
+ return existing;
71
+ }
72
+
73
+ // Issue a new id and queue the Set-Cookie via the next/headers cookie store.
74
+ const new_id = crypto.randomUUID();
75
+ const cookie_options = get_cookie_options({
76
+ httpOnly: true,
77
+ secure: process.env.NODE_ENV === "production",
78
+ sameSite: "lax" as const,
79
+ path: "/",
80
+ maxAge: TWO_YEARS_SECONDS,
81
+ });
82
+
83
+ const cookie_store = await cookies();
84
+ cookie_store.set(cookie_name, new_id, cookie_options);
85
+
86
+ issued_per_request.set(request, new_id);
87
+ return new_id;
88
+ }
@@ -11,6 +11,9 @@ export {
11
11
  } from "./auth_utils.server.js";
12
12
  export type { AuthResult, AuthUser } from "./auth_utils.server";
13
13
 
14
+ // section: anon_id_exports (v5.2)
15
+ export { ensure_anon_id } from "./ensure_anon_id.server.js";
16
+
14
17
  // section: tenant_auth_exports
15
18
  export {
16
19
  hazo_get_tenant_auth,
@@ -8,6 +8,7 @@ export const BASE_COOKIE_NAMES = {
8
8
  USER_EMAIL: "hazo_auth_user_email",
9
9
  SESSION: "hazo_auth_session",
10
10
  DEV_LOCK: "hazo_auth_dev_lock",
11
+ ANON_ID: "hazo_auth_anon_id", // v5.2: Stable opaque per-visitor ID for anonymous flows (e.g. hazo_feedback). Kept in sync with the server constant.
11
12
  } as const;
12
13
 
13
14
  // section: main_functions
@@ -29,6 +29,7 @@ export const BASE_COOKIE_NAMES = {
29
29
  SESSION: "hazo_auth_session",
30
30
  DEV_LOCK: "hazo_auth_dev_lock",
31
31
  SCOPE_ID: "hazo_auth_scope_id", // v5.2: Tenant context cookie for multi-tenancy
32
+ ANON_ID: "hazo_auth_anon_id", // v5.2: Stable opaque per-visitor ID for anonymous flows (e.g. hazo_feedback)
32
33
  } as const;
33
34
 
34
35
  // section: main_function
@@ -109,14 +109,6 @@ function get_hazo_connect_config(): {
109
109
  }
110
110
  }
111
111
 
112
- // Log the resolved path so consumers can see which DB file is being used
113
- logger.info("hazo_connect_sqlite_path_resolved", {
114
- filename: "hazo_connect_setup.server.ts",
115
- line_number: 0,
116
- sqlite_path,
117
- process_cwd: process.cwd(),
118
- config_source: hazo_connect_section ? "hazo_auth_config.ini" : "environment variables",
119
- });
120
112
 
121
113
  return {
122
114
  type: "sqlite",
@@ -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"}
@@ -34,8 +34,11 @@ export function ProfilePicMenu({ show_single_button = false, sign_up_label = "Si
34
34
  const [isLoggingOut, setIsLoggingOut] = useState(false);
35
35
  const [shiftKeyHeld, setShiftKeyHeld] = useState(false);
36
36
  const [showPermissionsDialog, setShowPermissionsDialog] = useState(false);
37
- // Use provided logout_path or default to context-based path
38
- const effectiveLogoutPath = logout_path || `${apiBasePath}/logout`;
37
+ // The logout API endpoint is always derived from apiBasePath. `logout_path`
38
+ // is a navigation destination for AFTER the logout completes (not the API
39
+ // URL — earlier versions conflated these and broke when consumers passed
40
+ // a non-API path like "/").
41
+ const logoutApiPath = `${apiBasePath}/logout`;
39
42
  // Get initials from name or email
40
43
  const getInitials = () => {
41
44
  var _a, _b;
@@ -55,7 +58,7 @@ export function ProfilePicMenu({ show_single_button = false, sign_up_label = "Si
55
58
  const handleLogout = async () => {
56
59
  setIsLoggingOut(true);
57
60
  try {
58
- const response = await fetch(effectiveLogoutPath, {
61
+ const response = await fetch(logoutApiPath, {
59
62
  method: "POST",
60
63
  headers: {
61
64
  "Content-Type": "application/json",
@@ -68,8 +71,13 @@ export function ProfilePicMenu({ show_single_button = false, sign_up_label = "Si
68
71
  toast.success("Logged out successfully");
69
72
  // Trigger auth status refresh in all components
70
73
  trigger_auth_status_refresh();
71
- // Refresh the page to update authentication state
72
- router.refresh();
74
+ // Navigate to the configured post-logout destination, or refresh in place.
75
+ if (logout_path) {
76
+ router.push(logout_path);
77
+ }
78
+ else {
79
+ router.refresh();
80
+ }
73
81
  }
74
82
  catch (error) {
75
83
  const errorMessage = error instanceof Error ? error.message : "Logout failed. Please try again.";
@@ -116,11 +124,13 @@ export function ProfilePicMenu({ show_single_button = false, sign_up_label = "Si
116
124
  order: 1,
117
125
  id: "default_settings",
118
126
  });
119
- // Logout (link, order: 2)
127
+ // Logout (link, order: 2). The href is a fallback for accessibility —
128
+ // `default_logout` items are click-intercepted by `handleLogout` which
129
+ // POSTs to the API path and then navigates per `logout_path`.
120
130
  items.push({
121
131
  type: "link",
122
132
  label: "Logout",
123
- href: effectiveLogoutPath,
133
+ href: logoutApiPath,
124
134
  order: 2,
125
135
  id: "default_logout",
126
136
  });
@@ -141,7 +151,7 @@ export function ProfilePicMenu({ show_single_button = false, sign_up_label = "Si
141
151
  return a.order - b.order;
142
152
  });
143
153
  return items;
144
- }, [authStatus.authenticated, authStatus.name, authStatus.email, settings_path, effectiveLogoutPath, custom_menu_items]);
154
+ }, [authStatus.authenticated, authStatus.name, authStatus.email, settings_path, logoutApiPath, custom_menu_items]);
145
155
  // Avatar size classes
146
156
  const avatarSizeClasses = {
147
157
  sm: "h-8 w-8",
@@ -2,6 +2,8 @@ export { AlreadyLoggedInGuard } from "./components/already_logged_in_guard.js";
2
2
  export { AuthNavbar } from "./components/auth_navbar.js";
3
3
  export type { AuthNavbarProps } from "./components/auth_navbar";
4
4
  export { FieldErrorMessage } from "./components/field_error_message.js";
5
+ export { FloatingHomeLink } from "./components/floating_home_link.js";
6
+ export type { FloatingHomeLinkProps } from "./components/floating_home_link";
5
7
  export { FormActionButtons } from "./components/form_action_buttons.js";
6
8
  export { FormFieldWrapper } from "./components/form_field_wrapper.js";
7
9
  export { FormHeader } from "./components/form_header.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/shared/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,YAAY,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEhE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,YAAY,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAE7F,OAAO,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,YAAY,EAAE,uBAAuB,EAAE,MAAM,oCAAoC,CAAC;AAClF,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AACjF,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAGnF,cAAc,+BAA+B,CAAC;AAG9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACnE,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAGlE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,cAAc,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/shared/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,YAAY,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEhE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,YAAY,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,YAAY,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAE7F,OAAO,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,YAAY,EAAE,uBAAuB,EAAE,MAAM,oCAAoC,CAAC;AAClF,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AACjF,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAGnF,cAAc,+BAA+B,CAAC;AAG9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACnE,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAGlE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,cAAc,oBAAoB,CAAC"}
@@ -4,6 +4,7 @@ export { AlreadyLoggedInGuard } from "./components/already_logged_in_guard.js";
4
4
  export { AuthNavbar } from "./components/auth_navbar.js";
5
5
  // AuthPageShell - NOT exported (test workspace component only)
6
6
  export { FieldErrorMessage } from "./components/field_error_message.js";
7
+ export { FloatingHomeLink } from "./components/floating_home_link.js";
7
8
  export { FormActionButtons } from "./components/form_action_buttons.js";
8
9
  export { FormFieldWrapper } from "./components/form_field_wrapper.js";
9
10
  export { FormHeader } from "./components/form_header.js";
@@ -1,8 +1,8 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const buttonVariants: (props?: ({
4
- variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | null | undefined;
5
- size?: "default" | "sm" | "lg" | "icon" | null | undefined;
4
+ variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
5
+ size?: "default" | "icon" | "sm" | "lg" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
8
8
  asChild?: boolean;