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
|
@@ -167,6 +167,161 @@ export async function handle_google_oauth_login(adapter, data) {
|
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Handles Facebook OAuth login/registration flow
|
|
172
|
+
* 1. Check if user exists with facebook_id -> login
|
|
173
|
+
* 2. Check if user exists with email -> link Facebook account (respects auto_link_unverified)
|
|
174
|
+
* 3. Create new user with Facebook data (email_verified always false — never trust Facebook)
|
|
175
|
+
*
|
|
176
|
+
* @param adapter - The hazo_connect adapter instance
|
|
177
|
+
* @param data - Facebook OAuth user data
|
|
178
|
+
* @param opts - Options (auto_link_unverified: whether to link unverified accounts)
|
|
179
|
+
* @returns OAuth login result with user_id and status flags
|
|
180
|
+
*/
|
|
181
|
+
export async function handle_facebook_oauth_login(adapter, data, opts) {
|
|
182
|
+
const logger = create_app_logger();
|
|
183
|
+
try {
|
|
184
|
+
const { facebook_id, email, name, profile_picture_url } = data;
|
|
185
|
+
const users_service = createCrudService(adapter, "hazo_users");
|
|
186
|
+
const now = new Date().toISOString();
|
|
187
|
+
// Step 1: Check if user exists with this facebook_id
|
|
188
|
+
const users_by_facebook_id = await users_service.findBy({ facebook_id });
|
|
189
|
+
if (Array.isArray(users_by_facebook_id) && users_by_facebook_id.length > 0) {
|
|
190
|
+
const user = users_by_facebook_id[0];
|
|
191
|
+
await users_service.updateById(user.id, {
|
|
192
|
+
last_logon: now,
|
|
193
|
+
changed_at: now,
|
|
194
|
+
});
|
|
195
|
+
logger.info("oauth_service_facebook_login_existing_facebook_user", {
|
|
196
|
+
filename: "oauth_service.ts",
|
|
197
|
+
line_number: get_line_number(),
|
|
198
|
+
user_id: user.id,
|
|
199
|
+
email: user.email_address,
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
success: true,
|
|
203
|
+
user_id: user.id,
|
|
204
|
+
is_new_user: false,
|
|
205
|
+
was_linked: false,
|
|
206
|
+
email: user.email_address,
|
|
207
|
+
name: user.name,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// Step 2: Check if user exists with this email
|
|
211
|
+
if (email) {
|
|
212
|
+
const users_by_email = await users_service.findBy({ email_address: email });
|
|
213
|
+
if (Array.isArray(users_by_email) && users_by_email.length > 0) {
|
|
214
|
+
const user = users_by_email[0];
|
|
215
|
+
const user_email_verified = user.email_verified;
|
|
216
|
+
if (!user_email_verified) {
|
|
217
|
+
if (!(opts === null || opts === void 0 ? void 0 : opts.auto_link_unverified)) {
|
|
218
|
+
return {
|
|
219
|
+
success: false,
|
|
220
|
+
error: "link_blocked_unverified",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// auto_link_unverified=true: link but do NOT change email_verified status
|
|
224
|
+
}
|
|
225
|
+
// Link Facebook account to existing user
|
|
226
|
+
const current_auth_providers = user.auth_providers || "email";
|
|
227
|
+
const new_auth_providers = current_auth_providers.includes("facebook")
|
|
228
|
+
? current_auth_providers
|
|
229
|
+
: `${current_auth_providers},facebook`;
|
|
230
|
+
const update_data = {
|
|
231
|
+
facebook_id,
|
|
232
|
+
auth_providers: new_auth_providers,
|
|
233
|
+
last_logon: now,
|
|
234
|
+
changed_at: now,
|
|
235
|
+
};
|
|
236
|
+
// Update name if not set and Facebook provides one
|
|
237
|
+
if (!user.name && name) {
|
|
238
|
+
update_data.name = name;
|
|
239
|
+
}
|
|
240
|
+
// Update profile picture if not set and Facebook provides one
|
|
241
|
+
if (!user.profile_picture_url && profile_picture_url) {
|
|
242
|
+
update_data.profile_picture_url = profile_picture_url;
|
|
243
|
+
update_data.profile_source = "custom";
|
|
244
|
+
}
|
|
245
|
+
await users_service.updateById(user.id, update_data);
|
|
246
|
+
logger.info("oauth_service_facebook_linked_to_existing", {
|
|
247
|
+
filename: "oauth_service.ts",
|
|
248
|
+
line_number: get_line_number(),
|
|
249
|
+
user_id: user.id,
|
|
250
|
+
email,
|
|
251
|
+
was_unverified: !user_email_verified,
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
success: true,
|
|
255
|
+
user_id: user.id,
|
|
256
|
+
is_new_user: false,
|
|
257
|
+
was_linked: true,
|
|
258
|
+
email: user.email_address,
|
|
259
|
+
name: update_data.name || user.name,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Step 3: Create new user with Facebook data
|
|
264
|
+
const user_id = randomUUID();
|
|
265
|
+
const insert_data = {
|
|
266
|
+
id: user_id,
|
|
267
|
+
email_address: email,
|
|
268
|
+
password_hash: "", // Empty string for Facebook-only users
|
|
269
|
+
email_verified: false, // Never trust Facebook's email_verified claim
|
|
270
|
+
status: "ACTIVE",
|
|
271
|
+
login_attempts: 0,
|
|
272
|
+
facebook_id,
|
|
273
|
+
auth_providers: "facebook",
|
|
274
|
+
created_at: now,
|
|
275
|
+
changed_at: now,
|
|
276
|
+
last_logon: now,
|
|
277
|
+
};
|
|
278
|
+
if (name) {
|
|
279
|
+
insert_data.name = name;
|
|
280
|
+
}
|
|
281
|
+
if (profile_picture_url) {
|
|
282
|
+
insert_data.profile_picture_url = profile_picture_url;
|
|
283
|
+
insert_data.profile_source = "custom";
|
|
284
|
+
}
|
|
285
|
+
const inserted_users = await users_service.insert(insert_data);
|
|
286
|
+
if (!Array.isArray(inserted_users) || inserted_users.length === 0) {
|
|
287
|
+
return {
|
|
288
|
+
success: false,
|
|
289
|
+
error: "Failed to create user account",
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
logger.info("oauth_service_facebook_new_user_created", {
|
|
293
|
+
filename: "oauth_service.ts",
|
|
294
|
+
line_number: get_line_number(),
|
|
295
|
+
user_id,
|
|
296
|
+
email,
|
|
297
|
+
});
|
|
298
|
+
return {
|
|
299
|
+
success: true,
|
|
300
|
+
user_id,
|
|
301
|
+
is_new_user: true,
|
|
302
|
+
was_linked: false,
|
|
303
|
+
email: email !== null && email !== void 0 ? email : undefined,
|
|
304
|
+
name,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
const user_friendly_error = sanitize_error_for_user(error, {
|
|
309
|
+
logToConsole: true,
|
|
310
|
+
logToLogger: true,
|
|
311
|
+
logger,
|
|
312
|
+
context: {
|
|
313
|
+
filename: "oauth_service.ts",
|
|
314
|
+
line_number: get_line_number(),
|
|
315
|
+
email: data.email,
|
|
316
|
+
operation: "handle_facebook_oauth_login",
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
return {
|
|
320
|
+
success: false,
|
|
321
|
+
error: user_friendly_error,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
170
325
|
/**
|
|
171
326
|
* Links a Google account to an existing user
|
|
172
327
|
* @param adapter - The hazo_connect adapter instance
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
/**
|
|
3
|
+
* Generates a cryptographically random 6-digit numeric OTP code (000000–999999).
|
|
4
|
+
* Uses crypto.randomInt for uniform distribution.
|
|
5
|
+
*/
|
|
6
|
+
export declare function generate_otp_code(): string;
|
|
7
|
+
export declare function hash_otp_code(code: string): Promise<string>;
|
|
8
|
+
export declare function verify_otp_code(otp_hash: string, code: string): Promise<boolean>;
|
|
9
|
+
export type RequestEmailOTPResult = {
|
|
10
|
+
ok: true;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
error: "rate_limited";
|
|
14
|
+
retry_after_seconds: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Initiates an OTP sign-in flow for the given email address.
|
|
18
|
+
*
|
|
19
|
+
* Behaviour:
|
|
20
|
+
* 1. Per-email rate limit — rejects if too many requests in the sliding window.
|
|
21
|
+
* 2. Per-IP rate limit — rejects if too many requests from this IP.
|
|
22
|
+
* 3. Unknown email + auto_register=false — silent no-op (constant-time padding).
|
|
23
|
+
* 4. Unknown email + auto_register=true — inserts OTP row with user_id=null, dispatches email.
|
|
24
|
+
* 5. Known email — marks prior unconsumed rows consumed, inserts fresh OTP row, dispatches email.
|
|
25
|
+
*
|
|
26
|
+
* Never reveals whether an email address is registered (always returns ok:true on success).
|
|
27
|
+
*/
|
|
28
|
+
export declare function request_email_otp(args: {
|
|
29
|
+
email: string;
|
|
30
|
+
ip: string;
|
|
31
|
+
}): Promise<RequestEmailOTPResult>;
|
|
32
|
+
export type VerifyEmailOTPResult = {
|
|
33
|
+
ok: true;
|
|
34
|
+
user_id: string;
|
|
35
|
+
email: string;
|
|
36
|
+
session_token: string;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
error: "invalid_or_expired";
|
|
40
|
+
};
|
|
41
|
+
export declare function verify_email_otp(args: {
|
|
42
|
+
email: string;
|
|
43
|
+
code: string;
|
|
44
|
+
ip: string;
|
|
45
|
+
}): Promise<VerifyEmailOTPResult>;
|
|
46
|
+
//# sourceMappingURL=otp_service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otp_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/otp_service.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAUrB;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAG1C;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEjE;AAED,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMtF;AAID,MAAM,MAAM,qBAAqB,GAC7B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,cAAc,CAAC;IAAC,mBAAmB,EAAE,MAAM,CAAA;CAAE,CAAC;AAItE;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;CACZ,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA8GjC;AAID,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GACnE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,oBAAoB,CAAA;CAAE,CAAC;AAE/C,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAsHhC"}
|
|
@@ -0,0 +1,238 @@
|
|
|
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
|
+
* Generates a cryptographically random 6-digit numeric OTP code (000000–999999).
|
|
12
|
+
* Uses crypto.randomInt for uniform distribution.
|
|
13
|
+
*/
|
|
14
|
+
export function generate_otp_code() {
|
|
15
|
+
const n = crypto.randomInt(0, 1000000);
|
|
16
|
+
return String(n).padStart(6, "0");
|
|
17
|
+
}
|
|
18
|
+
export async function hash_otp_code(code) {
|
|
19
|
+
return argon2.hash(code);
|
|
20
|
+
}
|
|
21
|
+
export async function verify_otp_code(otp_hash, code) {
|
|
22
|
+
try {
|
|
23
|
+
return await argon2.verify(otp_hash, code);
|
|
24
|
+
}
|
|
25
|
+
catch (_a) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// section: request_email_otp
|
|
30
|
+
/**
|
|
31
|
+
* Initiates an OTP sign-in flow for the given email address.
|
|
32
|
+
*
|
|
33
|
+
* Behaviour:
|
|
34
|
+
* 1. Per-email rate limit — rejects if too many requests in the sliding window.
|
|
35
|
+
* 2. Per-IP rate limit — rejects if too many requests from this IP.
|
|
36
|
+
* 3. Unknown email + auto_register=false — silent no-op (constant-time padding).
|
|
37
|
+
* 4. Unknown email + auto_register=true — inserts OTP row with user_id=null, dispatches email.
|
|
38
|
+
* 5. Known email — marks prior unconsumed rows consumed, inserts fresh OTP row, dispatches email.
|
|
39
|
+
*
|
|
40
|
+
* Never reveals whether an email address is registered (always returns ok:true on success).
|
|
41
|
+
*/
|
|
42
|
+
export async function request_email_otp(args) {
|
|
43
|
+
const logger = create_app_logger();
|
|
44
|
+
const cfg = get_otp_config();
|
|
45
|
+
const email = args.email.trim().toLowerCase();
|
|
46
|
+
const ip = args.ip;
|
|
47
|
+
const adapter = get_hazo_connect_instance();
|
|
48
|
+
const otp_table = createCrudService(adapter, "hazo_email_otps");
|
|
49
|
+
const users_table = createCrudService(adapter, "hazo_users");
|
|
50
|
+
// 1. Per-email rate limit
|
|
51
|
+
const email_window_ms = cfg.email_rate_limit_window_seconds * 1000;
|
|
52
|
+
const email_threshold = new Date(Date.now() - email_window_ms).toISOString();
|
|
53
|
+
const recent_for_email = await otp_table.list((qb) => qb
|
|
54
|
+
.select(["created_at"])
|
|
55
|
+
.where("email", "eq", email)
|
|
56
|
+
.where("created_at", "gte", email_threshold));
|
|
57
|
+
if (recent_for_email.length >= cfg.email_rate_limit_max) {
|
|
58
|
+
const oldest = recent_for_email
|
|
59
|
+
.map((r) => Date.parse(String(r.created_at)))
|
|
60
|
+
.sort((a, b) => a - b)[0];
|
|
61
|
+
const retry_after_seconds = Math.max(1, Math.ceil((oldest + email_window_ms - Date.now()) / 1000));
|
|
62
|
+
logger.warn("otp_request_email_rate_limited", { email, ip, retry_after_seconds });
|
|
63
|
+
return { ok: false, error: "rate_limited", retry_after_seconds };
|
|
64
|
+
}
|
|
65
|
+
// 2. Per-IP rate limit
|
|
66
|
+
const ip_window_ms = cfg.ip_rate_limit_window_seconds * 1000;
|
|
67
|
+
const ip_threshold = new Date(Date.now() - ip_window_ms).toISOString();
|
|
68
|
+
const recent_for_ip = await otp_table.list((qb) => qb
|
|
69
|
+
.select(["created_at"])
|
|
70
|
+
.where("requester_ip", "eq", ip)
|
|
71
|
+
.where("created_at", "gte", ip_threshold));
|
|
72
|
+
if (recent_for_ip.length >= cfg.ip_rate_limit_max) {
|
|
73
|
+
const oldest = recent_for_ip
|
|
74
|
+
.map((r) => Date.parse(String(r.created_at)))
|
|
75
|
+
.sort((a, b) => a - b)[0];
|
|
76
|
+
const retry_after_seconds = Math.max(1, Math.ceil((oldest + ip_window_ms - Date.now()) / 1000));
|
|
77
|
+
logger.warn("otp_request_ip_rate_limited", { email, ip, retry_after_seconds });
|
|
78
|
+
return { ok: false, error: "rate_limited", retry_after_seconds };
|
|
79
|
+
}
|
|
80
|
+
// 3. Lookup user
|
|
81
|
+
const existing_users = await users_table.findBy({ email_address: email });
|
|
82
|
+
const existing_user = existing_users.length > 0 ? existing_users[0] : null;
|
|
83
|
+
// 4. Unknown email + auto_register=false → silent no-op with constant-time padding
|
|
84
|
+
if (!existing_user && !cfg.auto_register) {
|
|
85
|
+
await argon2.hash("000000"); // constant-time padding — never stored
|
|
86
|
+
return { ok: true };
|
|
87
|
+
}
|
|
88
|
+
// 5. Mark any unconsumed rows for this email as superseded
|
|
89
|
+
try {
|
|
90
|
+
const unconsumed = await otp_table.list((qb) => qb
|
|
91
|
+
.select(["id"])
|
|
92
|
+
.where("email", "eq", email)
|
|
93
|
+
.where("consumed_at", "is", null));
|
|
94
|
+
for (const row of unconsumed) {
|
|
95
|
+
await otp_table.updateById(String(row.id), { consumed_at: new Date().toISOString() });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (_a) {
|
|
99
|
+
// IS NULL filter may not be supported in all adapter versions — not critical
|
|
100
|
+
}
|
|
101
|
+
// 6. Generate code + hash + insert row
|
|
102
|
+
const code = generate_otp_code();
|
|
103
|
+
const otp_hash = await hash_otp_code(code);
|
|
104
|
+
const expires_at = new Date(Date.now() + cfg.code_ttl_seconds * 1000).toISOString();
|
|
105
|
+
const row_id = crypto.randomUUID();
|
|
106
|
+
await otp_table.insert({
|
|
107
|
+
id: row_id,
|
|
108
|
+
user_id: existing_user ? String(existing_user.id) : null,
|
|
109
|
+
email,
|
|
110
|
+
otp_hash,
|
|
111
|
+
expires_at,
|
|
112
|
+
attempt_count: 0,
|
|
113
|
+
requester_ip: ip,
|
|
114
|
+
});
|
|
115
|
+
// 7. Dispatch email — fire-and-forget; errors are logged but do not surface to caller
|
|
116
|
+
try {
|
|
117
|
+
await send_template_email("otp_signin_code", email, {
|
|
118
|
+
otp_code: code,
|
|
119
|
+
expires_in_minutes: String(Math.round(cfg.code_ttl_seconds / 60)),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
logger.error("otp_request_email_dispatch_failed", {
|
|
124
|
+
email,
|
|
125
|
+
ip,
|
|
126
|
+
error: err instanceof Error ? err.message : String(err),
|
|
127
|
+
});
|
|
128
|
+
// Return ok:true to preserve no-enumeration property — caller cannot distinguish
|
|
129
|
+
// a missing user from a delivery failure.
|
|
130
|
+
}
|
|
131
|
+
return { ok: true };
|
|
132
|
+
}
|
|
133
|
+
export async function verify_email_otp(args) {
|
|
134
|
+
const logger = create_app_logger();
|
|
135
|
+
const cfg = get_otp_config();
|
|
136
|
+
const email = args.email.trim().toLowerCase();
|
|
137
|
+
const code = args.code.trim();
|
|
138
|
+
const adapter = get_hazo_connect_instance();
|
|
139
|
+
const otp_table = createCrudService(adapter, "hazo_email_otps");
|
|
140
|
+
const users_table = createCrudService(adapter, "hazo_users");
|
|
141
|
+
const user_scopes_table = createCrudService(adapter, "hazo_user_scopes");
|
|
142
|
+
const roles_table = createCrudService(adapter, "hazo_roles");
|
|
143
|
+
// 1. Find most-recent unconsumed row for this email
|
|
144
|
+
const now_iso = new Date().toISOString();
|
|
145
|
+
const candidates = await otp_table.list((qb) => qb
|
|
146
|
+
.select(["id", "user_id", "otp_hash", "expires_at", "attempt_count"])
|
|
147
|
+
.where("email", "eq", email)
|
|
148
|
+
.where("consumed_at", "is", null)
|
|
149
|
+
.order("created_at", "desc")
|
|
150
|
+
.limit(1));
|
|
151
|
+
const row = candidates.length > 0 ? candidates[0] : null;
|
|
152
|
+
if (!row) {
|
|
153
|
+
return { ok: false, error: "invalid_or_expired" };
|
|
154
|
+
}
|
|
155
|
+
// 2. Check expiry
|
|
156
|
+
const expires_at_ms = Date.parse(String(row.expires_at));
|
|
157
|
+
if (Number.isNaN(expires_at_ms) || expires_at_ms < Date.now()) {
|
|
158
|
+
return { ok: false, error: "invalid_or_expired" };
|
|
159
|
+
}
|
|
160
|
+
// 3. argon2 verify
|
|
161
|
+
const is_valid = await verify_otp_code(String(row.otp_hash), code);
|
|
162
|
+
if (!is_valid) {
|
|
163
|
+
const new_attempt_count = Number(row.attempt_count) + 1;
|
|
164
|
+
const updates = { attempt_count: new_attempt_count };
|
|
165
|
+
if (new_attempt_count >= cfg.max_verify_attempts) {
|
|
166
|
+
updates.consumed_at = now_iso; // poison
|
|
167
|
+
}
|
|
168
|
+
await otp_table.updateById(String(row.id), updates);
|
|
169
|
+
logger.info("otp_verify_invalid_code", {
|
|
170
|
+
email,
|
|
171
|
+
attempt_count: new_attempt_count,
|
|
172
|
+
poisoned: new_attempt_count >= cfg.max_verify_attempts,
|
|
173
|
+
});
|
|
174
|
+
return { ok: false, error: "invalid_or_expired" };
|
|
175
|
+
}
|
|
176
|
+
// 4. Mark consumed
|
|
177
|
+
await otp_table.updateById(String(row.id), { consumed_at: now_iso });
|
|
178
|
+
// 5. Resolve / create user
|
|
179
|
+
let user_id = row.user_id ? String(row.user_id) : null;
|
|
180
|
+
if (user_id) {
|
|
181
|
+
// Ensure email_verified=true
|
|
182
|
+
const user = await users_table.findById(user_id);
|
|
183
|
+
if (user && !user.email_verified) {
|
|
184
|
+
await users_table.updateById(user_id, { email_verified: true });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else if (cfg.auto_register) {
|
|
188
|
+
// Create user + bind scope/role
|
|
189
|
+
const new_user_id = crypto.randomUUID();
|
|
190
|
+
await users_table.insert({
|
|
191
|
+
id: new_user_id,
|
|
192
|
+
email_address: email,
|
|
193
|
+
email_verified: true,
|
|
194
|
+
password_hash: null,
|
|
195
|
+
name: null,
|
|
196
|
+
});
|
|
197
|
+
// Resolve role_id from name — try scope-specific first, then any
|
|
198
|
+
const scoped_roles = await roles_table.findBy({
|
|
199
|
+
name: cfg.auto_assign_role_name,
|
|
200
|
+
scope_id: cfg.auto_assign_scope_id,
|
|
201
|
+
});
|
|
202
|
+
let role_id = null;
|
|
203
|
+
if (scoped_roles.length > 0) {
|
|
204
|
+
role_id = String(scoped_roles[0].id);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const any_roles = await roles_table.findBy({ name: cfg.auto_assign_role_name });
|
|
208
|
+
if (any_roles.length > 0) {
|
|
209
|
+
role_id = String(any_roles[0].id);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!role_id) {
|
|
213
|
+
logger.error("otp_verify_auto_register_role_not_found", {
|
|
214
|
+
email,
|
|
215
|
+
scope_id: cfg.auto_assign_scope_id,
|
|
216
|
+
role_name: cfg.auto_assign_role_name,
|
|
217
|
+
});
|
|
218
|
+
return { ok: false, error: "invalid_or_expired" };
|
|
219
|
+
}
|
|
220
|
+
await user_scopes_table.insert({
|
|
221
|
+
user_id: new_user_id,
|
|
222
|
+
scope_id: cfg.auto_assign_scope_id,
|
|
223
|
+
root_scope_id: cfg.auto_assign_scope_id,
|
|
224
|
+
role_id,
|
|
225
|
+
});
|
|
226
|
+
user_id = new_user_id;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// Row exists but no user and auto_register=false — stale edge case
|
|
230
|
+
logger.warn("otp_verify_no_user_resolvable", { email });
|
|
231
|
+
return { ok: false, error: "invalid_or_expired" };
|
|
232
|
+
}
|
|
233
|
+
// 6. Issue session JWT with OTP TTL
|
|
234
|
+
const ttl_seconds = hazo_auth_otp_session_ttl_seconds();
|
|
235
|
+
const session_token = await create_session_token(user_id, email, undefined, ttl_seconds);
|
|
236
|
+
logger.info("otp_verify_ok", { email, user_id, ttl_seconds });
|
|
237
|
+
return { ok: true, user_id, email, session_token };
|
|
238
|
+
}
|
|
@@ -16,9 +16,11 @@ export type ValidateSessionTokenResult = {
|
|
|
16
16
|
* Token includes user_id, email, issued at time, and expiration
|
|
17
17
|
* @param user_id - User ID
|
|
18
18
|
* @param email - User email address
|
|
19
|
+
* @param managed_by_user_id - Optional: ID of the managing user (for impersonation)
|
|
20
|
+
* @param ttl_seconds - Optional: token lifetime in seconds (default: 30 days). Use 604800 for 7-day OTP sessions.
|
|
19
21
|
* @returns JWT token string
|
|
20
22
|
*/
|
|
21
|
-
export declare function create_session_token(user_id: string, email: string, managed_by_user_id?: string): Promise<string>;
|
|
23
|
+
export declare function create_session_token(user_id: string, email: string, managed_by_user_id?: string, ttl_seconds?: number): Promise<string>;
|
|
22
24
|
/**
|
|
23
25
|
* Validates a JWT session token
|
|
24
26
|
* Checks signature and expiration
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session_token_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/session_token_service.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAuCF
|
|
1
|
+
{"version":3,"file":"session_token_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/session_token_service.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAuCF;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,kBAAkB,CAAC,EAAE,MAAM,EAC3B,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CA4CjB;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,0BAA0B,CAAC,CAkDrC"}
|
|
@@ -41,14 +41,16 @@ function get_session_token_expiry_seconds() {
|
|
|
41
41
|
* Token includes user_id, email, issued at time, and expiration
|
|
42
42
|
* @param user_id - User ID
|
|
43
43
|
* @param email - User email address
|
|
44
|
+
* @param managed_by_user_id - Optional: ID of the managing user (for impersonation)
|
|
45
|
+
* @param ttl_seconds - Optional: token lifetime in seconds (default: 30 days). Use 604800 for 7-day OTP sessions.
|
|
44
46
|
* @returns JWT token string
|
|
45
47
|
*/
|
|
46
|
-
export async function create_session_token(user_id, email, managed_by_user_id) {
|
|
48
|
+
export async function create_session_token(user_id, email, managed_by_user_id, ttl_seconds) {
|
|
47
49
|
const logger = create_app_logger();
|
|
48
50
|
try {
|
|
49
51
|
const secret = get_jwt_secret();
|
|
50
52
|
const now = Math.floor(Date.now() / 1000); // Current time in seconds
|
|
51
|
-
const expiry_seconds = get_session_token_expiry_seconds();
|
|
53
|
+
const expiry_seconds = ttl_seconds !== null && ttl_seconds !== void 0 ? ttl_seconds : get_session_token_expiry_seconds();
|
|
52
54
|
const exp = now + expiry_seconds;
|
|
53
55
|
const payload = { user_id, email };
|
|
54
56
|
if (managed_by_user_id) {
|
|
@@ -2,7 +2,19 @@ import "server-only";
|
|
|
2
2
|
export type CreateFirmPageProps = {
|
|
3
3
|
/** Disable the navbar wrapper */
|
|
4
4
|
disableNavbar?: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Optional theme that controls visual appearance and layout mode.
|
|
7
|
+
* When `theme.layout` is `"split"`, activates the two-column split layout
|
|
8
|
+
* with the brand panel on the left.
|
|
9
|
+
*/
|
|
10
|
+
theme?: import("../theme/theme_types").HazoAuthTheme;
|
|
11
|
+
/** Override the page heading. Falls back to HazoAuthStringsProvider → DEFAULT_STRINGS. */
|
|
12
|
+
title?: string;
|
|
13
|
+
/** Override the page subtitle. Falls back to HazoAuthStringsProvider → DEFAULT_STRINGS. */
|
|
14
|
+
subtitle?: string;
|
|
15
|
+
/** Override the submit button label. Falls back to HazoAuthStringsProvider → DEFAULT_STRINGS. */
|
|
16
|
+
ctaText?: string;
|
|
5
17
|
};
|
|
6
|
-
export default function CreateFirmPage({ disableNavbar }?: CreateFirmPageProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export default function CreateFirmPage({ disableNavbar, theme, title, subtitle, ctaText }?: CreateFirmPageProps): import("react/jsx-runtime").JSX.Element;
|
|
7
19
|
export { CreateFirmPage };
|
|
8
20
|
//# sourceMappingURL=create_firm.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create_firm.d.ts","sourceRoot":"","sources":["../../src/page_components/create_firm.tsx"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"create_firm.d.ts","sourceRoot":"","sources":["../../src/page_components/create_firm.tsx"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AASrB,MAAM,MAAM,mBAAmB,GAAG;IAChC,iCAAiC;IACjC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,sBAAsB,EAAE,aAAa,CAAC;IACrD,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iGAAiG;IACjG,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAkCF,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAE,mBAAwB,2CA6BlH;AAGD,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
|
@@ -6,24 +6,28 @@ import "server-only";
|
|
|
6
6
|
import CreateFirmLayout from "../components/layouts/create_firm/index.js";
|
|
7
7
|
import { AuthPageShell } from "../components/layouts/shared/components/auth_page_shell.js";
|
|
8
8
|
import { get_config_value } from "../lib/config/config_loader.server.js";
|
|
9
|
+
import { DEFAULT_STRINGS, readStrings } from "../strings.js";
|
|
9
10
|
// section: config_loader
|
|
10
11
|
function get_create_firm_config() {
|
|
11
12
|
return {
|
|
12
|
-
image_src: get_config_value("hazo_auth__create_firm", "image_src", "/hazo_auth/images/new_firm_default.jpg"),
|
|
13
|
-
heading: get_config_value("hazo_auth__create_firm", "heading", "Create Your Firm"),
|
|
14
|
-
sub_heading: get_config_value("hazo_auth__create_firm", "sub_heading", "Set up your organisation to get started"),
|
|
15
13
|
firm_name_label: get_config_value("hazo_auth__create_firm", "firm_name_label", "Firm Name"),
|
|
16
14
|
org_structure_label: get_config_value("hazo_auth__create_firm", "org_structure_label", "Organisation Structure"),
|
|
17
15
|
org_structure_default: get_config_value("hazo_auth__create_firm", "org_structure_default", "Headquarters"),
|
|
18
|
-
submit_button_label: get_config_value("hazo_auth__create_firm", "submit_button_label", "Create Firm"),
|
|
19
16
|
success_message: get_config_value("hazo_auth__create_firm", "success_message", "Your firm has been created successfully!"),
|
|
20
17
|
redirect_route: get_config_value("hazo_auth__create_firm", "redirect_route", "/"),
|
|
21
18
|
};
|
|
22
19
|
}
|
|
23
20
|
// section: component
|
|
24
|
-
export default function CreateFirmPage({ disableNavbar } = {}) {
|
|
21
|
+
export default function CreateFirmPage({ disableNavbar, theme, title, subtitle, ctaText } = {}) {
|
|
22
|
+
var _a, _b, _c;
|
|
23
|
+
// Resolve strings: prop > HazoAuthStringsProvider > DEFAULT_STRINGS
|
|
24
|
+
const strings = readStrings();
|
|
25
|
+
const cf_strings = strings.create_firm;
|
|
26
|
+
const resolved_title = (_a = title !== null && title !== void 0 ? title : cf_strings.title) !== null && _a !== void 0 ? _a : DEFAULT_STRINGS.create_firm.title;
|
|
27
|
+
const resolved_subtitle = (_b = subtitle !== null && subtitle !== void 0 ? subtitle : cf_strings.subtitle) !== null && _b !== void 0 ? _b : DEFAULT_STRINGS.create_firm.subtitle;
|
|
28
|
+
const resolved_cta = (_c = ctaText !== null && ctaText !== void 0 ? ctaText : cf_strings.ctaText) !== null && _c !== void 0 ? _c : DEFAULT_STRINGS.create_firm.ctaText;
|
|
25
29
|
const config = get_create_firm_config();
|
|
26
|
-
const layoutContent = (_jsx(CreateFirmLayout, {
|
|
30
|
+
const layoutContent = (_jsx(CreateFirmLayout, { heading: resolved_title, sub_heading: resolved_subtitle, firm_name_label: config.firm_name_label, org_structure_label: config.org_structure_label, default_org_structure: config.org_structure_default, submit_button_label: resolved_cta, success_message: config.success_message, redirect_route: config.redirect_route, theme: theme }));
|
|
27
31
|
if (disableNavbar) {
|
|
28
32
|
return layoutContent;
|
|
29
33
|
}
|
|
@@ -4,9 +4,6 @@ export type ForgotPasswordPageProps = {
|
|
|
4
4
|
showReturnHomeButton?: boolean;
|
|
5
5
|
returnHomeButtonLabel?: string;
|
|
6
6
|
returnHomePath?: string;
|
|
7
|
-
imageSrc?: string;
|
|
8
|
-
imageAlt?: string;
|
|
9
|
-
imageBackgroundColor?: string;
|
|
10
7
|
};
|
|
11
8
|
/**
|
|
12
9
|
* Zero-config forgot password page component
|
|
@@ -14,6 +11,6 @@ export type ForgotPasswordPageProps = {
|
|
|
14
11
|
* @param props - Optional configuration overrides
|
|
15
12
|
* @returns Forgot password page component
|
|
16
13
|
*/
|
|
17
|
-
export declare function ForgotPasswordPage({ alreadyLoggedInMessage, showLogoutButton, showReturnHomeButton, returnHomeButtonLabel, returnHomePath,
|
|
14
|
+
export declare function ForgotPasswordPage({ alreadyLoggedInMessage, showLogoutButton, showReturnHomeButton, returnHomeButtonLabel, returnHomePath, }?: ForgotPasswordPageProps): import("react/jsx-runtime").JSX.Element;
|
|
18
15
|
export default ForgotPasswordPage;
|
|
19
16
|
//# sourceMappingURL=forgot_password.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forgot_password.d.ts","sourceRoot":"","sources":["../../src/page_components/forgot_password.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"forgot_password.d.ts","sourceRoot":"","sources":["../../src/page_components/forgot_password.tsx"],"names":[],"mappings":"AA0BA,MAAM,MAAM,uBAAuB,GAAG;IACpC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAGF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,sBAAoD,EACpD,gBAAuB,EACvB,oBAA4B,EAC5B,qBAAqC,EACrC,cAAoB,GACrB,GAAE,uBAA4B,2CAgC9B;AAED,eAAe,kBAAkB,CAAC"}
|
|
@@ -21,10 +21,6 @@ import { useEffect, useState } from "react";
|
|
|
21
21
|
import forgot_password_layout from "../components/layouts/forgot_password/index.js";
|
|
22
22
|
import { createLayoutDataClient } from "../components/layouts/shared/data/layout_data_client.js";
|
|
23
23
|
import { create_sqlite_hazo_connect } from "../lib/hazo_connect_setup.js";
|
|
24
|
-
// section: constants
|
|
25
|
-
const DEFAULT_IMAGE_SRC = "/hazo_auth/images/forgot_password_default.jpg";
|
|
26
|
-
const DEFAULT_IMAGE_ALT = "Illustration of a globe representing secure authentication workflows";
|
|
27
|
-
const DEFAULT_IMAGE_BG = "#f1f5f9";
|
|
28
24
|
// section: component
|
|
29
25
|
/**
|
|
30
26
|
* Zero-config forgot password page component
|
|
@@ -32,7 +28,7 @@ const DEFAULT_IMAGE_BG = "#f1f5f9";
|
|
|
32
28
|
* @param props - Optional configuration overrides
|
|
33
29
|
* @returns Forgot password page component
|
|
34
30
|
*/
|
|
35
|
-
export function ForgotPasswordPage({ alreadyLoggedInMessage = "You are already logged in", showLogoutButton = true, showReturnHomeButton = false, returnHomeButtonLabel = "Return home", returnHomePath = "/",
|
|
31
|
+
export function ForgotPasswordPage({ alreadyLoggedInMessage = "You are already logged in", showLogoutButton = true, showReturnHomeButton = false, returnHomeButtonLabel = "Return home", returnHomePath = "/", } = {}) {
|
|
36
32
|
const [dataClient, setDataClient] = useState(null);
|
|
37
33
|
useEffect(() => {
|
|
38
34
|
// Initialize hazo_connect on client side
|
|
@@ -45,6 +41,6 @@ export function ForgotPasswordPage({ alreadyLoggedInMessage = "You are already l
|
|
|
45
41
|
return (_jsx("div", { className: "cls_forgot_password_page_loading flex items-center justify-center min-h-screen", children: _jsx("div", { className: "text-slate-600 animate-pulse", children: "Loading..." }) }));
|
|
46
42
|
}
|
|
47
43
|
const ForgotPasswordLayout = forgot_password_layout;
|
|
48
|
-
return (_jsx(ForgotPasswordLayout, {
|
|
44
|
+
return (_jsx(ForgotPasswordLayout, { data_client: dataClient, alreadyLoggedInMessage: alreadyLoggedInMessage, showLogoutButton: showLogoutButton, showReturnHomeButton: showReturnHomeButton, returnHomeButtonLabel: returnHomeButtonLabel, returnHomePath: returnHomePath }));
|
|
49
45
|
}
|
|
50
46
|
export default ForgotPasswordPage;
|
|
@@ -15,9 +15,6 @@ export type LoginPageProps = {
|
|
|
15
15
|
createAccountPath?: string;
|
|
16
16
|
createAccountLabel?: string;
|
|
17
17
|
urlOnLogon?: string;
|
|
18
|
-
imageSrc?: string;
|
|
19
|
-
imageAlt?: string;
|
|
20
|
-
imageBackgroundColor?: string;
|
|
21
18
|
};
|
|
22
19
|
/**
|
|
23
20
|
* Zero-config login page component
|
|
@@ -25,6 +22,6 @@ export type LoginPageProps = {
|
|
|
25
22
|
* @param props - Optional configuration overrides
|
|
26
23
|
* @returns Login page component
|
|
27
24
|
*/
|
|
28
|
-
export declare function LoginPage({ redirectRoute, successMessage, alreadyLoggedInMessage, showLogoutButton, showReturnHomeButton, returnHomeButtonLabel, returnHomePath, forgotPasswordPath, forgotPasswordLabel, createAccountPath, createAccountLabel, urlOnLogon,
|
|
25
|
+
export declare function LoginPage({ redirectRoute, successMessage, alreadyLoggedInMessage, showLogoutButton, showReturnHomeButton, returnHomeButtonLabel, returnHomePath, forgotPasswordPath, forgotPasswordLabel, createAccountPath, createAccountLabel, urlOnLogon, }?: LoginPageProps): import("react/jsx-runtime").JSX.Element;
|
|
29
26
|
export default LoginPage;
|
|
30
27
|
//# sourceMappingURL=login.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/page_components/login.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/page_components/login.tsx"],"names":[],"mappings":"AA0BA;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAGF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EACxB,aAAa,EACb,cAAyC,EACzC,sBAAoD,EACpD,gBAAuB,EACvB,oBAA4B,EAC5B,qBAAqC,EACrC,cAAoB,EACpB,kBAAiD,EACjD,mBAAwC,EACxC,iBAAyC,EACzC,kBAAqC,EACrC,UAAU,GACX,GAAE,cAAmB,2CAuCrB;AAED,eAAe,SAAS,CAAC"}
|