hazo_auth 7.0.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 +73 -330
  2. package/SETUP_CHECKLIST.md +28 -248
  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 +27 -67
  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/email_verification_config.server.ts +34 -0
  13. package/cli-src/lib/forgot_password_config.server.ts +34 -0
  14. package/cli-src/lib/login_config.server.ts +29 -14
  15. package/cli-src/lib/my_settings_config.server.ts +3 -0
  16. package/cli-src/lib/oauth_config.server.ts +31 -57
  17. package/cli-src/lib/register_config.server.ts +35 -11
  18. package/cli-src/lib/reset_password_config.server.ts +31 -0
  19. package/cli-src/lib/services/email_template_manifest.ts +0 -17
  20. package/cli-src/lib/services/index.ts +2 -8
  21. package/cli-src/lib/services/oauth_service.ts +74 -128
  22. package/cli-src/lib/services/otp_service.ts +7 -2
  23. package/cli-src/lib/services/session_token_service.ts +0 -2
  24. package/config/hazo_auth_config.example.ini +41 -76
  25. package/dist/cli/generate.d.ts.map +1 -1
  26. package/dist/cli/generate.js +1 -10
  27. package/dist/cli/validate.d.ts.map +1 -1
  28. package/dist/cli/validate.js +0 -4
  29. package/dist/client.d.ts +0 -2
  30. package/dist/client.d.ts.map +1 -1
  31. package/dist/client.js +0 -1
  32. package/dist/components/layouts/create_firm/index.d.ts +8 -4
  33. package/dist/components/layouts/create_firm/index.d.ts.map +1 -1
  34. package/dist/components/layouts/create_firm/index.js +3 -3
  35. package/dist/components/layouts/email_verification/index.d.ts +5 -4
  36. package/dist/components/layouts/email_verification/index.d.ts.map +1 -1
  37. package/dist/components/layouts/email_verification/index.js +4 -4
  38. package/dist/components/layouts/forgot_password/index.d.ts +5 -4
  39. package/dist/components/layouts/forgot_password/index.d.ts.map +1 -1
  40. package/dist/components/layouts/forgot_password/index.js +2 -2
  41. package/dist/components/layouts/login/index.d.ts +13 -19
  42. package/dist/components/layouts/login/index.d.ts.map +1 -1
  43. package/dist/components/layouts/login/index.js +8 -11
  44. package/dist/components/layouts/otp/index.d.ts +5 -1
  45. package/dist/components/layouts/otp/index.d.ts.map +1 -1
  46. package/dist/components/layouts/otp/index.js +2 -2
  47. package/dist/components/layouts/register/index.d.ts +11 -11
  48. package/dist/components/layouts/register/index.d.ts.map +1 -1
  49. package/dist/components/layouts/register/index.js +6 -7
  50. package/dist/components/layouts/reset_password/index.d.ts +5 -4
  51. package/dist/components/layouts/reset_password/index.d.ts.map +1 -1
  52. package/dist/components/layouts/reset_password/index.js +5 -5
  53. package/dist/components/layouts/shared/components/already_logged_in_guard.d.ts +5 -3
  54. package/dist/components/layouts/shared/components/already_logged_in_guard.d.ts.map +1 -1
  55. package/dist/components/layouts/shared/components/already_logged_in_guard.js +2 -2
  56. package/dist/components/layouts/shared/components/facebook_sign_in_button.d.ts +2 -6
  57. package/dist/components/layouts/shared/components/facebook_sign_in_button.d.ts.map +1 -1
  58. package/dist/components/layouts/shared/components/facebook_sign_in_button.js +11 -13
  59. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
  60. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +3 -8
  61. package/dist/components/layouts/shared/components/two_column_auth_layout.d.ts +6 -3
  62. package/dist/components/layouts/shared/components/two_column_auth_layout.d.ts.map +1 -1
  63. package/dist/components/layouts/shared/components/two_column_auth_layout.js +5 -8
  64. package/dist/components/layouts/shared/index.d.ts +2 -0
  65. package/dist/components/layouts/shared/index.d.ts.map +1 -1
  66. package/dist/components/layouts/shared/index.js +1 -0
  67. package/dist/components/layouts/user_management/index.d.ts.map +1 -1
  68. package/dist/components/layouts/user_management/index.js +39 -2
  69. package/dist/index.d.ts +1 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/lib/auth/auth_types.d.ts +12 -13
  72. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  73. package/dist/lib/auth/auth_types.js +0 -8
  74. package/dist/lib/auth/hazo_get_tenant_auth.server.d.ts +7 -8
  75. package/dist/lib/auth/hazo_get_tenant_auth.server.d.ts.map +1 -1
  76. package/dist/lib/auth/hazo_get_tenant_auth.server.js +22 -23
  77. package/dist/lib/auth/index.d.ts +2 -2
  78. package/dist/lib/auth/index.d.ts.map +1 -1
  79. package/dist/lib/auth/nextauth_config.d.ts +0 -10
  80. package/dist/lib/auth/nextauth_config.d.ts.map +1 -1
  81. package/dist/lib/auth/nextauth_config.js +23 -52
  82. package/dist/lib/auth/with_auth.server.d.ts +13 -13
  83. package/dist/lib/auth/with_auth.server.d.ts.map +1 -1
  84. package/dist/lib/auth/with_auth.server.js +2 -2
  85. package/dist/lib/config/default_config.d.ts +16 -0
  86. package/dist/lib/config/default_config.d.ts.map +1 -1
  87. package/dist/lib/config/default_config.js +8 -0
  88. package/dist/lib/cookies_config.server.d.ts +1 -1
  89. package/dist/lib/cookies_config.server.js +1 -1
  90. package/dist/lib/email_verification_config.server.d.ts +3 -0
  91. package/dist/lib/email_verification_config.server.d.ts.map +1 -1
  92. package/dist/lib/email_verification_config.server.js +15 -0
  93. package/dist/lib/forgot_password_config.server.d.ts +3 -0
  94. package/dist/lib/forgot_password_config.server.d.ts.map +1 -1
  95. package/dist/lib/forgot_password_config.server.js +15 -0
  96. package/dist/lib/login_config.server.d.ts +3 -6
  97. package/dist/lib/login_config.server.d.ts.map +1 -1
  98. package/dist/lib/login_config.server.js +11 -7
  99. package/dist/lib/my_settings_config.server.d.ts +1 -0
  100. package/dist/lib/my_settings_config.server.d.ts.map +1 -1
  101. package/dist/lib/my_settings_config.server.js +2 -0
  102. package/dist/lib/oauth_config.server.d.ts +8 -17
  103. package/dist/lib/oauth_config.server.d.ts.map +1 -1
  104. package/dist/lib/oauth_config.server.js +10 -25
  105. package/dist/lib/register_config.server.d.ts +5 -2
  106. package/dist/lib/register_config.server.d.ts.map +1 -1
  107. package/dist/lib/register_config.server.js +15 -4
  108. package/dist/lib/reset_password_config.server.d.ts +3 -0
  109. package/dist/lib/reset_password_config.server.d.ts.map +1 -1
  110. package/dist/lib/reset_password_config.server.js +13 -0
  111. package/dist/lib/services/email_template_manifest.d.ts.map +1 -1
  112. package/dist/lib/services/email_template_manifest.js +0 -17
  113. package/dist/lib/services/index.d.ts +0 -2
  114. package/dist/lib/services/index.d.ts.map +1 -1
  115. package/dist/lib/services/index.js +0 -1
  116. package/dist/lib/services/oauth_service.d.ts +11 -22
  117. package/dist/lib/services/oauth_service.d.ts.map +1 -1
  118. package/dist/lib/services/oauth_service.js +63 -96
  119. package/dist/lib/services/otp_service.d.ts +1 -1
  120. package/dist/lib/services/otp_service.d.ts.map +1 -1
  121. package/dist/lib/services/otp_service.js +6 -1
  122. package/dist/lib/services/session_token_service.d.ts +0 -2
  123. package/dist/lib/services/session_token_service.d.ts.map +1 -1
  124. package/dist/lib/services/session_token_service.js +0 -2
  125. package/dist/page_components/create_firm.d.ts +1 -13
  126. package/dist/page_components/create_firm.d.ts.map +1 -1
  127. package/dist/page_components/create_firm.js +6 -10
  128. package/dist/page_components/forgot_password.d.ts +4 -1
  129. package/dist/page_components/forgot_password.d.ts.map +1 -1
  130. package/dist/page_components/forgot_password.js +6 -2
  131. package/dist/page_components/login.d.ts +4 -1
  132. package/dist/page_components/login.d.ts.map +1 -1
  133. package/dist/page_components/login.js +6 -2
  134. package/dist/page_components/register.d.ts +4 -1
  135. package/dist/page_components/register.d.ts.map +1 -1
  136. package/dist/page_components/register.js +6 -2
  137. package/dist/page_components/reset_password.d.ts +4 -1
  138. package/dist/page_components/reset_password.d.ts.map +1 -1
  139. package/dist/page_components/reset_password.js +6 -2
  140. package/dist/page_components/verify_email.d.ts +4 -1
  141. package/dist/page_components/verify_email.d.ts.map +1 -1
  142. package/dist/page_components/verify_email.js +6 -2
  143. package/dist/server/routes/assets.d.ts +8 -0
  144. package/dist/server/routes/assets.d.ts.map +1 -0
  145. package/dist/server/routes/assets.js +38 -0
  146. package/dist/server/routes/consent_me.d.ts +4 -0
  147. package/dist/server/routes/consent_me.d.ts.map +1 -0
  148. package/dist/server/routes/consent_me.js +15 -0
  149. package/dist/server/routes/index.d.ts +6 -4
  150. package/dist/server/routes/index.d.ts.map +1 -1
  151. package/dist/server/routes/index.js +9 -5
  152. package/dist/server/routes/me.d.ts.map +1 -1
  153. package/dist/server/routes/me.js +1 -43
  154. package/dist/server/routes/oauth_facebook_callback.d.ts +1 -1
  155. package/dist/server/routes/oauth_facebook_callback.d.ts.map +1 -1
  156. package/dist/server/routes/oauth_facebook_callback.js +8 -1
  157. package/dist/server/routes/oauth_google_callback.js +1 -1
  158. package/dist/server/routes/otp/verify.js +2 -2
  159. package/dist/server/routes/strings_defaults.d.ts +4 -0
  160. package/dist/server/routes/strings_defaults.d.ts.map +1 -0
  161. package/dist/server/routes/strings_defaults.js +7 -0
  162. package/dist/server/routes/user_management_users.d.ts +11 -0
  163. package/dist/server/routes/user_management_users.d.ts.map +1 -1
  164. package/dist/server/routes/user_management_users.js +50 -0
  165. package/dist/server-lib.d.ts +0 -3
  166. package/dist/server-lib.d.ts.map +1 -1
  167. package/dist/server-lib.js +0 -2
  168. package/dist/server_pages/forgot_password.d.ts +18 -14
  169. package/dist/server_pages/forgot_password.d.ts.map +1 -1
  170. package/dist/server_pages/forgot_password.js +14 -12
  171. package/dist/server_pages/forgot_password_client_wrapper.d.ts +8 -7
  172. package/dist/server_pages/forgot_password_client_wrapper.d.ts.map +1 -1
  173. package/dist/server_pages/forgot_password_client_wrapper.js +2 -2
  174. package/dist/server_pages/index.d.ts +2 -0
  175. package/dist/server_pages/index.d.ts.map +1 -1
  176. package/dist/server_pages/index.js +1 -0
  177. package/dist/server_pages/login.d.ts +22 -23
  178. package/dist/server_pages/login.d.ts.map +1 -1
  179. package/dist/server_pages/login.js +27 -14
  180. package/dist/server_pages/login_client_wrapper.d.ts +9 -10
  181. package/dist/server_pages/login_client_wrapper.d.ts.map +1 -1
  182. package/dist/server_pages/login_client_wrapper.js +2 -2
  183. package/dist/server_pages/my_settings.d.ts +1 -3
  184. package/dist/server_pages/my_settings.d.ts.map +1 -1
  185. package/dist/server_pages/my_settings.js +2 -9
  186. package/dist/server_pages/register.d.ts +17 -20
  187. package/dist/server_pages/register.d.ts.map +1 -1
  188. package/dist/server_pages/register.js +20 -15
  189. package/dist/server_pages/register_client_wrapper.d.ts +8 -10
  190. package/dist/server_pages/register_client_wrapper.d.ts.map +1 -1
  191. package/dist/server_pages/register_client_wrapper.js +2 -2
  192. package/dist/server_pages/reset_password.d.ts +16 -11
  193. package/dist/server_pages/reset_password.d.ts.map +1 -1
  194. package/dist/server_pages/reset_password.js +14 -10
  195. package/dist/server_pages/reset_password_client_wrapper.d.ts +8 -7
  196. package/dist/server_pages/reset_password_client_wrapper.d.ts.map +1 -1
  197. package/dist/server_pages/reset_password_client_wrapper.js +2 -2
  198. package/dist/server_pages/verify_email.d.ts +18 -12
  199. package/dist/server_pages/verify_email.d.ts.map +1 -1
  200. package/dist/server_pages/verify_email.js +13 -11
  201. package/dist/server_pages/verify_email_client_wrapper.d.ts +8 -7
  202. package/dist/server_pages/verify_email_client_wrapper.d.ts.map +1 -1
  203. package/dist/server_pages/verify_email_client_wrapper.js +2 -2
  204. package/dist/themes/index.d.ts +0 -1
  205. package/dist/themes/index.d.ts.map +1 -1
  206. package/dist/themes/index.js +1 -1
  207. package/package.json +26 -40
  208. package/dist/themes/preset_indigo_sunset.d.ts +0 -3
  209. package/dist/themes/preset_indigo_sunset.d.ts.map +0 -1
  210. package/dist/themes/preset_indigo_sunset.js +0 -20
@@ -9,6 +9,11 @@ import { get_already_logged_in_config } from "./already_logged_in_config.server.
9
9
  import { get_user_fields_config } from "./user_fields_config.server.js";
10
10
  import { get_oauth_config } from "./oauth_config.server.js";
11
11
 
12
+ // Default image path - consuming apps should either:
13
+ // 1. Configure their own image_src in hazo_auth_config.ini
14
+ // 2. Copy the default images from node_modules/hazo_auth/public/hazo_auth/images/ to their public folder
15
+ const DEFAULT_REGISTER_IMAGE_PATH = "/hazo_auth/images/register_default.jpg";
16
+
12
17
  // section: types
13
18
  export type RegisterConfig = {
14
19
  showNameField: boolean;
@@ -26,14 +31,17 @@ export type RegisterConfig = {
26
31
  returnHomePath: string;
27
32
  signInPath: string;
28
33
  signInLabel: string;
34
+ imageSrc: string;
35
+ imageAlt: string;
36
+ imageBackgroundColor: string;
29
37
  /** OAuth configuration */
30
38
  oauth: {
31
39
  enable_google: boolean;
32
- enable_facebook: boolean;
33
40
  enable_email_password: boolean;
34
41
  google_button_text: string;
35
- facebook_button_text: string;
36
42
  oauth_divider_text: string;
43
+ enable_facebook_oauth: boolean;
44
+ facebook_button_text: string;
37
45
  };
38
46
  };
39
47
 
@@ -71,6 +79,26 @@ export function get_register_config(): RegisterConfig {
71
79
  "Sign in"
72
80
  );
73
81
 
82
+ // Read image configuration
83
+ // If not set in config, falls back to default path-based image
84
+ // Consuming apps should copy images to public/hazo_auth/images/ or configure their own image_src
85
+ const imageSrc = get_config_value(
86
+ "hazo_auth__register_layout",
87
+ "image_src",
88
+ DEFAULT_REGISTER_IMAGE_PATH
89
+ );
90
+
91
+ const imageAlt = get_config_value(
92
+ "hazo_auth__register_layout",
93
+ "image_alt",
94
+ "Modern building representing user registration"
95
+ );
96
+ const imageBackgroundColor = get_config_value(
97
+ "hazo_auth__register_layout",
98
+ "image_background_color",
99
+ "#e2e8f0"
100
+ );
101
+
74
102
  // Get OAuth configuration (shared with login)
75
103
  const oauthConfig = get_oauth_config();
76
104
 
@@ -81,13 +109,6 @@ export function get_register_config(): RegisterConfig {
81
109
  "Sign up with Google"
82
110
  );
83
111
 
84
- // For the register page, default Facebook button text to "Sign up with Facebook" unless overridden
85
- const registerFacebookButtonText = get_config_value(
86
- "hazo_auth__oauth",
87
- "facebook_button_text_register",
88
- "Sign up with Facebook"
89
- );
90
-
91
112
  return {
92
113
  showNameField,
93
114
  passwordRequirements,
@@ -98,13 +119,16 @@ export function get_register_config(): RegisterConfig {
98
119
  returnHomePath: alreadyLoggedInConfig.returnHomePath,
99
120
  signInPath,
100
121
  signInLabel,
122
+ imageSrc,
123
+ imageAlt,
124
+ imageBackgroundColor,
101
125
  oauth: {
102
126
  enable_google: oauthConfig.enable_google,
103
- enable_facebook: oauthConfig.enable_facebook,
104
127
  enable_email_password: oauthConfig.enable_email_password,
105
128
  google_button_text: registerGoogleButtonText,
106
- facebook_button_text: registerFacebookButtonText,
107
129
  oauth_divider_text: oauthConfig.oauth_divider_text,
130
+ enable_facebook_oauth: oauthConfig.enable_facebook_oauth,
131
+ facebook_button_text: oauthConfig.facebook_button_text,
108
132
  },
109
133
  };
110
134
  }
@@ -7,6 +7,11 @@ import { get_config_value } 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_password_requirements_config } from "./password_requirements_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_RESET_PASSWORD_IMAGE_PATH = "/hazo_auth/images/reset_password_default.jpg";
14
+
10
15
  // section: types
11
16
  export type ResetPasswordConfig = {
12
17
  errorMessage: string;
@@ -25,6 +30,9 @@ export type ResetPasswordConfig = {
25
30
  require_number: boolean;
26
31
  require_special: boolean;
27
32
  };
33
+ imageSrc: string;
34
+ imageAlt: string;
35
+ imageBackgroundColor: string;
28
36
  };
29
37
 
30
38
  // section: helpers
@@ -62,6 +70,26 @@ export function get_reset_password_config(): ResetPasswordConfig {
62
70
  // Get shared password requirements
63
71
  const passwordRequirements = get_password_requirements_config();
64
72
 
73
+ // Read image configuration
74
+ // If not set in config, falls back to default path-based image
75
+ // Consuming apps should copy images to public/hazo_auth/images/ or configure their own image_src
76
+ const imageSrc = get_config_value(
77
+ section,
78
+ "image_src",
79
+ DEFAULT_RESET_PASSWORD_IMAGE_PATH
80
+ );
81
+
82
+ const imageAlt = get_config_value(
83
+ section,
84
+ "image_alt",
85
+ "Reset password illustration"
86
+ );
87
+ const imageBackgroundColor = get_config_value(
88
+ section,
89
+ "image_background_color",
90
+ "#f1f5f9"
91
+ );
92
+
65
93
  return {
66
94
  errorMessage,
67
95
  successMessage,
@@ -73,6 +101,9 @@ export function get_reset_password_config(): ResetPasswordConfig {
73
101
  returnHomeButtonLabel: alreadyLoggedInConfig.returnHomeButtonLabel,
74
102
  returnHomePath: alreadyLoggedInConfig.returnHomePath,
75
103
  passwordRequirements,
104
+ imageSrc,
105
+ imageAlt,
106
+ imageBackgroundColor,
76
107
  };
77
108
  }
78
109
 
@@ -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;
@@ -36,18 +45,6 @@ export type OAuthLoginResult = {
36
45
  error?: string;
37
46
  };
38
47
 
39
- export type FacebookOAuthData = {
40
- /** Facebook's unique user ID */
41
- facebook_id: string;
42
- /** User's email address from Facebook (may be null if user denied email permission) */
43
- email: string | null;
44
- /** User's full name from Facebook profile */
45
- name?: string;
46
- /** User's profile picture URL from Facebook */
47
- profile_picture_url?: string;
48
- // NOTE: no email_verified — we never trust Facebook's email_verified claim
49
- };
50
-
51
48
  export type LinkGoogleResult = {
52
49
  success: boolean;
53
50
  error?: string;
@@ -254,47 +251,31 @@ export async function handle_google_oauth_login(
254
251
  }
255
252
 
256
253
  /**
257
- * Handles Facebook OAuth login/registration flow
258
- * 1. Check if user exists with facebook_id -> login
259
- * 2. Check if user exists with email -> link Facebook account (respects auto_link_unverified)
260
- * 3. Create new user with Facebook data (email_verified always false — never trust Facebook)
261
- *
262
- * @param adapter - The hazo_connect adapter instance
263
- * @param data - Facebook OAuth user data
264
- * @param opts - Options (auto_link_unverified: whether to link unverified accounts)
265
- * @returns OAuth login result with user_id and status flags
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.
266
256
  */
267
257
  export async function handle_facebook_oauth_login(
268
258
  adapter: HazoConnectAdapter,
269
- data: FacebookOAuthData,
270
- opts?: { auto_link_unverified?: boolean }
259
+ data: FacebookOAuthData
271
260
  ): Promise<OAuthLoginResult> {
272
261
  const logger = create_app_logger();
273
262
 
274
263
  try {
275
- const { facebook_id, email, name, profile_picture_url } = data;
276
-
264
+ const { facebook_id, email, name, profile_picture_url, email_verified } = data;
265
+ const oauth_config = get_oauth_config();
277
266
  const users_service = createCrudService(adapter, "hazo_users");
278
267
  const now = new Date().toISOString();
279
268
 
280
- // Step 1: Check if user exists with this facebook_id
281
- const users_by_facebook_id = await users_service.findBy({ facebook_id });
282
-
283
- if (Array.isArray(users_by_facebook_id) && users_by_facebook_id.length > 0) {
284
- const user = users_by_facebook_id[0];
285
-
286
- await users_service.updateById(user.id, {
287
- last_logon: now,
288
- changed_at: now,
289
- });
290
-
291
- logger.info("oauth_service_facebook_login_existing_facebook_user", {
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", {
292
275
  filename: "oauth_service.ts",
293
- line_number: get_line_number(),
294
276
  user_id: user.id,
295
277
  email: user.email_address,
296
278
  });
297
-
298
279
  return {
299
280
  success: true,
300
281
  user_id: user.id as string,
@@ -305,117 +286,91 @@ export async function handle_facebook_oauth_login(
305
286
  };
306
287
  }
307
288
 
308
- // Step 2: Check if user exists with this email
309
- if (email) {
310
- const users_by_email = await users_service.findBy({ email_address: email });
311
-
312
- if (Array.isArray(users_by_email) && users_by_email.length > 0) {
313
- const user = users_by_email[0];
314
- const user_email_verified = user.email_verified as boolean;
315
-
316
- if (!user_email_verified) {
317
- if (!opts?.auto_link_unverified) {
318
- return {
319
- success: false,
320
- error: "link_blocked_unverified",
321
- };
322
- }
323
- // auto_link_unverified=true: link but do NOT change email_verified status
324
- }
325
-
326
- // Link Facebook account to existing user
327
- const current_auth_providers = (user.auth_providers as string) || "email";
328
- const new_auth_providers = current_auth_providers.includes("facebook")
329
- ? current_auth_providers
330
- : `${current_auth_providers},facebook`;
331
-
332
- const update_data: Record<string, unknown> = {
333
- facebook_id,
334
- auth_providers: new_auth_providers,
335
- last_logon: now,
336
- changed_at: now,
337
- };
338
-
339
- // Update name if not set and Facebook provides one
340
- if (!user.name && name) {
341
- update_data.name = name;
342
- }
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;
343
294
 
344
- // Update profile picture if not set and Facebook provides one
345
- if (!user.profile_picture_url && profile_picture_url) {
346
- update_data.profile_picture_url = profile_picture_url;
347
- update_data.profile_source = "custom";
348
- }
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
+ }
349
301
 
350
- await users_service.updateById(user.id, update_data);
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`;
351
306
 
352
- logger.info("oauth_service_facebook_linked_to_existing", {
353
- filename: "oauth_service.ts",
354
- line_number: get_line_number(),
355
- user_id: user.id,
356
- email,
357
- was_unverified: !user_email_verified,
358
- });
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
+ };
359
313
 
360
- return {
361
- success: true,
362
- user_id: user.id as string,
363
- is_new_user: false,
364
- was_linked: true,
365
- email: user.email_address as string,
366
- name: (update_data.name as string) || (user.name as string | undefined),
367
- };
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";
368
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
+ };
369
338
  }
370
339
 
371
- // Step 3: Create new user with Facebook data
340
+ // Step 3: create new user
372
341
  const user_id = randomUUID();
373
-
374
342
  const insert_data: Record<string, unknown> = {
375
343
  id: user_id,
376
344
  email_address: email,
377
- password_hash: "", // Empty string for Facebook-only users
378
- email_verified: false, // Never trust Facebook's email_verified claim
379
- status: "ACTIVE",
380
- login_attempts: 0,
381
345
  facebook_id,
382
346
  auth_providers: "facebook",
347
+ email_verified: email_verified,
348
+ last_logon: now,
383
349
  created_at: now,
384
350
  changed_at: now,
385
- last_logon: now,
386
351
  };
387
-
388
- if (name) {
389
- insert_data.name = name;
390
- }
391
-
352
+ if (name) insert_data.name = name;
392
353
  if (profile_picture_url) {
393
354
  insert_data.profile_picture_url = profile_picture_url;
394
355
  insert_data.profile_source = "custom";
395
356
  }
396
357
 
397
- const inserted_users = await users_service.insert(insert_data);
398
-
399
- if (!Array.isArray(inserted_users) || inserted_users.length === 0) {
400
- return {
401
- success: false,
402
- error: "Failed to create user account",
403
- };
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" };
404
361
  }
405
362
 
406
363
  logger.info("oauth_service_facebook_new_user_created", {
407
364
  filename: "oauth_service.ts",
408
- line_number: get_line_number(),
409
365
  user_id,
410
366
  email,
411
367
  });
412
-
413
368
  return {
414
369
  success: true,
415
370
  user_id,
416
371
  is_new_user: true,
417
372
  was_linked: false,
418
- email: email ?? undefined,
373
+ email,
419
374
  name,
420
375
  };
421
376
  } catch (error) {
@@ -423,18 +378,9 @@ export async function handle_facebook_oauth_login(
423
378
  logToConsole: true,
424
379
  logToLogger: true,
425
380
  logger,
426
- context: {
427
- filename: "oauth_service.ts",
428
- line_number: get_line_number(),
429
- email: data.email,
430
- operation: "handle_facebook_oauth_login",
431
- },
381
+ context: { filename: "oauth_service.ts", email: data.email, operation: "handle_facebook_oauth_login" },
432
382
  });
433
-
434
- return {
435
- success: false,
436
- error: user_friendly_error,
437
- };
383
+ return { success: false, error: user_friendly_error };
438
384
  }
439
385
  }
440
386
 
@@ -143,6 +143,11 @@ export async function request_email_otp(args: {
143
143
  expires_at,
144
144
  attempt_count: 0,
145
145
  requester_ip: ip,
146
+ // Explicitly pass created_at in JS ISO format ("2024-01-01T00:00:00.000Z") rather
147
+ // than relying on SQLite's DEFAULT (datetime('now') = "2024-01-01 00:00:00").
148
+ // The space-separated SQLite format compares as less-than the T-separated JS ISO
149
+ // threshold used in rate-limit WHERE clauses, causing the counter to always read 0.
150
+ created_at: new Date().toISOString(),
146
151
  });
147
152
 
148
153
  // 7. Dispatch email — fire-and-forget; errors are logged but do not surface to caller
@@ -168,7 +173,7 @@ export async function request_email_otp(args: {
168
173
 
169
174
  export type VerifyEmailOTPResult =
170
175
  | { ok: true; user_id: string; email: string; session_token: string }
171
- | { ok: false; error: "invalid_or_expired" };
176
+ | { ok: false; error: "invalid_or_expired" | "expired" };
172
177
 
173
178
  export async function verify_email_otp(args: {
174
179
  email: string;
@@ -205,7 +210,7 @@ export async function verify_email_otp(args: {
205
210
  // 2. Check expiry
206
211
  const expires_at_ms = Date.parse(String(row.expires_at));
207
212
  if (Number.isNaN(expires_at_ms) || expires_at_ms < Date.now()) {
208
- return { ok: false, error: "invalid_or_expired" };
213
+ return { ok: false, error: "expired" };
209
214
  }
210
215
 
211
216
  // 3. argon2 verify
@@ -63,8 +63,6 @@ function get_session_token_expiry_seconds(): number {
63
63
  * Token includes user_id, email, issued at time, and expiration
64
64
  * @param user_id - User ID
65
65
  * @param email - User email address
66
- * @param managed_by_user_id - Optional: ID of the managing user (for impersonation)
67
- * @param ttl_seconds - Optional: token lifetime in seconds (default: 30 days). Use 604800 for 7-day OTP sessions.
68
66
  * @returns JWT token string
69
67
  */
70
68
  export async function create_session_token(
@@ -74,9 +74,11 @@ enable_admin_ui = true
74
74
  # image_alt = Illustration of a globe representing secure authentication workflows
75
75
  # image_background_color = #e2e8f0
76
76
 
77
- ; Page text is no longer configured via INI.
78
- ; Use HazoAuthStringsProvider or per-page props instead.
79
- ; See MIGRATION.md for details.
77
+ # Labels
78
+ # heading = Create your hazo account
79
+ # sub_heading = Secure your access with editable fields powered by shadcn components.
80
+ # submit_button = Register
81
+ # cancel_button = Cancel
80
82
 
81
83
  # Field labels (defaults shown, uncomment to override)
82
84
  # name_label = Full name
@@ -127,9 +129,11 @@ enable_admin_ui = true
127
129
  # image_alt = Illustration of a globe representing secure authentication workflows
128
130
  # image_background_color = #e2e8f0
129
131
 
130
- ; Page text is no longer configured via INI.
131
- ; Use HazoAuthStringsProvider or per-page props instead.
132
- ; See MIGRATION.md for details.
132
+ # Labels
133
+ # heading = Sign in to your account
134
+ # sub_heading = Enter your credentials to access your secure workspace.
135
+ # submit_button = Login
136
+ # cancel_button = Cancel
133
137
 
134
138
  # Field labels (defaults shown, uncomment to override)
135
139
  # email_label = Email address
@@ -155,20 +159,17 @@ enable_admin_ui = true
155
159
  # Success message (shown when no redirect route is provided)
156
160
  # success_message = Successfully logged in
157
161
 
158
- ; OTP sign-in link in the login layout.
159
- otp_signin_enabled = false
160
- otp_signin_label = Sign in with email code
161
- otp_signin_href = /hazo_auth/otp
162
-
163
162
  [hazo_auth__forgot_password_layout]
164
163
  # Image configuration
165
164
  # image_src = /globe.svg
166
165
  # image_alt = Illustration of a globe representing secure authentication workflows
167
166
  # image_background_color = #e2e8f0
168
167
 
169
- ; Page text is no longer configured via INI.
170
- ; Use HazoAuthStringsProvider or per-page props instead.
171
- ; See MIGRATION.md for details.
168
+ # Labels
169
+ # heading = Forgot your password?
170
+ # sub_heading = Enter your email address and we'll send you a link to reset your password.
171
+ # submit_button = Send reset link
172
+ # cancel_button = Cancel
172
173
 
173
174
  [hazo_auth__reset_password_layout]
174
175
  # Image configuration
@@ -176,9 +177,11 @@ otp_signin_href = /hazo_auth/otp
176
177
  # image_alt = Illustration of a globe representing secure authentication workflows
177
178
  # image_background_color = #e2e8f0
178
179
 
179
- ; Page text is no longer configured via INI.
180
- ; Use HazoAuthStringsProvider or per-page props instead.
181
- ; See MIGRATION.md for details.
180
+ # Labels
181
+ # heading = Reset your password
182
+ # sub_heading = Enter your new password below.
183
+ # submit_button = Reset password
184
+ # cancel_button = Cancel
182
185
 
183
186
  # Field labels (defaults shown, uncomment to override)
184
187
  # password_label = New password
@@ -210,9 +213,22 @@ otp_signin_href = /hazo_auth/otp
210
213
  # image_alt = Illustration of a globe representing secure authentication workflows
211
214
  # image_background_color = #e2e8f0
212
215
 
213
- ; Page text is no longer configured via INI.
214
- ; Use HazoAuthStringsProvider or per-page props instead.
215
- ; See MIGRATION.md for details.
216
+ # Labels (verifying state)
217
+ # heading = Email verification
218
+ # sub_heading = Verifying your email address...
219
+ # submit_button = Resend verification email
220
+ # cancel_button = Cancel
221
+
222
+ # Success labels
223
+ # success_heading = Email verified successfully
224
+ # success_message = Your email address has been verified. You can now log in to your account.
225
+ # success_redirect_message = Redirecting to login page in
226
+ # success_go_to_login_button = Go to login
227
+
228
+ # Error labels
229
+ # error_heading = Verification failed
230
+ # error_message = The verification link is invalid or has expired.
231
+ # error_resend_form_heading = Resend verification email
216
232
 
217
233
  # Field labels (defaults shown, uncomment to override)
218
234
  # email_label = Email address
@@ -254,9 +270,11 @@ otp_signin_href = /hazo_auth/otp
254
270
  # - [hazo_auth__password_requirements] for password validation rules
255
271
  # - [hazo_auth__profile_picture] for profile picture settings and prioritization
256
272
 
257
- ; Page heading text is no longer configured via INI.
258
- ; Use HazoAuthStringsProvider or the title prop on MySettingsPage instead.
259
- ; See MIGRATION.md for details.
273
+ # Heading
274
+ # heading = Account Settings
275
+
276
+ # Sub heading
277
+ # sub_heading = Manage your profile, password, and email preferences.
260
278
 
261
279
  # Profile Photo section
262
280
  # profile_photo_label = Profile Photo
@@ -579,26 +597,6 @@ application_permission_list_defaults = admin_user_management,admin_role_manageme
579
597
  # Redirect when skip_invitation_check=true and user has no scope
580
598
  # no_scope_redirect = /
581
599
 
582
- [hazo_auth__create_firm]
583
- ; heading, sub_heading, and submit_button_label are no longer configured via INI in v7.0.0.
584
- ; Use HazoAuthStringsProvider or the title/subtitle/ctaText props on CreateFirmPage instead.
585
- ; See MIGRATION.md for details.
586
-
587
- ; Firm name field label
588
- # firm_name_label = Firm Name
589
-
590
- ; Organisation structure field label
591
- # org_structure_label = Organisation Structure
592
-
593
- ; Default organisation structure value
594
- # org_structure_default = Headquarters
595
-
596
- ; Success message shown after firm creation
597
- # success_message = Your firm has been created successfully!
598
-
599
- ; Route to redirect to after firm creation
600
- # redirect_route = /
601
-
602
600
  [hazo_auth__multi_tenancy]
603
601
  # Multi-tenancy configuration for organization hierarchy
604
602
  # Enables hierarchical organization structures for company-wide access control
@@ -731,36 +729,3 @@ company_name = My Company
731
729
  # [hazo_auth__login_layout]
732
730
  # image_src = /your-custom-image.jpg
733
731
 
734
- [hazo_auth__otp]
735
- ; Email-OTP sign-in configuration (v6.1.0+).
736
- ;
737
- ; Whether /otp/verify may create new hazo_users rows for unknown emails.
738
- ; false (default): /otp/request silently no-ops for unknown emails.
739
- ; true: /otp/verify creates a user row on first successful code match.
740
- otp_auto_register = false
741
-
742
- ; OTP code lifetime in seconds (default 600 = 10 minutes).
743
- otp_code_ttl_seconds = 600
744
-
745
- ; OTP session lifetime in seconds (default 604800 = 7 days).
746
- otp_session_ttl_seconds = 604800
747
-
748
- ; Sliding-session threshold: if a /me call lands and the JWT exp is within
749
- ; this many seconds, re-issue the session for another otp_session_ttl_seconds.
750
- otp_slide_when_within_seconds = 86400
751
-
752
- ; Per-email rate limit for /otp/request.
753
- otp_email_rate_limit_max = 3
754
- otp_email_rate_limit_window_seconds = 900
755
-
756
- ; Per-IP rate limit for /otp/request.
757
- otp_ip_rate_limit_max = 20
758
- otp_ip_rate_limit_window_seconds = 3600
759
-
760
- ; Wrong-guess limit per code; row is poisoned after this many failures.
761
- otp_max_verify_attempts = 5
762
-
763
- ; Scope binding for newly auto-registered users (only used when otp_auto_register=true).
764
- otp_auto_assign_scope_id = 00000000-0000-0000-0000-000000000001
765
- otp_auto_assign_role_name = member
766
-