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,554 @@
|
|
|
1
|
+
// file_description: service for managing user scope assignments in HRBAC using hazo_connect
|
|
2
|
+
// section: imports
|
|
3
|
+
import type { HazoConnectAdapter } from "hazo_connect";
|
|
4
|
+
import { createCrudService } from "hazo_connect/server";
|
|
5
|
+
import { create_app_logger } from "../app_logger";
|
|
6
|
+
import { sanitize_error_for_user } from "../utils/error_sanitizer";
|
|
7
|
+
import {
|
|
8
|
+
type ScopeLevel,
|
|
9
|
+
SCOPE_LEVELS,
|
|
10
|
+
SCOPE_LEVEL_NUMBERS,
|
|
11
|
+
get_scope_by_id,
|
|
12
|
+
get_scope_by_seq,
|
|
13
|
+
get_scope_ancestors,
|
|
14
|
+
is_valid_scope_level,
|
|
15
|
+
} from "./scope_service";
|
|
16
|
+
|
|
17
|
+
// section: types
|
|
18
|
+
export type UserScope = {
|
|
19
|
+
user_id: string;
|
|
20
|
+
scope_id: string;
|
|
21
|
+
scope_seq: string;
|
|
22
|
+
scope_type: ScopeLevel;
|
|
23
|
+
created_at: string;
|
|
24
|
+
changed_at: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type UserScopeResult = {
|
|
28
|
+
success: boolean;
|
|
29
|
+
scope?: UserScope;
|
|
30
|
+
scopes?: UserScope[];
|
|
31
|
+
error?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ScopeAccessCheckResult = {
|
|
35
|
+
has_access: boolean;
|
|
36
|
+
access_via?: {
|
|
37
|
+
scope_type: ScopeLevel;
|
|
38
|
+
scope_id: string;
|
|
39
|
+
scope_seq: string;
|
|
40
|
+
};
|
|
41
|
+
user_scopes?: UserScope[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// section: helpers
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Gets all scope assignments for a user
|
|
48
|
+
*/
|
|
49
|
+
export async function get_user_scopes(
|
|
50
|
+
adapter: HazoConnectAdapter,
|
|
51
|
+
user_id: string,
|
|
52
|
+
): Promise<UserScopeResult> {
|
|
53
|
+
try {
|
|
54
|
+
const user_scope_service = createCrudService(adapter, "hazo_user_scopes");
|
|
55
|
+
const scopes = await user_scope_service.findBy({ user_id });
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
success: true,
|
|
59
|
+
scopes: Array.isArray(scopes) ? (scopes as UserScope[]) : [],
|
|
60
|
+
};
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const logger = create_app_logger();
|
|
63
|
+
const error_message = sanitize_error_for_user(error, {
|
|
64
|
+
logToConsole: true,
|
|
65
|
+
logToLogger: true,
|
|
66
|
+
logger,
|
|
67
|
+
context: {
|
|
68
|
+
filename: "user_scope_service.ts",
|
|
69
|
+
line_number: 0,
|
|
70
|
+
operation: "get_user_scopes",
|
|
71
|
+
user_id,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
error: error_message,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Gets all users assigned to a specific scope
|
|
84
|
+
*/
|
|
85
|
+
export async function get_users_by_scope(
|
|
86
|
+
adapter: HazoConnectAdapter,
|
|
87
|
+
scope_type: ScopeLevel,
|
|
88
|
+
scope_id: string,
|
|
89
|
+
): Promise<UserScopeResult> {
|
|
90
|
+
try {
|
|
91
|
+
const user_scope_service = createCrudService(adapter, "hazo_user_scopes");
|
|
92
|
+
const scopes = await user_scope_service.findBy({ scope_type, scope_id });
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
success: true,
|
|
96
|
+
scopes: Array.isArray(scopes) ? (scopes as UserScope[]) : [],
|
|
97
|
+
};
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const logger = create_app_logger();
|
|
100
|
+
const error_message = sanitize_error_for_user(error, {
|
|
101
|
+
logToConsole: true,
|
|
102
|
+
logToLogger: true,
|
|
103
|
+
logger,
|
|
104
|
+
context: {
|
|
105
|
+
filename: "user_scope_service.ts",
|
|
106
|
+
line_number: 0,
|
|
107
|
+
operation: "get_users_by_scope",
|
|
108
|
+
scope_type,
|
|
109
|
+
scope_id,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
error: error_message,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Assigns a scope to a user
|
|
122
|
+
*/
|
|
123
|
+
export async function assign_user_scope(
|
|
124
|
+
adapter: HazoConnectAdapter,
|
|
125
|
+
user_id: string,
|
|
126
|
+
scope_type: ScopeLevel,
|
|
127
|
+
scope_id: string,
|
|
128
|
+
scope_seq: string,
|
|
129
|
+
): Promise<UserScopeResult> {
|
|
130
|
+
try {
|
|
131
|
+
const user_scope_service = createCrudService(adapter, "hazo_user_scopes");
|
|
132
|
+
const now = new Date().toISOString();
|
|
133
|
+
|
|
134
|
+
// Check if assignment already exists
|
|
135
|
+
const existing = await user_scope_service.findBy({
|
|
136
|
+
user_id,
|
|
137
|
+
scope_type,
|
|
138
|
+
scope_id,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (Array.isArray(existing) && existing.length > 0) {
|
|
142
|
+
return {
|
|
143
|
+
success: true,
|
|
144
|
+
scope: existing[0] as UserScope, // Already assigned
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Verify the scope exists
|
|
149
|
+
const scope_result = await get_scope_by_id(adapter, scope_type, scope_id);
|
|
150
|
+
if (!scope_result.success) {
|
|
151
|
+
return {
|
|
152
|
+
success: false,
|
|
153
|
+
error: "Scope not found",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Insert new assignment
|
|
158
|
+
const inserted = await user_scope_service.insert({
|
|
159
|
+
user_id,
|
|
160
|
+
scope_id,
|
|
161
|
+
scope_seq,
|
|
162
|
+
scope_type,
|
|
163
|
+
created_at: now,
|
|
164
|
+
changed_at: now,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (!Array.isArray(inserted) || inserted.length === 0) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
error: "Failed to assign scope to user",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
success: true,
|
|
176
|
+
scope: inserted[0] as UserScope,
|
|
177
|
+
};
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const logger = create_app_logger();
|
|
180
|
+
const error_message = sanitize_error_for_user(error, {
|
|
181
|
+
logToConsole: true,
|
|
182
|
+
logToLogger: true,
|
|
183
|
+
logger,
|
|
184
|
+
context: {
|
|
185
|
+
filename: "user_scope_service.ts",
|
|
186
|
+
line_number: 0,
|
|
187
|
+
operation: "assign_user_scope",
|
|
188
|
+
user_id,
|
|
189
|
+
scope_type,
|
|
190
|
+
scope_id,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
error: error_message,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Removes a scope assignment from a user
|
|
203
|
+
*/
|
|
204
|
+
export async function remove_user_scope(
|
|
205
|
+
adapter: HazoConnectAdapter,
|
|
206
|
+
user_id: string,
|
|
207
|
+
scope_type: ScopeLevel,
|
|
208
|
+
scope_id: string,
|
|
209
|
+
): Promise<UserScopeResult> {
|
|
210
|
+
try {
|
|
211
|
+
const user_scope_service = createCrudService(adapter, "hazo_user_scopes");
|
|
212
|
+
|
|
213
|
+
// Find the assignment
|
|
214
|
+
const existing = await user_scope_service.findBy({
|
|
215
|
+
user_id,
|
|
216
|
+
scope_type,
|
|
217
|
+
scope_id,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (!Array.isArray(existing) || existing.length === 0) {
|
|
221
|
+
return {
|
|
222
|
+
success: true, // Already not assigned
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Delete using a filter-based approach since there's no single ID
|
|
227
|
+
// Note: hazo_user_scopes uses composite primary key (user_id, scope_id, scope_type)
|
|
228
|
+
// We need to find and delete by the combination
|
|
229
|
+
const existing_scope = existing[0] as UserScope;
|
|
230
|
+
|
|
231
|
+
// Use raw delete with filters if available, otherwise try by the composite key pattern
|
|
232
|
+
// Most hazo_connect adapters support deleteBy or similar
|
|
233
|
+
try {
|
|
234
|
+
// Try to delete by finding records with matching criteria
|
|
235
|
+
const all_user_scopes = await user_scope_service.findBy({ user_id });
|
|
236
|
+
if (Array.isArray(all_user_scopes)) {
|
|
237
|
+
for (const scope of all_user_scopes) {
|
|
238
|
+
const s = scope as UserScope;
|
|
239
|
+
if (s.scope_type === scope_type && s.scope_id === scope_id) {
|
|
240
|
+
// If the record has an id field, use it
|
|
241
|
+
if ((scope as Record<string, unknown>).id) {
|
|
242
|
+
await user_scope_service.deleteById((scope as Record<string, unknown>).id as string);
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// Fallback: Some adapters might not support this pattern
|
|
250
|
+
const logger = create_app_logger();
|
|
251
|
+
logger.warn("user_scope_delete_fallback", {
|
|
252
|
+
filename: "user_scope_service.ts",
|
|
253
|
+
line_number: 0,
|
|
254
|
+
note: "Delete by composite key not fully supported",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
success: true,
|
|
260
|
+
scope: existing_scope,
|
|
261
|
+
};
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const logger = create_app_logger();
|
|
264
|
+
const error_message = sanitize_error_for_user(error, {
|
|
265
|
+
logToConsole: true,
|
|
266
|
+
logToLogger: true,
|
|
267
|
+
logger,
|
|
268
|
+
context: {
|
|
269
|
+
filename: "user_scope_service.ts",
|
|
270
|
+
line_number: 0,
|
|
271
|
+
operation: "remove_user_scope",
|
|
272
|
+
user_id,
|
|
273
|
+
scope_type,
|
|
274
|
+
scope_id,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
success: false,
|
|
280
|
+
error: error_message,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Bulk update user scope assignments
|
|
287
|
+
* Replaces all existing assignments with the new set
|
|
288
|
+
*/
|
|
289
|
+
export async function update_user_scopes(
|
|
290
|
+
adapter: HazoConnectAdapter,
|
|
291
|
+
user_id: string,
|
|
292
|
+
new_scopes: Array<{ scope_type: ScopeLevel; scope_id: string; scope_seq: string }>,
|
|
293
|
+
): Promise<UserScopeResult> {
|
|
294
|
+
try {
|
|
295
|
+
// Get current scopes
|
|
296
|
+
const current_result = await get_user_scopes(adapter, user_id);
|
|
297
|
+
if (!current_result.success) {
|
|
298
|
+
return current_result;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const current_scopes = current_result.scopes || [];
|
|
302
|
+
|
|
303
|
+
// Determine scopes to add and remove
|
|
304
|
+
const current_keys = new Set(
|
|
305
|
+
current_scopes.map((s) => `${s.scope_type}:${s.scope_id}`),
|
|
306
|
+
);
|
|
307
|
+
const new_keys = new Set(
|
|
308
|
+
new_scopes.map((s) => `${s.scope_type}:${s.scope_id}`),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// Remove scopes not in new set
|
|
312
|
+
for (const scope of current_scopes) {
|
|
313
|
+
const key = `${scope.scope_type}:${scope.scope_id}`;
|
|
314
|
+
if (!new_keys.has(key)) {
|
|
315
|
+
await remove_user_scope(adapter, user_id, scope.scope_type, scope.scope_id);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Add scopes not in current set
|
|
320
|
+
for (const scope of new_scopes) {
|
|
321
|
+
const key = `${scope.scope_type}:${scope.scope_id}`;
|
|
322
|
+
if (!current_keys.has(key)) {
|
|
323
|
+
const result = await assign_user_scope(
|
|
324
|
+
adapter,
|
|
325
|
+
user_id,
|
|
326
|
+
scope.scope_type,
|
|
327
|
+
scope.scope_id,
|
|
328
|
+
scope.scope_seq,
|
|
329
|
+
);
|
|
330
|
+
if (!result.success) {
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Return updated scopes
|
|
337
|
+
return get_user_scopes(adapter, user_id);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
const logger = create_app_logger();
|
|
340
|
+
const error_message = sanitize_error_for_user(error, {
|
|
341
|
+
logToConsole: true,
|
|
342
|
+
logToLogger: true,
|
|
343
|
+
logger,
|
|
344
|
+
context: {
|
|
345
|
+
filename: "user_scope_service.ts",
|
|
346
|
+
line_number: 0,
|
|
347
|
+
operation: "update_user_scopes",
|
|
348
|
+
user_id,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
success: false,
|
|
354
|
+
error: error_message,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Checks if a user has access to a specific scope
|
|
361
|
+
* Access is granted if:
|
|
362
|
+
* 1. User has the exact scope assigned, OR
|
|
363
|
+
* 2. User has access to an ancestor scope (L2 user can access L3, L4, etc.)
|
|
364
|
+
*
|
|
365
|
+
* @param adapter - HazoConnect adapter
|
|
366
|
+
* @param user_id - User ID to check
|
|
367
|
+
* @param target_scope_type - The scope level being accessed
|
|
368
|
+
* @param target_scope_id - The scope ID being accessed (optional if target_scope_seq provided)
|
|
369
|
+
* @param target_scope_seq - The scope seq being accessed (optional if target_scope_id provided)
|
|
370
|
+
*/
|
|
371
|
+
export async function check_user_scope_access(
|
|
372
|
+
adapter: HazoConnectAdapter,
|
|
373
|
+
user_id: string,
|
|
374
|
+
target_scope_type: ScopeLevel,
|
|
375
|
+
target_scope_id?: string,
|
|
376
|
+
target_scope_seq?: string,
|
|
377
|
+
): Promise<ScopeAccessCheckResult> {
|
|
378
|
+
try {
|
|
379
|
+
// Resolve scope ID if only seq provided
|
|
380
|
+
let resolved_scope_id = target_scope_id;
|
|
381
|
+
let resolved_scope_seq = target_scope_seq;
|
|
382
|
+
|
|
383
|
+
if (!resolved_scope_id && resolved_scope_seq) {
|
|
384
|
+
const scope_result = await get_scope_by_seq(
|
|
385
|
+
adapter,
|
|
386
|
+
target_scope_type,
|
|
387
|
+
resolved_scope_seq,
|
|
388
|
+
);
|
|
389
|
+
if (!scope_result.success || !scope_result.scope) {
|
|
390
|
+
return { has_access: false };
|
|
391
|
+
}
|
|
392
|
+
resolved_scope_id = scope_result.scope.id;
|
|
393
|
+
} else if (resolved_scope_id && !resolved_scope_seq) {
|
|
394
|
+
const scope_result = await get_scope_by_id(
|
|
395
|
+
adapter,
|
|
396
|
+
target_scope_type,
|
|
397
|
+
resolved_scope_id,
|
|
398
|
+
);
|
|
399
|
+
if (!scope_result.success || !scope_result.scope) {
|
|
400
|
+
return { has_access: false };
|
|
401
|
+
}
|
|
402
|
+
resolved_scope_seq = scope_result.scope.seq;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!resolved_scope_id) {
|
|
406
|
+
return { has_access: false };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Get user's assigned scopes
|
|
410
|
+
const user_scopes_result = await get_user_scopes(adapter, user_id);
|
|
411
|
+
if (!user_scopes_result.success || !user_scopes_result.scopes) {
|
|
412
|
+
return { has_access: false };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const user_scopes = user_scopes_result.scopes;
|
|
416
|
+
|
|
417
|
+
// Check 1: Does user have exact scope assigned?
|
|
418
|
+
for (const user_scope of user_scopes) {
|
|
419
|
+
if (
|
|
420
|
+
user_scope.scope_type === target_scope_type &&
|
|
421
|
+
user_scope.scope_id === resolved_scope_id
|
|
422
|
+
) {
|
|
423
|
+
return {
|
|
424
|
+
has_access: true,
|
|
425
|
+
access_via: {
|
|
426
|
+
scope_type: user_scope.scope_type,
|
|
427
|
+
scope_id: user_scope.scope_id,
|
|
428
|
+
scope_seq: user_scope.scope_seq,
|
|
429
|
+
},
|
|
430
|
+
user_scopes,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Check 2: Does user have access via an ancestor scope?
|
|
436
|
+
// Get all ancestors of the target scope
|
|
437
|
+
const ancestors_result = await get_scope_ancestors(
|
|
438
|
+
adapter,
|
|
439
|
+
target_scope_type,
|
|
440
|
+
resolved_scope_id,
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
if (ancestors_result.success && ancestors_result.scopes) {
|
|
444
|
+
const ancestors = ancestors_result.scopes;
|
|
445
|
+
|
|
446
|
+
// For each ancestor, check if user has it assigned
|
|
447
|
+
// Need to determine the level of each ancestor
|
|
448
|
+
let current_level = SCOPE_LEVEL_NUMBERS[target_scope_type];
|
|
449
|
+
|
|
450
|
+
for (const ancestor of ancestors) {
|
|
451
|
+
current_level--;
|
|
452
|
+
const ancestor_level = `hazo_scopes_l${current_level}` as ScopeLevel;
|
|
453
|
+
|
|
454
|
+
for (const user_scope of user_scopes) {
|
|
455
|
+
if (
|
|
456
|
+
user_scope.scope_type === ancestor_level &&
|
|
457
|
+
user_scope.scope_id === ancestor.id
|
|
458
|
+
) {
|
|
459
|
+
// User has access via this ancestor
|
|
460
|
+
return {
|
|
461
|
+
has_access: true,
|
|
462
|
+
access_via: {
|
|
463
|
+
scope_type: user_scope.scope_type,
|
|
464
|
+
scope_id: user_scope.scope_id,
|
|
465
|
+
scope_seq: user_scope.scope_seq,
|
|
466
|
+
},
|
|
467
|
+
user_scopes,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// No access
|
|
475
|
+
return {
|
|
476
|
+
has_access: false,
|
|
477
|
+
user_scopes,
|
|
478
|
+
};
|
|
479
|
+
} catch (error) {
|
|
480
|
+
const logger = create_app_logger();
|
|
481
|
+
logger.error("check_user_scope_access_error", {
|
|
482
|
+
filename: "user_scope_service.ts",
|
|
483
|
+
line_number: 0,
|
|
484
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
485
|
+
user_id,
|
|
486
|
+
target_scope_type,
|
|
487
|
+
target_scope_id,
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
return { has_access: false };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Gets the effective scopes a user has access to
|
|
496
|
+
* This includes directly assigned scopes and all their descendants
|
|
497
|
+
*/
|
|
498
|
+
export async function get_user_effective_scopes(
|
|
499
|
+
adapter: HazoConnectAdapter,
|
|
500
|
+
user_id: string,
|
|
501
|
+
): Promise<{
|
|
502
|
+
success: boolean;
|
|
503
|
+
direct_scopes?: UserScope[];
|
|
504
|
+
inherited_scope_types?: ScopeLevel[];
|
|
505
|
+
error?: string;
|
|
506
|
+
}> {
|
|
507
|
+
try {
|
|
508
|
+
const user_scopes_result = await get_user_scopes(adapter, user_id);
|
|
509
|
+
if (!user_scopes_result.success) {
|
|
510
|
+
return {
|
|
511
|
+
success: false,
|
|
512
|
+
error: user_scopes_result.error,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const direct_scopes = user_scopes_result.scopes || [];
|
|
517
|
+
|
|
518
|
+
// Determine which levels user has inherited access to
|
|
519
|
+
// If user has L2 access, they inherit L3, L4, L5, L6, L7
|
|
520
|
+
const inherited_levels = new Set<ScopeLevel>();
|
|
521
|
+
|
|
522
|
+
for (const scope of direct_scopes) {
|
|
523
|
+
const level_num = SCOPE_LEVEL_NUMBERS[scope.scope_type];
|
|
524
|
+
// Add all levels below this one
|
|
525
|
+
for (let i = level_num + 1; i <= 7; i++) {
|
|
526
|
+
inherited_levels.add(`hazo_scopes_l${i}` as ScopeLevel);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
success: true,
|
|
532
|
+
direct_scopes,
|
|
533
|
+
inherited_scope_types: Array.from(inherited_levels),
|
|
534
|
+
};
|
|
535
|
+
} catch (error) {
|
|
536
|
+
const logger = create_app_logger();
|
|
537
|
+
const error_message = sanitize_error_for_user(error, {
|
|
538
|
+
logToConsole: true,
|
|
539
|
+
logToLogger: true,
|
|
540
|
+
logger,
|
|
541
|
+
context: {
|
|
542
|
+
filename: "user_scope_service.ts",
|
|
543
|
+
line_number: 0,
|
|
544
|
+
operation: "get_user_effective_scopes",
|
|
545
|
+
user_id,
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
success: false,
|
|
551
|
+
error: error_message,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// file_description: service for updating user profile information using hazo_connect
|
|
2
|
+
// section: imports
|
|
3
|
+
import type { HazoConnectAdapter } from "hazo_connect";
|
|
4
|
+
import { createCrudService } from "hazo_connect/server";
|
|
5
|
+
import { map_ui_source_to_db, type ProfilePictureSourceUI } from "./profile_picture_source_mapper";
|
|
6
|
+
import { create_app_logger } from "../app_logger";
|
|
7
|
+
import { sanitize_error_for_user } from "../utils/error_sanitizer";
|
|
8
|
+
import { get_filename, get_line_number } from "../utils/api_route_helpers";
|
|
9
|
+
|
|
10
|
+
// section: types
|
|
11
|
+
export type UserUpdateData = {
|
|
12
|
+
name?: string;
|
|
13
|
+
email?: string;
|
|
14
|
+
profile_picture_url?: string;
|
|
15
|
+
profile_source?: ProfilePictureSourceUI;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type UserUpdateResult = {
|
|
19
|
+
success: boolean;
|
|
20
|
+
email_changed?: boolean;
|
|
21
|
+
error?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// section: helpers
|
|
25
|
+
/**
|
|
26
|
+
* Updates user profile information (name, email)
|
|
27
|
+
* If email is changed, sets email_verified to false
|
|
28
|
+
* @param adapter - The hazo_connect adapter instance
|
|
29
|
+
* @param user_id - The user ID to update
|
|
30
|
+
* @param data - User update data (name, email)
|
|
31
|
+
* @returns User update result with success status, email_changed flag, or error
|
|
32
|
+
*/
|
|
33
|
+
export async function update_user_profile(
|
|
34
|
+
adapter: HazoConnectAdapter,
|
|
35
|
+
user_id: string,
|
|
36
|
+
data: UserUpdateData,
|
|
37
|
+
): Promise<UserUpdateResult> {
|
|
38
|
+
try {
|
|
39
|
+
const { name, email } = data;
|
|
40
|
+
|
|
41
|
+
// Create CRUD service for hazo_users table
|
|
42
|
+
const users_service = createCrudService(adapter, "hazo_users");
|
|
43
|
+
|
|
44
|
+
// Get current user data
|
|
45
|
+
const users = await users_service.findBy({
|
|
46
|
+
id: user_id,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!Array.isArray(users) || users.length === 0) {
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
error: "User not found",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const current_user = users[0];
|
|
57
|
+
const current_email = current_user.email_address as string;
|
|
58
|
+
const email_changed = email !== undefined && email !== current_email;
|
|
59
|
+
|
|
60
|
+
// If email is being changed, check if new email already exists
|
|
61
|
+
if (email_changed) {
|
|
62
|
+
const existing_users = await users_service.findBy({
|
|
63
|
+
email_address: email,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(existing_users) && existing_users.length > 0) {
|
|
67
|
+
// Check if it's the same user
|
|
68
|
+
const existing_user = existing_users[0];
|
|
69
|
+
if (existing_user.id !== user_id) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
error: "Email address already registered",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Validate email format
|
|
78
|
+
const email_regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
79
|
+
if (!email_regex.test(email)) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: "Invalid email address format",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Prepare update data
|
|
88
|
+
const update_data: Record<string, unknown> = {
|
|
89
|
+
changed_at: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (name !== undefined) {
|
|
93
|
+
update_data.name = name;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (email !== undefined) {
|
|
97
|
+
update_data.email_address = email;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (data.profile_picture_url !== undefined) {
|
|
101
|
+
update_data.profile_picture_url = data.profile_picture_url;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (data.profile_source !== undefined) {
|
|
105
|
+
// Map UI source value to database enum value
|
|
106
|
+
update_data.profile_source = map_ui_source_to_db(data.profile_source);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// If email changed, set email_verified to false
|
|
110
|
+
if (email_changed) {
|
|
111
|
+
update_data.email_verified = false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Update user in database
|
|
115
|
+
await users_service.updateById(user_id, update_data);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
email_changed,
|
|
120
|
+
};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const logger = create_app_logger();
|
|
123
|
+
const user_friendly_error = sanitize_error_for_user(error, {
|
|
124
|
+
logToConsole: true,
|
|
125
|
+
logToLogger: true,
|
|
126
|
+
logger,
|
|
127
|
+
context: {
|
|
128
|
+
filename: "user_update_service.ts",
|
|
129
|
+
line_number: get_line_number(),
|
|
130
|
+
user_id,
|
|
131
|
+
operation: "update_user_profile",
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
error: user_friendly_error,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|