hazo_auth 6.1.1 → 7.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/README.md +65 -167
  2. package/SETUP_CHECKLIST.md +28 -100
  3. package/cli-src/cli/generate.ts +1 -10
  4. package/cli-src/cli/validate.ts +0 -4
  5. package/cli-src/lib/auth/auth_types.ts +12 -21
  6. package/cli-src/lib/auth/hazo_get_tenant_auth.server.ts +24 -25
  7. package/cli-src/lib/auth/index.ts +2 -2
  8. package/cli-src/lib/auth/nextauth_config.ts +61 -1
  9. package/cli-src/lib/auth/with_auth.server.ts +15 -15
  10. package/cli-src/lib/config/default_config.ts +8 -0
  11. package/cli-src/lib/cookies_config.server.ts +1 -1
  12. package/cli-src/lib/login_config.server.ts +2 -18
  13. package/cli-src/lib/oauth_config.server.ts +32 -0
  14. package/cli-src/lib/register_config.server.ts +4 -0
  15. package/cli-src/lib/services/email_template_manifest.ts +0 -17
  16. package/cli-src/lib/services/index.ts +2 -8
  17. package/cli-src/lib/services/oauth_service.ts +143 -0
  18. package/cli-src/lib/services/otp_service.ts +7 -2
  19. package/cli-src/lib/services/session_token_service.ts +0 -2
  20. package/config/hazo_auth_config.example.ini +0 -38
  21. package/dist/cli/generate.d.ts.map +1 -1
  22. package/dist/cli/generate.js +1 -10
  23. package/dist/cli/validate.d.ts.map +1 -1
  24. package/dist/cli/validate.js +0 -4
  25. package/dist/client.d.ts +0 -2
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +0 -1
  28. package/dist/components/layouts/login/index.d.ts +5 -7
  29. package/dist/components/layouts/login/index.d.ts.map +1 -1
  30. package/dist/components/layouts/login/index.js +5 -2
  31. package/dist/components/layouts/otp/index.d.ts +12 -1
  32. package/dist/components/layouts/otp/index.d.ts.map +1 -1
  33. package/dist/components/layouts/otp/index.js +4 -2
  34. package/dist/components/layouts/register/index.d.ts +4 -0
  35. package/dist/components/layouts/register/index.d.ts.map +1 -1
  36. package/dist/components/layouts/register/index.js +4 -1
  37. package/dist/components/layouts/shared/components/facebook_sign_in_button.d.ts +21 -0
  38. package/dist/components/layouts/shared/components/facebook_sign_in_button.d.ts.map +1 -0
  39. package/dist/components/layouts/shared/components/facebook_sign_in_button.js +47 -0
  40. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
  41. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +3 -8
  42. package/dist/components/layouts/shared/index.d.ts +2 -0
  43. package/dist/components/layouts/shared/index.d.ts.map +1 -1
  44. package/dist/components/layouts/shared/index.js +1 -0
  45. package/dist/components/layouts/user_management/index.d.ts.map +1 -1
  46. package/dist/components/layouts/user_management/index.js +39 -2
  47. package/dist/consent/consent_state.d.ts +18 -0
  48. package/dist/consent/consent_state.d.ts.map +1 -0
  49. package/dist/consent/consent_state.js +29 -0
  50. package/dist/consent/cookie_consent_banner.d.ts +11 -0
  51. package/dist/consent/cookie_consent_banner.d.ts.map +1 -0
  52. package/dist/consent/cookie_consent_banner.js +40 -0
  53. package/dist/consent/gtm_mapping.d.ts +13 -0
  54. package/dist/consent/gtm_mapping.d.ts.map +1 -0
  55. package/dist/consent/gtm_mapping.js +30 -0
  56. package/dist/consent/index.d.ts +7 -0
  57. package/dist/consent/index.d.ts.map +1 -0
  58. package/dist/consent/index.js +7 -0
  59. package/dist/consent/manage_modal.d.ts +2 -0
  60. package/dist/consent/manage_modal.d.ts.map +1 -0
  61. package/dist/consent/manage_modal.js +33 -0
  62. package/dist/consent/read_consent.d.ts +15 -0
  63. package/dist/consent/read_consent.d.ts.map +1 -0
  64. package/dist/consent/read_consent.js +23 -0
  65. package/dist/consent/use_consent.d.ts +7 -0
  66. package/dist/consent/use_consent.d.ts.map +1 -0
  67. package/dist/consent/use_consent.js +55 -0
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/lib/auth/auth_types.d.ts +12 -13
  71. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  72. package/dist/lib/auth/auth_types.js +0 -8
  73. package/dist/lib/auth/hazo_get_tenant_auth.server.d.ts +7 -8
  74. package/dist/lib/auth/hazo_get_tenant_auth.server.d.ts.map +1 -1
  75. package/dist/lib/auth/hazo_get_tenant_auth.server.js +22 -23
  76. package/dist/lib/auth/index.d.ts +2 -2
  77. package/dist/lib/auth/index.d.ts.map +1 -1
  78. package/dist/lib/auth/nextauth_config.d.ts.map +1 -1
  79. package/dist/lib/auth/nextauth_config.js +50 -1
  80. package/dist/lib/auth/with_auth.server.d.ts +13 -13
  81. package/dist/lib/auth/with_auth.server.d.ts.map +1 -1
  82. package/dist/lib/auth/with_auth.server.js +2 -2
  83. package/dist/lib/config/default_config.d.ts +16 -0
  84. package/dist/lib/config/default_config.d.ts.map +1 -1
  85. package/dist/lib/config/default_config.js +8 -0
  86. package/dist/lib/cookies_config.server.d.ts +1 -1
  87. package/dist/lib/cookies_config.server.js +1 -1
  88. package/dist/lib/login_config.server.d.ts +0 -6
  89. package/dist/lib/login_config.server.d.ts.map +1 -1
  90. package/dist/lib/login_config.server.js +2 -11
  91. package/dist/lib/oauth_config.server.d.ts +8 -0
  92. package/dist/lib/oauth_config.server.d.ts.map +1 -1
  93. package/dist/lib/oauth_config.server.js +10 -0
  94. package/dist/lib/register_config.server.d.ts +2 -0
  95. package/dist/lib/register_config.server.d.ts.map +1 -1
  96. package/dist/lib/register_config.server.js +2 -0
  97. package/dist/lib/services/email_template_manifest.d.ts.map +1 -1
  98. package/dist/lib/services/email_template_manifest.js +0 -17
  99. package/dist/lib/services/index.d.ts +0 -2
  100. package/dist/lib/services/index.d.ts.map +1 -1
  101. package/dist/lib/services/index.js +0 -1
  102. package/dist/lib/services/oauth_service.d.ts +13 -0
  103. package/dist/lib/services/oauth_service.d.ts.map +1 -1
  104. package/dist/lib/services/oauth_service.js +122 -0
  105. package/dist/lib/services/otp_service.d.ts +1 -1
  106. package/dist/lib/services/otp_service.d.ts.map +1 -1
  107. package/dist/lib/services/otp_service.js +6 -1
  108. package/dist/lib/services/session_token_service.d.ts +0 -2
  109. package/dist/lib/services/session_token_service.d.ts.map +1 -1
  110. package/dist/lib/services/session_token_service.js +0 -2
  111. package/dist/server/routes/assets.d.ts +8 -0
  112. package/dist/server/routes/assets.d.ts.map +1 -0
  113. package/dist/server/routes/assets.js +38 -0
  114. package/dist/server/routes/consent_me.d.ts +4 -0
  115. package/dist/server/routes/consent_me.d.ts.map +1 -0
  116. package/dist/server/routes/consent_me.js +15 -0
  117. package/dist/server/routes/index.d.ts +6 -3
  118. package/dist/server/routes/index.d.ts.map +1 -1
  119. package/dist/server/routes/index.js +9 -4
  120. package/dist/server/routes/me.d.ts.map +1 -1
  121. package/dist/server/routes/me.js +1 -43
  122. package/dist/server/routes/oauth_facebook_callback.d.ts +8 -0
  123. package/dist/server/routes/oauth_facebook_callback.d.ts.map +1 -0
  124. package/dist/server/routes/oauth_facebook_callback.js +164 -0
  125. package/dist/server/routes/otp/verify.js +2 -2
  126. package/dist/server/routes/strings_defaults.d.ts +4 -0
  127. package/dist/server/routes/strings_defaults.d.ts.map +1 -0
  128. package/dist/server/routes/strings_defaults.js +7 -0
  129. package/dist/server/routes/user_management_users.d.ts +11 -0
  130. package/dist/server/routes/user_management_users.d.ts.map +1 -1
  131. package/dist/server/routes/user_management_users.js +50 -0
  132. package/dist/server-lib.d.ts +0 -3
  133. package/dist/server-lib.d.ts.map +1 -1
  134. package/dist/server-lib.js +0 -2
  135. package/dist/server_pages/forgot_password.d.ts +1 -1
  136. package/dist/server_pages/forgot_password.d.ts.map +1 -1
  137. package/dist/server_pages/forgot_password.js +9 -3
  138. package/dist/server_pages/forgot_password_client_wrapper.d.ts +3 -1
  139. package/dist/server_pages/forgot_password_client_wrapper.d.ts.map +1 -1
  140. package/dist/server_pages/forgot_password_client_wrapper.js +2 -2
  141. package/dist/server_pages/index.d.ts +2 -0
  142. package/dist/server_pages/index.d.ts.map +1 -1
  143. package/dist/server_pages/index.js +1 -0
  144. package/dist/server_pages/login.d.ts +1 -1
  145. package/dist/server_pages/login.d.ts.map +1 -1
  146. package/dist/server_pages/login.js +12 -3
  147. package/dist/server_pages/login_client_wrapper.d.ts +4 -1
  148. package/dist/server_pages/login_client_wrapper.d.ts.map +1 -1
  149. package/dist/server_pages/login_client_wrapper.js +2 -2
  150. package/dist/server_pages/my_settings.d.ts +1 -1
  151. package/dist/server_pages/my_settings.d.ts.map +1 -1
  152. package/dist/server_pages/my_settings.js +1 -2
  153. package/dist/server_pages/otp.d.ts +16 -2
  154. package/dist/server_pages/otp.d.ts.map +1 -1
  155. package/dist/server_pages/otp.js +10 -3
  156. package/dist/server_pages/register.d.ts +1 -1
  157. package/dist/server_pages/register.d.ts.map +1 -1
  158. package/dist/server_pages/register.js +11 -3
  159. package/dist/server_pages/register_client_wrapper.d.ts +3 -1
  160. package/dist/server_pages/register_client_wrapper.d.ts.map +1 -1
  161. package/dist/server_pages/register_client_wrapper.js +2 -2
  162. package/dist/server_pages/reset_password.d.ts +1 -1
  163. package/dist/server_pages/reset_password.d.ts.map +1 -1
  164. package/dist/server_pages/reset_password.js +9 -3
  165. package/dist/server_pages/reset_password_client_wrapper.d.ts +3 -1
  166. package/dist/server_pages/reset_password_client_wrapper.d.ts.map +1 -1
  167. package/dist/server_pages/reset_password_client_wrapper.js +2 -2
  168. package/dist/server_pages/verify_email.d.ts +1 -1
  169. package/dist/server_pages/verify_email.d.ts.map +1 -1
  170. package/dist/server_pages/verify_email.js +8 -3
  171. package/dist/server_pages/verify_email_client_wrapper.d.ts +3 -1
  172. package/dist/server_pages/verify_email_client_wrapper.d.ts.map +1 -1
  173. package/dist/server_pages/verify_email_client_wrapper.js +2 -2
  174. package/dist/strings/default_strings.d.ts +47 -0
  175. package/dist/strings/default_strings.d.ts.map +1 -0
  176. package/dist/strings/default_strings.js +18 -0
  177. package/dist/strings/index.d.ts +4 -0
  178. package/dist/strings/index.d.ts.map +1 -0
  179. package/dist/strings/index.js +3 -0
  180. package/dist/strings/strings_context.d.ts +12 -0
  181. package/dist/strings/strings_context.d.ts.map +1 -0
  182. package/dist/strings/strings_context.js +23 -0
  183. package/dist/strings/strings_provider.d.ts +26 -0
  184. package/dist/strings/strings_provider.d.ts.map +1 -0
  185. package/dist/strings/strings_provider.js +45 -0
  186. package/dist/theme/create_theme.d.ts +7 -0
  187. package/dist/theme/create_theme.d.ts.map +1 -0
  188. package/dist/theme/create_theme.js +97 -0
  189. package/dist/theme/hex_to_hsl.d.ts +16 -0
  190. package/dist/theme/hex_to_hsl.d.ts.map +1 -0
  191. package/dist/theme/hex_to_hsl.js +110 -0
  192. package/dist/theme/index.d.ts +4 -0
  193. package/dist/theme/index.d.ts.map +1 -0
  194. package/dist/theme/index.js +3 -0
  195. package/dist/theme/luminance.d.ts +11 -0
  196. package/dist/theme/luminance.d.ts.map +1 -0
  197. package/dist/theme/luminance.js +45 -0
  198. package/dist/theme/theme_provider.d.ts +14 -0
  199. package/dist/theme/theme_provider.d.ts.map +1 -0
  200. package/dist/theme/theme_provider.js +23 -0
  201. package/dist/theme/theme_types.d.ts +36 -0
  202. package/dist/theme/theme_types.d.ts.map +1 -0
  203. package/dist/theme/theme_types.js +1 -0
  204. package/dist/themes/index.d.ts +2 -0
  205. package/dist/themes/index.d.ts.map +1 -0
  206. package/dist/themes/index.js +2 -0
  207. package/dist/themes/preset_neutral.d.ts +3 -0
  208. package/dist/themes/preset_neutral.d.ts.map +1 -0
  209. package/dist/themes/preset_neutral.js +14 -0
  210. package/package.json +25 -22
@@ -14,7 +14,7 @@ import type {
14
14
  TenantAuthOptions,
15
15
  TenantAuthResult,
16
16
  RequiredTenantAuthResult,
17
- SelectedScope,
17
+ TenantOrganization,
18
18
  ScopeDetails,
19
19
  } from "./auth_types";
20
20
  import {
@@ -62,15 +62,15 @@ export function extract_scope_id_from_request(
62
62
  }
63
63
 
64
64
  /**
65
- * Builds SelectedScope from scope details and access info.
65
+ * Builds TenantOrganization from scope details and access info
66
66
  * @param scope_details - Full scope details from cache
67
67
  * @param is_super_admin - Whether user is accessing as super admin
68
- * @returns SelectedScope object
68
+ * @returns TenantOrganization object
69
69
  */
70
- function build_selected_scope(
70
+ function build_tenant_organization(
71
71
  scope_details: ScopeDetails,
72
72
  is_super_admin: boolean,
73
- ): SelectedScope {
73
+ ): TenantOrganization {
74
74
  return {
75
75
  id: scope_details.id,
76
76
  name: scope_details.name,
@@ -96,21 +96,20 @@ function build_selected_scope(
96
96
  * Tenant-aware authentication function
97
97
  *
98
98
  * Extracts tenant/scope context from request headers or cookies,
99
- * validates access, and returns enriched result including the currently
100
- * selected scope.
99
+ * validates access, and returns enriched result with organization info.
101
100
  *
102
101
  * Header priority: X-Hazo-Scope-Id > Cookie
103
102
  *
104
103
  * @param request - NextRequest object
105
104
  * @param options - TenantAuthOptions for customization
106
- * @returns TenantAuthResult with user, permissions, selected_scope, and user_scopes
105
+ * @returns TenantAuthResult with user, permissions, organization, and user_scopes
107
106
  *
108
107
  * @example
109
108
  * ```typescript
110
109
  * const auth = await hazo_get_tenant_auth(request);
111
- * if (auth.authenticated && auth.selected_scope) {
110
+ * if (auth.authenticated && auth.organization) {
112
111
  * // Access tenant-specific data
113
- * const data = await getData(auth.selected_scope.id);
112
+ * const data = await getData(auth.organization.id);
114
113
  * }
115
114
  * ```
116
115
  */
@@ -137,8 +136,8 @@ export async function hazo_get_tenant_auth(
137
136
  user: null,
138
137
  permissions: [],
139
138
  permission_ok: false,
140
- selected_scope: null,
141
- selected_scope_id: null,
139
+ organization: null,
140
+ organization_id: null,
142
141
  user_scopes: [],
143
142
  scope_ok: false,
144
143
  };
@@ -156,8 +155,8 @@ export async function hazo_get_tenant_auth(
156
155
  // User scopes from cache or empty array
157
156
  const user_scopes: ScopeDetails[] = cached?.scopes || [];
158
157
 
159
- // Build selected_scope info if scope access was successful
160
- let selected_scope: SelectedScope | null = null;
158
+ // Build organization info if scope access was successful
159
+ let organization: TenantOrganization | null = null;
161
160
 
162
161
  if (scope_id && auth_result.scope_ok && auth_result.scope_access_via) {
163
162
  // Find the scope in user's scopes that matches the access_via scope
@@ -166,7 +165,7 @@ export async function hazo_get_tenant_auth(
166
165
  );
167
166
 
168
167
  if (access_scope) {
169
- selected_scope = build_selected_scope(
168
+ organization = build_tenant_organization(
170
169
  access_scope,
171
170
  auth_result.scope_access_via.is_super_admin || false,
172
171
  );
@@ -175,7 +174,7 @@ export async function hazo_get_tenant_auth(
175
174
  const hazoConnect = get_hazo_connect_instance();
176
175
  const scope_result = await get_scope_by_id(hazoConnect, scope_id);
177
176
  if (scope_result.success && scope_result.scope) {
178
- selected_scope = {
177
+ organization = {
179
178
  id: scope_result.scope.id,
180
179
  name: scope_result.scope.name,
181
180
  slug: null, // Could fetch from scope if slug column exists
@@ -201,8 +200,8 @@ export async function hazo_get_tenant_auth(
201
200
  permissions: auth_result.permissions,
202
201
  permission_ok: auth_result.permission_ok,
203
202
  missing_permissions: auth_result.missing_permissions,
204
- selected_scope,
205
- selected_scope_id: selected_scope?.id || null,
203
+ organization,
204
+ organization_id: organization?.id || null,
206
205
  user_scopes,
207
206
  scope_ok: auth_result.scope_ok,
208
207
  scope_access_via: auth_result.scope_access_via,
@@ -219,15 +218,15 @@ export async function hazo_get_tenant_auth(
219
218
  *
220
219
  * @param request - NextRequest object
221
220
  * @param options - TenantAuthOptions for customization
222
- * @returns RequiredTenantAuthResult with guaranteed non-null selected_scope
221
+ * @returns RequiredTenantAuthResult with guaranteed non-null organization
223
222
  * @throws AuthenticationRequiredError, TenantRequiredError, TenantAccessDeniedError
224
223
  *
225
224
  * @example
226
225
  * ```typescript
227
226
  * try {
228
227
  * const auth = await require_tenant_auth(request);
229
- * // auth.selected_scope is guaranteed non-null here
230
- * const data = await getData(auth.selected_scope.id);
228
+ * // auth.organization is guaranteed non-null here
229
+ * const data = await getData(auth.organization.id);
231
230
  * } catch (error) {
232
231
  * if (error instanceof HazoAuthError) {
233
232
  * return NextResponse.json(
@@ -258,14 +257,14 @@ export async function require_tenant_auth(
258
257
  throw new TenantAccessDeniedError(scope_id, result.user_scopes);
259
258
  }
260
259
 
261
- // Check if scope context is required but missing
262
- if (!result.selected_scope) {
260
+ // Check if organization context is required but missing
261
+ if (!result.organization) {
263
262
  throw new TenantRequiredError(
264
- "No tenant scope context provided. Include X-Hazo-Scope-Id header or scope cookie.",
263
+ "No organization context provided. Include X-Hazo-Scope-Id header or scope cookie.",
265
264
  result.user_scopes,
266
265
  );
267
266
  }
268
267
 
269
- // Type assertion: at this point we know selected_scope is non-null
268
+ // Type assertion: at this point we know organization is non-null
270
269
  return result as RequiredTenantAuthResult;
271
270
  }
@@ -22,7 +22,7 @@ export {
22
22
  } from "./hazo_get_tenant_auth.server.js";
23
23
  export type {
24
24
  ScopeDetails,
25
- SelectedScope,
25
+ TenantOrganization,
26
26
  TenantAuthOptions,
27
27
  TenantAuthResult,
28
28
  RequiredTenantAuthResult,
@@ -50,7 +50,7 @@ export {
50
50
  } from "./with_auth.server.js";
51
51
  export type {
52
52
  AuthenticatedTenantAuth,
53
- AuthenticatedTenantAuthWithSelectedScope,
53
+ AuthenticatedTenantAuthWithOrg,
54
54
  WithAuthOptions,
55
55
  } from "./with_auth.server";
56
56
 
@@ -5,6 +5,8 @@ import type { JWT } from "next-auth/jwt";
5
5
  // ESM/CJS interop: next-auth providers are CommonJS, handle both export scenarios
6
6
  import GoogleProviderImport from "next-auth/providers/google";
7
7
  const GoogleProvider = (GoogleProviderImport as any).default || GoogleProviderImport;
8
+ import FacebookProviderImport from "next-auth/providers/facebook";
9
+ const FacebookProvider = (FacebookProviderImport as any).default || FacebookProviderImport;
8
10
  import { get_oauth_config } from "../oauth_config.server.js";
9
11
  import { handle_google_oauth_login } from "../services/oauth_service.js";
10
12
  import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
@@ -67,6 +69,16 @@ export function get_nextauth_config(): AuthOptions {
67
69
  }
68
70
  }
69
71
 
72
+ // Add Facebook provider if enabled
73
+ if (oauth_config.enable_facebook_oauth && oauth_config.facebook_app_id) {
74
+ providers.push(
75
+ FacebookProvider({
76
+ clientId: oauth_config.facebook_app_id,
77
+ clientSecret: oauth_config.facebook_app_secret,
78
+ })
79
+ );
80
+ }
81
+
70
82
  return {
71
83
  providers,
72
84
  pages: {
@@ -85,7 +97,10 @@ export function get_nextauth_config(): AuthOptions {
85
97
 
86
98
  // Always redirect to our custom callback after sign-in to set hazo_auth cookies
87
99
  // The callbackUrl from signIn() comes through as `url`
88
- if (url.includes("/api/hazo_auth/oauth/google/callback")) {
100
+ if (
101
+ url.includes("/api/hazo_auth/oauth/google/callback") ||
102
+ url.includes("/api/hazo_auth/oauth/facebook/callback")
103
+ ) {
89
104
  return url;
90
105
  }
91
106
 
@@ -98,6 +113,9 @@ export function get_nextauth_config(): AuthOptions {
98
113
  }
99
114
 
100
115
  // Default: redirect to our custom OAuth callback to set cookies
116
+ if (url.includes("facebook")) {
117
+ return `${baseUrl}/api/hazo_auth/oauth/facebook/callback`;
118
+ }
101
119
  return `${baseUrl}/api/hazo_auth/oauth/google/callback`;
102
120
  },
103
121
  /**
@@ -162,6 +180,48 @@ export function get_nextauth_config(): AuthOptions {
162
180
  return false;
163
181
  }
164
182
  }
183
+
184
+ if (account?.provider === "facebook" && profile) {
185
+ try {
186
+ const fbProfile = profile as NextAuthCallbackProfile;
187
+ const hazoConnect = get_hazo_connect_instance();
188
+ const { handle_facebook_oauth_login } = await import("../services/oauth_service");
189
+
190
+ logger.info("nextauth_facebook_signin_attempt", {
191
+ email: user.email,
192
+ facebook_id: account.providerAccountId,
193
+ });
194
+
195
+ const result = await handle_facebook_oauth_login(hazoConnect, {
196
+ facebook_id: account.providerAccountId,
197
+ email: user.email || fbProfile.email || "",
198
+ name: user.name || fbProfile.name || undefined,
199
+ profile_picture_url: user.image || undefined,
200
+ // Facebook's email_verified is not exposed in the profile; default to false
201
+ // for safety — the user will be auto-verified if email matches a verified hazo user.
202
+ email_verified: false,
203
+ });
204
+
205
+ if (!result.success) {
206
+ logger.error("nextauth_facebook_signin_failed", { email: user.email, error: result.error });
207
+ return false;
208
+ }
209
+
210
+ logger.info("nextauth_facebook_signin_success", {
211
+ user_id: result.user_id,
212
+ email: result.email,
213
+ is_new_user: result.is_new_user,
214
+ was_linked: result.was_linked,
215
+ });
216
+
217
+ (account as Record<string, unknown>).hazo_user_id = result.user_id;
218
+ return true;
219
+ } catch (err) {
220
+ logger.error("nextauth_facebook_signin_exception", { error: String(err) });
221
+ return false;
222
+ }
223
+ }
224
+
165
225
  return true;
166
226
  },
167
227
  /**
@@ -10,7 +10,7 @@ import {
10
10
  PermissionError,
11
11
  type TenantAuthOptions,
12
12
  type TenantAuthResult,
13
- type SelectedScope,
13
+ type TenantOrganization,
14
14
  type HazoAuthUser,
15
15
  type ScopeDetails,
16
16
  type ScopeAccessInfo,
@@ -27,19 +27,19 @@ export type AuthenticatedTenantAuth = {
27
27
  permissions: string[];
28
28
  permission_ok: boolean;
29
29
  missing_permissions?: string[];
30
- selected_scope: SelectedScope | null;
31
- selected_scope_id: string | null;
30
+ organization: TenantOrganization | null;
31
+ organization_id: string | null;
32
32
  user_scopes: ScopeDetails[];
33
33
  scope_ok?: boolean;
34
34
  scope_access_via?: ScopeAccessInfo;
35
35
  };
36
36
 
37
37
  /**
38
- * Authenticated branch with guaranteed non-null selected_scope
38
+ * Authenticated branch with guaranteed non-null organization
39
39
  */
40
- export type AuthenticatedTenantAuthWithSelectedScope = AuthenticatedTenantAuth & {
41
- selected_scope: SelectedScope;
42
- selected_scope_id: string;
40
+ export type AuthenticatedTenantAuthWithOrg = AuthenticatedTenantAuth & {
41
+ organization: TenantOrganization;
42
+ organization_id: string;
43
43
  };
44
44
 
45
45
  /**
@@ -48,8 +48,8 @@ export type AuthenticatedTenantAuthWithSelectedScope = AuthenticatedTenantAuth &
48
48
  */
49
49
  export type WithAuthOptions = TenantAuthOptions & {
50
50
  /**
51
- * If true, requires tenant/scope context (403 if missing)
52
- * Narrows auth type to AuthenticatedTenantAuthWithSelectedScope
51
+ * If true, requires organization context (403 if missing)
52
+ * Narrows auth type to AuthenticatedTenantAuthWithOrg
53
53
  */
54
54
  require_tenant?: boolean;
55
55
  };
@@ -75,7 +75,7 @@ type AuthenticatedHandler<TParams> = (
75
75
  */
76
76
  type AuthenticatedTenantHandler<TParams> = (
77
77
  request: NextRequest,
78
- auth: AuthenticatedTenantAuthWithSelectedScope,
78
+ auth: AuthenticatedTenantAuthWithOrg,
79
79
  params: TParams,
80
80
  ) => Promise<NextResponse> | NextResponse;
81
81
 
@@ -138,7 +138,7 @@ async function resolve_params<TParams>(
138
138
  *
139
139
  * - Calls `hazo_get_tenant_auth` and returns 401 if not authenticated
140
140
  * - Returns 403 if `required_permissions` are specified and not satisfied
141
- * - Returns 403 if `require_tenant: true` and no tenant/scope context
141
+ * - Returns 403 if `require_tenant: true` and no organization context
142
142
  * - Resolves `await context.params` (Next.js 15 pattern)
143
143
  * - Catches HazoAuthError, PermissionError, and unexpected errors
144
144
  *
@@ -161,8 +161,8 @@ async function resolve_params<TParams>(
161
161
  * // With tenant requirement
162
162
  * export const GET = withAuth<{ id: string }>(
163
163
  * async (request, auth, { id }) => {
164
- * // auth.selected_scope is guaranteed non-null
165
- * const data = await getData(auth.selected_scope.id, id);
164
+ * // auth.organization is guaranteed non-null
165
+ * const data = await getData(auth.organization.id, id);
166
166
  * return NextResponse.json(data);
167
167
  * },
168
168
  * { require_tenant: true }
@@ -227,10 +227,10 @@ export function withAuth<TParams = Record<string, never>>(
227
227
  }
228
228
 
229
229
  // Check tenant requirement
230
- if (options.require_tenant && !auth.selected_scope) {
230
+ if (options.require_tenant && !auth.organization) {
231
231
  return NextResponse.json(
232
232
  {
233
- error: "Tenant scope context required",
233
+ error: "Organization context required",
234
234
  code: "TENANT_REQUIRED",
235
235
  },
236
236
  { status: 403 },
@@ -177,6 +177,14 @@ export const DEFAULT_OAUTH = {
177
177
  skip_invitation_check: false,
178
178
  /** Redirect when skip_invitation_check=true and user has no scope */
179
179
  no_scope_redirect: "/",
180
+ /** Enable Facebook OAuth login (requires HAZO_AUTH_FACEBOOK_APP_ID and HAZO_AUTH_FACEBOOK_APP_SECRET env vars) */
181
+ enable_facebook_oauth: false,
182
+ /** Facebook App ID — set via env var HAZO_AUTH_FACEBOOK_APP_ID or config */
183
+ facebook_app_id: "",
184
+ /** Facebook App Secret — set via env var HAZO_AUTH_FACEBOOK_APP_SECRET or config */
185
+ facebook_app_secret: "",
186
+ /** Text displayed on the Facebook sign-in button */
187
+ facebook_button_text: "Continue with Facebook",
180
188
  } as const;
181
189
 
182
190
  // section: navbar
@@ -27,10 +27,10 @@ export const BASE_COOKIE_NAMES = {
27
27
  USER_ID: "hazo_auth_user_id",
28
28
  USER_EMAIL: "hazo_auth_user_email",
29
29
  SESSION: "hazo_auth_session",
30
- SESSION_KIND: "hazo_auth_session_kind", // v6.1: marks OTP-issued sessions so /me can apply sliding expiry
31
30
  DEV_LOCK: "hazo_auth_dev_lock",
32
31
  SCOPE_ID: "hazo_auth_scope_id", // v5.2: Tenant context cookie for multi-tenancy
33
32
  ANON_ID: "hazo_auth_anon_id", // v5.2: Stable opaque per-visitor ID for anonymous flows (e.g. hazo_feedback)
33
+ SESSION_KIND: "hazo_auth_session_kind", // v5.4: Sign-in method identifier (e.g. "otp", "google", "password")
34
34
  } as const;
35
35
 
36
36
  // section: main_function
@@ -7,10 +7,8 @@ import { get_config_value, get_config_value_allow_empty } from "./config/config_
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
 
10
- // Default image path - consuming apps should either:
11
- // 1. Configure their own image_src in hazo_auth_config.ini
12
- // 2. Copy the default images from node_modules/hazo_auth/public/hazo_auth/images/ to their public folder
13
- const DEFAULT_LOGIN_IMAGE_PATH = "/hazo_auth/images/login_default.jpg";
10
+ // Assets served via the package's own API route — no manual copy needed.
11
+ const DEFAULT_LOGIN_IMAGE_PATH = "/api/hazo_auth/assets/login_default.jpg";
14
12
 
15
13
  // section: types
16
14
  export type LoginConfig = {
@@ -31,12 +29,6 @@ export type LoginConfig = {
31
29
  imageBackgroundColor: string;
32
30
  /** OAuth configuration */
33
31
  oauth: OAuthConfig;
34
- /** Whether the OTP sign-in link is shown below the login form */
35
- otpSigninEnabled: boolean;
36
- /** Label for the OTP sign-in link */
37
- otpSigninLabel: string;
38
- /** href for the OTP sign-in link */
39
- otpSigninHref: string;
40
32
  };
41
33
 
42
34
  // section: helpers
@@ -100,11 +92,6 @@ export function get_login_config(): LoginConfig {
100
92
  // Get OAuth configuration
101
93
  const oauth = get_oauth_config();
102
94
 
103
- // OTP sign-in link
104
- const otpSigninEnabled = get_config_value(section, "otp_signin_enabled", "false") === "true";
105
- const otpSigninLabel = get_config_value(section, "otp_signin_label", "Sign in with email code");
106
- const otpSigninHref = get_config_value(section, "otp_signin_href", "/hazo_auth/otp");
107
-
108
95
  return {
109
96
  redirectRoute,
110
97
  successMessage,
@@ -122,9 +109,6 @@ export function get_login_config(): LoginConfig {
122
109
  imageAlt,
123
110
  imageBackgroundColor,
124
111
  oauth,
125
- otpSigninEnabled,
126
- otpSigninLabel,
127
- otpSigninHref,
128
112
  };
129
113
  }
130
114
 
@@ -30,6 +30,14 @@ export type OAuthConfig = {
30
30
  skip_invitation_check: boolean;
31
31
  /** Redirect when skip_invitation_check=true and user has no scope */
32
32
  no_scope_redirect: string;
33
+ /** Enable Facebook OAuth login */
34
+ enable_facebook_oauth: boolean;
35
+ /** Facebook App ID (env: HAZO_AUTH_FACEBOOK_APP_ID overrides config) */
36
+ facebook_app_id: string;
37
+ /** Facebook App Secret (env: HAZO_AUTH_FACEBOOK_APP_SECRET overrides config) */
38
+ facebook_app_secret: string;
39
+ /** Text displayed on the Facebook sign-in button */
40
+ facebook_button_text: string;
33
41
  };
34
42
 
35
43
  // section: constants
@@ -108,6 +116,26 @@ export function get_oauth_config(): OAuthConfig {
108
116
  DEFAULT_OAUTH.no_scope_redirect
109
117
  );
110
118
 
119
+ const enable_facebook_oauth = get_config_boolean(
120
+ SECTION_NAME,
121
+ "enable_facebook_oauth",
122
+ false
123
+ );
124
+
125
+ const facebook_app_id =
126
+ process.env.HAZO_AUTH_FACEBOOK_APP_ID ||
127
+ get_config_value(SECTION_NAME, "facebook_app_id", "");
128
+
129
+ const facebook_app_secret =
130
+ process.env.HAZO_AUTH_FACEBOOK_APP_SECRET ||
131
+ get_config_value(SECTION_NAME, "facebook_app_secret", "");
132
+
133
+ const facebook_button_text = get_config_value(
134
+ SECTION_NAME,
135
+ "facebook_button_text",
136
+ "Continue with Facebook"
137
+ );
138
+
111
139
  return {
112
140
  enable_google,
113
141
  enable_email_password,
@@ -120,6 +148,10 @@ export function get_oauth_config(): OAuthConfig {
120
148
  default_redirect,
121
149
  skip_invitation_check,
122
150
  no_scope_redirect,
151
+ enable_facebook_oauth,
152
+ facebook_app_id,
153
+ facebook_app_secret,
154
+ facebook_button_text,
123
155
  };
124
156
  }
125
157
 
@@ -40,6 +40,8 @@ export type RegisterConfig = {
40
40
  enable_email_password: boolean;
41
41
  google_button_text: string;
42
42
  oauth_divider_text: string;
43
+ enable_facebook_oauth: boolean;
44
+ facebook_button_text: string;
43
45
  };
44
46
  };
45
47
 
@@ -125,6 +127,8 @@ export function get_register_config(): RegisterConfig {
125
127
  enable_email_password: oauthConfig.enable_email_password,
126
128
  google_button_text: registerGoogleButtonText,
127
129
  oauth_divider_text: oauthConfig.oauth_divider_text,
130
+ enable_facebook_oauth: oauthConfig.enable_facebook_oauth,
131
+ facebook_button_text: oauthConfig.facebook_button_text,
128
132
  },
129
133
  };
130
134
  }
@@ -101,21 +101,4 @@ export const hazo_auth_template_manifest: SystemTemplateManifest[] = [
101
101
  },
102
102
  ],
103
103
  },
104
- {
105
- template_name: "otp_signin_code",
106
- template_label: "OTP sign-in code",
107
- category: SYSTEM_CATEGORY,
108
- html: read_template("otp_signin_code", "html"),
109
- text: read_template("otp_signin_code", "txt"),
110
- variables: [
111
- {
112
- variable_name: "otp_code",
113
- variable_description: "6-digit OTP code for email sign-in (v6.1.0+)",
114
- },
115
- {
116
- variable_name: "expires_in_minutes",
117
- variable_description: "Number of minutes until the OTP code expires",
118
- },
119
- ],
120
- },
121
104
  ];
@@ -20,11 +20,5 @@ export * from "./scope_service.js";
20
20
  export * from "./user_scope_service.js";
21
21
  export * from "./oauth_service.js";
22
22
  export * from "./branding_service.js";
23
- export {
24
- request_email_otp,
25
- verify_email_otp,
26
- generate_otp_code,
27
- hash_otp_code,
28
- verify_otp_code,
29
- } from "./otp_service.js";
30
- export type { RequestEmailOTPResult, VerifyEmailOTPResult } from "./otp_service";
23
+
24
+
@@ -22,6 +22,15 @@ export type GoogleOAuthData = {
22
22
  email_verified: boolean;
23
23
  };
24
24
 
25
+ export type FacebookOAuthData = {
26
+ facebook_id: string;
27
+ email: string;
28
+ name?: string;
29
+ profile_picture_url?: string;
30
+ /** Facebook does not always verify emails — only link when hazo user is verified */
31
+ email_verified: boolean;
32
+ };
33
+
25
34
  export type OAuthLoginResult = {
26
35
  success: boolean;
27
36
  user_id?: string;
@@ -241,6 +250,140 @@ export async function handle_google_oauth_login(
241
250
  }
242
251
  }
243
252
 
253
+ /**
254
+ * Handles Facebook OAuth login: find-by-facebook_id → find-by-email+link → create new.
255
+ * Mirrors handle_google_oauth_login exactly. Uses auto_link_unverified_accounts gate.
256
+ */
257
+ export async function handle_facebook_oauth_login(
258
+ adapter: HazoConnectAdapter,
259
+ data: FacebookOAuthData
260
+ ): Promise<OAuthLoginResult> {
261
+ const logger = create_app_logger();
262
+
263
+ try {
264
+ const { facebook_id, email, name, profile_picture_url, email_verified } = data;
265
+ const oauth_config = get_oauth_config();
266
+ const users_service = createCrudService(adapter, "hazo_users");
267
+ const now = new Date().toISOString();
268
+
269
+ // Step 1: existing user with this facebook_id
270
+ const users_by_fb_id = await users_service.findBy({ facebook_id });
271
+ if (Array.isArray(users_by_fb_id) && users_by_fb_id.length > 0) {
272
+ const user = users_by_fb_id[0];
273
+ await users_service.updateById(user.id, { last_logon: now, changed_at: now });
274
+ logger.info("oauth_service_facebook_login_existing_fb_user", {
275
+ filename: "oauth_service.ts",
276
+ user_id: user.id,
277
+ email: user.email_address,
278
+ });
279
+ return {
280
+ success: true,
281
+ user_id: user.id as string,
282
+ is_new_user: false,
283
+ was_linked: false,
284
+ email: user.email_address as string,
285
+ name: user.name as string | undefined,
286
+ };
287
+ }
288
+
289
+ // Step 2: existing user with matching email
290
+ const users_by_email = await users_service.findBy({ email_address: email });
291
+ if (Array.isArray(users_by_email) && users_by_email.length > 0) {
292
+ const user = users_by_email[0];
293
+ const user_email_verified = user.email_verified as boolean;
294
+
295
+ if (!user_email_verified && !oauth_config.auto_link_unverified_accounts) {
296
+ return {
297
+ success: false,
298
+ error: "An account with this email exists but is not verified. Please verify your email first.",
299
+ };
300
+ }
301
+
302
+ const current_auth_providers = (user.auth_providers as string) || "email";
303
+ const new_auth_providers = current_auth_providers.includes("facebook")
304
+ ? current_auth_providers
305
+ : `${current_auth_providers},facebook`;
306
+
307
+ const update_data: Record<string, unknown> = {
308
+ facebook_id,
309
+ auth_providers: new_auth_providers,
310
+ last_logon: now,
311
+ changed_at: now,
312
+ };
313
+
314
+ if (!user_email_verified && email_verified) {
315
+ update_data.email_verified = true;
316
+ }
317
+ if (!user.name && name) update_data.name = name;
318
+ if (!user.profile_picture_url && profile_picture_url) {
319
+ update_data.profile_picture_url = profile_picture_url;
320
+ update_data.profile_source = "custom";
321
+ }
322
+
323
+ await users_service.updateById(user.id, update_data);
324
+ logger.info("oauth_service_facebook_linked_to_existing", {
325
+ filename: "oauth_service.ts",
326
+ user_id: user.id,
327
+ email,
328
+ was_unverified: !user_email_verified,
329
+ });
330
+ return {
331
+ success: true,
332
+ user_id: user.id as string,
333
+ is_new_user: false,
334
+ was_linked: true,
335
+ email: user.email_address as string,
336
+ name: user.name as string | undefined,
337
+ };
338
+ }
339
+
340
+ // Step 3: create new user
341
+ const user_id = randomUUID();
342
+ const insert_data: Record<string, unknown> = {
343
+ id: user_id,
344
+ email_address: email,
345
+ facebook_id,
346
+ auth_providers: "facebook",
347
+ email_verified: email_verified,
348
+ last_logon: now,
349
+ created_at: now,
350
+ changed_at: now,
351
+ };
352
+ if (name) insert_data.name = name;
353
+ if (profile_picture_url) {
354
+ insert_data.profile_picture_url = profile_picture_url;
355
+ insert_data.profile_source = "custom";
356
+ }
357
+
358
+ const inserted = await users_service.insert(insert_data);
359
+ if (!Array.isArray(inserted) || inserted.length === 0) {
360
+ return { success: false, error: "Failed to create user account" };
361
+ }
362
+
363
+ logger.info("oauth_service_facebook_new_user_created", {
364
+ filename: "oauth_service.ts",
365
+ user_id,
366
+ email,
367
+ });
368
+ return {
369
+ success: true,
370
+ user_id,
371
+ is_new_user: true,
372
+ was_linked: false,
373
+ email,
374
+ name,
375
+ };
376
+ } catch (error) {
377
+ const user_friendly_error = sanitize_error_for_user(error, {
378
+ logToConsole: true,
379
+ logToLogger: true,
380
+ logger,
381
+ context: { filename: "oauth_service.ts", email: data.email, operation: "handle_facebook_oauth_login" },
382
+ });
383
+ return { success: false, error: user_friendly_error };
384
+ }
385
+ }
386
+
244
387
  /**
245
388
  * Links a Google account to an existing user
246
389
  * @param adapter - The hazo_connect adapter instance