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.
Files changed (136) hide show
  1. package/bin/hazo_auth.mjs +35 -0
  2. package/cli-src/assets/images/forgot_password_default.jpg +0 -0
  3. package/cli-src/assets/images/login_default.jpg +0 -0
  4. package/cli-src/assets/images/register_default.jpg +0 -0
  5. package/cli-src/assets/images/reset_password_default.jpg +0 -0
  6. package/cli-src/assets/images/verify_email_default.jpg +0 -0
  7. package/cli-src/cli/generate.ts +276 -0
  8. package/cli-src/cli/index.ts +207 -0
  9. package/cli-src/cli/init.ts +254 -0
  10. package/cli-src/cli/init_users.ts +376 -0
  11. package/cli-src/cli/validate.ts +581 -0
  12. package/cli-src/lib/already_logged_in_config.server.ts +46 -0
  13. package/cli-src/lib/app_logger.ts +24 -0
  14. package/cli-src/lib/auth/auth_cache.ts +220 -0
  15. package/cli-src/lib/auth/auth_rate_limiter.ts +121 -0
  16. package/cli-src/lib/auth/auth_types.ts +117 -0
  17. package/cli-src/lib/auth/auth_utils.server.ts +196 -0
  18. package/cli-src/lib/auth/dev_lock_validator.edge.ts +171 -0
  19. package/cli-src/lib/auth/hazo_get_auth.server.ts +583 -0
  20. package/cli-src/lib/auth/index.ts +23 -0
  21. package/cli-src/lib/auth/nextauth_config.ts +227 -0
  22. package/cli-src/lib/auth/org_cache.ts +148 -0
  23. package/cli-src/lib/auth/scope_cache.ts +233 -0
  24. package/cli-src/lib/auth/server_auth.ts +88 -0
  25. package/cli-src/lib/auth/session_token_validator.edge.ts +92 -0
  26. package/cli-src/lib/auth_utility_config.server.ts +136 -0
  27. package/cli-src/lib/config/config_loader.server.ts +164 -0
  28. package/cli-src/lib/config/default_config.ts +243 -0
  29. package/cli-src/lib/dev_lock_config.server.ts +148 -0
  30. package/cli-src/lib/email_verification_config.server.ts +63 -0
  31. package/cli-src/lib/file_types_config.server.ts +25 -0
  32. package/cli-src/lib/forgot_password_config.server.ts +63 -0
  33. package/cli-src/lib/hazo_connect_instance.server.ts +101 -0
  34. package/cli-src/lib/hazo_connect_setup.server.ts +194 -0
  35. package/cli-src/lib/hazo_connect_setup.ts +54 -0
  36. package/cli-src/lib/index.ts +46 -0
  37. package/cli-src/lib/login_config.server.ts +106 -0
  38. package/cli-src/lib/messages_config.server.ts +45 -0
  39. package/cli-src/lib/migrations/apply_migration.ts +105 -0
  40. package/cli-src/lib/multi_tenancy_config.server.ts +94 -0
  41. package/cli-src/lib/my_settings_config.server.ts +135 -0
  42. package/cli-src/lib/oauth_config.server.ts +87 -0
  43. package/cli-src/lib/password_requirements_config.server.ts +40 -0
  44. package/cli-src/lib/profile_pic_menu_config.server.ts +138 -0
  45. package/cli-src/lib/profile_picture_config.server.ts +56 -0
  46. package/cli-src/lib/register_config.server.ts +101 -0
  47. package/cli-src/lib/reset_password_config.server.ts +103 -0
  48. package/cli-src/lib/scope_hierarchy_config.server.ts +151 -0
  49. package/cli-src/lib/services/email_service.ts +587 -0
  50. package/cli-src/lib/services/email_verification_service.ts +270 -0
  51. package/cli-src/lib/services/index.ts +16 -0
  52. package/cli-src/lib/services/login_service.ts +150 -0
  53. package/cli-src/lib/services/oauth_service.ts +494 -0
  54. package/cli-src/lib/services/org_service.ts +965 -0
  55. package/cli-src/lib/services/password_change_service.ts +154 -0
  56. package/cli-src/lib/services/password_reset_service.ts +418 -0
  57. package/cli-src/lib/services/profile_picture_remove_service.ts +120 -0
  58. package/cli-src/lib/services/profile_picture_service.ts +451 -0
  59. package/cli-src/lib/services/profile_picture_source_mapper.ts +62 -0
  60. package/cli-src/lib/services/registration_service.ts +185 -0
  61. package/cli-src/lib/services/scope_labels_service.ts +348 -0
  62. package/cli-src/lib/services/scope_service.ts +778 -0
  63. package/cli-src/lib/services/session_token_service.ts +178 -0
  64. package/cli-src/lib/services/token_service.ts +240 -0
  65. package/cli-src/lib/services/user_profiles_cache.ts +189 -0
  66. package/cli-src/lib/services/user_profiles_service.ts +264 -0
  67. package/cli-src/lib/services/user_scope_service.ts +554 -0
  68. package/cli-src/lib/services/user_update_service.ts +141 -0
  69. package/cli-src/lib/ui_shell_config.server.ts +73 -0
  70. package/cli-src/lib/ui_sizes_config.server.ts +37 -0
  71. package/cli-src/lib/user_fields_config.server.ts +31 -0
  72. package/cli-src/lib/user_management_config.server.ts +39 -0
  73. package/cli-src/lib/user_profiles_config.server.ts +55 -0
  74. package/cli-src/lib/utils/api_route_helpers.ts +60 -0
  75. package/cli-src/lib/utils/error_sanitizer.ts +75 -0
  76. package/cli-src/lib/utils/password_validator.ts +65 -0
  77. package/cli-src/lib/utils.ts +11 -0
  78. package/cli-src/server/logging/logger_service.ts +56 -0
  79. package/cli-src/server/types/app_types.ts +74 -0
  80. package/cli-src/server/types/express.d.ts +16 -0
  81. package/dist/cli/index.js +18 -0
  82. package/dist/cli/init_users.d.ts +17 -0
  83. package/dist/cli/init_users.d.ts.map +1 -0
  84. package/dist/cli/init_users.js +307 -0
  85. package/dist/components/layouts/dev_lock/index.d.ts +29 -0
  86. package/dist/components/layouts/dev_lock/index.d.ts.map +1 -0
  87. package/dist/components/layouts/dev_lock/index.js +60 -0
  88. package/dist/components/layouts/index.d.ts +2 -0
  89. package/dist/components/layouts/index.d.ts.map +1 -1
  90. package/dist/components/layouts/index.js +1 -0
  91. package/dist/components/layouts/org_management/index.d.ts +26 -0
  92. package/dist/components/layouts/org_management/index.d.ts.map +1 -0
  93. package/dist/components/layouts/org_management/index.js +75 -0
  94. package/dist/components/layouts/shared/config/layout_customization.d.ts +2 -7
  95. package/dist/components/layouts/shared/config/layout_customization.d.ts.map +1 -1
  96. package/dist/components/layouts/user_management/components/org_hierarchy_tab.d.ts +13 -0
  97. package/dist/components/layouts/user_management/components/org_hierarchy_tab.d.ts.map +1 -0
  98. package/dist/components/layouts/user_management/components/org_hierarchy_tab.js +276 -0
  99. package/dist/components/layouts/user_management/index.d.ts +3 -1
  100. package/dist/components/layouts/user_management/index.d.ts.map +1 -1
  101. package/dist/components/layouts/user_management/index.js +10 -4
  102. package/dist/lib/auth/auth_types.d.ts +6 -0
  103. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  104. package/dist/lib/auth/dev_lock_validator.edge.d.ts +38 -0
  105. package/dist/lib/auth/dev_lock_validator.edge.d.ts.map +1 -0
  106. package/dist/lib/auth/dev_lock_validator.edge.js +122 -0
  107. package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
  108. package/dist/lib/auth/hazo_get_auth.server.js +61 -1
  109. package/dist/lib/auth/org_cache.d.ts +65 -0
  110. package/dist/lib/auth/org_cache.d.ts.map +1 -0
  111. package/dist/lib/auth/org_cache.js +103 -0
  112. package/dist/lib/config/default_config.d.ts +76 -0
  113. package/dist/lib/config/default_config.d.ts.map +1 -1
  114. package/dist/lib/config/default_config.js +42 -0
  115. package/dist/lib/dev_lock_config.server.d.ts +41 -0
  116. package/dist/lib/dev_lock_config.server.d.ts.map +1 -0
  117. package/dist/lib/dev_lock_config.server.js +50 -0
  118. package/dist/lib/multi_tenancy_config.server.d.ts +30 -0
  119. package/dist/lib/multi_tenancy_config.server.d.ts.map +1 -0
  120. package/dist/lib/multi_tenancy_config.server.js +41 -0
  121. package/dist/lib/services/org_service.d.ts +191 -0
  122. package/dist/lib/services/org_service.d.ts.map +1 -0
  123. package/dist/lib/services/org_service.js +746 -0
  124. package/dist/lib/utils/password_validator.d.ts +7 -1
  125. package/dist/lib/utils/password_validator.d.ts.map +1 -1
  126. package/dist/page_components/dev_lock.d.ts +11 -0
  127. package/dist/page_components/dev_lock.d.ts.map +1 -0
  128. package/dist/page_components/dev_lock.js +17 -0
  129. package/dist/page_components/index.d.ts +1 -0
  130. package/dist/page_components/index.d.ts.map +1 -1
  131. package/dist/page_components/index.js +1 -0
  132. package/dist/page_components/org_management.d.ts +27 -0
  133. package/dist/page_components/org_management.d.ts.map +1 -0
  134. package/dist/page_components/org_management.js +18 -0
  135. package/hazo_auth_config.example.ini +30 -0
  136. package/package.json +27 -3
@@ -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.js";
7
+ import { handle_google_oauth_login } from "../services/oauth_service.js";
8
+ import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
9
+ import { create_app_logger } from "../app_logger.js";
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,148 @@
1
+ // file_description: LRU cache implementation for organization lookups with TTL and size limits
2
+ // section: types
3
+
4
+ /**
5
+ * Cached organization info for hazo_get_auth
6
+ */
7
+ export type OrgCacheEntry = {
8
+ org_id: string;
9
+ org_name: string;
10
+ parent_org_id: string | null;
11
+ parent_org_name: string | null;
12
+ root_org_id: string | null;
13
+ root_org_name: string | null;
14
+ };
15
+
16
+ /**
17
+ * Internal cache entry with metadata
18
+ */
19
+ type CacheItem = {
20
+ entry: OrgCacheEntry;
21
+ timestamp: number; // Unix timestamp in milliseconds
22
+ };
23
+
24
+ /**
25
+ * LRU cache implementation for organization lookups
26
+ * Uses Map to maintain insertion order for LRU eviction
27
+ */
28
+ class OrgCache {
29
+ private cache: Map<string, CacheItem>;
30
+ private max_size: number;
31
+ private ttl_ms: number;
32
+
33
+ constructor(max_size: number, ttl_minutes: number) {
34
+ this.cache = new Map();
35
+ this.max_size = max_size;
36
+ this.ttl_ms = ttl_minutes * 60 * 1000;
37
+ }
38
+
39
+ /**
40
+ * Gets a cache entry for an organization
41
+ * Returns undefined if not found or expired
42
+ * @param org_id - Organization ID to look up
43
+ * @returns Cache entry or undefined
44
+ */
45
+ get(org_id: string): OrgCacheEntry | undefined {
46
+ const item = this.cache.get(org_id);
47
+
48
+ if (!item) {
49
+ return undefined;
50
+ }
51
+
52
+ const now = Date.now();
53
+ const age = now - item.timestamp;
54
+
55
+ // Check if entry is expired (TTL)
56
+ if (age > this.ttl_ms) {
57
+ this.cache.delete(org_id);
58
+ return undefined;
59
+ }
60
+
61
+ // Move to end (most recently used)
62
+ this.cache.delete(org_id);
63
+ this.cache.set(org_id, item);
64
+
65
+ return item.entry;
66
+ }
67
+
68
+ /**
69
+ * Sets a cache entry for an organization
70
+ * Evicts least recently used entries if cache is full
71
+ * @param org_id - Organization ID
72
+ * @param entry - Organization cache entry
73
+ */
74
+ set(org_id: string, entry: OrgCacheEntry): void {
75
+ // Evict LRU entries if cache is full
76
+ while (this.cache.size >= this.max_size) {
77
+ const first_key = this.cache.keys().next().value;
78
+ if (first_key) {
79
+ this.cache.delete(first_key);
80
+ } else {
81
+ break;
82
+ }
83
+ }
84
+
85
+ const item: CacheItem = {
86
+ entry,
87
+ timestamp: Date.now(),
88
+ };
89
+
90
+ this.cache.set(org_id, item);
91
+ }
92
+
93
+ /**
94
+ * Invalidates cache for a specific organization
95
+ * @param org_id - Organization ID to invalidate
96
+ */
97
+ invalidate(org_id: string): void {
98
+ this.cache.delete(org_id);
99
+ }
100
+
101
+ /**
102
+ * Invalidates all cache entries
103
+ */
104
+ invalidate_all(): void {
105
+ this.cache.clear();
106
+ }
107
+
108
+ /**
109
+ * Gets cache statistics
110
+ * @returns Object with cache size and max size
111
+ */
112
+ get_stats(): {
113
+ size: number;
114
+ max_size: number;
115
+ } {
116
+ return {
117
+ size: this.cache.size,
118
+ max_size: this.max_size,
119
+ };
120
+ }
121
+ }
122
+
123
+ // section: singleton
124
+ // Global org cache instance (initialized with defaults, will be configured on first use)
125
+ let org_cache_instance: OrgCache | null = null;
126
+
127
+ /**
128
+ * Gets or creates the global org cache instance
129
+ * @param max_size - Maximum cache size (default: 1000)
130
+ * @param ttl_minutes - TTL in minutes (default: 15)
131
+ * @returns Org cache instance
132
+ */
133
+ export function get_org_cache(
134
+ max_size: number = 1000,
135
+ ttl_minutes: number = 15,
136
+ ): OrgCache {
137
+ if (!org_cache_instance) {
138
+ org_cache_instance = new OrgCache(max_size, ttl_minutes);
139
+ }
140
+ return org_cache_instance;
141
+ }
142
+
143
+ /**
144
+ * Resets the global org cache instance (useful for testing)
145
+ */
146
+ export function reset_org_cache(): void {
147
+ org_cache_instance = null;
148
+ }
@@ -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.js";
5
+ import { createCrudService } from "hazo_connect/server";
6
+ import { map_db_source_to_ui } from "../services/profile_picture_source_mapper.js";
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
+