hazo_auth 6.0.0 → 7.0.1
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 +233 -8
- package/SETUP_CHECKLIST.md +240 -0
- package/cli-src/cli/validate.ts +4 -0
- package/cli-src/lib/auth/nextauth_config.ts +101 -1
- package/cli-src/lib/cookies_config.server.ts +1 -0
- package/cli-src/lib/email_verification_config.server.ts +0 -34
- package/cli-src/lib/forgot_password_config.server.ts +0 -34
- package/cli-src/lib/login_config.server.ts +14 -31
- package/cli-src/lib/my_settings_config.server.ts +0 -3
- package/cli-src/lib/oauth_config.server.ts +58 -0
- package/cli-src/lib/otp_config.server.ts +91 -0
- package/cli-src/lib/register_config.server.ts +11 -31
- package/cli-src/lib/reset_password_config.server.ts +0 -31
- package/cli-src/lib/services/email_service.ts +3 -1
- package/cli-src/lib/services/email_template_manifest.ts +17 -0
- package/cli-src/lib/services/email_templates/otp_signin_code.html +13 -0
- package/cli-src/lib/services/email_templates/otp_signin_code.txt +5 -0
- package/cli-src/lib/services/index.ts +8 -2
- package/cli-src/lib/services/oauth_service.ts +197 -0
- package/cli-src/lib/services/otp_service.ts +295 -0
- package/cli-src/lib/services/session_token_service.ts +4 -1
- package/config/hazo_auth_config.example.ini +76 -41
- package/dist/cli/validate.d.ts.map +1 -1
- package/dist/cli/validate.js +4 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -0
- package/dist/components/layouts/create_firm/index.d.ts +4 -8
- package/dist/components/layouts/create_firm/index.d.ts.map +1 -1
- package/dist/components/layouts/create_firm/index.js +3 -3
- package/dist/components/layouts/email_verification/index.d.ts +4 -5
- package/dist/components/layouts/email_verification/index.d.ts.map +1 -1
- package/dist/components/layouts/email_verification/index.js +4 -4
- package/dist/components/layouts/forgot_password/index.d.ts +4 -5
- package/dist/components/layouts/forgot_password/index.d.ts.map +1 -1
- package/dist/components/layouts/forgot_password/index.js +2 -2
- package/dist/components/layouts/login/index.d.ts +19 -9
- package/dist/components/layouts/login/index.d.ts.map +1 -1
- package/dist/components/layouts/login/index.js +12 -6
- package/dist/components/layouts/otp/index.d.ts +17 -0
- package/dist/components/layouts/otp/index.d.ts.map +1 -0
- package/dist/components/layouts/otp/index.js +16 -0
- package/dist/components/layouts/register/index.d.ts +11 -7
- package/dist/components/layouts/register/index.d.ts.map +1 -1
- package/dist/components/layouts/register/index.js +8 -4
- package/dist/components/layouts/reset_password/index.d.ts +4 -5
- package/dist/components/layouts/reset_password/index.d.ts.map +1 -1
- package/dist/components/layouts/reset_password/index.js +5 -5
- package/dist/components/layouts/shared/components/already_logged_in_guard.d.ts +3 -5
- package/dist/components/layouts/shared/components/already_logged_in_guard.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/already_logged_in_guard.js +2 -2
- package/dist/components/layouts/shared/components/facebook_sign_in_button.d.ts +25 -0
- package/dist/components/layouts/shared/components/facebook_sign_in_button.d.ts.map +1 -0
- package/dist/components/layouts/shared/components/facebook_sign_in_button.js +49 -0
- package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +8 -3
- package/dist/components/layouts/shared/components/two_column_auth_layout.d.ts +3 -6
- package/dist/components/layouts/shared/components/two_column_auth_layout.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/two_column_auth_layout.js +8 -5
- package/dist/components/otp/OTPRequestForm.d.ts +11 -0
- package/dist/components/otp/OTPRequestForm.d.ts.map +1 -0
- package/dist/components/otp/OTPRequestForm.js +42 -0
- package/dist/components/otp/OTPVerifyForm.d.ts +16 -0
- package/dist/components/otp/OTPVerifyForm.d.ts.map +1 -0
- package/dist/components/otp/OTPVerifyForm.js +75 -0
- package/dist/components/otp/index.d.ts +5 -0
- package/dist/components/otp/index.d.ts.map +1 -0
- package/dist/components/otp/index.js +2 -0
- package/dist/components/ui/input-otp.d.ts +35 -0
- package/dist/components/ui/input-otp.d.ts.map +1 -0
- package/dist/components/ui/input-otp.js +44 -0
- package/dist/consent/consent_state.d.ts +18 -0
- package/dist/consent/consent_state.d.ts.map +1 -0
- package/dist/consent/consent_state.js +29 -0
- package/dist/consent/cookie_consent_banner.d.ts +11 -0
- package/dist/consent/cookie_consent_banner.d.ts.map +1 -0
- package/dist/consent/cookie_consent_banner.js +40 -0
- package/dist/consent/gtm_mapping.d.ts +13 -0
- package/dist/consent/gtm_mapping.d.ts.map +1 -0
- package/dist/consent/gtm_mapping.js +30 -0
- package/dist/consent/index.d.ts +7 -0
- package/dist/consent/index.d.ts.map +1 -0
- package/dist/consent/index.js +7 -0
- package/dist/consent/manage_modal.d.ts +2 -0
- package/dist/consent/manage_modal.d.ts.map +1 -0
- package/dist/consent/manage_modal.js +33 -0
- package/dist/consent/read_consent.d.ts +15 -0
- package/dist/consent/read_consent.d.ts.map +1 -0
- package/dist/consent/read_consent.js +23 -0
- package/dist/consent/use_consent.d.ts +7 -0
- package/dist/consent/use_consent.d.ts.map +1 -0
- package/dist/consent/use_consent.js +55 -0
- package/dist/lib/auth/nextauth_config.d.ts +10 -0
- package/dist/lib/auth/nextauth_config.d.ts.map +1 -1
- package/dist/lib/auth/nextauth_config.js +80 -2
- package/dist/lib/cookies_config.server.d.ts +1 -0
- package/dist/lib/cookies_config.server.d.ts.map +1 -1
- package/dist/lib/cookies_config.server.js +1 -0
- package/dist/lib/email_verification_config.server.d.ts +0 -3
- package/dist/lib/email_verification_config.server.d.ts.map +1 -1
- package/dist/lib/email_verification_config.server.js +0 -15
- package/dist/lib/forgot_password_config.server.d.ts +0 -3
- package/dist/lib/forgot_password_config.server.d.ts.map +1 -1
- package/dist/lib/forgot_password_config.server.js +0 -15
- package/dist/lib/login_config.server.d.ts +6 -3
- package/dist/lib/login_config.server.d.ts.map +1 -1
- package/dist/lib/login_config.server.js +7 -13
- package/dist/lib/my_settings_config.server.d.ts +0 -1
- package/dist/lib/my_settings_config.server.d.ts.map +1 -1
- package/dist/lib/my_settings_config.server.js +0 -2
- package/dist/lib/oauth_config.server.d.ts +17 -0
- package/dist/lib/oauth_config.server.d.ts.map +1 -1
- package/dist/lib/oauth_config.server.js +25 -0
- package/dist/lib/otp_config.server.d.ts +49 -0
- package/dist/lib/otp_config.server.d.ts.map +1 -0
- package/dist/lib/otp_config.server.js +48 -0
- package/dist/lib/register_config.server.d.ts +2 -3
- package/dist/lib/register_config.server.d.ts.map +1 -1
- package/dist/lib/register_config.server.js +4 -13
- package/dist/lib/reset_password_config.server.d.ts +0 -3
- package/dist/lib/reset_password_config.server.d.ts.map +1 -1
- package/dist/lib/reset_password_config.server.js +0 -13
- package/dist/lib/services/email_service.d.ts +1 -1
- package/dist/lib/services/email_service.d.ts.map +1 -1
- package/dist/lib/services/email_service.js +2 -0
- package/dist/lib/services/email_template_manifest.d.ts.map +1 -1
- package/dist/lib/services/email_template_manifest.js +17 -0
- package/dist/lib/services/email_templates/otp_signin_code.html +13 -0
- package/dist/lib/services/email_templates/otp_signin_code.txt +5 -0
- package/dist/lib/services/index.d.ts +2 -0
- package/dist/lib/services/index.d.ts.map +1 -1
- package/dist/lib/services/index.js +1 -0
- package/dist/lib/services/oauth_service.d.ts +24 -0
- package/dist/lib/services/oauth_service.d.ts.map +1 -1
- package/dist/lib/services/oauth_service.js +155 -0
- package/dist/lib/services/otp_service.d.ts +46 -0
- package/dist/lib/services/otp_service.d.ts.map +1 -0
- package/dist/lib/services/otp_service.js +238 -0
- package/dist/lib/services/session_token_service.d.ts +3 -1
- package/dist/lib/services/session_token_service.d.ts.map +1 -1
- package/dist/lib/services/session_token_service.js +4 -2
- package/dist/page_components/create_firm.d.ts +13 -1
- package/dist/page_components/create_firm.d.ts.map +1 -1
- package/dist/page_components/create_firm.js +10 -6
- package/dist/page_components/forgot_password.d.ts +1 -4
- package/dist/page_components/forgot_password.d.ts.map +1 -1
- package/dist/page_components/forgot_password.js +2 -6
- package/dist/page_components/login.d.ts +1 -4
- package/dist/page_components/login.d.ts.map +1 -1
- package/dist/page_components/login.js +2 -6
- package/dist/page_components/otp.d.ts +4 -0
- package/dist/page_components/otp.d.ts.map +1 -0
- package/dist/page_components/otp.js +5 -0
- package/dist/page_components/register.d.ts +1 -4
- package/dist/page_components/register.d.ts.map +1 -1
- package/dist/page_components/register.js +2 -6
- package/dist/page_components/reset_password.d.ts +1 -4
- package/dist/page_components/reset_password.d.ts.map +1 -1
- package/dist/page_components/reset_password.js +2 -6
- package/dist/page_components/verify_email.d.ts +1 -4
- package/dist/page_components/verify_email.d.ts.map +1 -1
- package/dist/page_components/verify_email.js +2 -6
- package/dist/server/routes/index.d.ts +3 -0
- package/dist/server/routes/index.d.ts.map +1 -1
- package/dist/server/routes/index.js +4 -0
- package/dist/server/routes/me.d.ts.map +1 -1
- package/dist/server/routes/me.js +43 -1
- package/dist/server/routes/oauth_facebook_callback.d.ts +8 -0
- package/dist/server/routes/oauth_facebook_callback.d.ts.map +1 -0
- package/dist/server/routes/oauth_facebook_callback.js +157 -0
- package/dist/server/routes/oauth_google_callback.js +1 -1
- package/dist/server/routes/otp/request.d.ts +3 -0
- package/dist/server/routes/otp/request.d.ts.map +1 -0
- package/dist/server/routes/otp/request.js +33 -0
- package/dist/server/routes/otp/verify.d.ts +3 -0
- package/dist/server/routes/otp/verify.d.ts.map +1 -0
- package/dist/server/routes/otp/verify.js +58 -0
- package/dist/server-lib.d.ts +3 -0
- package/dist/server-lib.d.ts.map +1 -1
- package/dist/server-lib.js +2 -0
- package/dist/server_pages/forgot_password.d.ts +13 -17
- package/dist/server_pages/forgot_password.d.ts.map +1 -1
- package/dist/server_pages/forgot_password.js +12 -8
- package/dist/server_pages/forgot_password_client_wrapper.d.ts +7 -6
- package/dist/server_pages/forgot_password_client_wrapper.d.ts.map +1 -1
- package/dist/server_pages/forgot_password_client_wrapper.js +2 -2
- package/dist/server_pages/login.d.ts +22 -21
- package/dist/server_pages/login.d.ts.map +1 -1
- package/dist/server_pages/login.js +15 -19
- package/dist/server_pages/login_client_wrapper.d.ts +10 -6
- package/dist/server_pages/login_client_wrapper.d.ts.map +1 -1
- package/dist/server_pages/login_client_wrapper.js +2 -2
- package/dist/server_pages/my_settings.d.ts +2 -0
- package/dist/server_pages/my_settings.d.ts.map +1 -1
- package/dist/server_pages/my_settings.js +8 -2
- package/dist/server_pages/otp.d.ts +56 -0
- package/dist/server_pages/otp.d.ts.map +1 -0
- package/dist/server_pages/otp.js +45 -0
- package/dist/server_pages/register.d.ts +19 -16
- package/dist/server_pages/register.d.ts.map +1 -1
- package/dist/server_pages/register.js +15 -12
- package/dist/server_pages/register_client_wrapper.d.ts +10 -6
- package/dist/server_pages/register_client_wrapper.d.ts.map +1 -1
- package/dist/server_pages/register_client_wrapper.js +2 -2
- package/dist/server_pages/reset_password.d.ts +11 -16
- package/dist/server_pages/reset_password.d.ts.map +1 -1
- package/dist/server_pages/reset_password.js +11 -9
- package/dist/server_pages/reset_password_client_wrapper.d.ts +7 -6
- package/dist/server_pages/reset_password_client_wrapper.d.ts.map +1 -1
- package/dist/server_pages/reset_password_client_wrapper.js +2 -2
- package/dist/server_pages/verify_email.d.ts +11 -17
- package/dist/server_pages/verify_email.d.ts.map +1 -1
- package/dist/server_pages/verify_email.js +11 -8
- package/dist/server_pages/verify_email_client_wrapper.d.ts +7 -6
- package/dist/server_pages/verify_email_client_wrapper.d.ts.map +1 -1
- package/dist/server_pages/verify_email_client_wrapper.js +2 -2
- package/dist/strings/default_strings.d.ts +47 -0
- package/dist/strings/default_strings.d.ts.map +1 -0
- package/dist/strings/default_strings.js +18 -0
- package/dist/strings/index.d.ts +4 -0
- package/dist/strings/index.d.ts.map +1 -0
- package/dist/strings/index.js +3 -0
- package/dist/strings/strings_context.d.ts +12 -0
- package/dist/strings/strings_context.d.ts.map +1 -0
- package/dist/strings/strings_context.js +23 -0
- package/dist/strings/strings_provider.d.ts +26 -0
- package/dist/strings/strings_provider.d.ts.map +1 -0
- package/dist/strings/strings_provider.js +45 -0
- package/dist/theme/create_theme.d.ts +7 -0
- package/dist/theme/create_theme.d.ts.map +1 -0
- package/dist/theme/create_theme.js +97 -0
- package/dist/theme/hex_to_hsl.d.ts +16 -0
- package/dist/theme/hex_to_hsl.d.ts.map +1 -0
- package/dist/theme/hex_to_hsl.js +110 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +3 -0
- package/dist/theme/luminance.d.ts +11 -0
- package/dist/theme/luminance.d.ts.map +1 -0
- package/dist/theme/luminance.js +45 -0
- package/dist/theme/theme_provider.d.ts +14 -0
- package/dist/theme/theme_provider.d.ts.map +1 -0
- package/dist/theme/theme_provider.js +23 -0
- package/dist/theme/theme_types.d.ts +36 -0
- package/dist/theme/theme_types.d.ts.map +1 -0
- package/dist/theme/theme_types.js +1 -0
- package/dist/themes/index.d.ts +3 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +2 -0
- package/dist/themes/preset_indigo_sunset.d.ts +3 -0
- package/dist/themes/preset_indigo_sunset.d.ts.map +1 -0
- package/dist/themes/preset_indigo_sunset.js +20 -0
- package/dist/themes/preset_neutral.d.ts +3 -0
- package/dist/themes/preset_neutral.d.ts.map +1 -0
- package/dist/themes/preset_neutral.js +14 -0
- package/package.json +36 -2
|
@@ -36,6 +36,18 @@ export type OAuthLoginResult = {
|
|
|
36
36
|
error?: string;
|
|
37
37
|
};
|
|
38
38
|
|
|
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
|
+
|
|
39
51
|
export type LinkGoogleResult = {
|
|
40
52
|
success: boolean;
|
|
41
53
|
error?: string;
|
|
@@ -241,6 +253,191 @@ export async function handle_google_oauth_login(
|
|
|
241
253
|
}
|
|
242
254
|
}
|
|
243
255
|
|
|
256
|
+
/**
|
|
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
|
|
266
|
+
*/
|
|
267
|
+
export async function handle_facebook_oauth_login(
|
|
268
|
+
adapter: HazoConnectAdapter,
|
|
269
|
+
data: FacebookOAuthData,
|
|
270
|
+
opts?: { auto_link_unverified?: boolean }
|
|
271
|
+
): Promise<OAuthLoginResult> {
|
|
272
|
+
const logger = create_app_logger();
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const { facebook_id, email, name, profile_picture_url } = data;
|
|
276
|
+
|
|
277
|
+
const users_service = createCrudService(adapter, "hazo_users");
|
|
278
|
+
const now = new Date().toISOString();
|
|
279
|
+
|
|
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", {
|
|
292
|
+
filename: "oauth_service.ts",
|
|
293
|
+
line_number: get_line_number(),
|
|
294
|
+
user_id: user.id,
|
|
295
|
+
email: user.email_address,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
success: true,
|
|
300
|
+
user_id: user.id as string,
|
|
301
|
+
is_new_user: false,
|
|
302
|
+
was_linked: false,
|
|
303
|
+
email: user.email_address as string,
|
|
304
|
+
name: user.name as string | undefined,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
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
|
+
}
|
|
343
|
+
|
|
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
|
+
}
|
|
349
|
+
|
|
350
|
+
await users_service.updateById(user.id, update_data);
|
|
351
|
+
|
|
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
|
+
});
|
|
359
|
+
|
|
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
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Step 3: Create new user with Facebook data
|
|
372
|
+
const user_id = randomUUID();
|
|
373
|
+
|
|
374
|
+
const insert_data: Record<string, unknown> = {
|
|
375
|
+
id: user_id,
|
|
376
|
+
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
|
+
facebook_id,
|
|
382
|
+
auth_providers: "facebook",
|
|
383
|
+
created_at: now,
|
|
384
|
+
changed_at: now,
|
|
385
|
+
last_logon: now,
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
if (name) {
|
|
389
|
+
insert_data.name = name;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (profile_picture_url) {
|
|
393
|
+
insert_data.profile_picture_url = profile_picture_url;
|
|
394
|
+
insert_data.profile_source = "custom";
|
|
395
|
+
}
|
|
396
|
+
|
|
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
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
logger.info("oauth_service_facebook_new_user_created", {
|
|
407
|
+
filename: "oauth_service.ts",
|
|
408
|
+
line_number: get_line_number(),
|
|
409
|
+
user_id,
|
|
410
|
+
email,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
success: true,
|
|
415
|
+
user_id,
|
|
416
|
+
is_new_user: true,
|
|
417
|
+
was_linked: false,
|
|
418
|
+
email: email ?? undefined,
|
|
419
|
+
name,
|
|
420
|
+
};
|
|
421
|
+
} catch (error) {
|
|
422
|
+
const user_friendly_error = sanitize_error_for_user(error, {
|
|
423
|
+
logToConsole: true,
|
|
424
|
+
logToLogger: true,
|
|
425
|
+
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
|
+
},
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
success: false,
|
|
436
|
+
error: user_friendly_error,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
244
441
|
/**
|
|
245
442
|
* Links a Google account to an existing user
|
|
246
443
|
* @param adapter - The hazo_connect adapter instance
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import argon2 from "argon2";
|
|
4
|
+
import { createCrudService } from "hazo_connect/server";
|
|
5
|
+
import { get_otp_config, hazo_auth_otp_session_ttl_seconds } from "../otp_config.server.js";
|
|
6
|
+
import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
|
|
7
|
+
import { send_template_email } from "./email_service.js";
|
|
8
|
+
import { create_app_logger } from "../app_logger.js";
|
|
9
|
+
import { create_session_token } from "./session_token_service.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generates a cryptographically random 6-digit numeric OTP code (000000–999999).
|
|
13
|
+
* Uses crypto.randomInt for uniform distribution.
|
|
14
|
+
*/
|
|
15
|
+
export function generate_otp_code(): string {
|
|
16
|
+
const n = crypto.randomInt(0, 1_000_000);
|
|
17
|
+
return String(n).padStart(6, "0");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function hash_otp_code(code: string): Promise<string> {
|
|
21
|
+
return argon2.hash(code);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function verify_otp_code(otp_hash: string, code: string): Promise<boolean> {
|
|
25
|
+
try {
|
|
26
|
+
return await argon2.verify(otp_hash, code);
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// section: types
|
|
33
|
+
|
|
34
|
+
export type RequestEmailOTPResult =
|
|
35
|
+
| { ok: true }
|
|
36
|
+
| { ok: false; error: "rate_limited"; retry_after_seconds: number };
|
|
37
|
+
|
|
38
|
+
// section: request_email_otp
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initiates an OTP sign-in flow for the given email address.
|
|
42
|
+
*
|
|
43
|
+
* Behaviour:
|
|
44
|
+
* 1. Per-email rate limit — rejects if too many requests in the sliding window.
|
|
45
|
+
* 2. Per-IP rate limit — rejects if too many requests from this IP.
|
|
46
|
+
* 3. Unknown email + auto_register=false — silent no-op (constant-time padding).
|
|
47
|
+
* 4. Unknown email + auto_register=true — inserts OTP row with user_id=null, dispatches email.
|
|
48
|
+
* 5. Known email — marks prior unconsumed rows consumed, inserts fresh OTP row, dispatches email.
|
|
49
|
+
*
|
|
50
|
+
* Never reveals whether an email address is registered (always returns ok:true on success).
|
|
51
|
+
*/
|
|
52
|
+
export async function request_email_otp(args: {
|
|
53
|
+
email: string;
|
|
54
|
+
ip: string;
|
|
55
|
+
}): Promise<RequestEmailOTPResult> {
|
|
56
|
+
const logger = create_app_logger();
|
|
57
|
+
const cfg = get_otp_config();
|
|
58
|
+
const email = args.email.trim().toLowerCase();
|
|
59
|
+
const ip = args.ip;
|
|
60
|
+
|
|
61
|
+
const adapter = get_hazo_connect_instance();
|
|
62
|
+
const otp_table = createCrudService(adapter, "hazo_email_otps");
|
|
63
|
+
const users_table = createCrudService(adapter, "hazo_users");
|
|
64
|
+
|
|
65
|
+
// 1. Per-email rate limit
|
|
66
|
+
const email_window_ms = cfg.email_rate_limit_window_seconds * 1000;
|
|
67
|
+
const email_threshold = new Date(Date.now() - email_window_ms).toISOString();
|
|
68
|
+
const recent_for_email = await otp_table.list((qb) =>
|
|
69
|
+
qb
|
|
70
|
+
.select(["created_at"])
|
|
71
|
+
.where("email", "eq", email)
|
|
72
|
+
.where("created_at", "gte", email_threshold)
|
|
73
|
+
);
|
|
74
|
+
if (recent_for_email.length >= cfg.email_rate_limit_max) {
|
|
75
|
+
const oldest = recent_for_email
|
|
76
|
+
.map((r) => Date.parse(String(r.created_at)))
|
|
77
|
+
.sort((a, b) => a - b)[0];
|
|
78
|
+
const retry_after_seconds = Math.max(
|
|
79
|
+
1,
|
|
80
|
+
Math.ceil((oldest + email_window_ms - Date.now()) / 1000),
|
|
81
|
+
);
|
|
82
|
+
logger.warn("otp_request_email_rate_limited", { email, ip, retry_after_seconds });
|
|
83
|
+
return { ok: false, error: "rate_limited", retry_after_seconds };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 2. Per-IP rate limit
|
|
87
|
+
const ip_window_ms = cfg.ip_rate_limit_window_seconds * 1000;
|
|
88
|
+
const ip_threshold = new Date(Date.now() - ip_window_ms).toISOString();
|
|
89
|
+
const recent_for_ip = await otp_table.list((qb) =>
|
|
90
|
+
qb
|
|
91
|
+
.select(["created_at"])
|
|
92
|
+
.where("requester_ip", "eq", ip)
|
|
93
|
+
.where("created_at", "gte", ip_threshold)
|
|
94
|
+
);
|
|
95
|
+
if (recent_for_ip.length >= cfg.ip_rate_limit_max) {
|
|
96
|
+
const oldest = recent_for_ip
|
|
97
|
+
.map((r) => Date.parse(String(r.created_at)))
|
|
98
|
+
.sort((a, b) => a - b)[0];
|
|
99
|
+
const retry_after_seconds = Math.max(
|
|
100
|
+
1,
|
|
101
|
+
Math.ceil((oldest + ip_window_ms - Date.now()) / 1000),
|
|
102
|
+
);
|
|
103
|
+
logger.warn("otp_request_ip_rate_limited", { email, ip, retry_after_seconds });
|
|
104
|
+
return { ok: false, error: "rate_limited", retry_after_seconds };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 3. Lookup user
|
|
108
|
+
const existing_users = await users_table.findBy({ email_address: email });
|
|
109
|
+
const existing_user = existing_users.length > 0 ? existing_users[0] : null;
|
|
110
|
+
|
|
111
|
+
// 4. Unknown email + auto_register=false → silent no-op with constant-time padding
|
|
112
|
+
if (!existing_user && !cfg.auto_register) {
|
|
113
|
+
await argon2.hash("000000"); // constant-time padding — never stored
|
|
114
|
+
return { ok: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 5. Mark any unconsumed rows for this email as superseded
|
|
118
|
+
try {
|
|
119
|
+
const unconsumed = await otp_table.list((qb) =>
|
|
120
|
+
qb
|
|
121
|
+
.select(["id"])
|
|
122
|
+
.where("email", "eq", email)
|
|
123
|
+
.where("consumed_at", "is", null)
|
|
124
|
+
);
|
|
125
|
+
for (const row of unconsumed) {
|
|
126
|
+
await otp_table.updateById(String(row.id), { consumed_at: new Date().toISOString() });
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// IS NULL filter may not be supported in all adapter versions — not critical
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 6. Generate code + hash + insert row
|
|
133
|
+
const code = generate_otp_code();
|
|
134
|
+
const otp_hash = await hash_otp_code(code);
|
|
135
|
+
const expires_at = new Date(Date.now() + cfg.code_ttl_seconds * 1000).toISOString();
|
|
136
|
+
const row_id = crypto.randomUUID();
|
|
137
|
+
|
|
138
|
+
await otp_table.insert({
|
|
139
|
+
id: row_id,
|
|
140
|
+
user_id: existing_user ? String(existing_user.id) : null,
|
|
141
|
+
email,
|
|
142
|
+
otp_hash,
|
|
143
|
+
expires_at,
|
|
144
|
+
attempt_count: 0,
|
|
145
|
+
requester_ip: ip,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// 7. Dispatch email — fire-and-forget; errors are logged but do not surface to caller
|
|
149
|
+
try {
|
|
150
|
+
await send_template_email("otp_signin_code", email, {
|
|
151
|
+
otp_code: code,
|
|
152
|
+
expires_in_minutes: String(Math.round(cfg.code_ttl_seconds / 60)),
|
|
153
|
+
});
|
|
154
|
+
} catch (err) {
|
|
155
|
+
logger.error("otp_request_email_dispatch_failed", {
|
|
156
|
+
email,
|
|
157
|
+
ip,
|
|
158
|
+
error: err instanceof Error ? err.message : String(err),
|
|
159
|
+
});
|
|
160
|
+
// Return ok:true to preserve no-enumeration property — caller cannot distinguish
|
|
161
|
+
// a missing user from a delivery failure.
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { ok: true };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// section: verify_email_otp
|
|
168
|
+
|
|
169
|
+
export type VerifyEmailOTPResult =
|
|
170
|
+
| { ok: true; user_id: string; email: string; session_token: string }
|
|
171
|
+
| { ok: false; error: "invalid_or_expired" };
|
|
172
|
+
|
|
173
|
+
export async function verify_email_otp(args: {
|
|
174
|
+
email: string;
|
|
175
|
+
code: string;
|
|
176
|
+
ip: string;
|
|
177
|
+
}): Promise<VerifyEmailOTPResult> {
|
|
178
|
+
const logger = create_app_logger();
|
|
179
|
+
const cfg = get_otp_config();
|
|
180
|
+
const email = args.email.trim().toLowerCase();
|
|
181
|
+
const code = args.code.trim();
|
|
182
|
+
|
|
183
|
+
const adapter = get_hazo_connect_instance();
|
|
184
|
+
const otp_table = createCrudService(adapter, "hazo_email_otps");
|
|
185
|
+
const users_table = createCrudService(adapter, "hazo_users");
|
|
186
|
+
const user_scopes_table = createCrudService(adapter, "hazo_user_scopes");
|
|
187
|
+
const roles_table = createCrudService(adapter, "hazo_roles");
|
|
188
|
+
|
|
189
|
+
// 1. Find most-recent unconsumed row for this email
|
|
190
|
+
const now_iso = new Date().toISOString();
|
|
191
|
+
const candidates = await otp_table.list((qb) =>
|
|
192
|
+
qb
|
|
193
|
+
.select(["id", "user_id", "otp_hash", "expires_at", "attempt_count"])
|
|
194
|
+
.where("email", "eq", email)
|
|
195
|
+
.where("consumed_at", "is", null)
|
|
196
|
+
.order("created_at", "desc")
|
|
197
|
+
.limit(1)
|
|
198
|
+
);
|
|
199
|
+
const row = candidates.length > 0 ? candidates[0] : null;
|
|
200
|
+
|
|
201
|
+
if (!row) {
|
|
202
|
+
return { ok: false, error: "invalid_or_expired" };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 2. Check expiry
|
|
206
|
+
const expires_at_ms = Date.parse(String(row.expires_at));
|
|
207
|
+
if (Number.isNaN(expires_at_ms) || expires_at_ms < Date.now()) {
|
|
208
|
+
return { ok: false, error: "invalid_or_expired" };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 3. argon2 verify
|
|
212
|
+
const is_valid = await verify_otp_code(String(row.otp_hash), code);
|
|
213
|
+
if (!is_valid) {
|
|
214
|
+
const new_attempt_count = Number(row.attempt_count) + 1;
|
|
215
|
+
const updates: Record<string, unknown> = { attempt_count: new_attempt_count };
|
|
216
|
+
if (new_attempt_count >= cfg.max_verify_attempts) {
|
|
217
|
+
updates.consumed_at = now_iso; // poison
|
|
218
|
+
}
|
|
219
|
+
await otp_table.updateById(String(row.id), updates);
|
|
220
|
+
logger.info("otp_verify_invalid_code", {
|
|
221
|
+
email,
|
|
222
|
+
attempt_count: new_attempt_count,
|
|
223
|
+
poisoned: new_attempt_count >= cfg.max_verify_attempts,
|
|
224
|
+
});
|
|
225
|
+
return { ok: false, error: "invalid_or_expired" };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 4. Mark consumed
|
|
229
|
+
await otp_table.updateById(String(row.id), { consumed_at: now_iso });
|
|
230
|
+
|
|
231
|
+
// 5. Resolve / create user
|
|
232
|
+
let user_id: string | null = row.user_id ? String(row.user_id) : null;
|
|
233
|
+
|
|
234
|
+
if (user_id) {
|
|
235
|
+
// Ensure email_verified=true
|
|
236
|
+
const user = await users_table.findById(user_id);
|
|
237
|
+
if (user && !user.email_verified) {
|
|
238
|
+
await users_table.updateById(user_id, { email_verified: true });
|
|
239
|
+
}
|
|
240
|
+
} else if (cfg.auto_register) {
|
|
241
|
+
// Create user + bind scope/role
|
|
242
|
+
const new_user_id = crypto.randomUUID();
|
|
243
|
+
await users_table.insert({
|
|
244
|
+
id: new_user_id,
|
|
245
|
+
email_address: email,
|
|
246
|
+
email_verified: true,
|
|
247
|
+
password_hash: null,
|
|
248
|
+
name: null,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Resolve role_id from name — try scope-specific first, then any
|
|
252
|
+
const scoped_roles = await roles_table.findBy({
|
|
253
|
+
name: cfg.auto_assign_role_name,
|
|
254
|
+
scope_id: cfg.auto_assign_scope_id,
|
|
255
|
+
});
|
|
256
|
+
let role_id: string | null = null;
|
|
257
|
+
if (scoped_roles.length > 0) {
|
|
258
|
+
role_id = String(scoped_roles[0].id);
|
|
259
|
+
} else {
|
|
260
|
+
const any_roles = await roles_table.findBy({ name: cfg.auto_assign_role_name });
|
|
261
|
+
if (any_roles.length > 0) {
|
|
262
|
+
role_id = String(any_roles[0].id);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!role_id) {
|
|
267
|
+
logger.error("otp_verify_auto_register_role_not_found", {
|
|
268
|
+
email,
|
|
269
|
+
scope_id: cfg.auto_assign_scope_id,
|
|
270
|
+
role_name: cfg.auto_assign_role_name,
|
|
271
|
+
});
|
|
272
|
+
return { ok: false, error: "invalid_or_expired" };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await user_scopes_table.insert({
|
|
276
|
+
user_id: new_user_id,
|
|
277
|
+
scope_id: cfg.auto_assign_scope_id,
|
|
278
|
+
root_scope_id: cfg.auto_assign_scope_id,
|
|
279
|
+
role_id,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
user_id = new_user_id;
|
|
283
|
+
} else {
|
|
284
|
+
// Row exists but no user and auto_register=false — stale edge case
|
|
285
|
+
logger.warn("otp_verify_no_user_resolvable", { email });
|
|
286
|
+
return { ok: false, error: "invalid_or_expired" };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 6. Issue session JWT with OTP TTL
|
|
290
|
+
const ttl_seconds = hazo_auth_otp_session_ttl_seconds();
|
|
291
|
+
const session_token = await create_session_token(user_id, email, undefined, ttl_seconds);
|
|
292
|
+
|
|
293
|
+
logger.info("otp_verify_ok", { email, user_id, ttl_seconds });
|
|
294
|
+
return { ok: true, user_id, email, session_token };
|
|
295
|
+
}
|
|
@@ -63,19 +63,22 @@ 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.
|
|
66
68
|
* @returns JWT token string
|
|
67
69
|
*/
|
|
68
70
|
export async function create_session_token(
|
|
69
71
|
user_id: string,
|
|
70
72
|
email: string,
|
|
71
73
|
managed_by_user_id?: string,
|
|
74
|
+
ttl_seconds?: number,
|
|
72
75
|
): Promise<string> {
|
|
73
76
|
const logger = create_app_logger();
|
|
74
77
|
|
|
75
78
|
try {
|
|
76
79
|
const secret = get_jwt_secret();
|
|
77
80
|
const now = Math.floor(Date.now() / 1000); // Current time in seconds
|
|
78
|
-
const expiry_seconds = get_session_token_expiry_seconds();
|
|
81
|
+
const expiry_seconds = ttl_seconds ?? get_session_token_expiry_seconds();
|
|
79
82
|
const exp = now + expiry_seconds;
|
|
80
83
|
|
|
81
84
|
const payload: Record<string, unknown> = { user_id, email };
|
|
@@ -74,11 +74,9 @@ 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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# submit_button = Register
|
|
81
|
-
# cancel_button = Cancel
|
|
77
|
+
; Page text is no longer configured via INI.
|
|
78
|
+
; Use HazoAuthStringsProvider or per-page props instead.
|
|
79
|
+
; See MIGRATION.md for details.
|
|
82
80
|
|
|
83
81
|
# Field labels (defaults shown, uncomment to override)
|
|
84
82
|
# name_label = Full name
|
|
@@ -129,11 +127,9 @@ enable_admin_ui = true
|
|
|
129
127
|
# image_alt = Illustration of a globe representing secure authentication workflows
|
|
130
128
|
# image_background_color = #e2e8f0
|
|
131
129
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
# submit_button = Login
|
|
136
|
-
# cancel_button = Cancel
|
|
130
|
+
; Page text is no longer configured via INI.
|
|
131
|
+
; Use HazoAuthStringsProvider or per-page props instead.
|
|
132
|
+
; See MIGRATION.md for details.
|
|
137
133
|
|
|
138
134
|
# Field labels (defaults shown, uncomment to override)
|
|
139
135
|
# email_label = Email address
|
|
@@ -159,17 +155,20 @@ enable_admin_ui = true
|
|
|
159
155
|
# Success message (shown when no redirect route is provided)
|
|
160
156
|
# success_message = Successfully logged in
|
|
161
157
|
|
|
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
|
+
|
|
162
163
|
[hazo_auth__forgot_password_layout]
|
|
163
164
|
# Image configuration
|
|
164
165
|
# image_src = /globe.svg
|
|
165
166
|
# image_alt = Illustration of a globe representing secure authentication workflows
|
|
166
167
|
# image_background_color = #e2e8f0
|
|
167
168
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
# submit_button = Send reset link
|
|
172
|
-
# cancel_button = Cancel
|
|
169
|
+
; Page text is no longer configured via INI.
|
|
170
|
+
; Use HazoAuthStringsProvider or per-page props instead.
|
|
171
|
+
; See MIGRATION.md for details.
|
|
173
172
|
|
|
174
173
|
[hazo_auth__reset_password_layout]
|
|
175
174
|
# Image configuration
|
|
@@ -177,11 +176,9 @@ enable_admin_ui = true
|
|
|
177
176
|
# image_alt = Illustration of a globe representing secure authentication workflows
|
|
178
177
|
# image_background_color = #e2e8f0
|
|
179
178
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
# submit_button = Reset password
|
|
184
|
-
# cancel_button = Cancel
|
|
179
|
+
; Page text is no longer configured via INI.
|
|
180
|
+
; Use HazoAuthStringsProvider or per-page props instead.
|
|
181
|
+
; See MIGRATION.md for details.
|
|
185
182
|
|
|
186
183
|
# Field labels (defaults shown, uncomment to override)
|
|
187
184
|
# password_label = New password
|
|
@@ -213,22 +210,9 @@ enable_admin_ui = true
|
|
|
213
210
|
# image_alt = Illustration of a globe representing secure authentication workflows
|
|
214
211
|
# image_background_color = #e2e8f0
|
|
215
212
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
213
|
+
; Page text is no longer configured via INI.
|
|
214
|
+
; Use HazoAuthStringsProvider or per-page props instead.
|
|
215
|
+
; See MIGRATION.md for details.
|
|
232
216
|
|
|
233
217
|
# Field labels (defaults shown, uncomment to override)
|
|
234
218
|
# email_label = Email address
|
|
@@ -270,11 +254,9 @@ enable_admin_ui = true
|
|
|
270
254
|
# - [hazo_auth__password_requirements] for password validation rules
|
|
271
255
|
# - [hazo_auth__profile_picture] for profile picture settings and prioritization
|
|
272
256
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
# Sub heading
|
|
277
|
-
# sub_heading = Manage your profile, password, and email preferences.
|
|
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.
|
|
278
260
|
|
|
279
261
|
# Profile Photo section
|
|
280
262
|
# profile_photo_label = Profile Photo
|
|
@@ -597,6 +579,26 @@ application_permission_list_defaults = admin_user_management,admin_role_manageme
|
|
|
597
579
|
# Redirect when skip_invitation_check=true and user has no scope
|
|
598
580
|
# no_scope_redirect = /
|
|
599
581
|
|
|
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
|
+
|
|
600
602
|
[hazo_auth__multi_tenancy]
|
|
601
603
|
# Multi-tenancy configuration for organization hierarchy
|
|
602
604
|
# Enables hierarchical organization structures for company-wide access control
|
|
@@ -729,3 +731,36 @@ company_name = My Company
|
|
|
729
731
|
# [hazo_auth__login_layout]
|
|
730
732
|
# image_src = /your-custom-image.jpg
|
|
731
733
|
|
|
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
|
+
|