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.
- package/cli-src/lib/auth/ensure_anon_id.server.ts +88 -0
- package/cli-src/lib/auth/index.ts +3 -0
- package/cli-src/lib/cookies_config.edge.ts +1 -0
- package/cli-src/lib/cookies_config.server.ts +1 -0
- package/cli-src/lib/hazo_connect_setup.server.ts +0 -8
- package/cli-src/lib/services/session_token_service.ts +2 -2
- package/cli-src/lib/ui_shell_config.server.ts +6 -2
- package/dist/components/layouts/login/index.d.ts +16 -3
- package/dist/components/layouts/login/index.d.ts.map +1 -1
- package/dist/components/layouts/login/index.js +49 -5
- package/dist/components/layouts/register/index.d.ts +7 -3
- package/dist/components/layouts/register/index.d.ts.map +1 -1
- package/dist/components/layouts/register/index.js +36 -3
- package/dist/components/layouts/shared/components/floating_home_link.d.ts +20 -0
- package/dist/components/layouts/shared/components/floating_home_link.d.ts.map +1 -0
- package/dist/components/layouts/shared/components/floating_home_link.js +29 -0
- package/dist/components/layouts/shared/components/profile_pic_menu.d.ts +8 -0
- package/dist/components/layouts/shared/components/profile_pic_menu.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/profile_pic_menu.js +18 -8
- package/dist/components/layouts/shared/index.d.ts +2 -0
- package/dist/components/layouts/shared/index.d.ts.map +1 -1
- package/dist/components/layouts/shared/index.js +1 -0
- package/dist/components/ui/button.d.ts +2 -2
- package/dist/lib/auth/ensure_anon_id.server.d.ts +21 -0
- package/dist/lib/auth/ensure_anon_id.server.d.ts.map +1 -0
- package/dist/lib/auth/ensure_anon_id.server.js +73 -0
- package/dist/lib/auth/index.d.ts +1 -0
- package/dist/lib/auth/index.d.ts.map +1 -1
- package/dist/lib/auth/index.js +2 -0
- package/dist/lib/cookies_config.edge.d.ts +1 -0
- package/dist/lib/cookies_config.edge.d.ts.map +1 -1
- package/dist/lib/cookies_config.edge.js +1 -0
- package/dist/lib/cookies_config.server.d.ts +1 -0
- package/dist/lib/cookies_config.server.d.ts.map +1 -1
- package/dist/lib/cookies_config.server.js +1 -0
- package/dist/lib/hazo_connect_setup.server.d.ts.map +1 -1
- package/dist/lib/hazo_connect_setup.server.js +0 -8
- package/dist/lib/services/session_token_service.js +2 -2
- package/dist/lib/ui_shell_config.server.d.ts.map +1 -1
- package/dist/lib/ui_shell_config.server.js +6 -2
- package/dist/server/routes/oauth_google_callback.d.ts.map +1 -1
- package/dist/server/routes/oauth_google_callback.js +33 -6
- package/dist/server_pages/login.d.ts +13 -1
- package/dist/server_pages/login.d.ts.map +1 -1
- package/dist/server_pages/login.js +25 -9
- package/dist/server_pages/login_client_wrapper.d.ts +6 -4
- package/dist/server_pages/login_client_wrapper.d.ts.map +1 -1
- package/dist/server_pages/login_client_wrapper.js +2 -2
- package/dist/server_pages/register.d.ts +7 -1
- package/dist/server_pages/register.d.ts.map +1 -1
- package/dist/server_pages/register.js +18 -9
- package/dist/server_pages/register_client_wrapper.d.ts +6 -4
- package/dist/server_pages/register_client_wrapper.d.ts.map +1 -1
- package/dist/server_pages/register_client_wrapper.js +2 -2
- 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.
|
|
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.
|
|
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
|
-
"
|
|
65
|
+
"false"
|
|
62
66
|
).toLowerCase() === "true";
|
|
63
67
|
const standalone_show_description = get_config_value(
|
|
64
68
|
section,
|
|
65
69
|
"standalone_show_description",
|
|
66
|
-
"
|
|
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
|
-
|
|
16
|
-
|
|
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;
|
|
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
|
-
//
|
|
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:
|
|
108
|
+
return (_jsx(TwoColumnAuthLayout, { imageSrc: image_src, imageAlt: image_alt, imageBackgroundColor: image_background_color, formContent: successContent }));
|
|
81
109
|
}
|
|
82
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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;
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
38
|
-
|
|
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(
|
|
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
|
-
//
|
|
72
|
-
|
|
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:
|
|
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,
|
|
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" |
|
|
5
|
-
size?: "default" | "
|
|
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;
|