hazo_auth 0.3.0 → 1.0.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 (87) hide show
  1. package/hazo_auth_config.example.ini +39 -0
  2. package/instrumentation.ts +1 -1
  3. package/next.config.mjs +1 -1
  4. package/package.json +3 -1
  5. package/src/app/api/{auth → hazo_auth/auth}/upload_profile_picture/route.ts +2 -2
  6. package/src/app/api/{auth → hazo_auth}/change_password/route.ts +23 -0
  7. package/src/app/api/hazo_auth/get_auth/route.ts +89 -0
  8. package/src/app/api/hazo_auth/invalidate_cache/route.ts +139 -0
  9. package/src/app/api/{auth → hazo_auth}/logout/route.ts +27 -0
  10. package/src/app/api/hazo_auth/upload_profile_picture/route.ts +268 -0
  11. package/src/app/api/hazo_auth/user_management/permissions/route.ts +367 -0
  12. package/src/app/api/hazo_auth/user_management/roles/route.ts +442 -0
  13. package/src/app/api/hazo_auth/user_management/users/roles/route.ts +367 -0
  14. package/src/app/api/hazo_auth/user_management/users/route.ts +239 -0
  15. package/src/app/api/{auth → hazo_auth}/validate_reset_token/route.ts +3 -0
  16. package/src/app/api/{auth → hazo_auth}/verify_email/route.ts +3 -0
  17. package/src/app/globals.css +1 -1
  18. package/src/app/hazo_auth/user_management/page.tsx +14 -0
  19. package/src/app/hazo_auth/user_management/user_management_page_client.tsx +16 -0
  20. package/src/app/hazo_connect/api/sqlite/data/route.ts +7 -1
  21. package/src/app/hazo_connect/api/sqlite/schema/route.ts +14 -4
  22. package/src/app/hazo_connect/api/sqlite/tables/route.ts +14 -4
  23. package/src/app/hazo_connect/sqlite_admin/sqlite-admin-client.tsx +40 -3
  24. package/src/app/layout.tsx +1 -1
  25. package/src/app/page.tsx +4 -4
  26. package/src/components/layouts/email_verification/hooks/use_email_verification.ts +4 -4
  27. package/src/components/layouts/email_verification/index.tsx +1 -1
  28. package/src/components/layouts/forgot_password/hooks/use_forgot_password_form.ts +1 -1
  29. package/src/components/layouts/login/hooks/use_login_form.ts +2 -2
  30. package/src/components/layouts/my_settings/components/profile_picture_dialog.tsx +1 -1
  31. package/src/components/layouts/my_settings/components/profile_picture_library_tab.tsx +2 -2
  32. package/src/components/layouts/my_settings/hooks/use_my_settings.ts +5 -5
  33. package/src/components/layouts/my_settings/index.tsx +1 -1
  34. package/src/components/layouts/register/hooks/use_register_form.ts +1 -1
  35. package/src/components/layouts/reset_password/hooks/use_reset_password_form.ts +3 -3
  36. package/src/components/layouts/reset_password/index.tsx +2 -2
  37. package/src/components/layouts/shared/components/logout_button.tsx +1 -1
  38. package/src/components/layouts/shared/components/profile_pic_menu.tsx +4 -4
  39. package/src/components/layouts/shared/components/sidebar_layout_wrapper.tsx +19 -7
  40. package/src/components/layouts/shared/components/unauthorized_guard.tsx +1 -1
  41. package/src/components/layouts/shared/hooks/use_auth_status.ts +1 -1
  42. package/src/components/layouts/shared/hooks/use_hazo_auth.ts +158 -0
  43. package/src/components/layouts/user_management/components/roles_matrix.tsx +607 -0
  44. package/src/components/layouts/user_management/index.tsx +1295 -0
  45. package/src/components/ui/alert-dialog.tsx +141 -0
  46. package/src/components/ui/checkbox.tsx +30 -0
  47. package/src/components/ui/table.tsx +120 -0
  48. package/src/lib/auth/auth_cache.ts +220 -0
  49. package/src/lib/auth/auth_rate_limiter.ts +121 -0
  50. package/src/lib/auth/auth_types.ts +65 -0
  51. package/src/lib/auth/hazo_get_auth.server.ts +333 -0
  52. package/src/lib/auth_utility_config.server.ts +136 -0
  53. package/src/lib/hazo_connect_setup.server.ts +2 -3
  54. package/src/lib/my_settings_config.server.ts +1 -1
  55. package/src/lib/profile_pic_menu_config.server.ts +4 -4
  56. package/src/lib/reset_password_config.server.ts +5 -5
  57. package/src/lib/services/email_service.ts +2 -2
  58. package/src/lib/services/profile_picture_remove_service.ts +1 -1
  59. package/src/lib/services/token_service.ts +2 -2
  60. package/src/lib/user_management_config.server.ts +40 -0
  61. package/src/lib/utils.ts +1 -1
  62. package/src/middleware.ts +15 -13
  63. package/src/server/types/express.d.ts +1 -0
  64. package/src/stories/project_overview.stories.tsx +1 -1
  65. package/tailwind.config.ts +1 -1
  66. /package/src/app/api/{auth → hazo_auth}/forgot_password/route.ts +0 -0
  67. /package/src/app/api/{auth → hazo_auth}/library_photos/route.ts +0 -0
  68. /package/src/app/api/{auth → hazo_auth}/login/route.ts +0 -0
  69. /package/src/app/api/{auth → hazo_auth}/me/route.ts +0 -0
  70. /package/src/app/api/{auth → hazo_auth}/profile_picture/[filename]/route.ts +0 -0
  71. /package/src/app/api/{auth → hazo_auth}/register/route.ts +0 -0
  72. /package/src/app/api/{auth → hazo_auth}/remove_profile_picture/route.ts +0 -0
  73. /package/src/app/api/{auth → hazo_auth}/resend_verification/route.ts +0 -0
  74. /package/src/app/api/{auth → hazo_auth}/reset_password/route.ts +0 -0
  75. /package/src/app/api/{auth → hazo_auth}/update_user/route.ts +0 -0
  76. /package/src/app/{forgot_password → hazo_auth/forgot_password}/forgot_password_page_client.tsx +0 -0
  77. /package/src/app/{forgot_password → hazo_auth/forgot_password}/page.tsx +0 -0
  78. /package/src/app/{login → hazo_auth/login}/login_page_client.tsx +0 -0
  79. /package/src/app/{login → hazo_auth/login}/page.tsx +0 -0
  80. /package/src/app/{my_settings → hazo_auth/my_settings}/my_settings_page_client.tsx +0 -0
  81. /package/src/app/{my_settings → hazo_auth/my_settings}/page.tsx +0 -0
  82. /package/src/app/{register → hazo_auth/register}/page.tsx +0 -0
  83. /package/src/app/{register → hazo_auth/register}/register_page_client.tsx +0 -0
  84. /package/src/app/{reset_password → hazo_auth/reset_password}/page.tsx +0 -0
  85. /package/src/app/{reset_password → hazo_auth/reset_password}/reset_password_page_client.tsx +0 -0
  86. /package/src/app/{verify_email → hazo_auth/verify_email}/page.tsx +0 -0
  87. /package/src/app/{verify_email → hazo_auth/verify_email}/verify_email_page_client.tsx +0 -0
@@ -0,0 +1,141 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { buttonVariants } from "@/components/ui/button"
8
+
9
+ const AlertDialog = AlertDialogPrimitive.Root
10
+
11
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12
+
13
+ const AlertDialogPortal = AlertDialogPrimitive.Portal
14
+
15
+ const AlertDialogOverlay = React.forwardRef<
16
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
17
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
18
+ >(({ className, ...props }, ref) => (
19
+ <AlertDialogPrimitive.Overlay
20
+ className={cn(
21
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
22
+ className
23
+ )}
24
+ {...props}
25
+ ref={ref}
26
+ />
27
+ ))
28
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29
+
30
+ const AlertDialogContent = React.forwardRef<
31
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
32
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
33
+ >(({ className, ...props }, ref) => (
34
+ <AlertDialogPortal>
35
+ <AlertDialogOverlay />
36
+ <AlertDialogPrimitive.Content
37
+ ref={ref}
38
+ className={cn(
39
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ </AlertDialogPortal>
45
+ ))
46
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47
+
48
+ const AlertDialogHeader = ({
49
+ className,
50
+ ...props
51
+ }: React.HTMLAttributes<HTMLDivElement>) => (
52
+ <div
53
+ className={cn(
54
+ "flex flex-col space-y-2 text-center sm:text-left",
55
+ className
56
+ )}
57
+ {...props}
58
+ />
59
+ )
60
+ AlertDialogHeader.displayName = "AlertDialogHeader"
61
+
62
+ const AlertDialogFooter = ({
63
+ className,
64
+ ...props
65
+ }: React.HTMLAttributes<HTMLDivElement>) => (
66
+ <div
67
+ className={cn(
68
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
69
+ className
70
+ )}
71
+ {...props}
72
+ />
73
+ )
74
+ AlertDialogFooter.displayName = "AlertDialogFooter"
75
+
76
+ const AlertDialogTitle = React.forwardRef<
77
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
78
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
79
+ >(({ className, ...props }, ref) => (
80
+ <AlertDialogPrimitive.Title
81
+ ref={ref}
82
+ className={cn("text-lg font-semibold", className)}
83
+ {...props}
84
+ />
85
+ ))
86
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87
+
88
+ const AlertDialogDescription = React.forwardRef<
89
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
90
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
91
+ >(({ className, ...props }, ref) => (
92
+ <AlertDialogPrimitive.Description
93
+ ref={ref}
94
+ className={cn("text-sm text-muted-foreground", className)}
95
+ {...props}
96
+ />
97
+ ))
98
+ AlertDialogDescription.displayName =
99
+ AlertDialogPrimitive.Description.displayName
100
+
101
+ const AlertDialogAction = React.forwardRef<
102
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
103
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
104
+ >(({ className, ...props }, ref) => (
105
+ <AlertDialogPrimitive.Action
106
+ ref={ref}
107
+ className={cn(buttonVariants(), className)}
108
+ {...props}
109
+ />
110
+ ))
111
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112
+
113
+ const AlertDialogCancel = React.forwardRef<
114
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
115
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
116
+ >(({ className, ...props }, ref) => (
117
+ <AlertDialogPrimitive.Cancel
118
+ ref={ref}
119
+ className={cn(
120
+ buttonVariants({ variant: "outline" }),
121
+ "mt-2 sm:mt-0",
122
+ className
123
+ )}
124
+ {...props}
125
+ />
126
+ ))
127
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128
+
129
+ export {
130
+ AlertDialog,
131
+ AlertDialogPortal,
132
+ AlertDialogOverlay,
133
+ AlertDialogTrigger,
134
+ AlertDialogContent,
135
+ AlertDialogHeader,
136
+ AlertDialogFooter,
137
+ AlertDialogTitle,
138
+ AlertDialogDescription,
139
+ AlertDialogAction,
140
+ AlertDialogCancel,
141
+ }
@@ -0,0 +1,30 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5
+ import { Check } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const Checkbox = React.forwardRef<
10
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
11
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
12
+ >(({ className, ...props }, ref) => (
13
+ <CheckboxPrimitive.Root
14
+ ref={ref}
15
+ className={cn(
16
+ "grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
17
+ className
18
+ )}
19
+ {...props}
20
+ >
21
+ <CheckboxPrimitive.Indicator
22
+ className={cn("grid place-content-center text-current")}
23
+ >
24
+ <Check className="h-4 w-4" />
25
+ </CheckboxPrimitive.Indicator>
26
+ </CheckboxPrimitive.Root>
27
+ ))
28
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName
29
+
30
+ export { Checkbox }
@@ -0,0 +1,120 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Table = React.forwardRef<
6
+ HTMLTableElement,
7
+ React.HTMLAttributes<HTMLTableElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div className="relative w-full overflow-auto">
10
+ <table
11
+ ref={ref}
12
+ className={cn("w-full caption-bottom text-sm", className)}
13
+ {...props}
14
+ />
15
+ </div>
16
+ ))
17
+ Table.displayName = "Table"
18
+
19
+ const TableHeader = React.forwardRef<
20
+ HTMLTableSectionElement,
21
+ React.HTMLAttributes<HTMLTableSectionElement>
22
+ >(({ className, ...props }, ref) => (
23
+ <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
24
+ ))
25
+ TableHeader.displayName = "TableHeader"
26
+
27
+ const TableBody = React.forwardRef<
28
+ HTMLTableSectionElement,
29
+ React.HTMLAttributes<HTMLTableSectionElement>
30
+ >(({ className, ...props }, ref) => (
31
+ <tbody
32
+ ref={ref}
33
+ className={cn("[&_tr:last-child]:border-0", className)}
34
+ {...props}
35
+ />
36
+ ))
37
+ TableBody.displayName = "TableBody"
38
+
39
+ const TableFooter = React.forwardRef<
40
+ HTMLTableSectionElement,
41
+ React.HTMLAttributes<HTMLTableSectionElement>
42
+ >(({ className, ...props }, ref) => (
43
+ <tfoot
44
+ ref={ref}
45
+ className={cn(
46
+ "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
47
+ className
48
+ )}
49
+ {...props}
50
+ />
51
+ ))
52
+ TableFooter.displayName = "TableFooter"
53
+
54
+ const TableRow = React.forwardRef<
55
+ HTMLTableRowElement,
56
+ React.HTMLAttributes<HTMLTableRowElement>
57
+ >(({ className, ...props }, ref) => (
58
+ <tr
59
+ ref={ref}
60
+ className={cn(
61
+ "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
62
+ className
63
+ )}
64
+ {...props}
65
+ />
66
+ ))
67
+ TableRow.displayName = "TableRow"
68
+
69
+ const TableHead = React.forwardRef<
70
+ HTMLTableCellElement,
71
+ React.ThHTMLAttributes<HTMLTableCellElement>
72
+ >(({ className, ...props }, ref) => (
73
+ <th
74
+ ref={ref}
75
+ className={cn(
76
+ "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ ))
82
+ TableHead.displayName = "TableHead"
83
+
84
+ const TableCell = React.forwardRef<
85
+ HTMLTableCellElement,
86
+ React.TdHTMLAttributes<HTMLTableCellElement>
87
+ >(({ className, ...props }, ref) => (
88
+ <td
89
+ ref={ref}
90
+ className={cn(
91
+ "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
92
+ className
93
+ )}
94
+ {...props}
95
+ />
96
+ ))
97
+ TableCell.displayName = "TableCell"
98
+
99
+ const TableCaption = React.forwardRef<
100
+ HTMLTableCaptionElement,
101
+ React.HTMLAttributes<HTMLTableCaptionElement>
102
+ >(({ className, ...props }, ref) => (
103
+ <caption
104
+ ref={ref}
105
+ className={cn("mt-4 text-sm text-muted-foreground", className)}
106
+ {...props}
107
+ />
108
+ ))
109
+ TableCaption.displayName = "TableCaption"
110
+
111
+ export {
112
+ Table,
113
+ TableHeader,
114
+ TableBody,
115
+ TableFooter,
116
+ TableHead,
117
+ TableRow,
118
+ TableCell,
119
+ TableCaption,
120
+ }
@@ -0,0 +1,220 @@
1
+ // file_description: LRU cache implementation for hazo_get_auth with TTL and size limits
2
+ // section: imports
3
+ import type { HazoAuthUser } from "./auth_types";
4
+
5
+ // section: types
6
+
7
+ /**
8
+ * Cache entry structure
9
+ */
10
+ type CacheEntry = {
11
+ user: HazoAuthUser;
12
+ permissions: string[];
13
+ role_ids: number[];
14
+ timestamp: number; // Unix timestamp in milliseconds
15
+ cache_version: number; // Version number for smart invalidation
16
+ };
17
+
18
+ /**
19
+ * LRU cache implementation with TTL and size limits
20
+ * Uses Map to maintain insertion order for LRU eviction
21
+ */
22
+ class AuthCache {
23
+ private cache: Map<string, CacheEntry>;
24
+ private max_size: number;
25
+ private ttl_ms: number;
26
+ private max_age_ms: number;
27
+ private role_version_map: Map<number, number>; // Track version per role for smart invalidation
28
+
29
+ constructor(
30
+ max_size: number,
31
+ ttl_minutes: number,
32
+ max_age_minutes: number,
33
+ ) {
34
+ this.cache = new Map();
35
+ this.max_size = max_size;
36
+ this.ttl_ms = ttl_minutes * 60 * 1000;
37
+ this.max_age_ms = max_age_minutes * 60 * 1000;
38
+ this.role_version_map = new Map();
39
+ }
40
+
41
+ /**
42
+ * Gets a cache entry for a user
43
+ * Returns undefined if not found, expired, or too old
44
+ * @param user_id - User ID to look up
45
+ * @returns Cache entry or undefined
46
+ */
47
+ get(user_id: string): CacheEntry | undefined {
48
+ const entry = this.cache.get(user_id);
49
+
50
+ if (!entry) {
51
+ return undefined;
52
+ }
53
+
54
+ const now = Date.now();
55
+ const age = now - entry.timestamp;
56
+
57
+ // Check if entry is expired (TTL)
58
+ if (age > this.ttl_ms) {
59
+ this.cache.delete(user_id);
60
+ return undefined;
61
+ }
62
+
63
+ // Check if entry is too old (force refresh threshold)
64
+ if (age > this.max_age_ms) {
65
+ // Don't delete, but mark as stale so caller can refresh
66
+ // Return undefined to force refresh
67
+ this.cache.delete(user_id);
68
+ return undefined;
69
+ }
70
+
71
+ // Move to end (most recently used)
72
+ this.cache.delete(user_id);
73
+ this.cache.set(user_id, entry);
74
+
75
+ return entry;
76
+ }
77
+
78
+ /**
79
+ * Sets a cache entry for a user
80
+ * Evicts least recently used entries if cache is full
81
+ * @param user_id - User ID
82
+ * @param user - User data
83
+ * @param permissions - User permissions
84
+ * @param role_ids - User role IDs
85
+ */
86
+ set(
87
+ user_id: string,
88
+ user: HazoAuthUser,
89
+ permissions: string[],
90
+ role_ids: number[],
91
+ ): void {
92
+ // Evict LRU entries if cache is full
93
+ while (this.cache.size >= this.max_size) {
94
+ const first_key = this.cache.keys().next().value;
95
+ if (first_key) {
96
+ this.cache.delete(first_key);
97
+ } else {
98
+ break;
99
+ }
100
+ }
101
+
102
+ // Get current cache version for user's roles
103
+ const cache_version = this.get_max_role_version(role_ids);
104
+
105
+ const entry: CacheEntry = {
106
+ user,
107
+ permissions,
108
+ role_ids,
109
+ timestamp: Date.now(),
110
+ cache_version,
111
+ };
112
+
113
+ this.cache.set(user_id, entry);
114
+ }
115
+
116
+ /**
117
+ * Invalidates cache for a specific user
118
+ * @param user_id - User ID to invalidate
119
+ */
120
+ invalidate_user(user_id: string): void {
121
+ this.cache.delete(user_id);
122
+ }
123
+
124
+ /**
125
+ * Invalidates cache for all users with specific roles
126
+ * Uses cache version to determine if invalidation is needed
127
+ * @param role_ids - Array of role IDs to invalidate
128
+ */
129
+ invalidate_by_roles(role_ids: number[]): void {
130
+ // Increment version for affected roles
131
+ for (const role_id of role_ids) {
132
+ const current_version = this.role_version_map.get(role_id) || 0;
133
+ this.role_version_map.set(role_id, current_version + 1);
134
+ }
135
+
136
+ // Remove entries where cache version is older than role version
137
+ const entries_to_remove: string[] = [];
138
+ for (const [user_id, entry] of this.cache.entries()) {
139
+ const max_role_version = this.get_max_role_version(entry.role_ids);
140
+ if (max_role_version > entry.cache_version) {
141
+ entries_to_remove.push(user_id);
142
+ }
143
+ }
144
+
145
+ for (const user_id of entries_to_remove) {
146
+ this.cache.delete(user_id);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Invalidates all cache entries
152
+ */
153
+ invalidate_all(): void {
154
+ this.cache.clear();
155
+ }
156
+
157
+ /**
158
+ * Gets the maximum cache version for a set of roles
159
+ * Used to determine if cache entry is stale
160
+ * @param role_ids - Array of role IDs
161
+ * @returns Maximum version number
162
+ */
163
+ private get_max_role_version(role_ids: number[]): number {
164
+ if (role_ids.length === 0) {
165
+ return 0;
166
+ }
167
+
168
+ let max_version = 0;
169
+ for (const role_id of role_ids) {
170
+ const version = this.role_version_map.get(role_id) || 0;
171
+ max_version = Math.max(max_version, version);
172
+ }
173
+
174
+ return max_version;
175
+ }
176
+
177
+ /**
178
+ * Gets cache statistics
179
+ * @returns Object with cache size, max size, and hit rate estimate
180
+ */
181
+ get_stats(): {
182
+ size: number;
183
+ max_size: number;
184
+ } {
185
+ return {
186
+ size: this.cache.size,
187
+ max_size: this.max_size,
188
+ };
189
+ }
190
+ }
191
+
192
+ // section: singleton
193
+ // Global cache instance (initialized with defaults, will be configured on first use)
194
+ let auth_cache_instance: AuthCache | null = null;
195
+
196
+ /**
197
+ * Gets or creates the global auth cache instance
198
+ * @param max_size - Maximum cache size (default: 10000)
199
+ * @param ttl_minutes - TTL in minutes (default: 15)
200
+ * @param max_age_minutes - Max age in minutes (default: 30)
201
+ * @returns Auth cache instance
202
+ */
203
+ export function get_auth_cache(
204
+ max_size: number = 10000,
205
+ ttl_minutes: number = 15,
206
+ max_age_minutes: number = 30,
207
+ ): AuthCache {
208
+ if (!auth_cache_instance) {
209
+ auth_cache_instance = new AuthCache(max_size, ttl_minutes, max_age_minutes);
210
+ }
211
+ return auth_cache_instance;
212
+ }
213
+
214
+ /**
215
+ * Resets the global cache instance (useful for testing)
216
+ */
217
+ export function reset_auth_cache(): void {
218
+ auth_cache_instance = null;
219
+ }
220
+
@@ -0,0 +1,121 @@
1
+ // file_description: Simple in-memory rate limiter for hazo_get_auth API endpoint
2
+ // section: types
3
+
4
+ /**
5
+ * Rate limit entry structure
6
+ */
7
+ type RateLimitEntry = {
8
+ count: number;
9
+ window_start: number; // Unix timestamp in milliseconds
10
+ };
11
+
12
+ /**
13
+ * Simple in-memory rate limiter
14
+ * Tracks request counts per key within a time window
15
+ */
16
+ class RateLimiter {
17
+ private limits: Map<string, RateLimitEntry>;
18
+ private window_ms: number; // 1 minute = 60000ms
19
+
20
+ constructor() {
21
+ this.limits = new Map();
22
+ this.window_ms = 60 * 1000; // 1 minute window
23
+ }
24
+
25
+ /**
26
+ * Checks if a request should be allowed
27
+ * @param key - Rate limit key (e.g., "user:123" or "ip:192.168.1.1")
28
+ * @param max_requests - Maximum requests allowed per window
29
+ * @returns true if allowed, false if rate limited
30
+ */
31
+ check(key: string, max_requests: number): boolean {
32
+ const now = Date.now();
33
+ const entry = this.limits.get(key);
34
+
35
+ if (!entry) {
36
+ // First request for this key
37
+ this.limits.set(key, {
38
+ count: 1,
39
+ window_start: now,
40
+ });
41
+ return true;
42
+ }
43
+
44
+ // Check if window has expired
45
+ if (now - entry.window_start >= this.window_ms) {
46
+ // Reset window
47
+ this.limits.set(key, {
48
+ count: 1,
49
+ window_start: now,
50
+ });
51
+ return true;
52
+ }
53
+
54
+ // Check if limit exceeded
55
+ if (entry.count >= max_requests) {
56
+ return false;
57
+ }
58
+
59
+ // Increment count
60
+ entry.count++;
61
+ return true;
62
+ }
63
+
64
+ /**
65
+ * Cleans up old entries (call periodically to prevent memory leak)
66
+ * Removes entries older than 2 windows
67
+ */
68
+ cleanup(): void {
69
+ const now = Date.now();
70
+ const cutoff = now - 2 * this.window_ms;
71
+
72
+ const keys_to_delete: string[] = [];
73
+ for (const [key, entry] of this.limits.entries()) {
74
+ if (entry.window_start < cutoff) {
75
+ keys_to_delete.push(key);
76
+ }
77
+ }
78
+
79
+ for (const key of keys_to_delete) {
80
+ this.limits.delete(key);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Gets rate limit statistics
86
+ * @returns Object with current limit entries count
87
+ */
88
+ get_stats(): { active_limits: number } {
89
+ return {
90
+ active_limits: this.limits.size,
91
+ };
92
+ }
93
+ }
94
+
95
+ // section: singleton
96
+ // Global rate limiter instance
97
+ let rate_limiter_instance: RateLimiter | null = null;
98
+
99
+ /**
100
+ * Gets or creates the global rate limiter instance
101
+ * @returns Rate limiter instance
102
+ */
103
+ export function get_rate_limiter(): RateLimiter {
104
+ if (!rate_limiter_instance) {
105
+ rate_limiter_instance = new RateLimiter();
106
+
107
+ // Cleanup old entries every 5 minutes
108
+ setInterval(() => {
109
+ rate_limiter_instance?.cleanup();
110
+ }, 5 * 60 * 1000);
111
+ }
112
+ return rate_limiter_instance;
113
+ }
114
+
115
+ /**
116
+ * Resets the global rate limiter instance (useful for testing)
117
+ */
118
+ export function reset_rate_limiter(): void {
119
+ rate_limiter_instance = null;
120
+ }
121
+
@@ -0,0 +1,65 @@
1
+ // file_description: Type definitions and error classes for hazo_get_auth utility
2
+ // section: types
3
+
4
+ /**
5
+ * User data structure returned by hazo_get_auth
6
+ */
7
+ export type HazoAuthUser = {
8
+ id: string;
9
+ name: string | null;
10
+ email_address: string;
11
+ is_active: boolean;
12
+ profile_picture_url: string | null;
13
+ };
14
+
15
+ /**
16
+ * Result type for hazo_get_auth function
17
+ * Returns authenticated state with user data and permissions, or unauthenticated state
18
+ */
19
+ export type HazoAuthResult =
20
+ | {
21
+ authenticated: true;
22
+ user: HazoAuthUser;
23
+ permissions: string[];
24
+ permission_ok: boolean;
25
+ missing_permissions?: string[];
26
+ }
27
+ | {
28
+ authenticated: false;
29
+ user: null;
30
+ permissions: [];
31
+ permission_ok: false;
32
+ };
33
+
34
+ /**
35
+ * Options for hazo_get_auth function
36
+ */
37
+ export type HazoAuthOptions = {
38
+ /**
39
+ * Array of required permissions to check
40
+ * If provided, permission_ok will be set based on whether user has all required permissions
41
+ */
42
+ required_permissions?: string[];
43
+ /**
44
+ * If true, throws PermissionError when user lacks required permissions
45
+ * If false (default), returns permission_ok: false without throwing
46
+ */
47
+ strict?: boolean;
48
+ };
49
+
50
+ /**
51
+ * Custom error class for permission denials
52
+ * Includes technical and user-friendly error messages
53
+ */
54
+ export class PermissionError extends Error {
55
+ constructor(
56
+ public missing_permissions: string[],
57
+ public user_permissions: string[],
58
+ public required_permissions: string[],
59
+ public user_friendly_message?: string,
60
+ ) {
61
+ super(`Missing permissions: ${missing_permissions.join(", ")}`);
62
+ this.name = "PermissionError";
63
+ }
64
+ }
65
+