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,227 @@
|
|
|
1
|
+
// file_description: NextAuth.js configuration for OAuth providers
|
|
2
|
+
// section: imports
|
|
3
|
+
import type { AuthOptions, Session } from "next-auth";
|
|
4
|
+
import type { JWT } from "next-auth/jwt";
|
|
5
|
+
import GoogleProvider from "next-auth/providers/google";
|
|
6
|
+
import { get_oauth_config } from "../oauth_config.server";
|
|
7
|
+
import { handle_google_oauth_login } from "../services/oauth_service";
|
|
8
|
+
import { get_hazo_connect_instance } from "../hazo_connect_instance.server";
|
|
9
|
+
import { create_app_logger } from "../app_logger";
|
|
10
|
+
|
|
11
|
+
// section: types
|
|
12
|
+
export type NextAuthCallbackUser = {
|
|
13
|
+
id?: string;
|
|
14
|
+
email?: string | null;
|
|
15
|
+
name?: string | null;
|
|
16
|
+
image?: string | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type NextAuthCallbackAccount = {
|
|
20
|
+
provider: string;
|
|
21
|
+
providerAccountId: string;
|
|
22
|
+
type: string;
|
|
23
|
+
access_token?: string;
|
|
24
|
+
id_token?: string;
|
|
25
|
+
expires_at?: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type NextAuthCallbackProfile = {
|
|
29
|
+
sub?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
email?: string;
|
|
32
|
+
picture?: string;
|
|
33
|
+
email_verified?: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// section: config
|
|
37
|
+
/**
|
|
38
|
+
* Gets NextAuth.js configuration with enabled OAuth providers
|
|
39
|
+
* Providers are dynamically configured based on hazo_auth_config.ini settings
|
|
40
|
+
* @returns NextAuth configuration object
|
|
41
|
+
*/
|
|
42
|
+
export function get_nextauth_config(): AuthOptions {
|
|
43
|
+
const oauth_config = get_oauth_config();
|
|
44
|
+
const providers = [];
|
|
45
|
+
|
|
46
|
+
// Add Google provider if enabled
|
|
47
|
+
if (oauth_config.enable_google) {
|
|
48
|
+
const client_id = process.env.HAZO_AUTH_GOOGLE_CLIENT_ID;
|
|
49
|
+
const client_secret = process.env.HAZO_AUTH_GOOGLE_CLIENT_SECRET;
|
|
50
|
+
|
|
51
|
+
if (client_id && client_secret) {
|
|
52
|
+
providers.push(
|
|
53
|
+
GoogleProvider({
|
|
54
|
+
clientId: client_id,
|
|
55
|
+
clientSecret: client_secret,
|
|
56
|
+
authorization: {
|
|
57
|
+
params: {
|
|
58
|
+
prompt: "consent",
|
|
59
|
+
access_type: "offline",
|
|
60
|
+
response_type: "code",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
providers,
|
|
70
|
+
pages: {
|
|
71
|
+
// Use hazo_auth login page for sign-in errors
|
|
72
|
+
signIn: "/hazo_auth/login",
|
|
73
|
+
error: "/hazo_auth/login",
|
|
74
|
+
},
|
|
75
|
+
callbacks: {
|
|
76
|
+
/**
|
|
77
|
+
* Redirect callback - controls where users go after authentication
|
|
78
|
+
* We redirect to our custom callback handler to create hazo_auth session
|
|
79
|
+
*/
|
|
80
|
+
async redirect({ url, baseUrl }: { url: string; baseUrl: string }) {
|
|
81
|
+
// Log for debugging
|
|
82
|
+
console.log("[NextAuth redirect callback]", { url, baseUrl });
|
|
83
|
+
|
|
84
|
+
// Always redirect to our custom callback after sign-in to set hazo_auth cookies
|
|
85
|
+
// The callbackUrl from signIn() comes through as `url`
|
|
86
|
+
if (url.includes("/api/hazo_auth/oauth/google/callback")) {
|
|
87
|
+
return url;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// If URL is relative or same origin, allow it
|
|
91
|
+
if (url.startsWith("/")) {
|
|
92
|
+
return `${baseUrl}${url}`;
|
|
93
|
+
}
|
|
94
|
+
if (url.startsWith(baseUrl)) {
|
|
95
|
+
return url;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Default: redirect to our custom OAuth callback to set cookies
|
|
99
|
+
return `${baseUrl}/api/hazo_auth/oauth/google/callback`;
|
|
100
|
+
},
|
|
101
|
+
/**
|
|
102
|
+
* Sign-in callback - handle user creation/linking for Google OAuth
|
|
103
|
+
*/
|
|
104
|
+
async signIn({
|
|
105
|
+
account,
|
|
106
|
+
profile,
|
|
107
|
+
user,
|
|
108
|
+
}: {
|
|
109
|
+
account: NextAuthCallbackAccount | null;
|
|
110
|
+
profile?: NextAuthCallbackProfile;
|
|
111
|
+
user: NextAuthCallbackUser;
|
|
112
|
+
}) {
|
|
113
|
+
const logger = create_app_logger();
|
|
114
|
+
|
|
115
|
+
if (account?.provider === "google" && profile) {
|
|
116
|
+
try {
|
|
117
|
+
const googleProfile = profile as NextAuthCallbackProfile;
|
|
118
|
+
const hazoConnect = get_hazo_connect_instance();
|
|
119
|
+
|
|
120
|
+
logger.info("nextauth_google_signin_attempt", {
|
|
121
|
+
email: user.email,
|
|
122
|
+
google_id: googleProfile.sub,
|
|
123
|
+
name: user.name,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Handle the Google OAuth login (create user or link account)
|
|
127
|
+
const result = await handle_google_oauth_login(hazoConnect, {
|
|
128
|
+
google_id: googleProfile.sub || account.providerAccountId,
|
|
129
|
+
email: user.email || googleProfile.email || "",
|
|
130
|
+
name: user.name || googleProfile.name || undefined,
|
|
131
|
+
profile_picture_url: user.image || googleProfile.picture || undefined,
|
|
132
|
+
email_verified: googleProfile.email_verified ?? true,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (!result.success) {
|
|
136
|
+
logger.error("nextauth_google_signin_failed", {
|
|
137
|
+
email: user.email,
|
|
138
|
+
error: result.error,
|
|
139
|
+
});
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
logger.info("nextauth_google_signin_success", {
|
|
144
|
+
user_id: result.user_id,
|
|
145
|
+
email: result.email,
|
|
146
|
+
is_new_user: result.is_new_user,
|
|
147
|
+
was_linked: result.was_linked,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Store user_id in account for the JWT callback to pick up
|
|
151
|
+
(account as Record<string, unknown>).hazo_user_id = result.user_id;
|
|
152
|
+
|
|
153
|
+
return true;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
156
|
+
logger.error("nextauth_google_signin_exception", {
|
|
157
|
+
email: user.email,
|
|
158
|
+
error: errorMessage,
|
|
159
|
+
});
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
},
|
|
165
|
+
/**
|
|
166
|
+
* JWT callback - add OAuth provider info to the token
|
|
167
|
+
*/
|
|
168
|
+
async jwt({ token, account, profile }) {
|
|
169
|
+
if (account && profile) {
|
|
170
|
+
(token as Record<string, unknown>).provider = account.provider;
|
|
171
|
+
(token as Record<string, unknown>).providerAccountId = account.providerAccountId;
|
|
172
|
+
|
|
173
|
+
// Store hazo_user_id from signIn callback
|
|
174
|
+
if ((account as Record<string, unknown>).hazo_user_id) {
|
|
175
|
+
(token as Record<string, unknown>).hazo_user_id = (account as Record<string, unknown>).hazo_user_id;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// For Google, store additional profile data
|
|
179
|
+
if (account.provider === "google") {
|
|
180
|
+
const googleProfile = profile as NextAuthCallbackProfile;
|
|
181
|
+
(token as Record<string, unknown>).google_id = googleProfile.sub;
|
|
182
|
+
(token as Record<string, unknown>).email_verified = googleProfile.email_verified;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return token;
|
|
186
|
+
},
|
|
187
|
+
/**
|
|
188
|
+
* Session callback - pass provider info to session
|
|
189
|
+
*/
|
|
190
|
+
async session({ session, token }: { session: Session; token: JWT }) {
|
|
191
|
+
if (token) {
|
|
192
|
+
const extSession = session as Session & Record<string, unknown>;
|
|
193
|
+
const extToken = token as JWT & Record<string, unknown>;
|
|
194
|
+
extSession.provider = extToken.provider;
|
|
195
|
+
extSession.providerAccountId = extToken.providerAccountId;
|
|
196
|
+
extSession.google_id = extToken.google_id;
|
|
197
|
+
extSession.email_verified = extToken.email_verified;
|
|
198
|
+
}
|
|
199
|
+
return session;
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
// Use JWT strategy - we don't need a database adapter since we manage users ourselves
|
|
203
|
+
session: {
|
|
204
|
+
strategy: "jwt",
|
|
205
|
+
maxAge: 60 * 10, // 10 minutes - short lived since we create our own session
|
|
206
|
+
},
|
|
207
|
+
// Disable debug in production
|
|
208
|
+
debug: process.env.NODE_ENV === "development",
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Checks if any OAuth providers are configured and enabled
|
|
214
|
+
* @returns true if at least one OAuth provider is available
|
|
215
|
+
*/
|
|
216
|
+
export function has_oauth_providers(): boolean {
|
|
217
|
+
const oauth_config = get_oauth_config();
|
|
218
|
+
|
|
219
|
+
if (oauth_config.enable_google) {
|
|
220
|
+
const has_google_credentials =
|
|
221
|
+
process.env.HAZO_AUTH_GOOGLE_CLIENT_ID &&
|
|
222
|
+
process.env.HAZO_AUTH_GOOGLE_CLIENT_SECRET;
|
|
223
|
+
if (has_google_credentials) return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// file_description: LRU cache implementation for HRBAC scope lookups with TTL and size limits
|
|
2
|
+
// section: imports
|
|
3
|
+
import type { ScopeLevel } from "../services/scope_service";
|
|
4
|
+
|
|
5
|
+
// section: types
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* User scope assignment record
|
|
9
|
+
*/
|
|
10
|
+
export type UserScopeEntry = {
|
|
11
|
+
scope_type: ScopeLevel;
|
|
12
|
+
scope_id: string;
|
|
13
|
+
scope_seq: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Cache entry structure for user scopes
|
|
18
|
+
*/
|
|
19
|
+
type ScopeCacheEntry = {
|
|
20
|
+
user_id: string;
|
|
21
|
+
scopes: UserScopeEntry[];
|
|
22
|
+
timestamp: number; // Unix timestamp in milliseconds
|
|
23
|
+
cache_version: number; // Version number for smart invalidation
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* LRU cache implementation for user scope lookups
|
|
28
|
+
* Uses Map to maintain insertion order for LRU eviction
|
|
29
|
+
*/
|
|
30
|
+
class ScopeCache {
|
|
31
|
+
private cache: Map<string, ScopeCacheEntry>;
|
|
32
|
+
private max_size: number;
|
|
33
|
+
private ttl_ms: number;
|
|
34
|
+
private scope_version_map: Map<string, number>; // Track version per scope for smart invalidation
|
|
35
|
+
|
|
36
|
+
constructor(max_size: number, ttl_minutes: number) {
|
|
37
|
+
this.cache = new Map();
|
|
38
|
+
this.max_size = max_size;
|
|
39
|
+
this.ttl_ms = ttl_minutes * 60 * 1000;
|
|
40
|
+
this.scope_version_map = new Map();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gets a cache entry for a user's scopes
|
|
45
|
+
* Returns undefined if not found or expired
|
|
46
|
+
* @param user_id - User ID to look up
|
|
47
|
+
* @returns Cache entry or undefined
|
|
48
|
+
*/
|
|
49
|
+
get(user_id: string): ScopeCacheEntry | undefined {
|
|
50
|
+
const entry = this.cache.get(user_id);
|
|
51
|
+
|
|
52
|
+
if (!entry) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
const age = now - entry.timestamp;
|
|
58
|
+
|
|
59
|
+
// Check if entry is expired (TTL)
|
|
60
|
+
if (age > this.ttl_ms) {
|
|
61
|
+
this.cache.delete(user_id);
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if any of user's scopes have been invalidated
|
|
66
|
+
const max_scope_version = this.get_max_scope_version(entry.scopes);
|
|
67
|
+
if (max_scope_version > entry.cache_version) {
|
|
68
|
+
this.cache.delete(user_id);
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Move to end (most recently used)
|
|
73
|
+
this.cache.delete(user_id);
|
|
74
|
+
this.cache.set(user_id, entry);
|
|
75
|
+
|
|
76
|
+
return entry;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Sets a cache entry for a user's scopes
|
|
81
|
+
* Evicts least recently used entries if cache is full
|
|
82
|
+
* @param user_id - User ID
|
|
83
|
+
* @param scopes - User's scope assignments
|
|
84
|
+
*/
|
|
85
|
+
set(user_id: string, scopes: UserScopeEntry[]): void {
|
|
86
|
+
// Evict LRU entries if cache is full
|
|
87
|
+
while (this.cache.size >= this.max_size) {
|
|
88
|
+
const first_key = this.cache.keys().next().value;
|
|
89
|
+
if (first_key) {
|
|
90
|
+
this.cache.delete(first_key);
|
|
91
|
+
} else {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Get current cache version for user's scopes
|
|
97
|
+
const cache_version = this.get_max_scope_version(scopes);
|
|
98
|
+
|
|
99
|
+
const entry: ScopeCacheEntry = {
|
|
100
|
+
user_id,
|
|
101
|
+
scopes,
|
|
102
|
+
timestamp: Date.now(),
|
|
103
|
+
cache_version,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
this.cache.set(user_id, entry);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Invalidates cache for a specific user
|
|
111
|
+
* @param user_id - User ID to invalidate
|
|
112
|
+
*/
|
|
113
|
+
invalidate_user(user_id: string): void {
|
|
114
|
+
this.cache.delete(user_id);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Invalidates cache for all users with a specific scope
|
|
119
|
+
* Uses cache version to determine if invalidation is needed
|
|
120
|
+
* @param scope_type - Scope level
|
|
121
|
+
* @param scope_id - Scope ID to invalidate
|
|
122
|
+
*/
|
|
123
|
+
invalidate_by_scope(scope_type: ScopeLevel, scope_id: string): void {
|
|
124
|
+
const scope_key = `${scope_type}:${scope_id}`;
|
|
125
|
+
const current_version = this.scope_version_map.get(scope_key) || 0;
|
|
126
|
+
this.scope_version_map.set(scope_key, current_version + 1);
|
|
127
|
+
|
|
128
|
+
// Remove entries where cache version is older than scope version
|
|
129
|
+
const entries_to_remove: string[] = [];
|
|
130
|
+
for (const [user_id, entry] of this.cache.entries()) {
|
|
131
|
+
// Check if user has this scope
|
|
132
|
+
const has_scope = entry.scopes.some(
|
|
133
|
+
(s) => s.scope_type === scope_type && s.scope_id === scope_id
|
|
134
|
+
);
|
|
135
|
+
if (has_scope) {
|
|
136
|
+
entries_to_remove.push(user_id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const user_id of entries_to_remove) {
|
|
141
|
+
this.cache.delete(user_id);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Invalidates cache for all users with any scope of a specific level
|
|
147
|
+
* @param scope_type - Scope level to invalidate
|
|
148
|
+
*/
|
|
149
|
+
invalidate_by_scope_level(scope_type: ScopeLevel): void {
|
|
150
|
+
const entries_to_remove: string[] = [];
|
|
151
|
+
for (const [user_id, entry] of this.cache.entries()) {
|
|
152
|
+
// Check if user has any scope of this level
|
|
153
|
+
const has_level = entry.scopes.some((s) => s.scope_type === scope_type);
|
|
154
|
+
if (has_level) {
|
|
155
|
+
entries_to_remove.push(user_id);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const user_id of entries_to_remove) {
|
|
160
|
+
this.cache.delete(user_id);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Invalidates all cache entries
|
|
166
|
+
*/
|
|
167
|
+
invalidate_all(): void {
|
|
168
|
+
this.cache.clear();
|
|
169
|
+
this.scope_version_map.clear();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Gets the maximum cache version for a set of scopes
|
|
174
|
+
* Used to determine if cache entry is stale
|
|
175
|
+
* @param scopes - Array of scope entries
|
|
176
|
+
* @returns Maximum version number
|
|
177
|
+
*/
|
|
178
|
+
private get_max_scope_version(scopes: UserScopeEntry[]): number {
|
|
179
|
+
if (scopes.length === 0) {
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let max_version = 0;
|
|
184
|
+
for (const scope of scopes) {
|
|
185
|
+
const scope_key = `${scope.scope_type}:${scope.scope_id}`;
|
|
186
|
+
const version = this.scope_version_map.get(scope_key) || 0;
|
|
187
|
+
max_version = Math.max(max_version, version);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return max_version;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Gets cache statistics
|
|
195
|
+
* @returns Object with cache size and max size
|
|
196
|
+
*/
|
|
197
|
+
get_stats(): {
|
|
198
|
+
size: number;
|
|
199
|
+
max_size: number;
|
|
200
|
+
} {
|
|
201
|
+
return {
|
|
202
|
+
size: this.cache.size,
|
|
203
|
+
max_size: this.max_size,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// section: singleton
|
|
209
|
+
// Global scope cache instance (initialized with defaults, will be configured on first use)
|
|
210
|
+
let scope_cache_instance: ScopeCache | null = null;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Gets or creates the global scope cache instance
|
|
214
|
+
* @param max_size - Maximum cache size (default: 5000)
|
|
215
|
+
* @param ttl_minutes - TTL in minutes (default: 15)
|
|
216
|
+
* @returns Scope cache instance
|
|
217
|
+
*/
|
|
218
|
+
export function get_scope_cache(
|
|
219
|
+
max_size: number = 5000,
|
|
220
|
+
ttl_minutes: number = 15
|
|
221
|
+
): ScopeCache {
|
|
222
|
+
if (!scope_cache_instance) {
|
|
223
|
+
scope_cache_instance = new ScopeCache(max_size, ttl_minutes);
|
|
224
|
+
}
|
|
225
|
+
return scope_cache_instance;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Resets the global scope cache instance (useful for testing)
|
|
230
|
+
*/
|
|
231
|
+
export function reset_scope_cache(): void {
|
|
232
|
+
scope_cache_instance = null;
|
|
233
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// file_description: server-side auth utilities for server components and pages
|
|
2
|
+
// section: imports
|
|
3
|
+
import { cookies } from "next/headers";
|
|
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 ServerAuthUser = {
|
|
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 ServerAuthResult =
|
|
22
|
+
| ServerAuthUser
|
|
23
|
+
| { authenticated: false };
|
|
24
|
+
|
|
25
|
+
// section: functions
|
|
26
|
+
/**
|
|
27
|
+
* Gets authenticated user in server components/pages
|
|
28
|
+
* Uses Next.js cookies() function to read authentication cookies
|
|
29
|
+
* @returns ServerAuthResult with user info or authenticated: false
|
|
30
|
+
*/
|
|
31
|
+
export async function get_server_auth_user(): Promise<ServerAuthResult> {
|
|
32
|
+
const cookie_store = await cookies();
|
|
33
|
+
const user_id = cookie_store.get("hazo_auth_user_id")?.value;
|
|
34
|
+
const user_email = cookie_store.get("hazo_auth_user_email")?.value;
|
|
35
|
+
|
|
36
|
+
if (!user_id || !user_email) {
|
|
37
|
+
return { authenticated: false };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const hazoConnect = get_hazo_connect_instance();
|
|
42
|
+
const users_service = createCrudService(hazoConnect, "hazo_users");
|
|
43
|
+
|
|
44
|
+
const users = await users_service.findBy({
|
|
45
|
+
id: user_id,
|
|
46
|
+
email_address: user_email,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!Array.isArray(users) || users.length === 0) {
|
|
50
|
+
return { authenticated: false };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const user = users[0];
|
|
54
|
+
|
|
55
|
+
// Check if user is active
|
|
56
|
+
if (user.is_active === false) {
|
|
57
|
+
return { authenticated: false };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Map database profile_source to UI representation
|
|
61
|
+
const profile_source_db = user.profile_source as string | null | undefined;
|
|
62
|
+
const profile_source_ui = profile_source_db ? map_db_source_to_ui(profile_source_db) : undefined;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
authenticated: true,
|
|
66
|
+
user_id: user.id as string,
|
|
67
|
+
email: user.email_address as string,
|
|
68
|
+
name: (user.name as string | null | undefined) || undefined,
|
|
69
|
+
email_verified: user.email_verified === true,
|
|
70
|
+
is_active: user.is_active === true,
|
|
71
|
+
last_logon: (user.last_logon as string | null | undefined) || undefined,
|
|
72
|
+
profile_picture_url: (user.profile_picture_url as string | null | undefined) || undefined,
|
|
73
|
+
profile_source: profile_source_ui,
|
|
74
|
+
};
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return { authenticated: false };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Checks if user is authenticated in server components/pages (simple boolean check)
|
|
82
|
+
* @returns true if authenticated, false otherwise
|
|
83
|
+
*/
|
|
84
|
+
export async function is_server_authenticated(): Promise<boolean> {
|
|
85
|
+
const result = await get_server_auth_user();
|
|
86
|
+
return result.authenticated;
|
|
87
|
+
}
|
|
88
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// file_description: Edge-compatible JWT session token validator for Next.js proxy/middleware
|
|
2
|
+
// Uses jose library which works in Edge Runtime
|
|
3
|
+
// section: imports
|
|
4
|
+
import { jwtVerify } from "jose";
|
|
5
|
+
import type { NextRequest } from "next/server";
|
|
6
|
+
|
|
7
|
+
// section: types
|
|
8
|
+
export type ValidateSessionCookieResult = {
|
|
9
|
+
valid: boolean;
|
|
10
|
+
user_id?: string;
|
|
11
|
+
email?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// section: helpers
|
|
15
|
+
/**
|
|
16
|
+
* Gets JWT secret from environment variables
|
|
17
|
+
* Works in Edge Runtime (no Node.js APIs)
|
|
18
|
+
* @returns JWT secret as Uint8Array for jose library
|
|
19
|
+
*/
|
|
20
|
+
function get_jwt_secret(): Uint8Array | null {
|
|
21
|
+
const jwt_secret = process.env.JWT_SECRET;
|
|
22
|
+
|
|
23
|
+
if (!jwt_secret) {
|
|
24
|
+
// In Edge Runtime, we can't use logger, so we just return null
|
|
25
|
+
// The validation will fail gracefully
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Convert string secret to Uint8Array for jose
|
|
30
|
+
return new TextEncoder().encode(jwt_secret);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// section: main_function
|
|
34
|
+
/**
|
|
35
|
+
* Validates session cookie from NextRequest (Edge-compatible)
|
|
36
|
+
* Extracts hazo_auth_session cookie and validates JWT signature and expiry
|
|
37
|
+
* Works in Edge Runtime (Next.js proxy/middleware)
|
|
38
|
+
* @param request - NextRequest object
|
|
39
|
+
* @returns Validation result with user_id and email if valid
|
|
40
|
+
*/
|
|
41
|
+
export async function validate_session_cookie(
|
|
42
|
+
request: NextRequest,
|
|
43
|
+
): Promise<ValidateSessionCookieResult> {
|
|
44
|
+
try {
|
|
45
|
+
// Extract session cookie
|
|
46
|
+
const session_cookie = request.cookies.get("hazo_auth_session")?.value;
|
|
47
|
+
|
|
48
|
+
if (!session_cookie) {
|
|
49
|
+
return { valid: false };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Get JWT secret
|
|
53
|
+
const secret = get_jwt_secret();
|
|
54
|
+
|
|
55
|
+
if (!secret) {
|
|
56
|
+
// JWT_SECRET not set - cannot validate
|
|
57
|
+
return { valid: false };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Verify JWT signature and expiration
|
|
61
|
+
const { payload } = await jwtVerify(session_cookie, secret, {
|
|
62
|
+
algorithms: ["HS256"],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Extract user_id and email from payload
|
|
66
|
+
const user_id = payload.user_id as string;
|
|
67
|
+
const email = payload.email as string;
|
|
68
|
+
|
|
69
|
+
if (!user_id || !email) {
|
|
70
|
+
return { valid: false };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
valid: true,
|
|
75
|
+
user_id,
|
|
76
|
+
email,
|
|
77
|
+
};
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// jose throws JWTExpired, JWTInvalid, etc. - these are expected for invalid tokens
|
|
80
|
+
// In Edge Runtime, we can't log, so we just return invalid
|
|
81
|
+
return { valid: false };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|