hazo_auth 1.0.0 → 1.0.2
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 +911 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,915 @@ After installing the package, you need to set up configuration files in your pro
|
|
|
29
29
|
|
|
30
30
|
**Important:** The configuration files must be located in your project root directory (where `process.cwd()` points to), not inside `node_modules`. The package reads configuration from `process.cwd()` at runtime.
|
|
31
31
|
|
|
32
|
+
## Authentication Service
|
|
33
|
+
|
|
34
|
+
The `hazo_auth` package provides a comprehensive authentication and authorization system with role-based access control (RBAC). The main authentication utility is `hazo_get_auth`, which provides user details, permissions, and permission checking with built-in caching and rate limiting.
|
|
35
|
+
|
|
36
|
+
### Server-Side Functions
|
|
37
|
+
|
|
38
|
+
#### `hazo_get_auth` (Recommended)
|
|
39
|
+
|
|
40
|
+
The primary authentication utility for server-side use in API routes. Returns user details, permissions, and optionally checks required permissions.
|
|
41
|
+
|
|
42
|
+
**Location:** `src/lib/auth/hazo_get_auth.server.ts`
|
|
43
|
+
|
|
44
|
+
**Function Signature:**
|
|
45
|
+
```typescript
|
|
46
|
+
import { hazo_get_auth } from "hazo_auth/lib/auth/hazo_get_auth.server";
|
|
47
|
+
import type { HazoAuthResult, HazoAuthOptions } from "hazo_auth/lib/auth/auth_types";
|
|
48
|
+
|
|
49
|
+
async function hazo_get_auth(
|
|
50
|
+
request: NextRequest,
|
|
51
|
+
options?: HazoAuthOptions
|
|
52
|
+
): Promise<HazoAuthResult>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Options:**
|
|
56
|
+
- `required_permissions?: string[]` - Array of permission names to check
|
|
57
|
+
- `strict?: boolean` - If `true`, throws `PermissionError` when permissions are missing (default: `false`)
|
|
58
|
+
|
|
59
|
+
**Return Type:**
|
|
60
|
+
```typescript
|
|
61
|
+
type HazoAuthResult =
|
|
62
|
+
| {
|
|
63
|
+
authenticated: true;
|
|
64
|
+
user: {
|
|
65
|
+
id: string;
|
|
66
|
+
name: string | null;
|
|
67
|
+
email_address: string;
|
|
68
|
+
is_active: boolean;
|
|
69
|
+
profile_picture_url: string | null;
|
|
70
|
+
};
|
|
71
|
+
permissions: string[];
|
|
72
|
+
permission_ok: boolean;
|
|
73
|
+
missing_permissions?: string[];
|
|
74
|
+
}
|
|
75
|
+
| {
|
|
76
|
+
authenticated: false;
|
|
77
|
+
user: null;
|
|
78
|
+
permissions: [];
|
|
79
|
+
permission_ok: false;
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Features:**
|
|
84
|
+
- **Caching:** LRU cache with TTL (configurable, default: 15 minutes)
|
|
85
|
+
- **Rate Limiting:** Per-user and per-IP rate limiting
|
|
86
|
+
- **Permission Checking:** Optional permission validation with detailed error messages
|
|
87
|
+
- **Audit Logging:** Logs permission denials for security auditing
|
|
88
|
+
- **Performance:** Optimized database queries with single JOIN
|
|
89
|
+
|
|
90
|
+
**Example Usage:**
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// In an API route (src/app/api/protected/route.ts)
|
|
94
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
95
|
+
import { hazo_get_auth } from "hazo_auth/lib/auth/hazo_get_auth.server";
|
|
96
|
+
import { PermissionError } from "hazo_auth/lib/auth/auth_types";
|
|
97
|
+
|
|
98
|
+
export async function GET(request: NextRequest) {
|
|
99
|
+
try {
|
|
100
|
+
// Check authentication and permissions
|
|
101
|
+
const authResult = await hazo_get_auth(request, {
|
|
102
|
+
required_permissions: ["admin_user_management"],
|
|
103
|
+
strict: true, // Throws PermissionError if missing permissions
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!authResult.authenticated) {
|
|
107
|
+
return NextResponse.json(
|
|
108
|
+
{ error: "Authentication required" },
|
|
109
|
+
{ status: 401 }
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// User is authenticated and has required permissions
|
|
114
|
+
return NextResponse.json({
|
|
115
|
+
message: "Access granted",
|
|
116
|
+
user: authResult.user,
|
|
117
|
+
permissions: authResult.permissions,
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error instanceof PermissionError) {
|
|
121
|
+
return NextResponse.json(
|
|
122
|
+
{
|
|
123
|
+
error: "Permission denied",
|
|
124
|
+
missing_permissions: error.missing_permissions,
|
|
125
|
+
user_friendly_message: error.user_friendly_message,
|
|
126
|
+
},
|
|
127
|
+
{ status: 403 }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Non-strict mode (returns permission status without throwing):**
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// Check permissions without throwing errors
|
|
139
|
+
const authResult = await hazo_get_auth(request, {
|
|
140
|
+
required_permissions: ["admin_user_management"],
|
|
141
|
+
strict: false, // Returns permission_ok: false if missing
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (authResult.authenticated && authResult.permission_ok) {
|
|
145
|
+
// User has required permissions
|
|
146
|
+
} else if (authResult.authenticated) {
|
|
147
|
+
// User is authenticated but missing permissions
|
|
148
|
+
console.log("Missing permissions:", authResult.missing_permissions);
|
|
149
|
+
} else {
|
|
150
|
+
// User is not authenticated
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### `get_authenticated_user`
|
|
155
|
+
|
|
156
|
+
Basic authentication check for API routes. Returns user info if authenticated, or `{ authenticated: false }` if not.
|
|
157
|
+
|
|
158
|
+
**Location:** `src/lib/auth/auth_utils.server.ts`
|
|
159
|
+
|
|
160
|
+
**Function Signature:**
|
|
161
|
+
```typescript
|
|
162
|
+
import { get_authenticated_user } from "hazo_auth/lib/auth/auth_utils.server";
|
|
163
|
+
import type { AuthResult } from "hazo_auth/lib/auth/auth_utils.server";
|
|
164
|
+
|
|
165
|
+
async function get_authenticated_user(request: NextRequest): Promise<AuthResult>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Example Usage:**
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
172
|
+
import { get_authenticated_user } from "hazo_auth/lib/auth/auth_utils.server";
|
|
173
|
+
|
|
174
|
+
export async function GET(request: NextRequest) {
|
|
175
|
+
const authResult = await get_authenticated_user(request);
|
|
176
|
+
|
|
177
|
+
if (!authResult.authenticated) {
|
|
178
|
+
return NextResponse.json(
|
|
179
|
+
{ error: "Authentication required" },
|
|
180
|
+
{ status: 401 }
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return NextResponse.json({
|
|
185
|
+
user_id: authResult.user_id,
|
|
186
|
+
email: authResult.email,
|
|
187
|
+
name: authResult.name,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
#### `require_auth`
|
|
193
|
+
|
|
194
|
+
Requires authentication and throws an error if the user is not authenticated. Useful for protected API routes.
|
|
195
|
+
|
|
196
|
+
**Location:** `src/lib/auth/auth_utils.server.ts`
|
|
197
|
+
|
|
198
|
+
**Function Signature:**
|
|
199
|
+
```typescript
|
|
200
|
+
import { require_auth } from "hazo_auth/lib/auth/auth_utils.server";
|
|
201
|
+
import type { AuthUser } from "hazo_auth/lib/auth/auth_utils.server";
|
|
202
|
+
|
|
203
|
+
async function require_auth(request: NextRequest): Promise<AuthUser>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Example Usage:**
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
210
|
+
import { require_auth } from "hazo_auth/lib/auth/auth_utils.server";
|
|
211
|
+
|
|
212
|
+
export async function GET(request: NextRequest) {
|
|
213
|
+
try {
|
|
214
|
+
const user = await require_auth(request);
|
|
215
|
+
// User is guaranteed to be authenticated here
|
|
216
|
+
return NextResponse.json({ user_id: user.user_id, email: user.email });
|
|
217
|
+
} catch (error) {
|
|
218
|
+
return NextResponse.json(
|
|
219
|
+
{ error: "Authentication required" },
|
|
220
|
+
{ status: 401 }
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### `is_authenticated`
|
|
227
|
+
|
|
228
|
+
Simple boolean check for authentication status.
|
|
229
|
+
|
|
230
|
+
**Location:** `src/lib/auth/auth_utils.server.ts`
|
|
231
|
+
|
|
232
|
+
**Function Signature:**
|
|
233
|
+
```typescript
|
|
234
|
+
import { is_authenticated } from "hazo_auth/lib/auth/auth_utils.server";
|
|
235
|
+
|
|
236
|
+
async function is_authenticated(request: NextRequest): Promise<boolean>
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Example Usage:**
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
243
|
+
import { is_authenticated } from "hazo_auth/lib/auth/auth_utils.server";
|
|
244
|
+
|
|
245
|
+
export async function GET(request: NextRequest) {
|
|
246
|
+
const authenticated = await is_authenticated(request);
|
|
247
|
+
|
|
248
|
+
if (!authenticated) {
|
|
249
|
+
return NextResponse.json(
|
|
250
|
+
{ error: "Authentication required" },
|
|
251
|
+
{ status: 401 }
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Continue with authenticated logic
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### `get_server_auth_user`
|
|
260
|
+
|
|
261
|
+
Gets authenticated user in server components and pages (uses Next.js `cookies()` function).
|
|
262
|
+
|
|
263
|
+
**Location:** `src/lib/auth/server_auth.ts`
|
|
264
|
+
|
|
265
|
+
**Function Signature:**
|
|
266
|
+
```typescript
|
|
267
|
+
import { get_server_auth_user } from "hazo_auth/lib/auth/server_auth";
|
|
268
|
+
import type { ServerAuthResult } from "hazo_auth/lib/auth/server_auth";
|
|
269
|
+
|
|
270
|
+
async function get_server_auth_user(): Promise<ServerAuthResult>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Example Usage:**
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// In a server component (src/app/dashboard/page.tsx)
|
|
277
|
+
import { get_server_auth_user } from "hazo_auth/lib/auth/server_auth";
|
|
278
|
+
|
|
279
|
+
export default async function DashboardPage() {
|
|
280
|
+
const authResult = await get_server_auth_user();
|
|
281
|
+
|
|
282
|
+
if (!authResult.authenticated) {
|
|
283
|
+
return <div>Please log in to access this page.</div>;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<div>
|
|
288
|
+
<h1>Welcome, {authResult.name || authResult.email}!</h1>
|
|
289
|
+
<p>User ID: {authResult.user_id}</p>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Client-Side Hooks
|
|
296
|
+
|
|
297
|
+
#### `use_hazo_auth`
|
|
298
|
+
|
|
299
|
+
React hook for fetching authentication status and permissions on the client side.
|
|
300
|
+
|
|
301
|
+
**Location:** `src/components/layouts/shared/hooks/use_hazo_auth.ts`
|
|
302
|
+
|
|
303
|
+
**Function Signature:**
|
|
304
|
+
```typescript
|
|
305
|
+
import { use_hazo_auth } from "hazo_auth/components/layouts/shared/hooks/use_hazo_auth";
|
|
306
|
+
import type { UseHazoAuthOptions, UseHazoAuthResult } from "hazo_auth/components/layouts/shared/hooks/use_hazo_auth";
|
|
307
|
+
|
|
308
|
+
function use_hazo_auth(options?: UseHazoAuthOptions): UseHazoAuthResult
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Options:**
|
|
312
|
+
- `required_permissions?: string[]` - Array of permission names to check
|
|
313
|
+
- `strict?: boolean` - If `true`, throws error when permissions are missing (default: `false`)
|
|
314
|
+
- `skip?: boolean` - Skip fetch (for conditional use)
|
|
315
|
+
|
|
316
|
+
**Return Type:**
|
|
317
|
+
```typescript
|
|
318
|
+
type UseHazoAuthResult = HazoAuthResult & {
|
|
319
|
+
loading: boolean;
|
|
320
|
+
error: Error | null;
|
|
321
|
+
refetch: () => Promise<void>;
|
|
322
|
+
};
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Example Usage:**
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
"use client";
|
|
329
|
+
|
|
330
|
+
import { use_hazo_auth } from "hazo_auth/components/layouts/shared/hooks/use_hazo_auth";
|
|
331
|
+
|
|
332
|
+
export function ProtectedComponent() {
|
|
333
|
+
const { authenticated, user, permissions, permission_ok, loading, error, refetch } =
|
|
334
|
+
use_hazo_auth({
|
|
335
|
+
required_permissions: ["admin_user_management"],
|
|
336
|
+
strict: false,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
if (loading) {
|
|
340
|
+
return <div>Loading...</div>;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (error) {
|
|
344
|
+
return <div>Error: {error.message}</div>;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!authenticated) {
|
|
348
|
+
return <div>Please log in to access this page.</div>;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!permission_ok) {
|
|
352
|
+
return <div>You don't have permission to access this page.</div>;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<div>
|
|
357
|
+
<h1>Welcome, {user?.name || user?.email_address}!</h1>
|
|
358
|
+
<p>Your permissions: {permissions.join(", ")}</p>
|
|
359
|
+
<button onClick={() => refetch()}>Refresh Auth Status</button>
|
|
360
|
+
</div>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**Conditional Permission Checks:**
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
"use client";
|
|
369
|
+
|
|
370
|
+
import { use_hazo_auth } from "hazo_auth/components/layouts/shared/hooks/use_hazo_auth";
|
|
371
|
+
|
|
372
|
+
export function ConditionalComponent() {
|
|
373
|
+
// Check multiple permissions
|
|
374
|
+
const userManagementAuth = use_hazo_auth({
|
|
375
|
+
required_permissions: ["admin_user_management"],
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const roleManagementAuth = use_hazo_auth({
|
|
379
|
+
required_permissions: ["admin_role_management"],
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<div>
|
|
384
|
+
{userManagementAuth.permission_ok && (
|
|
385
|
+
<button>Manage Users</button>
|
|
386
|
+
)}
|
|
387
|
+
{roleManagementAuth.permission_ok && (
|
|
388
|
+
<button>Manage Roles</button>
|
|
389
|
+
)}
|
|
390
|
+
</div>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
#### `trigger_hazo_auth_refresh`
|
|
396
|
+
|
|
397
|
+
Triggers a refresh of authentication status across all components using `use_hazo_auth`. Useful after login, logout, or permission changes.
|
|
398
|
+
|
|
399
|
+
**Location:** `src/components/layouts/shared/hooks/use_hazo_auth.ts`
|
|
400
|
+
|
|
401
|
+
**Function Signature:**
|
|
402
|
+
```typescript
|
|
403
|
+
import { trigger_hazo_auth_refresh } from "hazo_auth/components/layouts/shared/hooks/use_hazo_auth";
|
|
404
|
+
|
|
405
|
+
function trigger_hazo_auth_refresh(): void
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**Example Usage:**
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
"use client";
|
|
412
|
+
|
|
413
|
+
import { trigger_hazo_auth_refresh } from "hazo_auth/components/layouts/shared/hooks/use_hazo_auth";
|
|
414
|
+
|
|
415
|
+
export function LogoutButton() {
|
|
416
|
+
const handleLogout = async () => {
|
|
417
|
+
await fetch("/api/hazo_auth/logout", { method: "POST" });
|
|
418
|
+
trigger_hazo_auth_refresh(); // Notify all components to refresh auth status
|
|
419
|
+
window.location.href = "/hazo_auth/login";
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
return <button onClick={handleLogout}>Logout</button>;
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Configuration
|
|
427
|
+
|
|
428
|
+
The authentication utility can be configured in `hazo_auth_config.ini` under the `[hazo_auth__auth_utility]` section:
|
|
429
|
+
|
|
430
|
+
```ini
|
|
431
|
+
[hazo_auth__auth_utility]
|
|
432
|
+
# Cache settings
|
|
433
|
+
# Maximum number of users to cache (LRU eviction, default: 10000)
|
|
434
|
+
cache_max_users = 10000
|
|
435
|
+
|
|
436
|
+
# Cache TTL in minutes (default: 15)
|
|
437
|
+
cache_ttl_minutes = 15
|
|
438
|
+
|
|
439
|
+
# Force cache refresh if older than this many minutes (default: 30)
|
|
440
|
+
cache_max_age_minutes = 30
|
|
441
|
+
|
|
442
|
+
# Rate limiting for /api/hazo_auth/get_auth endpoint
|
|
443
|
+
# Per-user rate limit (requests per minute, default: 100)
|
|
444
|
+
rate_limit_per_user = 100
|
|
445
|
+
|
|
446
|
+
# Per-IP rate limit for unauthenticated requests (default: 200)
|
|
447
|
+
rate_limit_per_ip = 200
|
|
448
|
+
|
|
449
|
+
# Permission check behavior
|
|
450
|
+
# Log all permission denials for security audit (default: true)
|
|
451
|
+
log_permission_denials = true
|
|
452
|
+
|
|
453
|
+
# User-friendly error messages
|
|
454
|
+
# Enable mapping of technical permissions to user-friendly messages (default: true)
|
|
455
|
+
enable_friendly_error_messages = true
|
|
456
|
+
|
|
457
|
+
# Permission message mappings (optional, comma-separated: permission_name:user_message)
|
|
458
|
+
# Example: admin_user_management:You don't have access to user management,admin_role_management:You don't have access to role management
|
|
459
|
+
permission_error_messages =
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Testing Authentication and RBAC
|
|
463
|
+
|
|
464
|
+
#### Testing User Authentication
|
|
465
|
+
|
|
466
|
+
To test if a user is authenticated, use the `hazo_get_auth` function or the `use_hazo_auth` hook:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
// Server-side test
|
|
470
|
+
const authResult = await hazo_get_auth(request);
|
|
471
|
+
if (authResult.authenticated) {
|
|
472
|
+
console.log("User is authenticated:", authResult.user.email_address);
|
|
473
|
+
console.log("User permissions:", authResult.permissions);
|
|
474
|
+
} else {
|
|
475
|
+
console.log("User is not authenticated");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Client-side test
|
|
479
|
+
const { authenticated, user, permissions } = use_hazo_auth();
|
|
480
|
+
if (authenticated) {
|
|
481
|
+
console.log("User is authenticated:", user?.email_address);
|
|
482
|
+
console.log("User permissions:", permissions);
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
#### Testing RBAC Permissions
|
|
487
|
+
|
|
488
|
+
To test if a user has specific permissions:
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
// Server-side - strict mode (throws error if missing)
|
|
492
|
+
try {
|
|
493
|
+
const authResult = await hazo_get_auth(request, {
|
|
494
|
+
required_permissions: ["admin_user_management", "admin_role_management"],
|
|
495
|
+
strict: true,
|
|
496
|
+
});
|
|
497
|
+
// User has all required permissions
|
|
498
|
+
console.log("Access granted");
|
|
499
|
+
} catch (error) {
|
|
500
|
+
if (error instanceof PermissionError) {
|
|
501
|
+
console.log("Missing permissions:", error.missing_permissions);
|
|
502
|
+
console.log("User permissions:", error.user_permissions);
|
|
503
|
+
console.log("User-friendly message:", error.user_friendly_message);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Server-side - non-strict mode (returns status)
|
|
508
|
+
const authResult = await hazo_get_auth(request, {
|
|
509
|
+
required_permissions: ["admin_user_management"],
|
|
510
|
+
strict: false,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
if (authResult.authenticated && authResult.permission_ok) {
|
|
514
|
+
console.log("User has required permissions");
|
|
515
|
+
} else if (authResult.authenticated) {
|
|
516
|
+
console.log("User is missing permissions:", authResult.missing_permissions);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Client-side test
|
|
520
|
+
const { permission_ok, missing_permissions, permissions } = use_hazo_auth({
|
|
521
|
+
required_permissions: ["admin_user_management"],
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
if (permission_ok) {
|
|
525
|
+
console.log("User has required permissions");
|
|
526
|
+
} else {
|
|
527
|
+
console.log("Missing permissions:", missing_permissions);
|
|
528
|
+
console.log("User permissions:", permissions);
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### Getting All User Permissions
|
|
533
|
+
|
|
534
|
+
To get all permissions for the current user:
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
// Server-side
|
|
538
|
+
const authResult = await hazo_get_auth(request);
|
|
539
|
+
if (authResult.authenticated) {
|
|
540
|
+
console.log("All user permissions:", authResult.permissions);
|
|
541
|
+
// Check if user has a specific permission
|
|
542
|
+
const hasPermission = authResult.permissions.includes("admin_user_management");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Client-side
|
|
546
|
+
const { permissions } = use_hazo_auth();
|
|
547
|
+
console.log("All user permissions:", permissions);
|
|
548
|
+
const hasPermission = permissions.includes("admin_user_management");
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
#### Testing in API Routes
|
|
552
|
+
|
|
553
|
+
Example of a protected API route with permission checking:
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
// src/app/api/admin/users/route.ts
|
|
557
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
558
|
+
import { hazo_get_auth } from "hazo_auth/lib/auth/hazo_get_auth.server";
|
|
559
|
+
import { PermissionError } from "hazo_auth/lib/auth/auth_types";
|
|
560
|
+
|
|
561
|
+
export async function GET(request: NextRequest) {
|
|
562
|
+
try {
|
|
563
|
+
// Require authentication and specific permission
|
|
564
|
+
const authResult = await hazo_get_auth(request, {
|
|
565
|
+
required_permissions: ["admin_user_management"],
|
|
566
|
+
strict: true,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// Fetch users (only accessible to admins)
|
|
570
|
+
const users = await fetchUsers();
|
|
571
|
+
|
|
572
|
+
return NextResponse.json({ users });
|
|
573
|
+
} catch (error) {
|
|
574
|
+
if (error instanceof PermissionError) {
|
|
575
|
+
return NextResponse.json(
|
|
576
|
+
{
|
|
577
|
+
error: "Permission denied",
|
|
578
|
+
missing_permissions: error.missing_permissions,
|
|
579
|
+
user_friendly_message: error.user_friendly_message,
|
|
580
|
+
},
|
|
581
|
+
{ status: 403 }
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return NextResponse.json(
|
|
586
|
+
{ error: "Authentication required" },
|
|
587
|
+
{ status: 401 }
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
#### Testing in React Components
|
|
594
|
+
|
|
595
|
+
Example of a protected component with permission-based UI:
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
"use client";
|
|
599
|
+
|
|
600
|
+
import { use_hazo_auth } from "hazo_auth/components/layouts/shared/hooks/use_hazo_auth";
|
|
601
|
+
|
|
602
|
+
export function AdminDashboard() {
|
|
603
|
+
const userManagementAuth = use_hazo_auth({
|
|
604
|
+
required_permissions: ["admin_user_management"],
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const roleManagementAuth = use_hazo_auth({
|
|
608
|
+
required_permissions: ["admin_role_management"],
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
if (userManagementAuth.loading || roleManagementAuth.loading) {
|
|
612
|
+
return <div>Loading...</div>;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (!userManagementAuth.authenticated) {
|
|
616
|
+
return <div>Please log in to access this page.</div>;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return (
|
|
620
|
+
<div>
|
|
621
|
+
<h1>Admin Dashboard</h1>
|
|
622
|
+
{userManagementAuth.permission_ok && (
|
|
623
|
+
<section>
|
|
624
|
+
<h2>User Management</h2>
|
|
625
|
+
{/* User management UI */}
|
|
626
|
+
</section>
|
|
627
|
+
)}
|
|
628
|
+
{roleManagementAuth.permission_ok && (
|
|
629
|
+
<section>
|
|
630
|
+
<h2>Role Management</h2>
|
|
631
|
+
{/* Role management UI */}
|
|
632
|
+
</section>
|
|
633
|
+
)}
|
|
634
|
+
{!userManagementAuth.permission_ok && !roleManagementAuth.permission_ok && (
|
|
635
|
+
<div>You don't have permission to access any admin features.</div>
|
|
636
|
+
)}
|
|
637
|
+
</div>
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### Cache Invalidation
|
|
643
|
+
|
|
644
|
+
The authentication cache is automatically invalidated in the following scenarios:
|
|
645
|
+
- User logout
|
|
646
|
+
- Password change
|
|
647
|
+
- User deactivation
|
|
648
|
+
- Role assignment changes
|
|
649
|
+
- Permission changes to roles
|
|
650
|
+
|
|
651
|
+
You can also manually invalidate the cache using the API endpoint:
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
// POST /api/hazo_auth/invalidate_cache
|
|
655
|
+
// Body: { user_id?: string, role_ids?: number[], invalidate_all?: boolean }
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
## Profile Picture Menu Widget
|
|
659
|
+
|
|
660
|
+
The Profile Picture Menu is a versatile component for navbar or sidebar that automatically displays:
|
|
661
|
+
- **When authenticated**: User's profile picture with a dropdown menu containing user info, settings link, logout, and custom menu items
|
|
662
|
+
- **When not authenticated**: Sign Up and Sign In buttons (or a single button, configurable)
|
|
663
|
+
|
|
664
|
+
### Basic Usage (Recommended)
|
|
665
|
+
|
|
666
|
+
Use the `ProfilePicMenuWrapper` component which automatically loads configuration from `hazo_auth_config.ini`:
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
// In your navbar or layout component
|
|
670
|
+
import { ProfilePicMenuWrapper } from "hazo_auth/components/layouts/shared/components/profile_pic_menu_wrapper";
|
|
671
|
+
|
|
672
|
+
export function Navbar() {
|
|
673
|
+
return (
|
|
674
|
+
<nav className="flex items-center justify-between p-4">
|
|
675
|
+
<div>Logo</div>
|
|
676
|
+
<ProfilePicMenuWrapper
|
|
677
|
+
avatar_size="default" // "sm" | "default" | "lg"
|
|
678
|
+
className="ml-auto"
|
|
679
|
+
/>
|
|
680
|
+
</nav>
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### Direct Usage (Manual Configuration)
|
|
686
|
+
|
|
687
|
+
If you prefer to configure the component directly without using the config file:
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
"use client";
|
|
691
|
+
|
|
692
|
+
import { ProfilePicMenu } from "hazo_auth/components/layouts/shared/components/profile_pic_menu";
|
|
693
|
+
|
|
694
|
+
export function Navbar() {
|
|
695
|
+
return (
|
|
696
|
+
<nav className="flex items-center justify-between p-4">
|
|
697
|
+
<div>Logo</div>
|
|
698
|
+
<ProfilePicMenu
|
|
699
|
+
show_single_button={false}
|
|
700
|
+
sign_up_label="Sign Up"
|
|
701
|
+
sign_in_label="Sign In"
|
|
702
|
+
register_path="/hazo_auth/register"
|
|
703
|
+
login_path="/hazo_auth/login"
|
|
704
|
+
settings_path="/hazo_auth/my_settings"
|
|
705
|
+
logout_path="/api/hazo_auth/logout"
|
|
706
|
+
avatar_size="default"
|
|
707
|
+
className="ml-auto"
|
|
708
|
+
/>
|
|
709
|
+
</nav>
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### Configuration
|
|
715
|
+
|
|
716
|
+
Configure the Profile Picture Menu in `hazo_auth_config.ini` under the `[hazo_auth__profile_pic_menu]` section:
|
|
717
|
+
|
|
718
|
+
```ini
|
|
719
|
+
[hazo_auth__profile_pic_menu]
|
|
720
|
+
# Button configuration for unauthenticated users
|
|
721
|
+
# Show only "Sign Up" button when true, show both "Sign Up" and "Sign In" buttons when false (default)
|
|
722
|
+
show_single_button = false
|
|
723
|
+
|
|
724
|
+
# Sign up button label
|
|
725
|
+
sign_up_label = Sign Up
|
|
726
|
+
|
|
727
|
+
# Sign in button label
|
|
728
|
+
sign_in_label = Sign In
|
|
729
|
+
|
|
730
|
+
# Register page path
|
|
731
|
+
register_path = /hazo_auth/register
|
|
732
|
+
|
|
733
|
+
# Login page path
|
|
734
|
+
login_path = /hazo_auth/login
|
|
735
|
+
|
|
736
|
+
# Settings page path (shown in dropdown menu when authenticated)
|
|
737
|
+
settings_path = /hazo_auth/my_settings
|
|
738
|
+
|
|
739
|
+
# Logout API endpoint path
|
|
740
|
+
logout_path = /api/hazo_auth/logout
|
|
741
|
+
|
|
742
|
+
# Custom menu items (optional)
|
|
743
|
+
# Format: "type:label:value_or_href:order" for info/link, or "separator:order" for separator
|
|
744
|
+
# Examples:
|
|
745
|
+
# - Info item: "info:Phone:+1234567890:3"
|
|
746
|
+
# - Link item: "link:My Account:/account:4"
|
|
747
|
+
# - Separator: "separator:2"
|
|
748
|
+
# Custom items are added to the default menu items (name, email, separator, Settings, Logout)
|
|
749
|
+
# Items are sorted by type (info first, then separators, then links) and then by order within each type
|
|
750
|
+
custom_menu_items =
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
### Component Props
|
|
754
|
+
|
|
755
|
+
#### `ProfilePicMenuWrapper` Props
|
|
756
|
+
|
|
757
|
+
- `className?: string` - Additional CSS classes
|
|
758
|
+
- `avatar_size?: "sm" | "default" | "lg"` - Size of the profile picture avatar (default: "default")
|
|
759
|
+
|
|
760
|
+
#### `ProfilePicMenu` Props
|
|
761
|
+
|
|
762
|
+
- `show_single_button?: boolean` - Show only "Sign Up" button when true (default: false)
|
|
763
|
+
- `sign_up_label?: string` - Label for sign up button (default: "Sign Up")
|
|
764
|
+
- `sign_in_label?: string` - Label for sign in button (default: "Sign In")
|
|
765
|
+
- `register_path?: string` - Path to registration page (default: "/hazo_auth/register")
|
|
766
|
+
- `login_path?: string` - Path to login page (default: "/hazo_auth/login")
|
|
767
|
+
- `settings_path?: string` - Path to settings page (default: "/hazo_auth/my_settings")
|
|
768
|
+
- `logout_path?: string` - Path to logout API endpoint (default: "/api/hazo_auth/logout")
|
|
769
|
+
- `custom_menu_items?: ProfilePicMenuMenuItem[]` - Array of custom menu items
|
|
770
|
+
- `className?: string` - Additional CSS classes
|
|
771
|
+
- `avatar_size?: "sm" | "default" | "lg"` - Size of the profile picture avatar (default: "default")
|
|
772
|
+
|
|
773
|
+
### Custom Menu Items
|
|
774
|
+
|
|
775
|
+
You can add custom menu items to the dropdown menu. Items are automatically sorted by type (info → separator → link) and then by order.
|
|
776
|
+
|
|
777
|
+
**Menu Item Types:**
|
|
778
|
+
|
|
779
|
+
1. **Info** - Display-only text (e.g., phone number, department)
|
|
780
|
+
- Format: `"info:label:value:order"`
|
|
781
|
+
- Example: `"info:Phone:+1234567890:3"`
|
|
782
|
+
|
|
783
|
+
2. **Link** - Clickable menu item that navigates to a URL
|
|
784
|
+
- Format: `"link:label:href:order"`
|
|
785
|
+
- Example: `"link:My Account:/account:4"`
|
|
786
|
+
|
|
787
|
+
3. **Separator** - Visual separator line
|
|
788
|
+
- Format: `"separator:order"`
|
|
789
|
+
- Example: `"separator:2"`
|
|
790
|
+
|
|
791
|
+
**Example Configuration:**
|
|
792
|
+
|
|
793
|
+
```ini
|
|
794
|
+
[hazo_auth__profile_pic_menu]
|
|
795
|
+
# Add custom menu items
|
|
796
|
+
custom_menu_items = info:Phone:+1234567890:3,separator:2,link:My Account:/account:4,link:Help:/help:5
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
This will create a menu with:
|
|
800
|
+
1. Default items (name, email, separator, Settings, Logout)
|
|
801
|
+
2. Custom info item: "Phone: +1234567890" (order 3)
|
|
802
|
+
3. Custom separator (order 2)
|
|
803
|
+
4. Custom link: "My Account" → `/account` (order 4)
|
|
804
|
+
5. Custom link: "Help" → `/help` (order 5)
|
|
805
|
+
|
|
806
|
+
Items are sorted by type priority (info < separator < link) and then by order within each type.
|
|
807
|
+
|
|
808
|
+
### Default Menu Items
|
|
809
|
+
|
|
810
|
+
When authenticated, the dropdown menu automatically includes:
|
|
811
|
+
- User's name (if available)
|
|
812
|
+
- User's email address
|
|
813
|
+
- Separator
|
|
814
|
+
- Settings link (with Settings icon)
|
|
815
|
+
- Logout link (with LogOut icon, triggers logout action)
|
|
816
|
+
|
|
817
|
+
### Examples
|
|
818
|
+
|
|
819
|
+
#### Example 1: Simple Navbar Integration
|
|
820
|
+
|
|
821
|
+
```typescript
|
|
822
|
+
// app/components/navbar.tsx
|
|
823
|
+
import { ProfilePicMenuWrapper } from "hazo_auth/components/layouts/shared/components/profile_pic_menu_wrapper";
|
|
824
|
+
|
|
825
|
+
export function Navbar() {
|
|
826
|
+
return (
|
|
827
|
+
<header className="border-b">
|
|
828
|
+
<nav className="container mx-auto flex items-center justify-between p-4">
|
|
829
|
+
<div className="text-xl font-bold">My App</div>
|
|
830
|
+
<ProfilePicMenuWrapper />
|
|
831
|
+
</nav>
|
|
832
|
+
</header>
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
#### Example 2: Custom Styling and Size
|
|
838
|
+
|
|
839
|
+
```typescript
|
|
840
|
+
// app/components/navbar.tsx
|
|
841
|
+
import { ProfilePicMenuWrapper } from "hazo_auth/components/layouts/shared/components/profile_pic_menu_wrapper";
|
|
842
|
+
|
|
843
|
+
export function Navbar() {
|
|
844
|
+
return (
|
|
845
|
+
<header className="bg-slate-900 text-white">
|
|
846
|
+
<nav className="container mx-auto flex items-center justify-between p-4">
|
|
847
|
+
<div className="text-xl font-bold">My App</div>
|
|
848
|
+
<ProfilePicMenuWrapper
|
|
849
|
+
avatar_size="sm"
|
|
850
|
+
className="bg-slate-800 rounded-lg p-2"
|
|
851
|
+
/>
|
|
852
|
+
</nav>
|
|
853
|
+
</header>
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
#### Example 3: With Custom Menu Items (Programmatic)
|
|
859
|
+
|
|
860
|
+
```typescript
|
|
861
|
+
"use client";
|
|
862
|
+
|
|
863
|
+
import { ProfilePicMenu } from "hazo_auth/components/layouts/shared/components/profile_pic_menu";
|
|
864
|
+
import type { ProfilePicMenuMenuItem } from "hazo_auth/lib/profile_pic_menu_config.server";
|
|
865
|
+
|
|
866
|
+
export function Navbar() {
|
|
867
|
+
const customItems: ProfilePicMenuMenuItem[] = [
|
|
868
|
+
{
|
|
869
|
+
type: "info",
|
|
870
|
+
label: "Department",
|
|
871
|
+
value: "Engineering",
|
|
872
|
+
order: 3,
|
|
873
|
+
id: "dept_info",
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
type: "separator",
|
|
877
|
+
order: 2,
|
|
878
|
+
id: "custom_sep",
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
type: "link",
|
|
882
|
+
label: "Documentation",
|
|
883
|
+
href: "/docs",
|
|
884
|
+
order: 4,
|
|
885
|
+
id: "docs_link",
|
|
886
|
+
},
|
|
887
|
+
];
|
|
888
|
+
|
|
889
|
+
return (
|
|
890
|
+
<nav className="flex items-center justify-between p-4">
|
|
891
|
+
<div>Logo</div>
|
|
892
|
+
<ProfilePicMenu
|
|
893
|
+
custom_menu_items={customItems}
|
|
894
|
+
avatar_size="default"
|
|
895
|
+
/>
|
|
896
|
+
</nav>
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
#### Example 4: Single Button Mode
|
|
902
|
+
|
|
903
|
+
```typescript
|
|
904
|
+
// In hazo_auth_config.ini
|
|
905
|
+
[hazo_auth__profile_pic_menu]
|
|
906
|
+
show_single_button = true
|
|
907
|
+
sign_up_label = Get Started
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
When `show_single_button` is `true`, only the "Sign Up" button is shown for unauthenticated users (no "Sign In" button).
|
|
911
|
+
|
|
912
|
+
### Behavior
|
|
913
|
+
|
|
914
|
+
- **Loading State**: Shows a pulsing placeholder while checking authentication status
|
|
915
|
+
- **Unauthenticated**: Shows Sign Up/Sign In buttons (or single button if configured)
|
|
916
|
+
- **Authenticated**: Shows profile picture with dropdown menu
|
|
917
|
+
- **Profile Picture Fallback**: If no profile picture is set, shows user's initials
|
|
918
|
+
- **Logout**: Handles logout action, refreshes auth status, and redirects appropriately
|
|
919
|
+
- **Responsive**: Works well in both navbar and sidebar layouts
|
|
920
|
+
|
|
921
|
+
### Styling
|
|
922
|
+
|
|
923
|
+
The component uses TailwindCSS classes and can be customized with:
|
|
924
|
+
- `className` prop for additional styling
|
|
925
|
+
- `avatar_size` prop for different avatar sizes
|
|
926
|
+
- CSS class names prefixed with `cls_profile_pic_menu_*` for targeted styling
|
|
927
|
+
|
|
928
|
+
Example custom styling:
|
|
929
|
+
|
|
930
|
+
```css
|
|
931
|
+
/* Target specific elements */
|
|
932
|
+
.cls_profile_pic_menu_avatar {
|
|
933
|
+
border: 2px solid #3b82f6;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
.cls_profile_pic_menu_dropdown {
|
|
937
|
+
min-width: 200px;
|
|
938
|
+
}
|
|
939
|
+
```
|
|
940
|
+
|
|
32
941
|
### Local Development (for package contributors)
|
|
33
942
|
|
|
34
943
|
- `npm install` to install dependencies.
|
|
@@ -38,8 +947,9 @@ After installing the package, you need to set up configuration files in your pro
|
|
|
38
947
|
### Project Structure
|
|
39
948
|
|
|
40
949
|
- `src/app` contains the application shell and route composition.
|
|
950
|
+
- `src/lib` is the home for shared utilities and authentication functions.
|
|
951
|
+
- `src/components` contains React components and hooks.
|
|
41
952
|
- `src/stories` holds Storybook stories for documenting components.
|
|
42
|
-
- `src/lib` is the home for shared utilities.
|
|
43
953
|
|
|
44
954
|
### Next Steps
|
|
45
955
|
|