hazo_auth 4.2.0 → 4.4.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/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 +117 -0
- package/cli-src/lib/auth/auth_utils.server.ts +196 -0
- package/cli-src/lib/auth/dev_lock_validator.edge.ts +171 -0
- package/cli-src/lib/auth/hazo_get_auth.server.ts +583 -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/org_cache.ts +148 -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 +92 -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 +243 -0
- package/cli-src/lib/dev_lock_config.server.ts +148 -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/multi_tenancy_config.server.ts +94 -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/org_service.ts +965 -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 +178 -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/cli-src/server/types/app_types.ts +74 -0
- package/cli-src/server/types/express.d.ts +16 -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/dev_lock/index.d.ts +29 -0
- package/dist/components/layouts/dev_lock/index.d.ts.map +1 -0
- package/dist/components/layouts/dev_lock/index.js +60 -0
- package/dist/components/layouts/index.d.ts +2 -0
- package/dist/components/layouts/index.d.ts.map +1 -1
- package/dist/components/layouts/index.js +1 -0
- package/dist/components/layouts/org_management/index.d.ts +26 -0
- package/dist/components/layouts/org_management/index.d.ts.map +1 -0
- package/dist/components/layouts/org_management/index.js +75 -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/user_management/components/org_hierarchy_tab.d.ts +13 -0
- package/dist/components/layouts/user_management/components/org_hierarchy_tab.d.ts.map +1 -0
- package/dist/components/layouts/user_management/components/org_hierarchy_tab.js +276 -0
- package/dist/components/layouts/user_management/index.d.ts +3 -1
- package/dist/components/layouts/user_management/index.d.ts.map +1 -1
- package/dist/components/layouts/user_management/index.js +10 -4
- package/dist/lib/auth/auth_types.d.ts +6 -0
- package/dist/lib/auth/auth_types.d.ts.map +1 -1
- package/dist/lib/auth/dev_lock_validator.edge.d.ts +38 -0
- package/dist/lib/auth/dev_lock_validator.edge.d.ts.map +1 -0
- package/dist/lib/auth/dev_lock_validator.edge.js +122 -0
- package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
- package/dist/lib/auth/hazo_get_auth.server.js +61 -1
- package/dist/lib/auth/org_cache.d.ts +65 -0
- package/dist/lib/auth/org_cache.d.ts.map +1 -0
- package/dist/lib/auth/org_cache.js +103 -0
- package/dist/lib/config/default_config.d.ts +76 -0
- package/dist/lib/config/default_config.d.ts.map +1 -1
- package/dist/lib/config/default_config.js +42 -0
- package/dist/lib/dev_lock_config.server.d.ts +41 -0
- package/dist/lib/dev_lock_config.server.d.ts.map +1 -0
- package/dist/lib/dev_lock_config.server.js +50 -0
- package/dist/lib/multi_tenancy_config.server.d.ts +30 -0
- package/dist/lib/multi_tenancy_config.server.d.ts.map +1 -0
- package/dist/lib/multi_tenancy_config.server.js +41 -0
- package/dist/lib/services/org_service.d.ts +191 -0
- package/dist/lib/services/org_service.d.ts.map +1 -0
- package/dist/lib/services/org_service.js +746 -0
- package/dist/lib/utils/password_validator.d.ts +7 -1
- package/dist/lib/utils/password_validator.d.ts.map +1 -1
- package/dist/page_components/dev_lock.d.ts +11 -0
- package/dist/page_components/dev_lock.d.ts.map +1 -0
- package/dist/page_components/dev_lock.js +17 -0
- package/dist/page_components/index.d.ts +1 -0
- package/dist/page_components/index.d.ts.map +1 -1
- package/dist/page_components/index.js +1 -0
- package/dist/page_components/org_management.d.ts +27 -0
- package/dist/page_components/org_management.d.ts.map +1 -0
- package/dist/page_components/org_management.js +18 -0
- package/hazo_auth_config.example.ini +30 -0
- package/package.json +27 -3
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// file_description: service for creating and validating JWT session tokens for authentication
|
|
2
|
+
// Uses jose library for Edge-compatible JWT operations
|
|
3
|
+
// section: imports
|
|
4
|
+
import { SignJWT, jwtVerify } from "jose";
|
|
5
|
+
import { create_app_logger } from "../app_logger.js";
|
|
6
|
+
import { get_filename, get_line_number } from "../utils/api_route_helpers.js";
|
|
7
|
+
|
|
8
|
+
// section: types
|
|
9
|
+
export type SessionTokenPayload = {
|
|
10
|
+
user_id: string;
|
|
11
|
+
email: string;
|
|
12
|
+
iat: number;
|
|
13
|
+
exp: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type ValidateSessionTokenResult = {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
user_id?: string;
|
|
19
|
+
email?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// section: helpers
|
|
23
|
+
/**
|
|
24
|
+
* Gets JWT secret from environment variables
|
|
25
|
+
* @returns JWT secret as Uint8Array for jose library
|
|
26
|
+
* @throws Error if JWT_SECRET is not set
|
|
27
|
+
*/
|
|
28
|
+
function get_jwt_secret(): Uint8Array {
|
|
29
|
+
const jwt_secret = process.env.JWT_SECRET;
|
|
30
|
+
|
|
31
|
+
if (!jwt_secret) {
|
|
32
|
+
const logger = create_app_logger();
|
|
33
|
+
logger.error("session_token_jwt_secret_missing", {
|
|
34
|
+
filename: get_filename(),
|
|
35
|
+
line_number: get_line_number(),
|
|
36
|
+
error: "JWT_SECRET environment variable is required",
|
|
37
|
+
});
|
|
38
|
+
throw new Error("JWT_SECRET environment variable is required");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Convert string secret to Uint8Array for jose
|
|
42
|
+
return new TextEncoder().encode(jwt_secret);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Gets session token expiry in seconds (default: 30 days)
|
|
47
|
+
* @returns Number of seconds until token expires
|
|
48
|
+
*/
|
|
49
|
+
function get_session_token_expiry_seconds(): number {
|
|
50
|
+
// Default: 30 days = 30 * 24 * 60 * 60 = 2,592,000 seconds
|
|
51
|
+
const default_expiry_seconds = 60 * 60 * 24 * 30;
|
|
52
|
+
|
|
53
|
+
// Could be extended to read from config in the future
|
|
54
|
+
// For now, use default 30 days to match cookie expiry
|
|
55
|
+
return default_expiry_seconds;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// section: main_functions
|
|
59
|
+
/**
|
|
60
|
+
* Creates a JWT session token for a user
|
|
61
|
+
* Token includes user_id, email, issued at time, and expiration
|
|
62
|
+
* @param user_id - User ID
|
|
63
|
+
* @param email - User email address
|
|
64
|
+
* @returns JWT token string
|
|
65
|
+
*/
|
|
66
|
+
export async function create_session_token(
|
|
67
|
+
user_id: string,
|
|
68
|
+
email: string,
|
|
69
|
+
): Promise<string> {
|
|
70
|
+
const logger = create_app_logger();
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const secret = get_jwt_secret();
|
|
74
|
+
const now = Math.floor(Date.now() / 1000); // Current time in seconds
|
|
75
|
+
const expiry_seconds = get_session_token_expiry_seconds();
|
|
76
|
+
const exp = now + expiry_seconds;
|
|
77
|
+
|
|
78
|
+
const jwt = await new SignJWT({
|
|
79
|
+
user_id,
|
|
80
|
+
email,
|
|
81
|
+
})
|
|
82
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
83
|
+
.setIssuedAt(now)
|
|
84
|
+
.setExpirationTime(exp)
|
|
85
|
+
.sign(secret);
|
|
86
|
+
|
|
87
|
+
logger.info("session_token_created", {
|
|
88
|
+
filename: get_filename(),
|
|
89
|
+
line_number: get_line_number(),
|
|
90
|
+
user_id,
|
|
91
|
+
email,
|
|
92
|
+
expires_in_seconds: expiry_seconds,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return jwt;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const error_message = error instanceof Error ? error.message : "Unknown error";
|
|
98
|
+
const error_stack = error instanceof Error ? error.stack : undefined;
|
|
99
|
+
|
|
100
|
+
logger.error("session_token_creation_failed", {
|
|
101
|
+
filename: get_filename(),
|
|
102
|
+
line_number: get_line_number(),
|
|
103
|
+
user_id,
|
|
104
|
+
email,
|
|
105
|
+
error_message,
|
|
106
|
+
error_stack,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
throw new Error("Failed to create session token");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validates a JWT session token
|
|
115
|
+
* Checks signature and expiration
|
|
116
|
+
* @param token - JWT token string
|
|
117
|
+
* @returns Validation result with user_id and email if valid
|
|
118
|
+
*/
|
|
119
|
+
export async function validate_session_token(
|
|
120
|
+
token: string,
|
|
121
|
+
): Promise<ValidateSessionTokenResult> {
|
|
122
|
+
const logger = create_app_logger();
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const secret = get_jwt_secret();
|
|
126
|
+
|
|
127
|
+
const { payload } = await jwtVerify(token, secret, {
|
|
128
|
+
algorithms: ["HS256"],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Extract user_id and email from payload
|
|
132
|
+
const user_id = payload.user_id as string;
|
|
133
|
+
const email = payload.email as string;
|
|
134
|
+
|
|
135
|
+
if (!user_id || !email) {
|
|
136
|
+
logger.warn("session_token_invalid_payload", {
|
|
137
|
+
filename: get_filename(),
|
|
138
|
+
line_number: get_line_number(),
|
|
139
|
+
error: "Token payload missing user_id or email",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return { valid: false };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
logger.info("session_token_validated", {
|
|
146
|
+
filename: get_filename(),
|
|
147
|
+
line_number: get_line_number(),
|
|
148
|
+
user_id,
|
|
149
|
+
email,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
valid: true,
|
|
154
|
+
user_id,
|
|
155
|
+
email,
|
|
156
|
+
};
|
|
157
|
+
} catch (error) {
|
|
158
|
+
const error_message = error instanceof Error ? error.message : "Unknown error";
|
|
159
|
+
|
|
160
|
+
// jose throws JWTExpired, JWTInvalid, etc. - these are expected for invalid tokens
|
|
161
|
+
logger.debug("session_token_validation_failed", {
|
|
162
|
+
filename: get_filename(),
|
|
163
|
+
line_number: get_line_number(),
|
|
164
|
+
error_message,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return { valid: false };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// file_description: shared service for creating and managing tokens in hazo_refresh_tokens table
|
|
2
|
+
// section: imports
|
|
3
|
+
import type { HazoConnectAdapter } from "hazo_connect";
|
|
4
|
+
import { createCrudService } from "hazo_connect/server";
|
|
5
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
6
|
+
import argon2 from "argon2";
|
|
7
|
+
import { read_config_section } from "../config/config_loader.server.js";
|
|
8
|
+
import { create_app_logger } from "../app_logger.js";
|
|
9
|
+
|
|
10
|
+
// section: types
|
|
11
|
+
export type TokenType = "refresh" | "password_reset" | "email_verification";
|
|
12
|
+
|
|
13
|
+
export type CreateTokenParams = {
|
|
14
|
+
adapter: HazoConnectAdapter;
|
|
15
|
+
user_id: string;
|
|
16
|
+
token_type: TokenType;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type CreateTokenResult = {
|
|
20
|
+
success: boolean;
|
|
21
|
+
raw_token?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// section: helpers
|
|
26
|
+
/**
|
|
27
|
+
* Gets token expiry hours from hazo_auth_config.ini for a specific token type
|
|
28
|
+
* Falls back to defaults if config is not found
|
|
29
|
+
* @param token_type - The type of token (refresh, password_reset, email_verification)
|
|
30
|
+
* @returns Number of hours until token expires
|
|
31
|
+
*/
|
|
32
|
+
function get_token_expiry_hours(token_type: TokenType): number {
|
|
33
|
+
const default_expiries: Record<TokenType, number> = {
|
|
34
|
+
refresh: 720, // 30 days
|
|
35
|
+
password_reset: 0.167, // 10 minutes
|
|
36
|
+
email_verification: 48, // 48 hours
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const logger = create_app_logger();
|
|
40
|
+
const token_config_section = read_config_section("hazo_auth__tokens");
|
|
41
|
+
|
|
42
|
+
// Get expiry from config or environment variable or default
|
|
43
|
+
const config_key = `${token_type}_expiry_hours`;
|
|
44
|
+
const env_key = `HAZO_AUTH_${token_type.toUpperCase()}_TOKEN_EXPIRY_HOURS`;
|
|
45
|
+
|
|
46
|
+
const expiry_hours =
|
|
47
|
+
token_config_section?.[config_key] ||
|
|
48
|
+
process.env[env_key] ||
|
|
49
|
+
default_expiries[token_type];
|
|
50
|
+
|
|
51
|
+
return parseFloat(String(expiry_hours)) || default_expiries[token_type];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a token for a user and stores it in hazo_refresh_tokens table
|
|
56
|
+
* Invalidates any existing tokens of the same type for the user before creating a new one
|
|
57
|
+
* @param params - Token creation parameters (adapter, user_id, token_type)
|
|
58
|
+
* @returns Token creation result with raw_token (for sending to user) or error
|
|
59
|
+
*/
|
|
60
|
+
export async function create_token(
|
|
61
|
+
params: CreateTokenParams,
|
|
62
|
+
): Promise<CreateTokenResult> {
|
|
63
|
+
try {
|
|
64
|
+
const { adapter, user_id, token_type } = params;
|
|
65
|
+
|
|
66
|
+
// Create CRUD service for hazo_refresh_tokens table
|
|
67
|
+
const tokens_service = createCrudService(adapter, "hazo_refresh_tokens");
|
|
68
|
+
|
|
69
|
+
// Invalidate any existing tokens of this type for this user
|
|
70
|
+
// If token_type column doesn't exist, this will fail - catch and continue
|
|
71
|
+
let existing_tokens: unknown[] = [];
|
|
72
|
+
try {
|
|
73
|
+
existing_tokens = (await tokens_service.findBy({
|
|
74
|
+
user_id: user_id,
|
|
75
|
+
token_type: token_type,
|
|
76
|
+
})) as unknown[];
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// If token_type column doesn't exist, try without it
|
|
79
|
+
// This is a fallback for databases that haven't had the migration applied
|
|
80
|
+
const logger = create_app_logger();
|
|
81
|
+
const error_message = error instanceof Error ? error.message : "Unknown error";
|
|
82
|
+
logger.warn("token_service_token_type_column_missing", {
|
|
83
|
+
filename: "token_service.ts",
|
|
84
|
+
line_number: 0,
|
|
85
|
+
user_id,
|
|
86
|
+
token_type,
|
|
87
|
+
error: error_message,
|
|
88
|
+
note: "token_type column may not exist, trying without filter",
|
|
89
|
+
});
|
|
90
|
+
// Try to find tokens by user_id only (less precise but works without migration)
|
|
91
|
+
try {
|
|
92
|
+
existing_tokens = (await tokens_service.findBy({
|
|
93
|
+
user_id: user_id,
|
|
94
|
+
})) as unknown[];
|
|
95
|
+
} catch (fallbackError) {
|
|
96
|
+
// If that also fails, log and continue (will just create new token)
|
|
97
|
+
const fallback_error_message = fallbackError instanceof Error ? fallbackError.message : "Unknown error";
|
|
98
|
+
logger.warn("token_service_query_existing_tokens_failed", {
|
|
99
|
+
filename: "token_service.ts",
|
|
100
|
+
line_number: 0,
|
|
101
|
+
user_id,
|
|
102
|
+
error: fallback_error_message,
|
|
103
|
+
note: "Could not query existing tokens, will create new token anyway",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (Array.isArray(existing_tokens) && existing_tokens.length > 0) {
|
|
109
|
+
// Delete existing tokens (of this type if token_type exists, or all for user if not)
|
|
110
|
+
for (const token of existing_tokens) {
|
|
111
|
+
try {
|
|
112
|
+
await tokens_service.deleteById((token as { id: unknown }).id);
|
|
113
|
+
} catch (deleteError) {
|
|
114
|
+
const logger = create_app_logger();
|
|
115
|
+
const error_message = deleteError instanceof Error ? deleteError.message : "Unknown error";
|
|
116
|
+
logger.warn("token_service_delete_existing_token_failed", {
|
|
117
|
+
filename: "token_service.ts",
|
|
118
|
+
line_number: 0,
|
|
119
|
+
user_id,
|
|
120
|
+
token_id: (token as { id: unknown }).id,
|
|
121
|
+
error: error_message,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Generate a secure random token
|
|
128
|
+
const raw_token = randomBytes(32).toString("hex");
|
|
129
|
+
|
|
130
|
+
// Hash the token before storing
|
|
131
|
+
const token_hash = await argon2.hash(raw_token);
|
|
132
|
+
|
|
133
|
+
// Get expiry hours from config
|
|
134
|
+
const expiry_hours = get_token_expiry_hours(token_type);
|
|
135
|
+
|
|
136
|
+
// Calculate expiration time (convert hours to milliseconds)
|
|
137
|
+
const expires_at = new Date();
|
|
138
|
+
expires_at.setTime(expires_at.getTime() + expiry_hours * 60 * 60 * 1000);
|
|
139
|
+
|
|
140
|
+
const now = new Date().toISOString();
|
|
141
|
+
|
|
142
|
+
// Insert the token into the database
|
|
143
|
+
// Try with token_type first, fallback to without if column doesn't exist
|
|
144
|
+
let inserted_tokens: unknown[];
|
|
145
|
+
try {
|
|
146
|
+
inserted_tokens = (await tokens_service.insert({
|
|
147
|
+
id: randomUUID(),
|
|
148
|
+
user_id: user_id,
|
|
149
|
+
token_hash: token_hash,
|
|
150
|
+
token_type: token_type,
|
|
151
|
+
expires_at: expires_at.toISOString(),
|
|
152
|
+
created_at: now,
|
|
153
|
+
})) as unknown[];
|
|
154
|
+
} catch (error) {
|
|
155
|
+
// If token_type column doesn't exist, try without it
|
|
156
|
+
const logger = create_app_logger();
|
|
157
|
+
const error_message = error instanceof Error ? error.message : "Unknown error";
|
|
158
|
+
logger.warn("token_service_insert_with_token_type_failed", {
|
|
159
|
+
filename: "token_service.ts",
|
|
160
|
+
line_number: 0,
|
|
161
|
+
user_id,
|
|
162
|
+
token_type,
|
|
163
|
+
error: error_message,
|
|
164
|
+
note: "token_type column may not exist, inserting without it",
|
|
165
|
+
});
|
|
166
|
+
// Fallback: insert without token_type (will use default if column exists with default)
|
|
167
|
+
inserted_tokens = (await tokens_service.insert({
|
|
168
|
+
id: randomUUID(),
|
|
169
|
+
user_id: user_id,
|
|
170
|
+
token_hash: token_hash,
|
|
171
|
+
expires_at: expires_at.toISOString(),
|
|
172
|
+
created_at: now,
|
|
173
|
+
})) as unknown[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Verify insertion was successful
|
|
177
|
+
if (!Array.isArray(inserted_tokens) || inserted_tokens.length === 0) {
|
|
178
|
+
const logger = create_app_logger();
|
|
179
|
+
const error_msg = `Failed to create ${token_type} token - no rows inserted`;
|
|
180
|
+
logger.error("token_service_insertion_failed", {
|
|
181
|
+
filename: "token_service.ts",
|
|
182
|
+
line_number: 0,
|
|
183
|
+
user_id,
|
|
184
|
+
token_type,
|
|
185
|
+
error: error_msg,
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
success: false,
|
|
189
|
+
error: error_msg,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const logger = create_app_logger();
|
|
194
|
+
logger.info("token_service_token_created", {
|
|
195
|
+
filename: "token_service.ts",
|
|
196
|
+
line_number: 0,
|
|
197
|
+
user_id,
|
|
198
|
+
token_type,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Log raw token and test URLs in debug mode (logger handles dev mode)
|
|
202
|
+
logger.debug("token_service_raw_token", {
|
|
203
|
+
filename: "token_service.ts",
|
|
204
|
+
line_number: 0,
|
|
205
|
+
user_id,
|
|
206
|
+
token_type,
|
|
207
|
+
raw_token,
|
|
208
|
+
test_url: token_type === "email_verification"
|
|
209
|
+
? `/hazo_auth/verify_email?token=${raw_token}`
|
|
210
|
+
: token_type === "password_reset"
|
|
211
|
+
? `/hazo_auth/reset_password?token=${raw_token}`
|
|
212
|
+
: undefined,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
raw_token,
|
|
218
|
+
};
|
|
219
|
+
} catch (error) {
|
|
220
|
+
const logger = create_app_logger();
|
|
221
|
+
const error_message =
|
|
222
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
223
|
+
const error_stack = error instanceof Error ? error.stack : undefined;
|
|
224
|
+
|
|
225
|
+
logger.error("token_service_create_token_error", {
|
|
226
|
+
filename: "token_service.ts",
|
|
227
|
+
line_number: 0,
|
|
228
|
+
user_id: params.user_id,
|
|
229
|
+
token_type: params.token_type,
|
|
230
|
+
error: error_message,
|
|
231
|
+
error_stack,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
success: false,
|
|
236
|
+
error: error_message,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// file_description: LRU cache implementation for hazo_get_user_profiles with TTL and size limits
|
|
2
|
+
// section: imports
|
|
3
|
+
import type { UserProfileInfo } from "./user_profiles_service";
|
|
4
|
+
|
|
5
|
+
// section: types
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Cache entry structure for user profiles
|
|
9
|
+
*/
|
|
10
|
+
type ProfileCacheEntry = {
|
|
11
|
+
profile: UserProfileInfo;
|
|
12
|
+
timestamp: number; // Unix timestamp in milliseconds
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// section: cache_class
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* LRU cache implementation with TTL and size limits for user profiles
|
|
19
|
+
* Uses Map to maintain insertion order for LRU eviction
|
|
20
|
+
*/
|
|
21
|
+
class UserProfilesCache {
|
|
22
|
+
private cache: Map<string, ProfileCacheEntry>;
|
|
23
|
+
private max_size: number;
|
|
24
|
+
private ttl_ms: number;
|
|
25
|
+
|
|
26
|
+
constructor(max_size: number, ttl_minutes: number) {
|
|
27
|
+
this.cache = new Map();
|
|
28
|
+
this.max_size = max_size;
|
|
29
|
+
this.ttl_ms = ttl_minutes * 60 * 1000;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Gets a cached profile for a user
|
|
34
|
+
* Returns undefined if not found or expired
|
|
35
|
+
* @param user_id - User ID to look up
|
|
36
|
+
* @returns Profile or undefined
|
|
37
|
+
*/
|
|
38
|
+
get(user_id: string): UserProfileInfo | undefined {
|
|
39
|
+
const entry = this.cache.get(user_id);
|
|
40
|
+
|
|
41
|
+
if (!entry) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const age = now - entry.timestamp;
|
|
47
|
+
|
|
48
|
+
// Check if entry is expired
|
|
49
|
+
if (age > this.ttl_ms) {
|
|
50
|
+
this.cache.delete(user_id);
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Move to end (most recently used)
|
|
55
|
+
this.cache.delete(user_id);
|
|
56
|
+
this.cache.set(user_id, entry);
|
|
57
|
+
|
|
58
|
+
return entry.profile;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Gets multiple profiles from cache
|
|
63
|
+
* Returns object with found profiles and missing IDs
|
|
64
|
+
* @param user_ids - Array of user IDs to look up
|
|
65
|
+
* @returns Object with cached profiles and IDs not in cache
|
|
66
|
+
*/
|
|
67
|
+
get_many(user_ids: string[]): {
|
|
68
|
+
cached: UserProfileInfo[];
|
|
69
|
+
missing_ids: string[];
|
|
70
|
+
} {
|
|
71
|
+
const cached: UserProfileInfo[] = [];
|
|
72
|
+
const missing_ids: string[] = [];
|
|
73
|
+
|
|
74
|
+
for (const user_id of user_ids) {
|
|
75
|
+
const profile = this.get(user_id);
|
|
76
|
+
if (profile) {
|
|
77
|
+
cached.push(profile);
|
|
78
|
+
} else {
|
|
79
|
+
missing_ids.push(user_id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { cached, missing_ids };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Sets a cache entry for a user profile
|
|
88
|
+
* Evicts least recently used entries if cache is full
|
|
89
|
+
* @param user_id - User ID
|
|
90
|
+
* @param profile - User profile data
|
|
91
|
+
*/
|
|
92
|
+
set(user_id: string, profile: UserProfileInfo): void {
|
|
93
|
+
// Evict LRU entries if cache is full
|
|
94
|
+
while (this.cache.size >= this.max_size) {
|
|
95
|
+
const first_key = this.cache.keys().next().value;
|
|
96
|
+
if (first_key) {
|
|
97
|
+
this.cache.delete(first_key);
|
|
98
|
+
} else {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const entry: ProfileCacheEntry = {
|
|
104
|
+
profile,
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
this.cache.set(user_id, entry);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Sets multiple cache entries at once
|
|
113
|
+
* @param profiles - Array of user profiles to cache
|
|
114
|
+
*/
|
|
115
|
+
set_many(profiles: UserProfileInfo[]): void {
|
|
116
|
+
for (const profile of profiles) {
|
|
117
|
+
this.set(profile.user_id, profile);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Invalidates cache for a specific user
|
|
123
|
+
* @param user_id - User ID to invalidate
|
|
124
|
+
*/
|
|
125
|
+
invalidate_user(user_id: string): void {
|
|
126
|
+
this.cache.delete(user_id);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Invalidates cache for multiple users
|
|
131
|
+
* @param user_ids - Array of user IDs to invalidate
|
|
132
|
+
*/
|
|
133
|
+
invalidate_users(user_ids: string[]): void {
|
|
134
|
+
for (const user_id of user_ids) {
|
|
135
|
+
this.cache.delete(user_id);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Invalidates all cache entries
|
|
141
|
+
*/
|
|
142
|
+
invalidate_all(): void {
|
|
143
|
+
this.cache.clear();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Gets cache statistics
|
|
148
|
+
* @returns Object with cache size and max size
|
|
149
|
+
*/
|
|
150
|
+
get_stats(): {
|
|
151
|
+
size: number;
|
|
152
|
+
max_size: number;
|
|
153
|
+
ttl_minutes: number;
|
|
154
|
+
} {
|
|
155
|
+
return {
|
|
156
|
+
size: this.cache.size,
|
|
157
|
+
max_size: this.max_size,
|
|
158
|
+
ttl_minutes: this.ttl_ms / 60000,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// section: singleton
|
|
164
|
+
// Global cache instance (initialized with defaults, will be configured on first use)
|
|
165
|
+
let user_profiles_cache_instance: UserProfilesCache | null = null;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Gets or creates the global user profiles cache instance
|
|
169
|
+
* @param max_size - Maximum cache size (default: 5000)
|
|
170
|
+
* @param ttl_minutes - TTL in minutes (default: 5)
|
|
171
|
+
* @returns User profiles cache instance
|
|
172
|
+
*/
|
|
173
|
+
export function get_user_profiles_cache(
|
|
174
|
+
max_size: number = 5000,
|
|
175
|
+
ttl_minutes: number = 5,
|
|
176
|
+
): UserProfilesCache {
|
|
177
|
+
if (!user_profiles_cache_instance) {
|
|
178
|
+
user_profiles_cache_instance = new UserProfilesCache(max_size, ttl_minutes);
|
|
179
|
+
}
|
|
180
|
+
return user_profiles_cache_instance;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resets the global cache instance (useful for testing)
|
|
185
|
+
*/
|
|
186
|
+
export function reset_user_profiles_cache(): void {
|
|
187
|
+
user_profiles_cache_instance = null;
|
|
188
|
+
}
|
|
189
|
+
|