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 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
- const forgotPasswordPath = get_config_value(
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 = get_config_value(
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 = get_config_value(section, "create_account_path", "/hazo_auth/register");
63
- const createAccountLabel = get_config_value(
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,2CAmO3B"}
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,CAwE9C"}
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
- const forgotPasswordPath = get_config_value(section, "forgot_password_path", "/hazo_auth/forgot_password");
26
- const forgotPasswordLabel = get_config_value(section, "forgot_password_label", "Forgot password?");
27
- const createAccountPath = get_config_value(section, "create_account_path", "/hazo_auth/register");
28
- const createAccountLabel = get_config_value(section, "create_account_label", "Create account");
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;AAuBxD;;;;GAIG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW,kCAgJ7C"}
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 = new URL("/hazo_auth/login", request.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 = new URL("/hazo_auth/login", request.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 = new URL(determined_redirect, request.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 = new URL("/hazo_auth/login", request.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_auth",
3
- "version": "5.1.29",
3
+ "version": "5.1.31",
4
4
  "description": "Zero-config authentication UI components for Next.js with RBAC, OAuth, scope-based multi-tenancy, and invitations",
5
5
  "keywords": [
6
6
  "authentication",