hazo_auth 5.1.29 → 5.1.31
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 +12 -0
- package/cli-src/lib/config/config_loader.server.ts +24 -0
- package/cli-src/lib/login_config.server.ts +6 -5
- package/cli-src/lib/utils/get_origin_url.ts +61 -0
- package/dist/components/layouts/login/index.d.ts.map +1 -1
- package/dist/components/layouts/login/index.js +1 -1
- package/dist/lib/config/config_loader.server.d.ts +12 -0
- package/dist/lib/config/config_loader.server.d.ts.map +1 -1
- package/dist/lib/config/config_loader.server.js +18 -0
- package/dist/lib/login_config.server.d.ts.map +1 -1
- package/dist/lib/login_config.server.js +6 -5
- package/dist/lib/utils/get_origin_url.d.ts +23 -0
- package/dist/lib/utils/get_origin_url.d.ts.map +1 -0
- package/dist/lib/utils/get_origin_url.js +57 -0
- package/dist/server/routes/oauth_google_callback.d.ts.map +1 -1
- package/dist/server/routes/oauth_google_callback.js +5 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1097,6 +1097,18 @@ enable_email_password = false
|
|
|
1097
1097
|
show_create_account_link = false
|
|
1098
1098
|
```
|
|
1099
1099
|
|
|
1100
|
+
**Hide links by setting path/label to empty (v5.1.30+):**
|
|
1101
|
+
```ini
|
|
1102
|
+
[hazo_auth__login_layout]
|
|
1103
|
+
; Hide "Forgot password?" link
|
|
1104
|
+
forgot_password_path =
|
|
1105
|
+
forgot_password_label =
|
|
1106
|
+
|
|
1107
|
+
; Hide "Create account" link (alternative to show_create_account_link = false)
|
|
1108
|
+
create_account_path =
|
|
1109
|
+
create_account_label =
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1100
1112
|
**Disable Google OAuth (email/password only):**
|
|
1101
1113
|
```ini
|
|
1102
1114
|
[hazo_auth__oauth]
|
|
@@ -82,6 +82,30 @@ export function get_config_value(
|
|
|
82
82
|
return section[key].trim() || default_value;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Gets a single config value from a section, preserving empty strings.
|
|
87
|
+
* Unlike get_config_value, this returns "" when the INI key exists with an empty value,
|
|
88
|
+
* rather than falling back to the default. Useful for config keys where "" means
|
|
89
|
+
* "intentionally empty" (e.g., hiding a link by setting its path to empty).
|
|
90
|
+
* @param section_name - Name of the section
|
|
91
|
+
* @param key - Key name within the section
|
|
92
|
+
* @param default_value - Default value if key is not found in config
|
|
93
|
+
* @param file_path - Optional custom config file path
|
|
94
|
+
* @returns Config value as string, or "" if key exists but empty, or default_value if key missing
|
|
95
|
+
*/
|
|
96
|
+
export function get_config_value_allow_empty(
|
|
97
|
+
section_name: string,
|
|
98
|
+
key: string,
|
|
99
|
+
default_value: string,
|
|
100
|
+
file_path?: string,
|
|
101
|
+
): string {
|
|
102
|
+
const section = read_config_section(section_name, file_path);
|
|
103
|
+
if (!section || section[key] === undefined) {
|
|
104
|
+
return default_value;
|
|
105
|
+
}
|
|
106
|
+
return section[key].trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
85
109
|
/**
|
|
86
110
|
* Gets a boolean config value from a section
|
|
87
111
|
* @param section_name - Name of the section
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import "server-only";
|
|
4
4
|
|
|
5
5
|
// section: imports
|
|
6
|
-
import { get_config_value } from "./config/config_loader.server.js";
|
|
6
|
+
import { get_config_value, get_config_value_allow_empty } from "./config/config_loader.server.js";
|
|
7
7
|
import { get_already_logged_in_config } from "./already_logged_in_config.server.js";
|
|
8
8
|
import { get_oauth_config, type OAuthConfig } from "./oauth_config.server.js";
|
|
9
9
|
|
|
@@ -49,18 +49,19 @@ export function get_login_config(): LoginConfig {
|
|
|
49
49
|
// Read success message (defaults to "Successfully logged in")
|
|
50
50
|
const successMessage = get_config_value(section, "success_message", "Successfully logged in");
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
// Use allow_empty variant so that setting path/label to "" in config hides the link
|
|
53
|
+
const forgotPasswordPath = get_config_value_allow_empty(
|
|
53
54
|
section,
|
|
54
55
|
"forgot_password_path",
|
|
55
56
|
"/hazo_auth/forgot_password"
|
|
56
57
|
);
|
|
57
|
-
const forgotPasswordLabel =
|
|
58
|
+
const forgotPasswordLabel = get_config_value_allow_empty(
|
|
58
59
|
section,
|
|
59
60
|
"forgot_password_label",
|
|
60
61
|
"Forgot password?"
|
|
61
62
|
);
|
|
62
|
-
const createAccountPath =
|
|
63
|
-
const createAccountLabel =
|
|
63
|
+
const createAccountPath = get_config_value_allow_empty(section, "create_account_path", "/hazo_auth/register");
|
|
64
|
+
const createAccountLabel = get_config_value_allow_empty(
|
|
64
65
|
section,
|
|
65
66
|
"create_account_label",
|
|
66
67
|
"Create account"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// file_description: Resolves the public-facing origin URL for constructing redirects
|
|
2
|
+
// When running behind a reverse proxy (Cloudflare, nginx), request.url resolves to
|
|
3
|
+
// the internal address (e.g. http://localhost:3000). This utility returns the correct
|
|
4
|
+
// public origin using NEXTAUTH_URL, falling back to request.url.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Gets the public-facing origin URL for redirect construction.
|
|
8
|
+
*
|
|
9
|
+
* Behind reverse proxies (Cloudflare, nginx, etc.), `request.url` contains the
|
|
10
|
+
* internal address (e.g. `http://localhost:3000`), not the public domain.
|
|
11
|
+
* This function returns the correct origin from environment variables.
|
|
12
|
+
*
|
|
13
|
+
* Priority: NEXTAUTH_URL > APP_DOMAIN_NAME > NEXT_PUBLIC_APP_URL > APP_URL > request.url
|
|
14
|
+
*
|
|
15
|
+
* @param request_url - The request.url to use as fallback
|
|
16
|
+
* @returns The origin URL (e.g. "https://gotimer.org")
|
|
17
|
+
*/
|
|
18
|
+
export function get_origin_url(request_url: string): string {
|
|
19
|
+
// NEXTAUTH_URL is the standard for NextAuth.js apps
|
|
20
|
+
const nextauth_url = process.env.NEXTAUTH_URL;
|
|
21
|
+
if (nextauth_url) {
|
|
22
|
+
return nextauth_url.replace(/\/$/, "");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// APP_DOMAIN_NAME (with protocol handling)
|
|
26
|
+
const app_domain = process.env.APP_DOMAIN_NAME;
|
|
27
|
+
if (app_domain) {
|
|
28
|
+
const domain = app_domain.trim();
|
|
29
|
+
if (domain.startsWith("http://") || domain.startsWith("https://")) {
|
|
30
|
+
return domain.replace(/\/$/, "");
|
|
31
|
+
}
|
|
32
|
+
return `https://${domain}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Other common env vars
|
|
36
|
+
const env_url = process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL;
|
|
37
|
+
if (env_url) {
|
|
38
|
+
return env_url.replace(/\/$/, "");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Fallback to request.url (works in development without a proxy)
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(request_url);
|
|
44
|
+
return url.origin;
|
|
45
|
+
} catch {
|
|
46
|
+
return request_url;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a URL using the public-facing origin instead of request.url.
|
|
52
|
+
* Drop-in replacement for `new URL(path, request.url)` in route handlers.
|
|
53
|
+
*
|
|
54
|
+
* @param path - The path or relative URL (e.g. "/hazo_auth/login")
|
|
55
|
+
* @param request_url - The request.url (used as fallback only)
|
|
56
|
+
* @returns A URL object with the correct public origin
|
|
57
|
+
*/
|
|
58
|
+
export function create_redirect_url(path: string, request_url: string): URL {
|
|
59
|
+
const origin = get_origin_url(request_url);
|
|
60
|
+
return new URL(path, origin);
|
|
61
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/login/index.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAWlD,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAC1B,MAAM,uCAAuC,CAAC;AAW/C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAG1E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,gCAAgC;IAChC,aAAa,EAAE,OAAO,CAAC;IACvB,8CAA8C;IAC9C,qBAAqB,EAAE,OAAO,CAAC;IAC/B,kDAAkD;IAClD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,OAAO,GAAG,OAAO,IAAI;IAChD,SAAS,EAAE,MAAM,GAAG,eAAe,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,WAAW,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,CAAC,EAAE;QACP,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QACjE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;KAClE,CAAC;IACF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,sDAAsD;IACtD,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0BAA0B;IAC1B,KAAK,CAAC,EAAE,iBAAiB,CAAC;CAC3B,CAAC;AAUF,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,OAAO,EAAE,EAC5C,SAAS,EACT,SAAS,EACT,sBAAkC,EAClC,eAAe,EACf,MAAM,EACN,aAAa,EACb,WAAW,EACX,MAAM,EACN,aAAa,EACb,cAAyC,EACzC,sBAAoD,EACpD,gBAAuB,EACvB,oBAA4B,EAC5B,qBAAqC,EACrC,cAAoB,EACpB,oBAAmD,EACnD,qBAA0C,EAC1C,mBAA2C,EAC3C,oBAAuC,EACvC,wBAA+B,EAC/B,UAAU,EACV,KAAK,GACN,EAAE,gBAAgB,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/login/index.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAWlD,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAC1B,MAAM,uCAAuC,CAAC;AAW/C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAG1E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,gCAAgC;IAChC,aAAa,EAAE,OAAO,CAAC;IACvB,8CAA8C;IAC9C,qBAAqB,EAAE,OAAO,CAAC;IAC/B,kDAAkD;IAClD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,OAAO,GAAG,OAAO,IAAI;IAChD,SAAS,EAAE,MAAM,GAAG,eAAe,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,WAAW,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,CAAC,EAAE;QACP,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QACjE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;KAClE,CAAC;IACF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,sDAAsD;IACtD,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0BAA0B;IAC1B,KAAK,CAAC,EAAE,iBAAiB,CAAC;CAC3B,CAAC;AAUF,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,OAAO,EAAE,EAC5C,SAAS,EACT,SAAS,EACT,sBAAkC,EAClC,eAAe,EACf,MAAM,EACN,aAAa,EACb,WAAW,EACX,MAAM,EACN,aAAa,EACb,cAAyC,EACzC,sBAAoD,EACpD,gBAAuB,EACvB,oBAA4B,EAC5B,qBAAqC,EACrC,cAAoB,EACpB,oBAAmD,EACnD,qBAA0C,EAC1C,mBAA2C,EAC3C,oBAAuC,EACvC,wBAA+B,EAC/B,UAAU,EACV,KAAK,GACN,EAAE,gBAAgB,CAAC,OAAO,CAAC,2CAuO3B"}
|
|
@@ -79,5 +79,5 @@ export default function login_layout({ image_src, image_alt, image_background_co
|
|
|
79
79
|
if (form.isSuccess) {
|
|
80
80
|
return (_jsx(TwoColumnAuthLayout, { imageSrc: image_src, imageAlt: image_alt, imageBackgroundColor: image_background_color, formContent: _jsxs(_Fragment, { children: [_jsx(FormHeader, { heading: resolvedLabels.heading, subHeading: resolvedLabels.subHeading }), _jsxs("div", { className: "cls_login_layout_success flex flex-col items-center justify-center gap-4 p-8 text-center", children: [_jsx(CheckCircle, { className: "cls_login_layout_success_icon h-16 w-16 text-green-600", "aria-hidden": "true" }), _jsx("p", { className: "cls_login_layout_success_message text-lg font-medium text-slate-900", children: successMessage })] })] }) }));
|
|
81
81
|
}
|
|
82
|
-
return (_jsx(AlreadyLoggedInGuard, { image_src: image_src, image_alt: image_alt, image_background_color: image_background_color, message: alreadyLoggedInMessage, showLogoutButton: showLogoutButton, showReturnHomeButton: showReturnHomeButton, returnHomeButtonLabel: returnHomeButtonLabel, returnHomePath: returnHomePath, children: _jsx(TwoColumnAuthLayout, { imageSrc: image_src, imageAlt: image_alt, imageBackgroundColor: image_background_color, formContent: _jsxs(_Fragment, { children: [_jsx(FormHeader, { heading: resolvedLabels.heading, subHeading: resolvedLabels.subHeading }), oauthError && (_jsxs("div", { className: "cls_login_layout_oauth_error flex items-center gap-2 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700", children: [_jsx(AlertCircle, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }), _jsx("span", { children: getOAuthErrorMessage(oauthError) })] })), oauthConfig.enable_google && (_jsx("div", { className: "cls_login_layout_oauth_section", children: _jsx(GoogleSignInButton, { label: oauthConfig.google_button_text }) })), oauthConfig.enable_google && oauthConfig.enable_email_password && (_jsx(OAuthDivider, { text: oauthConfig.oauth_divider_text })), oauthConfig.enable_email_password && (_jsxs("form", { className: "cls_login_layout_form_fields flex flex-col gap-5", onSubmit: form.handleSubmit, "aria-label": "Login form", children: [renderFields(form), _jsx(FormActionButtons, { submitLabel: resolvedLabels.submitButton, cancelLabel: resolvedLabels.cancelButton, buttonPalette: resolvedButtonPalette, isSubmitDisabled: form.isSubmitDisabled, onCancel: form.handleCancel, submitAriaLabel: "Submit login form", cancelAriaLabel: "Cancel login form" }), _jsxs("div", { className: "cls_login_layout_support_links flex flex-col gap-1 text-sm text-muted-foreground", children: [_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 && (_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 && !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 }) }))] }) }) }));
|
|
82
|
+
return (_jsx(AlreadyLoggedInGuard, { image_src: image_src, image_alt: image_alt, image_background_color: image_background_color, message: alreadyLoggedInMessage, showLogoutButton: showLogoutButton, showReturnHomeButton: showReturnHomeButton, returnHomeButtonLabel: returnHomeButtonLabel, returnHomePath: returnHomePath, children: _jsx(TwoColumnAuthLayout, { imageSrc: image_src, imageAlt: image_alt, imageBackgroundColor: image_background_color, formContent: _jsxs(_Fragment, { children: [_jsx(FormHeader, { heading: resolvedLabels.heading, subHeading: resolvedLabels.subHeading }), oauthError && (_jsxs("div", { className: "cls_login_layout_oauth_error flex items-center gap-2 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700", children: [_jsx(AlertCircle, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }), _jsx("span", { children: getOAuthErrorMessage(oauthError) })] })), oauthConfig.enable_google && (_jsx("div", { className: "cls_login_layout_oauth_section", children: _jsx(GoogleSignInButton, { label: oauthConfig.google_button_text }) })), oauthConfig.enable_google && oauthConfig.enable_email_password && (_jsx(OAuthDivider, { text: oauthConfig.oauth_divider_text })), oauthConfig.enable_email_password && (_jsxs("form", { className: "cls_login_layout_form_fields flex flex-col gap-5", onSubmit: form.handleSubmit, "aria-label": "Login form", children: [renderFields(form), _jsx(FormActionButtons, { submitLabel: resolvedLabels.submitButton, cancelLabel: resolvedLabels.cancelButton, buttonPalette: resolvedButtonPalette, isSubmitDisabled: form.isSubmitDisabled, onCancel: form.handleCancel, submitAriaLabel: "Submit login form", cancelAriaLabel: "Cancel login form" }), ((forgot_password_path && forgot_password_label) || (show_create_account_link && create_account_path && create_account_label)) && (_jsxs("div", { className: "cls_login_layout_support_links flex flex-col gap-1 text-sm text-muted-foreground", children: [forgot_password_path && forgot_password_label && (_jsx(Link, { href: forgot_password_path, className: "cls_login_layout_forgot_password_link text-primary underline-offset-4 hover:underline", "aria-label": "Go to forgot password page", children: forgot_password_label })), show_create_account_link && create_account_path && create_account_label && (_jsx(Link, { href: create_account_path, className: "cls_login_layout_create_account_link text-primary underline-offset-4 hover:underline", "aria-label": "Go to create account page", children: create_account_label }))] }))] })), show_create_account_link && create_account_path && create_account_label && !oauthConfig.enable_email_password && oauthConfig.enable_google && (_jsx("div", { className: "cls_login_layout_support_links mt-4 text-center text-sm text-muted-foreground", children: _jsx(Link, { href: create_account_path, className: "cls_login_layout_create_account_link text-primary underline-offset-4 hover:underline", "aria-label": "Go to create account page", children: create_account_label }) }))] }) }) }));
|
|
83
83
|
}
|
|
@@ -15,6 +15,18 @@ export declare function read_config_section(section_name: string, file_path?: st
|
|
|
15
15
|
* @returns Config value as string or default value
|
|
16
16
|
*/
|
|
17
17
|
export declare function get_config_value(section_name: string, key: string, default_value: string, file_path?: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Gets a single config value from a section, preserving empty strings.
|
|
20
|
+
* Unlike get_config_value, this returns "" when the INI key exists with an empty value,
|
|
21
|
+
* rather than falling back to the default. Useful for config keys where "" means
|
|
22
|
+
* "intentionally empty" (e.g., hiding a link by setting its path to empty).
|
|
23
|
+
* @param section_name - Name of the section
|
|
24
|
+
* @param key - Key name within the section
|
|
25
|
+
* @param default_value - Default value if key is not found in config
|
|
26
|
+
* @param file_path - Optional custom config file path
|
|
27
|
+
* @returns Config value as string, or "" if key exists but empty, or default_value if key missing
|
|
28
|
+
*/
|
|
29
|
+
export declare function get_config_value_allow_empty(section_name: string, key: string, default_value: string, file_path?: string): string;
|
|
18
30
|
/**
|
|
19
31
|
* Gets a boolean config value from a section
|
|
20
32
|
* @param section_name - Name of the section
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config_loader.server.d.ts","sourceRoot":"","sources":["../../../src/lib/config/config_loader.server.ts"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAwBrB;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,MAAM,EACpB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAwBpC;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,EACrB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAQR;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,OAAO,EACtB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAST;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,EACrB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAeR;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,EAAE,EACvB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,EAAE,CAcV"}
|
|
1
|
+
{"version":3,"file":"config_loader.server.d.ts","sourceRoot":"","sources":["../../../src/lib/config/config_loader.server.ts"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAwBrB;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,MAAM,EACpB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAwBpC;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,EACrB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAQR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAC1C,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,EACrB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAMR;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,OAAO,EACtB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAST;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,EACrB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAeR;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,EAAE,EACvB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,EAAE,CAcV"}
|
|
@@ -67,6 +67,24 @@ export function get_config_value(section_name, key, default_value, file_path) {
|
|
|
67
67
|
}
|
|
68
68
|
return section[key].trim() || default_value;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Gets a single config value from a section, preserving empty strings.
|
|
72
|
+
* Unlike get_config_value, this returns "" when the INI key exists with an empty value,
|
|
73
|
+
* rather than falling back to the default. Useful for config keys where "" means
|
|
74
|
+
* "intentionally empty" (e.g., hiding a link by setting its path to empty).
|
|
75
|
+
* @param section_name - Name of the section
|
|
76
|
+
* @param key - Key name within the section
|
|
77
|
+
* @param default_value - Default value if key is not found in config
|
|
78
|
+
* @param file_path - Optional custom config file path
|
|
79
|
+
* @returns Config value as string, or "" if key exists but empty, or default_value if key missing
|
|
80
|
+
*/
|
|
81
|
+
export function get_config_value_allow_empty(section_name, key, default_value, file_path) {
|
|
82
|
+
const section = read_config_section(section_name, file_path);
|
|
83
|
+
if (!section || section[key] === undefined) {
|
|
84
|
+
return default_value;
|
|
85
|
+
}
|
|
86
|
+
return section[key].trim();
|
|
87
|
+
}
|
|
70
88
|
/**
|
|
71
89
|
* Gets a boolean config value from a section
|
|
72
90
|
* @param section_name - Name of the section
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login_config.server.d.ts","sourceRoot":"","sources":["../../src/lib/login_config.server.ts"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAKrB,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAQ3E,MAAM,MAAM,WAAW,GAAG;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,0BAA0B;IAC1B,KAAK,EAAE,WAAW,CAAC;CACpB,CAAC;AAGF;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,WAAW,
|
|
1
|
+
{"version":3,"file":"login_config.server.d.ts","sourceRoot":"","sources":["../../src/lib/login_config.server.ts"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAKrB,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAQ3E,MAAM,MAAM,WAAW,GAAG;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,0BAA0B;IAC1B,KAAK,EAAE,WAAW,CAAC;CACpB,CAAC;AAGF;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,WAAW,CAyE9C"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// section: server-only-guard
|
|
3
3
|
import "server-only";
|
|
4
4
|
// section: imports
|
|
5
|
-
import { get_config_value } from "./config/config_loader.server.js";
|
|
5
|
+
import { get_config_value, get_config_value_allow_empty } from "./config/config_loader.server.js";
|
|
6
6
|
import { get_already_logged_in_config } from "./already_logged_in_config.server.js";
|
|
7
7
|
import { get_oauth_config } from "./oauth_config.server.js";
|
|
8
8
|
// Default image path - consuming apps should either:
|
|
@@ -22,10 +22,11 @@ export function get_login_config() {
|
|
|
22
22
|
const redirectRoute = redirectRouteValue || undefined;
|
|
23
23
|
// Read success message (defaults to "Successfully logged in")
|
|
24
24
|
const successMessage = get_config_value(section, "success_message", "Successfully logged in");
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
25
|
+
// Use allow_empty variant so that setting path/label to "" in config hides the link
|
|
26
|
+
const forgotPasswordPath = get_config_value_allow_empty(section, "forgot_password_path", "/hazo_auth/forgot_password");
|
|
27
|
+
const forgotPasswordLabel = get_config_value_allow_empty(section, "forgot_password_label", "Forgot password?");
|
|
28
|
+
const createAccountPath = get_config_value_allow_empty(section, "create_account_path", "/hazo_auth/register");
|
|
29
|
+
const createAccountLabel = get_config_value_allow_empty(section, "create_account_label", "Create account");
|
|
29
30
|
const showCreateAccountLink = get_config_value(section, "show_create_account_link", "true") === "true";
|
|
30
31
|
// Get shared already logged in config
|
|
31
32
|
const alreadyLoggedInConfig = get_already_logged_in_config();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gets the public-facing origin URL for redirect construction.
|
|
3
|
+
*
|
|
4
|
+
* Behind reverse proxies (Cloudflare, nginx, etc.), `request.url` contains the
|
|
5
|
+
* internal address (e.g. `http://localhost:3000`), not the public domain.
|
|
6
|
+
* This function returns the correct origin from environment variables.
|
|
7
|
+
*
|
|
8
|
+
* Priority: NEXTAUTH_URL > APP_DOMAIN_NAME > NEXT_PUBLIC_APP_URL > APP_URL > request.url
|
|
9
|
+
*
|
|
10
|
+
* @param request_url - The request.url to use as fallback
|
|
11
|
+
* @returns The origin URL (e.g. "https://gotimer.org")
|
|
12
|
+
*/
|
|
13
|
+
export declare function get_origin_url(request_url: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Creates a URL using the public-facing origin instead of request.url.
|
|
16
|
+
* Drop-in replacement for `new URL(path, request.url)` in route handlers.
|
|
17
|
+
*
|
|
18
|
+
* @param path - The path or relative URL (e.g. "/hazo_auth/login")
|
|
19
|
+
* @param request_url - The request.url (used as fallback only)
|
|
20
|
+
* @returns A URL object with the correct public origin
|
|
21
|
+
*/
|
|
22
|
+
export declare function create_redirect_url(path: string, request_url: string): URL;
|
|
23
|
+
//# sourceMappingURL=get_origin_url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get_origin_url.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/get_origin_url.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CA8B1D;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,GAAG,CAG1E"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// file_description: Resolves the public-facing origin URL for constructing redirects
|
|
2
|
+
// When running behind a reverse proxy (Cloudflare, nginx), request.url resolves to
|
|
3
|
+
// the internal address (e.g. http://localhost:3000). This utility returns the correct
|
|
4
|
+
// public origin using NEXTAUTH_URL, falling back to request.url.
|
|
5
|
+
/**
|
|
6
|
+
* Gets the public-facing origin URL for redirect construction.
|
|
7
|
+
*
|
|
8
|
+
* Behind reverse proxies (Cloudflare, nginx, etc.), `request.url` contains the
|
|
9
|
+
* internal address (e.g. `http://localhost:3000`), not the public domain.
|
|
10
|
+
* This function returns the correct origin from environment variables.
|
|
11
|
+
*
|
|
12
|
+
* Priority: NEXTAUTH_URL > APP_DOMAIN_NAME > NEXT_PUBLIC_APP_URL > APP_URL > request.url
|
|
13
|
+
*
|
|
14
|
+
* @param request_url - The request.url to use as fallback
|
|
15
|
+
* @returns The origin URL (e.g. "https://gotimer.org")
|
|
16
|
+
*/
|
|
17
|
+
export function get_origin_url(request_url) {
|
|
18
|
+
// NEXTAUTH_URL is the standard for NextAuth.js apps
|
|
19
|
+
const nextauth_url = process.env.NEXTAUTH_URL;
|
|
20
|
+
if (nextauth_url) {
|
|
21
|
+
return nextauth_url.replace(/\/$/, "");
|
|
22
|
+
}
|
|
23
|
+
// APP_DOMAIN_NAME (with protocol handling)
|
|
24
|
+
const app_domain = process.env.APP_DOMAIN_NAME;
|
|
25
|
+
if (app_domain) {
|
|
26
|
+
const domain = app_domain.trim();
|
|
27
|
+
if (domain.startsWith("http://") || domain.startsWith("https://")) {
|
|
28
|
+
return domain.replace(/\/$/, "");
|
|
29
|
+
}
|
|
30
|
+
return `https://${domain}`;
|
|
31
|
+
}
|
|
32
|
+
// Other common env vars
|
|
33
|
+
const env_url = process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL;
|
|
34
|
+
if (env_url) {
|
|
35
|
+
return env_url.replace(/\/$/, "");
|
|
36
|
+
}
|
|
37
|
+
// Fallback to request.url (works in development without a proxy)
|
|
38
|
+
try {
|
|
39
|
+
const url = new URL(request_url);
|
|
40
|
+
return url.origin;
|
|
41
|
+
}
|
|
42
|
+
catch (_a) {
|
|
43
|
+
return request_url;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Creates a URL using the public-facing origin instead of request.url.
|
|
48
|
+
* Drop-in replacement for `new URL(path, request.url)` in route handlers.
|
|
49
|
+
*
|
|
50
|
+
* @param path - The path or relative URL (e.g. "/hazo_auth/login")
|
|
51
|
+
* @param request_url - The request.url (used as fallback only)
|
|
52
|
+
* @returns A URL object with the correct public origin
|
|
53
|
+
*/
|
|
54
|
+
export function create_redirect_url(path, request_url) {
|
|
55
|
+
const origin = get_origin_url(request_url);
|
|
56
|
+
return new URL(path, origin);
|
|
57
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth_google_callback.d.ts","sourceRoot":"","sources":["../../../src/server/routes/oauth_google_callback.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"oauth_google_callback.d.ts","sourceRoot":"","sources":["../../../src/server/routes/oauth_google_callback.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAwBxD;;;;GAIG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW,kCAgJ7C"}
|
|
@@ -10,6 +10,7 @@ import { get_cookie_name, get_cookie_options, BASE_COOKIE_NAMES } from "../../li
|
|
|
10
10
|
import { get_hazo_connect_instance } from "../../lib/hazo_connect_instance.server.js";
|
|
11
11
|
import { get_post_login_redirect } from "../../lib/services/post_verification_service.js";
|
|
12
12
|
import { get_oauth_config } from "../../lib/oauth_config.server.js";
|
|
13
|
+
import { create_redirect_url } from "../../lib/utils/get_origin_url.js";
|
|
13
14
|
// section: api_handler
|
|
14
15
|
/**
|
|
15
16
|
* Handles the OAuth callback after Google sign-in
|
|
@@ -36,7 +37,7 @@ export async function GET(request) {
|
|
|
36
37
|
note: "No NextAuth token found - user may not have completed Google sign-in",
|
|
37
38
|
});
|
|
38
39
|
// Redirect to login with error
|
|
39
|
-
const login_url =
|
|
40
|
+
const login_url = create_redirect_url("/hazo_auth/login", request.url);
|
|
40
41
|
login_url.searchParams.set("error", "oauth_failed");
|
|
41
42
|
return NextResponse.redirect(login_url);
|
|
42
43
|
}
|
|
@@ -49,7 +50,7 @@ export async function GET(request) {
|
|
|
49
50
|
has_hazo_user_id: !!token.hazo_user_id,
|
|
50
51
|
has_google_id: !!token.google_id,
|
|
51
52
|
});
|
|
52
|
-
const login_url =
|
|
53
|
+
const login_url = create_redirect_url("/hazo_auth/login", request.url);
|
|
53
54
|
login_url.searchParams.set("error", "oauth_incomplete");
|
|
54
55
|
return NextResponse.redirect(login_url);
|
|
55
56
|
}
|
|
@@ -93,7 +94,7 @@ export async function GET(request) {
|
|
|
93
94
|
invitation_table_error,
|
|
94
95
|
});
|
|
95
96
|
// Create redirect response
|
|
96
|
-
const redirect_url =
|
|
97
|
+
const redirect_url = create_redirect_url(determined_redirect, request.url);
|
|
97
98
|
const response = NextResponse.redirect(redirect_url);
|
|
98
99
|
// Set authentication cookies (same as login route, with configurable prefix and domain)
|
|
99
100
|
const base_cookie_options = {
|
|
@@ -133,7 +134,7 @@ export async function GET(request) {
|
|
|
133
134
|
error_message,
|
|
134
135
|
error_stack,
|
|
135
136
|
});
|
|
136
|
-
const login_url =
|
|
137
|
+
const login_url = create_redirect_url("/hazo_auth/login", request.url);
|
|
137
138
|
login_url.searchParams.set("error", "oauth_error");
|
|
138
139
|
return NextResponse.redirect(login_url);
|
|
139
140
|
}
|
package/package.json
CHANGED