hazo_auth 4.1.0 → 4.3.0
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 +230 -0
- package/SETUP_CHECKLIST.md +202 -0
- package/bin/hazo_auth.mjs +35 -0
- package/cli-src/assets/images/forgot_password_default.jpg +0 -0
- package/cli-src/assets/images/login_default.jpg +0 -0
- package/cli-src/assets/images/register_default.jpg +0 -0
- package/cli-src/assets/images/reset_password_default.jpg +0 -0
- package/cli-src/assets/images/verify_email_default.jpg +0 -0
- package/cli-src/cli/generate.ts +276 -0
- package/cli-src/cli/index.ts +207 -0
- package/cli-src/cli/init.ts +254 -0
- package/cli-src/cli/init_users.ts +376 -0
- package/cli-src/cli/validate.ts +581 -0
- package/cli-src/lib/already_logged_in_config.server.ts +46 -0
- package/cli-src/lib/app_logger.ts +24 -0
- package/cli-src/lib/auth/auth_cache.ts +220 -0
- package/cli-src/lib/auth/auth_rate_limiter.ts +121 -0
- package/cli-src/lib/auth/auth_types.ts +110 -0
- package/cli-src/lib/auth/auth_utils.server.ts +196 -0
- package/cli-src/lib/auth/hazo_get_auth.server.ts +512 -0
- package/cli-src/lib/auth/index.ts +23 -0
- package/cli-src/lib/auth/nextauth_config.ts +227 -0
- package/cli-src/lib/auth/scope_cache.ts +233 -0
- package/cli-src/lib/auth/server_auth.ts +88 -0
- package/cli-src/lib/auth/session_token_validator.edge.ts +91 -0
- package/cli-src/lib/auth_utility_config.server.ts +136 -0
- package/cli-src/lib/config/config_loader.server.ts +164 -0
- package/cli-src/lib/config/default_config.ts +199 -0
- package/cli-src/lib/email_verification_config.server.ts +63 -0
- package/cli-src/lib/file_types_config.server.ts +25 -0
- package/cli-src/lib/forgot_password_config.server.ts +63 -0
- package/cli-src/lib/hazo_connect_instance.server.ts +101 -0
- package/cli-src/lib/hazo_connect_setup.server.ts +194 -0
- package/cli-src/lib/hazo_connect_setup.ts +54 -0
- package/cli-src/lib/index.ts +46 -0
- package/cli-src/lib/login_config.server.ts +106 -0
- package/cli-src/lib/messages_config.server.ts +45 -0
- package/cli-src/lib/migrations/apply_migration.ts +105 -0
- package/cli-src/lib/my_settings_config.server.ts +135 -0
- package/cli-src/lib/oauth_config.server.ts +87 -0
- package/cli-src/lib/password_requirements_config.server.ts +40 -0
- package/cli-src/lib/profile_pic_menu_config.server.ts +138 -0
- package/cli-src/lib/profile_picture_config.server.ts +56 -0
- package/cli-src/lib/register_config.server.ts +101 -0
- package/cli-src/lib/reset_password_config.server.ts +103 -0
- package/cli-src/lib/scope_hierarchy_config.server.ts +151 -0
- package/cli-src/lib/services/email_service.ts +587 -0
- package/cli-src/lib/services/email_verification_service.ts +270 -0
- package/cli-src/lib/services/index.ts +16 -0
- package/cli-src/lib/services/login_service.ts +150 -0
- package/cli-src/lib/services/oauth_service.ts +494 -0
- package/cli-src/lib/services/password_change_service.ts +154 -0
- package/cli-src/lib/services/password_reset_service.ts +418 -0
- package/cli-src/lib/services/profile_picture_remove_service.ts +120 -0
- package/cli-src/lib/services/profile_picture_service.ts +451 -0
- package/cli-src/lib/services/profile_picture_source_mapper.ts +62 -0
- package/cli-src/lib/services/registration_service.ts +185 -0
- package/cli-src/lib/services/scope_labels_service.ts +348 -0
- package/cli-src/lib/services/scope_service.ts +778 -0
- package/cli-src/lib/services/session_token_service.ts +177 -0
- package/cli-src/lib/services/token_service.ts +240 -0
- package/cli-src/lib/services/user_profiles_cache.ts +189 -0
- package/cli-src/lib/services/user_profiles_service.ts +264 -0
- package/cli-src/lib/services/user_scope_service.ts +554 -0
- package/cli-src/lib/services/user_update_service.ts +141 -0
- package/cli-src/lib/ui_shell_config.server.ts +73 -0
- package/cli-src/lib/ui_sizes_config.server.ts +37 -0
- package/cli-src/lib/user_fields_config.server.ts +31 -0
- package/cli-src/lib/user_management_config.server.ts +39 -0
- package/cli-src/lib/user_profiles_config.server.ts +55 -0
- package/cli-src/lib/utils/api_route_helpers.ts +60 -0
- package/cli-src/lib/utils/error_sanitizer.ts +75 -0
- package/cli-src/lib/utils/password_validator.ts +65 -0
- package/cli-src/lib/utils.ts +11 -0
- package/cli-src/server/logging/logger_service.ts +56 -0
- package/dist/app/api/hazo_auth/forgot_password/route.d.ts.map +1 -1
- package/dist/app/api/hazo_auth/forgot_password/route.js +15 -0
- package/dist/app/api/hazo_auth/logout/route.d.ts.map +1 -1
- package/dist/app/api/hazo_auth/logout/route.js +31 -0
- package/dist/app/api/hazo_auth/me/route.d.ts.map +1 -1
- package/dist/app/api/hazo_auth/me/route.js +10 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/init_users.d.ts +17 -0
- package/dist/cli/init_users.d.ts.map +1 -0
- package/dist/cli/init_users.js +307 -0
- package/dist/components/layouts/forgot_password/hooks/use_forgot_password_form.d.ts +2 -0
- package/dist/components/layouts/forgot_password/hooks/use_forgot_password_form.d.ts.map +1 -1
- package/dist/components/layouts/forgot_password/hooks/use_forgot_password_form.js +8 -0
- package/dist/components/layouts/forgot_password/index.d.ts +7 -1
- package/dist/components/layouts/forgot_password/index.d.ts.map +1 -1
- package/dist/components/layouts/forgot_password/index.js +7 -2
- package/dist/components/layouts/login/index.d.ts +13 -1
- package/dist/components/layouts/login/index.d.ts.map +1 -1
- package/dist/components/layouts/login/index.js +11 -2
- package/dist/components/layouts/my_settings/components/connected_accounts_section.d.ts +17 -0
- package/dist/components/layouts/my_settings/components/connected_accounts_section.d.ts.map +1 -0
- package/dist/components/layouts/my_settings/components/connected_accounts_section.js +17 -0
- package/dist/components/layouts/my_settings/components/set_password_section.d.ts +26 -0
- package/dist/components/layouts/my_settings/components/set_password_section.d.ts.map +1 -0
- package/dist/components/layouts/my_settings/components/set_password_section.js +127 -0
- package/dist/components/layouts/my_settings/hooks/use_my_settings.d.ts +3 -0
- package/dist/components/layouts/my_settings/hooks/use_my_settings.d.ts.map +1 -1
- package/dist/components/layouts/my_settings/hooks/use_my_settings.js +9 -0
- package/dist/components/layouts/my_settings/index.d.ts.map +1 -1
- package/dist/components/layouts/my_settings/index.js +4 -2
- package/dist/components/layouts/shared/components/google_icon.d.ts +12 -0
- package/dist/components/layouts/shared/components/google_icon.d.ts.map +1 -0
- package/dist/components/layouts/shared/components/google_icon.js +9 -0
- package/dist/components/layouts/shared/components/google_sign_in_button.d.ts +21 -0
- package/dist/components/layouts/shared/components/google_sign_in_button.d.ts.map +1 -0
- package/dist/components/layouts/shared/components/google_sign_in_button.js +50 -0
- package/dist/components/layouts/shared/components/oauth_divider.d.ts +13 -0
- package/dist/components/layouts/shared/components/oauth_divider.d.ts.map +1 -0
- package/dist/components/layouts/shared/components/oauth_divider.js +13 -0
- package/dist/components/layouts/shared/config/layout_customization.d.ts +2 -7
- package/dist/components/layouts/shared/config/layout_customization.d.ts.map +1 -1
- package/dist/components/layouts/shared/hooks/use_auth_status.d.ts +3 -0
- package/dist/components/layouts/shared/hooks/use_auth_status.d.ts.map +1 -1
- package/dist/components/layouts/shared/hooks/use_auth_status.js +4 -0
- package/dist/components/layouts/shared/index.d.ts +5 -0
- package/dist/components/layouts/shared/index.d.ts.map +1 -1
- package/dist/components/layouts/shared/index.js +3 -0
- package/dist/components/ui/button.d.ts +1 -1
- package/dist/lib/auth/nextauth_config.d.ts +34 -0
- package/dist/lib/auth/nextauth_config.d.ts.map +1 -0
- package/dist/lib/auth/nextauth_config.js +171 -0
- package/dist/lib/config/default_config.d.ts +24 -0
- package/dist/lib/config/default_config.d.ts.map +1 -1
- package/dist/lib/config/default_config.js +14 -0
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -0
- package/dist/lib/login_config.server.d.ts +3 -0
- package/dist/lib/login_config.server.d.ts.map +1 -1
- package/dist/lib/login_config.server.js +4 -0
- package/dist/lib/oauth_config.server.d.ts +29 -0
- package/dist/lib/oauth_config.server.d.ts.map +1 -0
- package/dist/lib/oauth_config.server.js +40 -0
- package/dist/lib/services/login_service.d.ts.map +1 -1
- package/dist/lib/services/login_service.js +16 -1
- package/dist/lib/services/oauth_service.d.ts +88 -0
- package/dist/lib/services/oauth_service.d.ts.map +1 -0
- package/dist/lib/services/oauth_service.js +376 -0
- package/dist/lib/services/password_reset_service.d.ts +2 -0
- package/dist/lib/services/password_reset_service.d.ts.map +1 -1
- package/dist/lib/services/password_reset_service.js +10 -0
- package/dist/lib/services/registration_service.d.ts.map +1 -1
- package/dist/lib/services/registration_service.js +1 -0
- package/dist/lib/utils/password_validator.d.ts +19 -0
- package/dist/lib/utils/password_validator.d.ts.map +1 -0
- package/dist/lib/utils/password_validator.js +36 -0
- package/dist/server_pages/login.d.ts.map +1 -1
- package/dist/server_pages/login.js +6 -1
- package/dist/server_pages/login_client_wrapper.d.ts +5 -2
- package/dist/server_pages/login_client_wrapper.d.ts.map +1 -1
- package/dist/server_pages/login_client_wrapper.js +2 -2
- package/package.json +6 -2
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
// file_description: service for OAuth authentication operations using hazo_connect
|
|
2
|
+
// section: imports
|
|
3
|
+
import type { HazoConnectAdapter } from "hazo_connect";
|
|
4
|
+
import { createCrudService } from "hazo_connect/server";
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import { create_app_logger } from "../app_logger";
|
|
7
|
+
import { sanitize_error_for_user } from "../utils/error_sanitizer";
|
|
8
|
+
import { get_line_number } from "../utils/api_route_helpers";
|
|
9
|
+
import { get_oauth_config } from "../oauth_config.server";
|
|
10
|
+
|
|
11
|
+
// section: types
|
|
12
|
+
export type GoogleOAuthData = {
|
|
13
|
+
/** Google's unique user ID (sub claim from JWT) */
|
|
14
|
+
google_id: string;
|
|
15
|
+
/** User's email address from Google */
|
|
16
|
+
email: string;
|
|
17
|
+
/** User's full name from Google profile */
|
|
18
|
+
name?: string;
|
|
19
|
+
/** User's profile picture URL from Google */
|
|
20
|
+
profile_picture_url?: string;
|
|
21
|
+
/** Whether Google has verified this email */
|
|
22
|
+
email_verified: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type OAuthLoginResult = {
|
|
26
|
+
success: boolean;
|
|
27
|
+
user_id?: string;
|
|
28
|
+
/** True if this was a newly created account */
|
|
29
|
+
is_new_user?: boolean;
|
|
30
|
+
/** True if Google was linked to an existing account */
|
|
31
|
+
was_linked?: boolean;
|
|
32
|
+
/** The user's email address */
|
|
33
|
+
email?: string;
|
|
34
|
+
/** The user's name */
|
|
35
|
+
name?: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type LinkGoogleResult = {
|
|
40
|
+
success: boolean;
|
|
41
|
+
error?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type AuthProvidersResult = {
|
|
45
|
+
success: boolean;
|
|
46
|
+
auth_providers?: string[];
|
|
47
|
+
has_password?: boolean;
|
|
48
|
+
error?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// section: helpers
|
|
52
|
+
/**
|
|
53
|
+
* Handles Google OAuth login/registration flow
|
|
54
|
+
* 1. Check if user exists with google_id -> login
|
|
55
|
+
* 2. Check if user exists with email -> link Google account
|
|
56
|
+
* 3. Create new user with Google data
|
|
57
|
+
*
|
|
58
|
+
* @param adapter - The hazo_connect adapter instance
|
|
59
|
+
* @param data - Google OAuth user data
|
|
60
|
+
* @returns OAuth login result with user_id and status flags
|
|
61
|
+
*/
|
|
62
|
+
export async function handle_google_oauth_login(
|
|
63
|
+
adapter: HazoConnectAdapter,
|
|
64
|
+
data: GoogleOAuthData
|
|
65
|
+
): Promise<OAuthLoginResult> {
|
|
66
|
+
const logger = create_app_logger();
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const { google_id, email, name, profile_picture_url, email_verified } = data;
|
|
70
|
+
const oauth_config = get_oauth_config();
|
|
71
|
+
|
|
72
|
+
const users_service = createCrudService(adapter, "hazo_users");
|
|
73
|
+
const now = new Date().toISOString();
|
|
74
|
+
|
|
75
|
+
// Step 1: Check if user exists with this google_id
|
|
76
|
+
const users_by_google_id = await users_service.findBy({ google_id });
|
|
77
|
+
|
|
78
|
+
if (Array.isArray(users_by_google_id) && users_by_google_id.length > 0) {
|
|
79
|
+
const user = users_by_google_id[0];
|
|
80
|
+
|
|
81
|
+
// Update last_logon timestamp
|
|
82
|
+
await users_service.updateById(user.id, {
|
|
83
|
+
last_logon: now,
|
|
84
|
+
changed_at: now,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
logger.info("oauth_service_google_login_existing_google_user", {
|
|
88
|
+
filename: "oauth_service.ts",
|
|
89
|
+
line_number: get_line_number(),
|
|
90
|
+
user_id: user.id,
|
|
91
|
+
email: user.email_address,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
success: true,
|
|
96
|
+
user_id: user.id as string,
|
|
97
|
+
is_new_user: false,
|
|
98
|
+
was_linked: false,
|
|
99
|
+
email: user.email_address as string,
|
|
100
|
+
name: user.name as string | undefined,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Step 2: Check if user exists with this email
|
|
105
|
+
const users_by_email = await users_service.findBy({ email_address: email });
|
|
106
|
+
|
|
107
|
+
if (Array.isArray(users_by_email) && users_by_email.length > 0) {
|
|
108
|
+
const user = users_by_email[0];
|
|
109
|
+
const user_email_verified = user.email_verified as boolean;
|
|
110
|
+
|
|
111
|
+
// Check if auto-linking is enabled for unverified accounts
|
|
112
|
+
if (!user_email_verified && !oauth_config.auto_link_unverified_accounts) {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
error: "An account with this email exists but is not verified. Please verify your email first.",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Link Google account to existing user
|
|
120
|
+
const current_auth_providers = (user.auth_providers as string) || "email";
|
|
121
|
+
const new_auth_providers = current_auth_providers.includes("google")
|
|
122
|
+
? current_auth_providers
|
|
123
|
+
: `${current_auth_providers},google`;
|
|
124
|
+
|
|
125
|
+
const update_data: Record<string, unknown> = {
|
|
126
|
+
google_id,
|
|
127
|
+
auth_providers: new_auth_providers,
|
|
128
|
+
last_logon: now,
|
|
129
|
+
changed_at: now,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// If user was unverified and Google verified the email, mark as verified
|
|
133
|
+
if (!user_email_verified && email_verified) {
|
|
134
|
+
update_data.email_verified = true;
|
|
135
|
+
logger.info("oauth_service_auto_verified_email", {
|
|
136
|
+
filename: "oauth_service.ts",
|
|
137
|
+
line_number: get_line_number(),
|
|
138
|
+
user_id: user.id,
|
|
139
|
+
email,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Update name if not set and Google provides one
|
|
144
|
+
if (!user.name && name) {
|
|
145
|
+
update_data.name = name;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Update profile picture if not set and Google provides one
|
|
149
|
+
if (!user.profile_picture_url && profile_picture_url) {
|
|
150
|
+
update_data.profile_picture_url = profile_picture_url;
|
|
151
|
+
update_data.profile_source = "custom"; // Use 'custom' for external URLs (Google profile pics)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await users_service.updateById(user.id, update_data);
|
|
155
|
+
|
|
156
|
+
logger.info("oauth_service_google_linked_to_existing", {
|
|
157
|
+
filename: "oauth_service.ts",
|
|
158
|
+
line_number: get_line_number(),
|
|
159
|
+
user_id: user.id,
|
|
160
|
+
email,
|
|
161
|
+
was_unverified: !user_email_verified,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
success: true,
|
|
166
|
+
user_id: user.id as string,
|
|
167
|
+
is_new_user: false,
|
|
168
|
+
was_linked: true,
|
|
169
|
+
email: user.email_address as string,
|
|
170
|
+
name: (update_data.name as string) || (user.name as string | undefined),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Step 3: Create new user with Google data
|
|
175
|
+
const user_id = randomUUID();
|
|
176
|
+
|
|
177
|
+
const insert_data: Record<string, unknown> = {
|
|
178
|
+
id: user_id,
|
|
179
|
+
email_address: email,
|
|
180
|
+
password_hash: "", // Empty string for Google-only users
|
|
181
|
+
email_verified: email_verified, // Trust Google's verification
|
|
182
|
+
is_active: true,
|
|
183
|
+
login_attempts: 0,
|
|
184
|
+
google_id,
|
|
185
|
+
auth_providers: "google",
|
|
186
|
+
created_at: now,
|
|
187
|
+
changed_at: now,
|
|
188
|
+
last_logon: now,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if (name) {
|
|
192
|
+
insert_data.name = name;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (profile_picture_url) {
|
|
196
|
+
insert_data.profile_picture_url = profile_picture_url;
|
|
197
|
+
insert_data.profile_source = "custom"; // Use 'custom' for external URLs (Google profile pics)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const inserted_users = await users_service.insert(insert_data);
|
|
201
|
+
|
|
202
|
+
if (!Array.isArray(inserted_users) || inserted_users.length === 0) {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
error: "Failed to create user account",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
logger.info("oauth_service_google_new_user_created", {
|
|
210
|
+
filename: "oauth_service.ts",
|
|
211
|
+
line_number: get_line_number(),
|
|
212
|
+
user_id,
|
|
213
|
+
email,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
success: true,
|
|
218
|
+
user_id,
|
|
219
|
+
is_new_user: true,
|
|
220
|
+
was_linked: false,
|
|
221
|
+
email,
|
|
222
|
+
name,
|
|
223
|
+
};
|
|
224
|
+
} catch (error) {
|
|
225
|
+
const user_friendly_error = sanitize_error_for_user(error, {
|
|
226
|
+
logToConsole: true,
|
|
227
|
+
logToLogger: true,
|
|
228
|
+
logger,
|
|
229
|
+
context: {
|
|
230
|
+
filename: "oauth_service.ts",
|
|
231
|
+
line_number: get_line_number(),
|
|
232
|
+
email: data.email,
|
|
233
|
+
operation: "handle_google_oauth_login",
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
success: false,
|
|
239
|
+
error: user_friendly_error,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Links a Google account to an existing user
|
|
246
|
+
* @param adapter - The hazo_connect adapter instance
|
|
247
|
+
* @param user_id - The user's ID
|
|
248
|
+
* @param google_id - Google's unique user ID
|
|
249
|
+
* @returns Result indicating success or failure
|
|
250
|
+
*/
|
|
251
|
+
export async function link_google_account(
|
|
252
|
+
adapter: HazoConnectAdapter,
|
|
253
|
+
user_id: string,
|
|
254
|
+
google_id: string
|
|
255
|
+
): Promise<LinkGoogleResult> {
|
|
256
|
+
const logger = create_app_logger();
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const users_service = createCrudService(adapter, "hazo_users");
|
|
260
|
+
const now = new Date().toISOString();
|
|
261
|
+
|
|
262
|
+
// Get current user
|
|
263
|
+
const users = await users_service.findBy({ id: user_id });
|
|
264
|
+
|
|
265
|
+
if (!Array.isArray(users) || users.length === 0) {
|
|
266
|
+
return {
|
|
267
|
+
success: false,
|
|
268
|
+
error: "User not found",
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const user = users[0];
|
|
273
|
+
|
|
274
|
+
// Check if Google is already linked
|
|
275
|
+
if (user.google_id) {
|
|
276
|
+
return {
|
|
277
|
+
success: false,
|
|
278
|
+
error: "Google account is already linked",
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Update auth_providers
|
|
283
|
+
const current_auth_providers = (user.auth_providers as string) || "email";
|
|
284
|
+
const new_auth_providers = current_auth_providers.includes("google")
|
|
285
|
+
? current_auth_providers
|
|
286
|
+
: `${current_auth_providers},google`;
|
|
287
|
+
|
|
288
|
+
await users_service.updateById(user_id, {
|
|
289
|
+
google_id,
|
|
290
|
+
auth_providers: new_auth_providers,
|
|
291
|
+
changed_at: now,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
logger.info("oauth_service_google_account_linked", {
|
|
295
|
+
filename: "oauth_service.ts",
|
|
296
|
+
line_number: get_line_number(),
|
|
297
|
+
user_id,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return { success: true };
|
|
301
|
+
} catch (error) {
|
|
302
|
+
const user_friendly_error = sanitize_error_for_user(error, {
|
|
303
|
+
logToConsole: true,
|
|
304
|
+
logToLogger: true,
|
|
305
|
+
logger,
|
|
306
|
+
context: {
|
|
307
|
+
filename: "oauth_service.ts",
|
|
308
|
+
line_number: get_line_number(),
|
|
309
|
+
user_id,
|
|
310
|
+
operation: "link_google_account",
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
success: false,
|
|
316
|
+
error: user_friendly_error,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Checks if a user has a password set (non-empty password_hash)
|
|
323
|
+
* @param adapter - The hazo_connect adapter instance
|
|
324
|
+
* @param user_id - The user's ID
|
|
325
|
+
* @returns True if user has a password set
|
|
326
|
+
*/
|
|
327
|
+
export async function user_has_password(
|
|
328
|
+
adapter: HazoConnectAdapter,
|
|
329
|
+
user_id: string
|
|
330
|
+
): Promise<boolean> {
|
|
331
|
+
try {
|
|
332
|
+
const users_service = createCrudService(adapter, "hazo_users");
|
|
333
|
+
const users = await users_service.findBy({ id: user_id });
|
|
334
|
+
|
|
335
|
+
if (!Array.isArray(users) || users.length === 0) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const password_hash = users[0].password_hash as string;
|
|
340
|
+
return password_hash !== null && password_hash !== undefined && password_hash !== "";
|
|
341
|
+
} catch {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Checks if a user has a password set by email
|
|
348
|
+
* @param adapter - The hazo_connect adapter instance
|
|
349
|
+
* @param email - The user's email address
|
|
350
|
+
* @returns True if user has a password set
|
|
351
|
+
*/
|
|
352
|
+
export async function user_has_password_by_email(
|
|
353
|
+
adapter: HazoConnectAdapter,
|
|
354
|
+
email: string
|
|
355
|
+
): Promise<boolean> {
|
|
356
|
+
try {
|
|
357
|
+
const users_service = createCrudService(adapter, "hazo_users");
|
|
358
|
+
const users = await users_service.findBy({ email_address: email });
|
|
359
|
+
|
|
360
|
+
if (!Array.isArray(users) || users.length === 0) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const password_hash = users[0].password_hash as string;
|
|
365
|
+
return password_hash !== null && password_hash !== undefined && password_hash !== "";
|
|
366
|
+
} catch {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Gets a user's authentication providers and password status
|
|
373
|
+
* @param adapter - The hazo_connect adapter instance
|
|
374
|
+
* @param user_id - The user's ID
|
|
375
|
+
* @returns Auth providers array and has_password flag
|
|
376
|
+
*/
|
|
377
|
+
export async function get_user_auth_providers(
|
|
378
|
+
adapter: HazoConnectAdapter,
|
|
379
|
+
user_id: string
|
|
380
|
+
): Promise<AuthProvidersResult> {
|
|
381
|
+
try {
|
|
382
|
+
const users_service = createCrudService(adapter, "hazo_users");
|
|
383
|
+
const users = await users_service.findBy({ id: user_id });
|
|
384
|
+
|
|
385
|
+
if (!Array.isArray(users) || users.length === 0) {
|
|
386
|
+
return {
|
|
387
|
+
success: false,
|
|
388
|
+
error: "User not found",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const user = users[0];
|
|
393
|
+
const auth_providers_str = (user.auth_providers as string) || "email";
|
|
394
|
+
const auth_providers = auth_providers_str.split(",").map((p) => p.trim());
|
|
395
|
+
|
|
396
|
+
const password_hash = user.password_hash as string;
|
|
397
|
+
const has_password = password_hash !== null && password_hash !== undefined && password_hash !== "";
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
success: true,
|
|
401
|
+
auth_providers,
|
|
402
|
+
has_password,
|
|
403
|
+
};
|
|
404
|
+
} catch (error) {
|
|
405
|
+
const logger = create_app_logger();
|
|
406
|
+
const user_friendly_error = sanitize_error_for_user(error, {
|
|
407
|
+
logToConsole: true,
|
|
408
|
+
logToLogger: true,
|
|
409
|
+
logger,
|
|
410
|
+
context: {
|
|
411
|
+
filename: "oauth_service.ts",
|
|
412
|
+
line_number: get_line_number(),
|
|
413
|
+
user_id,
|
|
414
|
+
operation: "get_user_auth_providers",
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
success: false,
|
|
420
|
+
error: user_friendly_error,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Sets a password for a user who doesn't have one (e.g., Google-only users)
|
|
427
|
+
* @param adapter - The hazo_connect adapter instance
|
|
428
|
+
* @param user_id - The user's ID
|
|
429
|
+
* @param password_hash - The hashed password to set
|
|
430
|
+
* @returns Result indicating success or failure
|
|
431
|
+
*/
|
|
432
|
+
export async function set_user_password(
|
|
433
|
+
adapter: HazoConnectAdapter,
|
|
434
|
+
user_id: string,
|
|
435
|
+
password_hash: string
|
|
436
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
437
|
+
const logger = create_app_logger();
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
const users_service = createCrudService(adapter, "hazo_users");
|
|
441
|
+
const now = new Date().toISOString();
|
|
442
|
+
|
|
443
|
+
// Get current user
|
|
444
|
+
const users = await users_service.findBy({ id: user_id });
|
|
445
|
+
|
|
446
|
+
if (!Array.isArray(users) || users.length === 0) {
|
|
447
|
+
return {
|
|
448
|
+
success: false,
|
|
449
|
+
error: "User not found",
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const user = users[0];
|
|
454
|
+
|
|
455
|
+
// Update password and auth_providers
|
|
456
|
+
const current_auth_providers = (user.auth_providers as string) || "";
|
|
457
|
+
const new_auth_providers = current_auth_providers.includes("email")
|
|
458
|
+
? current_auth_providers
|
|
459
|
+
: current_auth_providers
|
|
460
|
+
? `${current_auth_providers},email`
|
|
461
|
+
: "email";
|
|
462
|
+
|
|
463
|
+
await users_service.updateById(user_id, {
|
|
464
|
+
password_hash,
|
|
465
|
+
auth_providers: new_auth_providers,
|
|
466
|
+
changed_at: now,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
logger.info("oauth_service_password_set", {
|
|
470
|
+
filename: "oauth_service.ts",
|
|
471
|
+
line_number: get_line_number(),
|
|
472
|
+
user_id,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
return { success: true };
|
|
476
|
+
} catch (error) {
|
|
477
|
+
const user_friendly_error = sanitize_error_for_user(error, {
|
|
478
|
+
logToConsole: true,
|
|
479
|
+
logToLogger: true,
|
|
480
|
+
logger,
|
|
481
|
+
context: {
|
|
482
|
+
filename: "oauth_service.ts",
|
|
483
|
+
line_number: get_line_number(),
|
|
484
|
+
user_id,
|
|
485
|
+
operation: "set_user_password",
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
success: false,
|
|
491
|
+
error: user_friendly_error,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// file_description: service for changing user password using hazo_connect
|
|
2
|
+
// section: imports
|
|
3
|
+
import type { HazoConnectAdapter } from "hazo_connect";
|
|
4
|
+
import { createCrudService } from "hazo_connect/server";
|
|
5
|
+
import argon2 from "argon2";
|
|
6
|
+
import { get_password_requirements_config } from "../password_requirements_config.server";
|
|
7
|
+
import { send_template_email } from "./email_service";
|
|
8
|
+
import { create_app_logger } from "../app_logger";
|
|
9
|
+
|
|
10
|
+
// section: types
|
|
11
|
+
export type PasswordChangeData = {
|
|
12
|
+
current_password: string;
|
|
13
|
+
new_password: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type PasswordChangeResult = {
|
|
17
|
+
success: boolean;
|
|
18
|
+
error?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// section: helpers
|
|
22
|
+
/**
|
|
23
|
+
* Changes a user's password
|
|
24
|
+
* Verifies the current password, validates the new password, and updates the password hash
|
|
25
|
+
* @param adapter - The hazo_connect adapter instance
|
|
26
|
+
* @param user_id - The user ID to update
|
|
27
|
+
* @param data - Password change data (current_password, new_password)
|
|
28
|
+
* @returns Password change result with success status or error
|
|
29
|
+
*/
|
|
30
|
+
export async function change_password(
|
|
31
|
+
adapter: HazoConnectAdapter,
|
|
32
|
+
user_id: string,
|
|
33
|
+
data: PasswordChangeData,
|
|
34
|
+
): Promise<PasswordChangeResult> {
|
|
35
|
+
try {
|
|
36
|
+
const { current_password, new_password } = data;
|
|
37
|
+
|
|
38
|
+
// Create CRUD service for hazo_users table
|
|
39
|
+
const users_service = createCrudService(adapter, "hazo_users");
|
|
40
|
+
|
|
41
|
+
// Get current user data
|
|
42
|
+
const users = await users_service.findBy({
|
|
43
|
+
id: user_id,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!Array.isArray(users) || users.length === 0) {
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
error: "User not found",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const user = users[0];
|
|
54
|
+
const password_hash = user.password_hash as string;
|
|
55
|
+
const email = user.email_address as string;
|
|
56
|
+
const user_name = user.name as string | undefined;
|
|
57
|
+
|
|
58
|
+
// Verify current password
|
|
59
|
+
try {
|
|
60
|
+
const is_valid = await argon2.verify(password_hash, current_password);
|
|
61
|
+
if (!is_valid) {
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
error: "Current password is incorrect",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
error: "Failed to verify current password",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Get password requirements from config
|
|
75
|
+
const password_requirements = get_password_requirements_config();
|
|
76
|
+
|
|
77
|
+
// Validate new password
|
|
78
|
+
if (!new_password || new_password.length < password_requirements.minimum_length) {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
error: `Password must be at least ${password_requirements.minimum_length} characters long`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (password_requirements.require_uppercase && !/[A-Z]/.test(new_password)) {
|
|
86
|
+
return {
|
|
87
|
+
success: false,
|
|
88
|
+
error: "Password must contain at least one uppercase letter",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (password_requirements.require_lowercase && !/[a-z]/.test(new_password)) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
error: "Password must contain at least one lowercase letter",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (password_requirements.require_number && !/[0-9]/.test(new_password)) {
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
error: "Password must contain at least one number",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (password_requirements.require_special && !/[^A-Za-z0-9]/.test(new_password)) {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: "Password must contain at least one special character",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Hash the new password
|
|
114
|
+
const new_password_hash = await argon2.hash(new_password);
|
|
115
|
+
|
|
116
|
+
// Update password hash in database
|
|
117
|
+
const now = new Date().toISOString();
|
|
118
|
+
await users_service.updateById(user_id, {
|
|
119
|
+
password_hash: new_password_hash,
|
|
120
|
+
changed_at: now,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Send password changed notification email
|
|
124
|
+
const email_result = await send_template_email("password_changed", email, {
|
|
125
|
+
user_email: email,
|
|
126
|
+
user_name: user_name,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (!email_result.success) {
|
|
130
|
+
const logger = create_app_logger();
|
|
131
|
+
logger.error("password_change_service_email_failed", {
|
|
132
|
+
filename: "password_change_service.ts",
|
|
133
|
+
line_number: 0,
|
|
134
|
+
user_id,
|
|
135
|
+
email,
|
|
136
|
+
error: email_result.error,
|
|
137
|
+
note: "Password was changed successfully but notification email failed to send",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
success: true,
|
|
143
|
+
};
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const error_message =
|
|
146
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
error: error_message,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|