hazo_auth 5.1.38 → 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/README.md +10 -0
- 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/schema/sqlite_schema.ts +12 -6
- 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/schema/sqlite_schema.d.ts +1 -1
- package/dist/lib/schema/sqlite_schema.d.ts.map +1 -1
- package/dist/lib/schema/sqlite_schema.js +12 -6
- 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 +40 -34
package/README.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
A reusable authentication UI component package powered by Next.js, TailwindCSS, and shadcn. It integrates `hazo_config` for configuration management and `hazo_connect` for data access, enabling future components to stay aligned with platform conventions.
|
|
4
4
|
|
|
5
|
+
### What's New in v5.1.39 🔧
|
|
6
|
+
|
|
7
|
+
**Postgres-Compatibility Schema Alignment** — fixes two long-standing drifts between the canonical SQLite schema and what the runtime actually writes. SQLite consumers see no behaviour change; Postgres + PostgREST consumers can now sign-up users without bespoke schema patches.
|
|
8
|
+
|
|
9
|
+
- **`hazo_refresh_tokens.token_hash`** — `token_service.ts` writes the argon2-hashed token, but the canonical schema only declared `token`. Added `token_hash TEXT` column and an index on it; relaxed the legacy plaintext `token` column to nullable. Runtime no longer writes plaintext, so this is purely additive for new installs.
|
|
10
|
+
- **Boolean-typed columns** — `hazo_users.email_verified` and the four flags on `hazo_user_relationships` (`can_view_progress`, `can_edit_profile`, `can_delete`, `is_self`) are now declared `BOOLEAN` instead of `INTEGER`. SQLite tolerates `BOOLEAN` as a NUMERIC affinity (existing 0/1 data evaluates correctly). PostgreSQL via PostgREST was previously rejecting the runtime's `email_verified: false` payload with `400 — invalid input syntax for type integer: "false"`; now it accepts the boolean cleanly.
|
|
11
|
+
- **Migration `014_align_schema_with_runtime.sql`** — applies the two changes above to existing deployments. SQLite version is active (additive `ADD COLUMN`); PostgreSQL ALTERs are commented blocks for consumers to opt in (with `NOTIFY pgrst, 'reload schema';` reminders).
|
|
12
|
+
|
|
13
|
+
Reported by Kinstripe (Postgres + PostgREST consumer) during sign-up smoke tests on 2026-04-28.
|
|
14
|
+
|
|
5
15
|
### What's New in v5.1.28
|
|
6
16
|
|
|
7
17
|
**Schema Validation, Permission Constants & DX Improvements**
|
|
@@ -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",
|
|
@@ -12,7 +12,7 @@ CREATE TABLE IF NOT EXISTS hazo_users (
|
|
|
12
12
|
email_address TEXT NOT NULL UNIQUE,
|
|
13
13
|
password_hash TEXT,
|
|
14
14
|
name TEXT,
|
|
15
|
-
email_verified
|
|
15
|
+
email_verified BOOLEAN DEFAULT false,
|
|
16
16
|
login_attempts INTEGER DEFAULT 0,
|
|
17
17
|
last_logon TEXT,
|
|
18
18
|
profile_picture_url TEXT,
|
|
@@ -35,10 +35,15 @@ CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);
|
|
|
35
35
|
CREATE INDEX IF NOT EXISTS idx_hazo_users_status ON hazo_users(status);
|
|
36
36
|
|
|
37
37
|
-- Refresh tokens table
|
|
38
|
+
-- Note: runtime (token_service.ts) writes token_hash (argon2-hashed value) on
|
|
39
|
+
-- insert and verifies via argon2.verify(token_hash, plaintext_token) on read.
|
|
40
|
+
-- The plaintext "token" column is retained nullable for legacy compatibility
|
|
41
|
+
-- but new writes only set token_hash.
|
|
38
42
|
CREATE TABLE IF NOT EXISTS hazo_refresh_tokens (
|
|
39
43
|
id TEXT PRIMARY KEY,
|
|
40
44
|
user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
41
|
-
token TEXT
|
|
45
|
+
token TEXT UNIQUE,
|
|
46
|
+
token_hash TEXT,
|
|
42
47
|
token_type TEXT DEFAULT 'refresh',
|
|
43
48
|
expires_at TEXT NOT NULL,
|
|
44
49
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
@@ -46,6 +51,7 @@ CREATE TABLE IF NOT EXISTS hazo_refresh_tokens (
|
|
|
46
51
|
|
|
47
52
|
CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_user ON hazo_refresh_tokens(user_id);
|
|
48
53
|
CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_token ON hazo_refresh_tokens(token);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_token_hash ON hazo_refresh_tokens(token_hash);
|
|
49
55
|
|
|
50
56
|
-- Roles table
|
|
51
57
|
CREATE TABLE IF NOT EXISTS hazo_roles (
|
|
@@ -145,10 +151,10 @@ CREATE TABLE IF NOT EXISTS hazo_user_relationships (
|
|
|
145
151
|
parent_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
146
152
|
child_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
147
153
|
relationship_type TEXT NOT NULL DEFAULT 'parent',
|
|
148
|
-
can_view_progress
|
|
149
|
-
can_edit_profile
|
|
150
|
-
can_delete
|
|
151
|
-
is_self
|
|
154
|
+
can_view_progress BOOLEAN DEFAULT true,
|
|
155
|
+
can_edit_profile BOOLEAN DEFAULT true,
|
|
156
|
+
can_delete BOOLEAN DEFAULT false,
|
|
157
|
+
is_self BOOLEAN DEFAULT false,
|
|
152
158
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
153
159
|
UNIQUE(parent_user_id, child_user_id)
|
|
154
160
|
);
|
|
@@ -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":"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"}
|
|
@@ -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"}
|