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,220 @@
|
|
|
1
|
+
// file_description: LRU cache implementation for hazo_get_auth with TTL and size limits
|
|
2
|
+
// section: imports
|
|
3
|
+
import type { HazoAuthUser } from "./auth_types";
|
|
4
|
+
|
|
5
|
+
// section: types
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Cache entry structure
|
|
9
|
+
*/
|
|
10
|
+
type CacheEntry = {
|
|
11
|
+
user: HazoAuthUser;
|
|
12
|
+
permissions: string[];
|
|
13
|
+
role_ids: number[];
|
|
14
|
+
timestamp: number; // Unix timestamp in milliseconds
|
|
15
|
+
cache_version: number; // Version number for smart invalidation
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* LRU cache implementation with TTL and size limits
|
|
20
|
+
* Uses Map to maintain insertion order for LRU eviction
|
|
21
|
+
*/
|
|
22
|
+
class AuthCache {
|
|
23
|
+
private cache: Map<string, CacheEntry>;
|
|
24
|
+
private max_size: number;
|
|
25
|
+
private ttl_ms: number;
|
|
26
|
+
private max_age_ms: number;
|
|
27
|
+
private role_version_map: Map<number, number>; // Track version per role for smart invalidation
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
max_size: number,
|
|
31
|
+
ttl_minutes: number,
|
|
32
|
+
max_age_minutes: number,
|
|
33
|
+
) {
|
|
34
|
+
this.cache = new Map();
|
|
35
|
+
this.max_size = max_size;
|
|
36
|
+
this.ttl_ms = ttl_minutes * 60 * 1000;
|
|
37
|
+
this.max_age_ms = max_age_minutes * 60 * 1000;
|
|
38
|
+
this.role_version_map = new Map();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Gets a cache entry for a user
|
|
43
|
+
* Returns undefined if not found, expired, or too old
|
|
44
|
+
* @param user_id - User ID to look up
|
|
45
|
+
* @returns Cache entry or undefined
|
|
46
|
+
*/
|
|
47
|
+
get(user_id: string): CacheEntry | undefined {
|
|
48
|
+
const entry = this.cache.get(user_id);
|
|
49
|
+
|
|
50
|
+
if (!entry) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const age = now - entry.timestamp;
|
|
56
|
+
|
|
57
|
+
// Check if entry is expired (TTL)
|
|
58
|
+
if (age > this.ttl_ms) {
|
|
59
|
+
this.cache.delete(user_id);
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check if entry is too old (force refresh threshold)
|
|
64
|
+
if (age > this.max_age_ms) {
|
|
65
|
+
// Don't delete, but mark as stale so caller can refresh
|
|
66
|
+
// Return undefined to force refresh
|
|
67
|
+
this.cache.delete(user_id);
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Move to end (most recently used)
|
|
72
|
+
this.cache.delete(user_id);
|
|
73
|
+
this.cache.set(user_id, entry);
|
|
74
|
+
|
|
75
|
+
return entry;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sets a cache entry for a user
|
|
80
|
+
* Evicts least recently used entries if cache is full
|
|
81
|
+
* @param user_id - User ID
|
|
82
|
+
* @param user - User data
|
|
83
|
+
* @param permissions - User permissions
|
|
84
|
+
* @param role_ids - User role IDs
|
|
85
|
+
*/
|
|
86
|
+
set(
|
|
87
|
+
user_id: string,
|
|
88
|
+
user: HazoAuthUser,
|
|
89
|
+
permissions: string[],
|
|
90
|
+
role_ids: number[],
|
|
91
|
+
): void {
|
|
92
|
+
// Evict LRU entries if cache is full
|
|
93
|
+
while (this.cache.size >= this.max_size) {
|
|
94
|
+
const first_key = this.cache.keys().next().value;
|
|
95
|
+
if (first_key) {
|
|
96
|
+
this.cache.delete(first_key);
|
|
97
|
+
} else {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Get current cache version for user's roles
|
|
103
|
+
const cache_version = this.get_max_role_version(role_ids);
|
|
104
|
+
|
|
105
|
+
const entry: CacheEntry = {
|
|
106
|
+
user,
|
|
107
|
+
permissions,
|
|
108
|
+
role_ids,
|
|
109
|
+
timestamp: Date.now(),
|
|
110
|
+
cache_version,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
this.cache.set(user_id, entry);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Invalidates cache for a specific user
|
|
118
|
+
* @param user_id - User ID to invalidate
|
|
119
|
+
*/
|
|
120
|
+
invalidate_user(user_id: string): void {
|
|
121
|
+
this.cache.delete(user_id);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Invalidates cache for all users with specific roles
|
|
126
|
+
* Uses cache version to determine if invalidation is needed
|
|
127
|
+
* @param role_ids - Array of role IDs to invalidate
|
|
128
|
+
*/
|
|
129
|
+
invalidate_by_roles(role_ids: number[]): void {
|
|
130
|
+
// Increment version for affected roles
|
|
131
|
+
for (const role_id of role_ids) {
|
|
132
|
+
const current_version = this.role_version_map.get(role_id) || 0;
|
|
133
|
+
this.role_version_map.set(role_id, current_version + 1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Remove entries where cache version is older than role version
|
|
137
|
+
const entries_to_remove: string[] = [];
|
|
138
|
+
for (const [user_id, entry] of this.cache.entries()) {
|
|
139
|
+
const max_role_version = this.get_max_role_version(entry.role_ids);
|
|
140
|
+
if (max_role_version > entry.cache_version) {
|
|
141
|
+
entries_to_remove.push(user_id);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const user_id of entries_to_remove) {
|
|
146
|
+
this.cache.delete(user_id);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Invalidates all cache entries
|
|
152
|
+
*/
|
|
153
|
+
invalidate_all(): void {
|
|
154
|
+
this.cache.clear();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Gets the maximum cache version for a set of roles
|
|
159
|
+
* Used to determine if cache entry is stale
|
|
160
|
+
* @param role_ids - Array of role IDs
|
|
161
|
+
* @returns Maximum version number
|
|
162
|
+
*/
|
|
163
|
+
private get_max_role_version(role_ids: number[]): number {
|
|
164
|
+
if (role_ids.length === 0) {
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let max_version = 0;
|
|
169
|
+
for (const role_id of role_ids) {
|
|
170
|
+
const version = this.role_version_map.get(role_id) || 0;
|
|
171
|
+
max_version = Math.max(max_version, version);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return max_version;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Gets cache statistics
|
|
179
|
+
* @returns Object with cache size, max size, and hit rate estimate
|
|
180
|
+
*/
|
|
181
|
+
get_stats(): {
|
|
182
|
+
size: number;
|
|
183
|
+
max_size: number;
|
|
184
|
+
} {
|
|
185
|
+
return {
|
|
186
|
+
size: this.cache.size,
|
|
187
|
+
max_size: this.max_size,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// section: singleton
|
|
193
|
+
// Global cache instance (initialized with defaults, will be configured on first use)
|
|
194
|
+
let auth_cache_instance: AuthCache | null = null;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Gets or creates the global auth cache instance
|
|
198
|
+
* @param max_size - Maximum cache size (default: 10000)
|
|
199
|
+
* @param ttl_minutes - TTL in minutes (default: 15)
|
|
200
|
+
* @param max_age_minutes - Max age in minutes (default: 30)
|
|
201
|
+
* @returns Auth cache instance
|
|
202
|
+
*/
|
|
203
|
+
export function get_auth_cache(
|
|
204
|
+
max_size: number = 10000,
|
|
205
|
+
ttl_minutes: number = 15,
|
|
206
|
+
max_age_minutes: number = 30,
|
|
207
|
+
): AuthCache {
|
|
208
|
+
if (!auth_cache_instance) {
|
|
209
|
+
auth_cache_instance = new AuthCache(max_size, ttl_minutes, max_age_minutes);
|
|
210
|
+
}
|
|
211
|
+
return auth_cache_instance;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Resets the global cache instance (useful for testing)
|
|
216
|
+
*/
|
|
217
|
+
export function reset_auth_cache(): void {
|
|
218
|
+
auth_cache_instance = null;
|
|
219
|
+
}
|
|
220
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// file_description: Simple in-memory rate limiter for hazo_get_auth API endpoint
|
|
2
|
+
// section: types
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rate limit entry structure
|
|
6
|
+
*/
|
|
7
|
+
type RateLimitEntry = {
|
|
8
|
+
count: number;
|
|
9
|
+
window_start: number; // Unix timestamp in milliseconds
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Simple in-memory rate limiter
|
|
14
|
+
* Tracks request counts per key within a time window
|
|
15
|
+
*/
|
|
16
|
+
class RateLimiter {
|
|
17
|
+
private limits: Map<string, RateLimitEntry>;
|
|
18
|
+
private window_ms: number; // 1 minute = 60000ms
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
this.limits = new Map();
|
|
22
|
+
this.window_ms = 60 * 1000; // 1 minute window
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Checks if a request should be allowed
|
|
27
|
+
* @param key - Rate limit key (e.g., "user:123" or "ip:192.168.1.1")
|
|
28
|
+
* @param max_requests - Maximum requests allowed per window
|
|
29
|
+
* @returns true if allowed, false if rate limited
|
|
30
|
+
*/
|
|
31
|
+
check(key: string, max_requests: number): boolean {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const entry = this.limits.get(key);
|
|
34
|
+
|
|
35
|
+
if (!entry) {
|
|
36
|
+
// First request for this key
|
|
37
|
+
this.limits.set(key, {
|
|
38
|
+
count: 1,
|
|
39
|
+
window_start: now,
|
|
40
|
+
});
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if window has expired
|
|
45
|
+
if (now - entry.window_start >= this.window_ms) {
|
|
46
|
+
// Reset window
|
|
47
|
+
this.limits.set(key, {
|
|
48
|
+
count: 1,
|
|
49
|
+
window_start: now,
|
|
50
|
+
});
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if limit exceeded
|
|
55
|
+
if (entry.count >= max_requests) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Increment count
|
|
60
|
+
entry.count++;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Cleans up old entries (call periodically to prevent memory leak)
|
|
66
|
+
* Removes entries older than 2 windows
|
|
67
|
+
*/
|
|
68
|
+
cleanup(): void {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const cutoff = now - 2 * this.window_ms;
|
|
71
|
+
|
|
72
|
+
const keys_to_delete: string[] = [];
|
|
73
|
+
for (const [key, entry] of this.limits.entries()) {
|
|
74
|
+
if (entry.window_start < cutoff) {
|
|
75
|
+
keys_to_delete.push(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const key of keys_to_delete) {
|
|
80
|
+
this.limits.delete(key);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gets rate limit statistics
|
|
86
|
+
* @returns Object with current limit entries count
|
|
87
|
+
*/
|
|
88
|
+
get_stats(): { active_limits: number } {
|
|
89
|
+
return {
|
|
90
|
+
active_limits: this.limits.size,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// section: singleton
|
|
96
|
+
// Global rate limiter instance
|
|
97
|
+
let rate_limiter_instance: RateLimiter | null = null;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Gets or creates the global rate limiter instance
|
|
101
|
+
* @returns Rate limiter instance
|
|
102
|
+
*/
|
|
103
|
+
export function get_rate_limiter(): RateLimiter {
|
|
104
|
+
if (!rate_limiter_instance) {
|
|
105
|
+
rate_limiter_instance = new RateLimiter();
|
|
106
|
+
|
|
107
|
+
// Cleanup old entries every 5 minutes
|
|
108
|
+
setInterval(() => {
|
|
109
|
+
rate_limiter_instance?.cleanup();
|
|
110
|
+
}, 5 * 60 * 1000);
|
|
111
|
+
}
|
|
112
|
+
return rate_limiter_instance;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resets the global rate limiter instance (useful for testing)
|
|
117
|
+
*/
|
|
118
|
+
export function reset_rate_limiter(): void {
|
|
119
|
+
rate_limiter_instance = null;
|
|
120
|
+
}
|
|
121
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// file_description: Type definitions and error classes for hazo_get_auth utility
|
|
2
|
+
// section: types
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* User data structure returned by hazo_get_auth
|
|
6
|
+
*/
|
|
7
|
+
export type HazoAuthUser = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string | null;
|
|
10
|
+
email_address: string;
|
|
11
|
+
is_active: boolean;
|
|
12
|
+
profile_picture_url: string | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Scope access information returned when HRBAC scope checking is used
|
|
17
|
+
*/
|
|
18
|
+
export type ScopeAccessInfo = {
|
|
19
|
+
scope_type: string;
|
|
20
|
+
scope_id: string;
|
|
21
|
+
scope_seq: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Result type for hazo_get_auth function
|
|
26
|
+
* Returns authenticated state with user data and permissions, or unauthenticated state
|
|
27
|
+
* Optionally includes scope access information when HRBAC is used
|
|
28
|
+
*/
|
|
29
|
+
export type HazoAuthResult =
|
|
30
|
+
| {
|
|
31
|
+
authenticated: true;
|
|
32
|
+
user: HazoAuthUser;
|
|
33
|
+
permissions: string[];
|
|
34
|
+
permission_ok: boolean;
|
|
35
|
+
missing_permissions?: string[];
|
|
36
|
+
// HRBAC scope access fields (only present when scope options are provided)
|
|
37
|
+
scope_ok?: boolean;
|
|
38
|
+
scope_access_via?: ScopeAccessInfo;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
authenticated: false;
|
|
42
|
+
user: null;
|
|
43
|
+
permissions: [];
|
|
44
|
+
permission_ok: false;
|
|
45
|
+
scope_ok?: false;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Options for hazo_get_auth function
|
|
50
|
+
*/
|
|
51
|
+
export type HazoAuthOptions = {
|
|
52
|
+
/**
|
|
53
|
+
* Array of required permissions to check
|
|
54
|
+
* If provided, permission_ok will be set based on whether user has all required permissions
|
|
55
|
+
*/
|
|
56
|
+
required_permissions?: string[];
|
|
57
|
+
/**
|
|
58
|
+
* If true, throws PermissionError when user lacks required permissions
|
|
59
|
+
* If false (default), returns permission_ok: false without throwing
|
|
60
|
+
*/
|
|
61
|
+
strict?: boolean;
|
|
62
|
+
// HRBAC (Hierarchical Role-Based Access Control) options
|
|
63
|
+
/**
|
|
64
|
+
* The scope level to check access for (e.g., "hazo_scopes_l3")
|
|
65
|
+
* If provided along with scope_id or scope_seq, enables HRBAC checking
|
|
66
|
+
*/
|
|
67
|
+
scope_type?: string;
|
|
68
|
+
/**
|
|
69
|
+
* The scope ID (UUID) to check access for
|
|
70
|
+
* Takes precedence over scope_seq if both provided
|
|
71
|
+
*/
|
|
72
|
+
scope_id?: string;
|
|
73
|
+
/**
|
|
74
|
+
* The scope seq (friendly ID like "L3_001") to check access for
|
|
75
|
+
* Used if scope_id is not provided
|
|
76
|
+
*/
|
|
77
|
+
scope_seq?: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Custom error class for permission denials
|
|
82
|
+
* Includes technical and user-friendly error messages
|
|
83
|
+
*/
|
|
84
|
+
export class PermissionError extends Error {
|
|
85
|
+
constructor(
|
|
86
|
+
public missing_permissions: string[],
|
|
87
|
+
public user_permissions: string[],
|
|
88
|
+
public required_permissions: string[],
|
|
89
|
+
public user_friendly_message?: string,
|
|
90
|
+
) {
|
|
91
|
+
super(`Missing permissions: ${missing_permissions.join(", ")}`);
|
|
92
|
+
this.name = "PermissionError";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Custom error class for scope access denials in HRBAC
|
|
98
|
+
* Thrown when strict mode is enabled and user lacks access to required scope
|
|
99
|
+
*/
|
|
100
|
+
export class ScopeAccessError extends Error {
|
|
101
|
+
constructor(
|
|
102
|
+
public scope_type: string,
|
|
103
|
+
public scope_identifier: string,
|
|
104
|
+
public user_scopes: Array<{ scope_type: string; scope_id: string; scope_seq: string }>,
|
|
105
|
+
) {
|
|
106
|
+
super(`Access denied to scope: ${scope_type} / ${scope_identifier}`);
|
|
107
|
+
this.name = "ScopeAccessError";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// file_description: server-side authentication utilities for checking login status in API routes
|
|
2
|
+
// section: imports
|
|
3
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
4
|
+
import { get_hazo_connect_instance } from "../hazo_connect_instance.server";
|
|
5
|
+
import { createCrudService } from "hazo_connect/server";
|
|
6
|
+
import { map_db_source_to_ui } from "../services/profile_picture_source_mapper";
|
|
7
|
+
|
|
8
|
+
// section: types
|
|
9
|
+
export type AuthUser = {
|
|
10
|
+
authenticated: true;
|
|
11
|
+
user_id: string;
|
|
12
|
+
email: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
email_verified: boolean;
|
|
15
|
+
is_active: boolean;
|
|
16
|
+
last_logon?: string;
|
|
17
|
+
profile_picture_url?: string;
|
|
18
|
+
profile_source?: "upload" | "library" | "gravatar" | "custom";
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type AuthResult =
|
|
22
|
+
| AuthUser
|
|
23
|
+
| { authenticated: false };
|
|
24
|
+
|
|
25
|
+
// section: helpers
|
|
26
|
+
/**
|
|
27
|
+
* Clears authentication cookies from response
|
|
28
|
+
* @param response - NextResponse object to clear cookies from
|
|
29
|
+
* @returns The response with cleared cookies
|
|
30
|
+
*/
|
|
31
|
+
function clear_auth_cookies(response: NextResponse): NextResponse {
|
|
32
|
+
response.cookies.set("hazo_auth_user_email", "", {
|
|
33
|
+
expires: new Date(0),
|
|
34
|
+
path: "/",
|
|
35
|
+
});
|
|
36
|
+
response.cookies.set("hazo_auth_user_id", "", {
|
|
37
|
+
expires: new Date(0),
|
|
38
|
+
path: "/",
|
|
39
|
+
});
|
|
40
|
+
return response;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// section: functions
|
|
44
|
+
/**
|
|
45
|
+
* Checks if a user is authenticated from request cookies
|
|
46
|
+
* Validates user exists, is active, and cookies match
|
|
47
|
+
* @param request - NextRequest object
|
|
48
|
+
* @returns AuthResult with user info or authenticated: false
|
|
49
|
+
*/
|
|
50
|
+
export async function get_authenticated_user(request: NextRequest): Promise<AuthResult> {
|
|
51
|
+
const user_id = request.cookies.get("hazo_auth_user_id")?.value;
|
|
52
|
+
const user_email = request.cookies.get("hazo_auth_user_email")?.value;
|
|
53
|
+
|
|
54
|
+
if (!user_id || !user_email) {
|
|
55
|
+
return { authenticated: false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const hazoConnect = get_hazo_connect_instance();
|
|
60
|
+
const users_service = createCrudService(hazoConnect, "hazo_users");
|
|
61
|
+
|
|
62
|
+
const users = await users_service.findBy({
|
|
63
|
+
id: user_id,
|
|
64
|
+
email_address: user_email,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!Array.isArray(users) || users.length === 0) {
|
|
68
|
+
return { authenticated: false };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const user = users[0];
|
|
72
|
+
|
|
73
|
+
// Check if user is active
|
|
74
|
+
if (user.is_active === false) {
|
|
75
|
+
return { authenticated: false };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Map database profile_source to UI representation
|
|
79
|
+
const profile_source_db = user.profile_source as string | null | undefined;
|
|
80
|
+
const profile_source_ui = profile_source_db ? map_db_source_to_ui(profile_source_db) : undefined;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
authenticated: true,
|
|
84
|
+
user_id: user.id as string,
|
|
85
|
+
email: user.email_address as string,
|
|
86
|
+
name: (user.name as string | null | undefined) || undefined,
|
|
87
|
+
email_verified: user.email_verified === true,
|
|
88
|
+
is_active: user.is_active === true,
|
|
89
|
+
last_logon: (user.last_logon as string | null | undefined) || undefined,
|
|
90
|
+
profile_picture_url: (user.profile_picture_url as string | null | undefined) || undefined,
|
|
91
|
+
profile_source: profile_source_ui,
|
|
92
|
+
};
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return { authenticated: false };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Checks if user is authenticated (simple boolean check)
|
|
100
|
+
* @param request - NextRequest object
|
|
101
|
+
* @returns true if authenticated, false otherwise
|
|
102
|
+
*/
|
|
103
|
+
export async function is_authenticated(request: NextRequest): Promise<boolean> {
|
|
104
|
+
const result = await get_authenticated_user(request);
|
|
105
|
+
return result.authenticated;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Requires authentication - throws error if not authenticated
|
|
110
|
+
* Use in API routes that require authentication
|
|
111
|
+
* @param request - NextRequest object
|
|
112
|
+
* @returns AuthUser (never returns authenticated: false, throws instead)
|
|
113
|
+
* @throws Error if not authenticated
|
|
114
|
+
*/
|
|
115
|
+
export async function require_auth(request: NextRequest): Promise<AuthUser> {
|
|
116
|
+
const result = await get_authenticated_user(request);
|
|
117
|
+
|
|
118
|
+
if (!result.authenticated) {
|
|
119
|
+
throw new Error("Authentication required");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Gets authenticated user and returns response with cleared cookies if invalid
|
|
127
|
+
* Useful for /api/auth/me endpoint that needs to clear cookies on invalid auth
|
|
128
|
+
* @param request - NextRequest object
|
|
129
|
+
* @returns Object with auth_result and response (with cleared cookies if invalid)
|
|
130
|
+
*/
|
|
131
|
+
export async function get_authenticated_user_with_response(request: NextRequest): Promise<{
|
|
132
|
+
auth_result: AuthResult;
|
|
133
|
+
response?: NextResponse;
|
|
134
|
+
}> {
|
|
135
|
+
const user_id = request.cookies.get("hazo_auth_user_id")?.value;
|
|
136
|
+
const user_email = request.cookies.get("hazo_auth_user_email")?.value;
|
|
137
|
+
|
|
138
|
+
if (!user_id || !user_email) {
|
|
139
|
+
return { auth_result: { authenticated: false } };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const hazoConnect = get_hazo_connect_instance();
|
|
144
|
+
const users_service = createCrudService(hazoConnect, "hazo_users");
|
|
145
|
+
|
|
146
|
+
const users = await users_service.findBy({
|
|
147
|
+
id: user_id,
|
|
148
|
+
email_address: user_email,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (!Array.isArray(users) || users.length === 0) {
|
|
152
|
+
// User not found - clear cookies
|
|
153
|
+
const response = NextResponse.json(
|
|
154
|
+
{ authenticated: false },
|
|
155
|
+
{ status: 200 }
|
|
156
|
+
);
|
|
157
|
+
clear_auth_cookies(response);
|
|
158
|
+
return { auth_result: { authenticated: false }, response };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const user = users[0];
|
|
162
|
+
|
|
163
|
+
// Check if user is still active
|
|
164
|
+
if (user.is_active === false) {
|
|
165
|
+
// User is inactive - clear cookies
|
|
166
|
+
const response = NextResponse.json(
|
|
167
|
+
{ authenticated: false },
|
|
168
|
+
{ status: 200 }
|
|
169
|
+
);
|
|
170
|
+
clear_auth_cookies(response);
|
|
171
|
+
return { auth_result: { authenticated: false }, response };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Map database profile_source to UI representation
|
|
175
|
+
const profile_source_db = user.profile_source as string | null | undefined;
|
|
176
|
+
const profile_source_ui = profile_source_db ? map_db_source_to_ui(profile_source_db) : undefined;
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
auth_result: {
|
|
180
|
+
authenticated: true,
|
|
181
|
+
user_id: user.id as string,
|
|
182
|
+
email: user.email_address as string,
|
|
183
|
+
name: (user.name as string | null | undefined) || undefined,
|
|
184
|
+
email_verified: user.email_verified === true,
|
|
185
|
+
is_active: user.is_active === true,
|
|
186
|
+
last_logon: (user.last_logon as string | null | undefined) || undefined,
|
|
187
|
+
profile_picture_url: (user.profile_picture_url as string | null | undefined) || undefined,
|
|
188
|
+
profile_source: profile_source_ui,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
} catch (error) {
|
|
192
|
+
// On error, assume not authenticated
|
|
193
|
+
return { auth_result: { authenticated: false } };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|