next-api-layer 0.1.5
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/LICENSE +21 -0
- package/README.md +743 -0
- package/dist/api.cjs +2 -0
- package/dist/api.cjs.map +1 -0
- package/dist/api.d.cts +42 -0
- package/dist/api.d.ts +42 -0
- package/dist/api.js +2 -0
- package/dist/api.js.map +1 -0
- package/dist/chunk-6ENVQMWQ.cjs +2 -0
- package/dist/chunk-6ENVQMWQ.cjs.map +1 -0
- package/dist/chunk-NBYI46RO.js +2 -0
- package/dist/chunk-NBYI46RO.js.map +1 -0
- package/dist/chunk-OXXKU4OM.cjs +2 -0
- package/dist/chunk-OXXKU4OM.cjs.map +1 -0
- package/dist/chunk-XBAO7FJN.js +2 -0
- package/dist/chunk-XBAO7FJN.js.map +1 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +14 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/client.cjs +3 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +169 -0
- package/dist/client.d.ts +169 -0
- package/dist/client.js +3 -0
- package/dist/client.js.map +1 -0
- package/dist/createApiClient-CIDYcpNI.d.cts +383 -0
- package/dist/createApiClient-CIDYcpNI.d.ts +383 -0
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +300 -0
- package/dist/index.d.ts +300 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +2 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +77 -0
- package/dist/server.d.ts +77 -0
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -0
- package/package.json +124 -0
package/README.md
ADDED
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
# next-api-layer
|
|
2
|
+
|
|
3
|
+
> Production-grade API layer for Next.js + External JWT Backend (Laravel, Django, .NET, Go, Express)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/next-api-layer)
|
|
6
|
+
[](https://github.com/dijinar/next-api-layer)
|
|
7
|
+
[](https://www.typescriptlang.org)
|
|
8
|
+
[](https://nextjs.org)
|
|
9
|
+
|
|
10
|
+
## The Problem
|
|
11
|
+
|
|
12
|
+
Building Next.js apps with external JWT backends (not NextAuth/Clerk) requires:
|
|
13
|
+
- Token validation middleware
|
|
14
|
+
- Guest token handling
|
|
15
|
+
- XSS sanitization
|
|
16
|
+
- i18n support
|
|
17
|
+
- Cookie management with httpOnly
|
|
18
|
+
- Token refresh with caching
|
|
19
|
+
- ...and 15+ other concerns
|
|
20
|
+
|
|
21
|
+
**This library solves all of them in one package.**
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install next-api-layer
|
|
27
|
+
# or
|
|
28
|
+
pnpm add next-api-layer
|
|
29
|
+
# or
|
|
30
|
+
yarn add next-api-layer
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
### 1. Create the Auth Proxy (Middleware)
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// middleware.ts
|
|
39
|
+
import { createAuthProxy } from 'next-api-layer';
|
|
40
|
+
|
|
41
|
+
const authProxy = createAuthProxy({
|
|
42
|
+
apiBaseUrl: process.env.API_BASE_URL!,
|
|
43
|
+
cookies: {
|
|
44
|
+
user: 'userAuthToken',
|
|
45
|
+
guest: 'guestAuthToken',
|
|
46
|
+
},
|
|
47
|
+
guestToken: {
|
|
48
|
+
enabled: true,
|
|
49
|
+
credentials: {
|
|
50
|
+
username: process.env.GUEST_USERNAME!,
|
|
51
|
+
password: process.env.GUEST_PASSWORD!,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
access: {
|
|
55
|
+
protectedRoutes: ['/dashboard', '/profile', '/settings'],
|
|
56
|
+
authRoutes: ['/login', '/register'],
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export default authProxy;
|
|
61
|
+
|
|
62
|
+
export const config = {
|
|
63
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
#### Custom Middleware (Composable)
|
|
68
|
+
|
|
69
|
+
Kendi middleware lojiğinizi eklemek istiyorsanız `beforeAuth` ve `afterAuth` hook'larını kullanabilirsiniz:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// middleware.ts
|
|
73
|
+
import { createAuthProxy } from 'next-api-layer';
|
|
74
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
75
|
+
|
|
76
|
+
const authProxy = createAuthProxy({
|
|
77
|
+
apiBaseUrl: process.env.API_BASE_URL!,
|
|
78
|
+
cookies: {
|
|
79
|
+
user: 'userAuthToken',
|
|
80
|
+
guest: 'guestAuthToken',
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Auth kontrolünden ÖNCE çalışır
|
|
84
|
+
beforeAuth: async (req: NextRequest) => {
|
|
85
|
+
const { pathname } = req.nextUrl;
|
|
86
|
+
|
|
87
|
+
// Rate limiting
|
|
88
|
+
if (pathname.startsWith('/api/') && isRateLimited(req)) {
|
|
89
|
+
return NextResponse.json({ error: 'Too many requests' }, { status: 429 });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Maintenance mode
|
|
93
|
+
if (process.env.MAINTENANCE_MODE === 'true' && !pathname.startsWith('/maintenance')) {
|
|
94
|
+
return NextResponse.redirect(new URL('/maintenance', req.url));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Logging
|
|
98
|
+
console.log(`[${new Date().toISOString()}] ${req.method} ${pathname}`);
|
|
99
|
+
|
|
100
|
+
// null döndür = auth kontrolüne devam et
|
|
101
|
+
return null;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// Auth kontrolünden SONRA çalışır
|
|
105
|
+
afterAuth: async (req, response, authResult) => {
|
|
106
|
+
// Custom header ekle
|
|
107
|
+
response.headers.set('x-auth-status', authResult.isAuthenticated ? 'authenticated' : 'guest');
|
|
108
|
+
|
|
109
|
+
// Admin kontrolü
|
|
110
|
+
if (req.nextUrl.pathname.startsWith('/admin') && authResult.user?.role !== 'admin') {
|
|
111
|
+
return NextResponse.redirect(new URL('/403', req.url));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return response;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export default authProxy;
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 2. Create the API Client
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// lib/api.ts - Configure ONCE, use everywhere
|
|
125
|
+
import { createApiClient } from 'next-api-layer';
|
|
126
|
+
|
|
127
|
+
export const api = createApiClient({
|
|
128
|
+
sanitization: {
|
|
129
|
+
enabled: true,
|
|
130
|
+
// Skip specific fields (e.g., fields containing intentional HTML)
|
|
131
|
+
skipFields: ['html_content', 'raw_markdown'],
|
|
132
|
+
// Skip entire endpoints (glob patterns supported)
|
|
133
|
+
skipEndpoints: ['cms/*', 'pages/raw/**', 'content/html'],
|
|
134
|
+
},
|
|
135
|
+
i18n: {
|
|
136
|
+
enabled: true,
|
|
137
|
+
paramName: 'lang',
|
|
138
|
+
},
|
|
139
|
+
methodSpoofing: true, // For Laravel PUT/PATCH support
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// Usage - ONE LINE anywhere in your app!
|
|
145
|
+
import { api } from '@/lib/api';
|
|
146
|
+
|
|
147
|
+
// Simple GET
|
|
148
|
+
const { data, success } = await api.get('users/profile');
|
|
149
|
+
|
|
150
|
+
// POST with body
|
|
151
|
+
const result = await api.post('projects', { body: { name: 'New Project' } });
|
|
152
|
+
|
|
153
|
+
// Per-request skip (overrides global config)
|
|
154
|
+
const rawHtml = await api.get('editor/content', { skipSanitize: true });
|
|
155
|
+
|
|
156
|
+
// With query params
|
|
157
|
+
const users = await api.get('users', { params: { page: 1, limit: 20 } });
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 3. Setup Auth Provider (Client-Side)
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
// app/providers.tsx
|
|
164
|
+
'use client';
|
|
165
|
+
|
|
166
|
+
import { AuthProvider } from 'next-api-layer/client';
|
|
167
|
+
|
|
168
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
169
|
+
return (
|
|
170
|
+
<AuthProvider
|
|
171
|
+
userEndpoint="/api/auth/me"
|
|
172
|
+
loginEndpoint="/api/auth/login"
|
|
173
|
+
logoutEndpoint="/api/auth/logout"
|
|
174
|
+
swrConfig={{ revalidateOnFocus: true }}
|
|
175
|
+
>
|
|
176
|
+
{children}
|
|
177
|
+
</AuthProvider>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 4. Use the Auth Hook
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
// components/UserProfile.tsx
|
|
186
|
+
'use client';
|
|
187
|
+
|
|
188
|
+
import { useAuth } from 'next-api-layer/client';
|
|
189
|
+
|
|
190
|
+
export function UserProfile() {
|
|
191
|
+
const { user, isLoading, isAuthenticated, logout } = useAuth();
|
|
192
|
+
|
|
193
|
+
if (isLoading) return <div>Loading...</div>;
|
|
194
|
+
if (!isAuthenticated) return <div>Please login</div>;
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div>
|
|
198
|
+
<h1>Welcome, {user.name}</h1>
|
|
199
|
+
<p>{user.email}</p>
|
|
200
|
+
<button onClick={logout}>Logout</button>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 5. Server Components
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
// app/dashboard/page.tsx
|
|
210
|
+
import { getServerUser } from 'next-api-layer/server';
|
|
211
|
+
import { redirect } from 'next/navigation';
|
|
212
|
+
|
|
213
|
+
export default async function DashboardPage() {
|
|
214
|
+
const { user, isAuthenticated } = await getServerUser({
|
|
215
|
+
userCookie: 'userAuthToken',
|
|
216
|
+
apiBaseUrl: process.env.API_BASE_URL,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (!isAuthenticated) {
|
|
220
|
+
redirect('/login');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return <div>Welcome, {user.name}!</div>;
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Architecture
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
Developer writes: return api.get('client/projects/list');
|
|
231
|
+
Behind the scenes: 8-stage secure pipeline
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### The Pipeline
|
|
235
|
+
|
|
236
|
+
1. **Token Cascade** - Check user token → guest token → create guest
|
|
237
|
+
2. **Token Validation** - Validate with backend, handle expiry
|
|
238
|
+
3. **Token Refresh** - Auto-refresh expired tokens
|
|
239
|
+
4. **Request Deduplication** - Prevent concurrent validation calls
|
|
240
|
+
5. **XSS Sanitization** - Clean all response data
|
|
241
|
+
6. **i18n Injection** - Add language parameter
|
|
242
|
+
7. **Method Spoofing** - Laravel PUT/PATCH support
|
|
243
|
+
8. **Error Handling** - Consistent error format
|
|
244
|
+
|
|
245
|
+
## Security
|
|
246
|
+
|
|
247
|
+
`createAuthProxy` includes built-in security controls for production deployments.
|
|
248
|
+
|
|
249
|
+
### CSRF Protection
|
|
250
|
+
|
|
251
|
+
Protects against Cross-Site Request Forgery using Fetch Metadata (modern browsers) and/or Signed Double-Submit Cookie pattern.
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const authProxy = createAuthProxy({
|
|
255
|
+
apiBaseUrl: process.env.API_BASE_URL!,
|
|
256
|
+
cookies: { user: 'userAuthToken', guest: 'guestAuthToken' },
|
|
257
|
+
|
|
258
|
+
csrf: {
|
|
259
|
+
enabled: true,
|
|
260
|
+
strategy: 'both', // 'fetch-metadata' | 'double-submit' | 'both'
|
|
261
|
+
cookieName: '__csrf', // Cookie name for token
|
|
262
|
+
headerName: 'x-csrf-token', // Header name for token
|
|
263
|
+
ignoreMethods: ['GET', 'HEAD', 'OPTIONS'],
|
|
264
|
+
trustSameSite: false, // Trust same-site requests
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Behavior:**
|
|
270
|
+
- Safe methods (`GET`, `HEAD`, `OPTIONS`) are automatically skipped
|
|
271
|
+
- Unsafe methods are validated using Fetch Metadata headers and/or double-submit cookie
|
|
272
|
+
- Failed validation returns `403 Forbidden` and emits `csrf:fail` audit event
|
|
273
|
+
|
|
274
|
+
### Rate Limiting
|
|
275
|
+
|
|
276
|
+
Token bucket algorithm for preventing abuse. In-memory store for single-instance deployments.
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
const authProxy = createAuthProxy({
|
|
280
|
+
// ...base config
|
|
281
|
+
|
|
282
|
+
rateLimit: {
|
|
283
|
+
enabled: true,
|
|
284
|
+
windowMs: 60_000, // 1 minute window
|
|
285
|
+
maxRequests: 100, // Max requests per window
|
|
286
|
+
skipRoutes: ['/health', '/public/*'],
|
|
287
|
+
keyFn: (req) => req.headers.get('x-forwarded-for') || 'unknown',
|
|
288
|
+
onRateLimited: (req) => NextResponse.json(
|
|
289
|
+
{ error: 'Too many requests' },
|
|
290
|
+
{ status: 429 }
|
|
291
|
+
),
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Response Headers:**
|
|
297
|
+
- `X-RateLimit-Limit`: Max requests allowed
|
|
298
|
+
- `X-RateLimit-Remaining`: Requests remaining in window
|
|
299
|
+
- `X-RateLimit-Reset`: Unix timestamp when window resets
|
|
300
|
+
- `Retry-After`: Seconds until retry (on 429)
|
|
301
|
+
|
|
302
|
+
### Audit Logging
|
|
303
|
+
|
|
304
|
+
Event-based security logging for monitoring and compliance.
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
const authProxy = createAuthProxy({
|
|
308
|
+
// ...base config
|
|
309
|
+
|
|
310
|
+
audit: {
|
|
311
|
+
enabled: true,
|
|
312
|
+
events: [
|
|
313
|
+
'auth:success',
|
|
314
|
+
'auth:fail',
|
|
315
|
+
'auth:refresh',
|
|
316
|
+
'auth:guest',
|
|
317
|
+
'access:denied',
|
|
318
|
+
'csrf:fail',
|
|
319
|
+
'rateLimit:exceeded',
|
|
320
|
+
'error',
|
|
321
|
+
],
|
|
322
|
+
logger: async (event) => {
|
|
323
|
+
// Send to your SIEM, logging service, or database
|
|
324
|
+
console.log('[AUDIT]', event.type, event.path, event.ip, event.success);
|
|
325
|
+
|
|
326
|
+
// Example: Send to external service
|
|
327
|
+
// await fetch('https://logs.example.com/audit', {
|
|
328
|
+
// method: 'POST',
|
|
329
|
+
// body: JSON.stringify(event),
|
|
330
|
+
// });
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Event Structure:**
|
|
337
|
+
```ts
|
|
338
|
+
interface AuditEvent {
|
|
339
|
+
type: AuditEventType; // Event type
|
|
340
|
+
timestamp: Date; // When it occurred
|
|
341
|
+
ip: string | null; // Client IP
|
|
342
|
+
userId?: string; // User ID if authenticated
|
|
343
|
+
path: string; // Request path
|
|
344
|
+
method: string; // HTTP method
|
|
345
|
+
success: boolean; // Whether action succeeded
|
|
346
|
+
metadata?: Record<string, unknown>; // Additional context
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Full Security Example
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import { createAuthProxy } from 'next-api-layer';
|
|
354
|
+
|
|
355
|
+
export default createAuthProxy({
|
|
356
|
+
apiBaseUrl: process.env.API_BASE_URL!,
|
|
357
|
+
cookies: { user: 'userAuthToken', guest: 'guestAuthToken' },
|
|
358
|
+
|
|
359
|
+
csrf: {
|
|
360
|
+
enabled: true,
|
|
361
|
+
strategy: 'both',
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
rateLimit: {
|
|
365
|
+
enabled: true,
|
|
366
|
+
windowMs: 60_000,
|
|
367
|
+
maxRequests: 100,
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
audit: {
|
|
371
|
+
enabled: true,
|
|
372
|
+
events: ['auth:success', 'auth:fail', 'csrf:fail', 'rateLimit:exceeded'],
|
|
373
|
+
logger: async (event) => {
|
|
374
|
+
console.log('[AUDIT]', JSON.stringify(event));
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## API Reference
|
|
383
|
+
|
|
384
|
+
### createAuthProxy(config)
|
|
385
|
+
|
|
386
|
+
Creates a Next.js middleware for authentication.
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
interface AuthProxyConfig {
|
|
390
|
+
apiBaseUrl: string; // Backend API URL
|
|
391
|
+
|
|
392
|
+
cookies: {
|
|
393
|
+
user: string; // User token cookie name
|
|
394
|
+
guest: string; // Guest token cookie name
|
|
395
|
+
options?: CookieOptions; // httpOnly, secure, sameSite, etc.
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
endpoints?: {
|
|
399
|
+
validate?: string; // Default: 'auth/me'
|
|
400
|
+
refresh?: string; // Default: 'auth/refresh'
|
|
401
|
+
guest?: string; // Default: 'auth/guest'
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
guestToken?: {
|
|
405
|
+
enabled: boolean;
|
|
406
|
+
credentials?: {
|
|
407
|
+
username: string;
|
|
408
|
+
password: string;
|
|
409
|
+
};
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
access?: {
|
|
413
|
+
protectedRoutes?: string[]; // Routes requiring auth
|
|
414
|
+
authRoutes?: string[]; // Routes for non-auth users (login, register)
|
|
415
|
+
publicRoutes?: string[]; // Completely public routes
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
cache?: {
|
|
419
|
+
ttl?: number; // Default: 2000ms
|
|
420
|
+
maxSize?: number; // Default: 100 tokens
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// ======== Composability Hooks ========
|
|
424
|
+
|
|
425
|
+
// Runs BEFORE auth validation. Return NextResponse to bypass auth.
|
|
426
|
+
beforeAuth?: (req: NextRequest) => NextResponse | null | Promise<NextResponse | null>;
|
|
427
|
+
|
|
428
|
+
// Runs AFTER auth validation. Modify response or add custom logic.
|
|
429
|
+
afterAuth?: (req: NextRequest, response: NextResponse, authResult: AuthResult) => NextResponse | Promise<NextResponse>;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// AuthResult passed to afterAuth hook
|
|
433
|
+
interface AuthResult {
|
|
434
|
+
isAuthenticated: boolean; // true if valid user token
|
|
435
|
+
isGuest: boolean; // true if guest token
|
|
436
|
+
tokenType: string | null; // 'user', 'guest', etc.
|
|
437
|
+
user: Record<string, unknown> | null; // User data from token validation
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Different Backend Formats
|
|
442
|
+
|
|
443
|
+
Backend'iniz farklı response formatı dönüyorsa `responseMappers` ile uyumlu hale getirebilirsiniz:
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
// Laravel (default format - no mapping needed)
|
|
447
|
+
// { success: true, data: { type: 'user', exp: 123, user: {...} } }
|
|
448
|
+
|
|
449
|
+
// Django REST Framework
|
|
450
|
+
const authProxy = createAuthProxy({
|
|
451
|
+
apiBaseUrl: process.env.API_BASE_URL!,
|
|
452
|
+
cookies: { user: 'token', guest: 'guest_token' },
|
|
453
|
+
|
|
454
|
+
responseMappers: {
|
|
455
|
+
// Django: { user: {...}, token_type: 'Bearer', exp: 123 }
|
|
456
|
+
parseAuthMe: (res: any) => {
|
|
457
|
+
if (!res?.user) return null;
|
|
458
|
+
return {
|
|
459
|
+
isValid: true,
|
|
460
|
+
tokenType: res.is_guest ? 'guest' : 'user',
|
|
461
|
+
exp: res.exp || null,
|
|
462
|
+
userData: res.user,
|
|
463
|
+
};
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
// Django: { access: 'new_token' }
|
|
467
|
+
parseRefreshToken: (res: any) => res?.access || null,
|
|
468
|
+
|
|
469
|
+
// Django: { token: 'guest_token' }
|
|
470
|
+
parseGuestToken: (res: any) => res?.token || null,
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// .NET / ASP.NET Core
|
|
475
|
+
const authProxy = createAuthProxy({
|
|
476
|
+
apiBaseUrl: process.env.API_BASE_URL!,
|
|
477
|
+
cookies: { user: 'AuthToken', guest: 'GuestToken' },
|
|
478
|
+
|
|
479
|
+
responseMappers: {
|
|
480
|
+
// .NET: { isSuccess: true, result: { userId, email, role } }
|
|
481
|
+
parseAuthMe: (res: any) => {
|
|
482
|
+
if (!res?.isSuccess) return null;
|
|
483
|
+
return {
|
|
484
|
+
isValid: true,
|
|
485
|
+
tokenType: res.result?.role === 'Guest' ? 'guest' : 'user',
|
|
486
|
+
exp: res.result?.expiresAt,
|
|
487
|
+
userData: res.result,
|
|
488
|
+
};
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
// .NET: { token: 'xxx', expiresIn: 3600 }
|
|
492
|
+
parseRefreshToken: (res: any) => res?.token || null,
|
|
493
|
+
parseGuestToken: (res: any) => res?.token || null,
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Express.js / Custom Format
|
|
498
|
+
const authProxy = createAuthProxy({
|
|
499
|
+
apiBaseUrl: process.env.API_BASE_URL!,
|
|
500
|
+
cookies: { user: 'jwt', guest: 'guest_jwt' },
|
|
501
|
+
|
|
502
|
+
responseMappers: {
|
|
503
|
+
// Custom: { ok: true, payload: { sub, name, email } }
|
|
504
|
+
parseAuthMe: (res: any) => {
|
|
505
|
+
if (!res?.ok) return null;
|
|
506
|
+
return {
|
|
507
|
+
isValid: true,
|
|
508
|
+
tokenType: res.payload?.isGuest ? 'guest' : 'user',
|
|
509
|
+
exp: res.payload?.exp,
|
|
510
|
+
userData: res.payload,
|
|
511
|
+
};
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
parseRefreshToken: (res: any) => res?.newToken || res?.accessToken || null,
|
|
515
|
+
parseGuestToken: (res: any) => res?.guestToken || res?.token || null,
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### createApiClient(config)
|
|
521
|
+
|
|
522
|
+
Creates an API client for making requests.
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
interface ApiClientConfig {
|
|
526
|
+
sanitization?: {
|
|
527
|
+
enabled?: boolean; // Default: true
|
|
528
|
+
allowedTags?: string[]; // HTML tags to allow
|
|
529
|
+
skipFields?: string[]; // Fields to skip sanitization
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
i18n?: {
|
|
533
|
+
enabled?: boolean; // Default: false
|
|
534
|
+
paramName?: string; // Default: 'lang'
|
|
535
|
+
locales?: string[]; // Valid locale codes
|
|
536
|
+
defaultLocale?: string; // Fallback locale
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
methodSpoofing?: boolean; // Default: false (for Laravel)
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
## i18n Integration
|
|
544
|
+
|
|
545
|
+
Automatic locale detection and injection for multilingual applications.
|
|
546
|
+
|
|
547
|
+
### How It Works
|
|
548
|
+
|
|
549
|
+
1. **Middleware** extracts locale from URL path (e.g., `/tr/blog` → `tr`)
|
|
550
|
+
2. **Sets `x-locale` header** on the request for downstream handlers
|
|
551
|
+
3. **API Client** reads the header and appends `?lang={locale}` to backend requests
|
|
552
|
+
|
|
553
|
+
### Configuration
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
// middleware.ts - Proxy config
|
|
557
|
+
createAuthProxy({
|
|
558
|
+
apiBaseUrl: process.env.API_BASE_URL!,
|
|
559
|
+
cookies: { user: 'userToken', guest: 'guestToken' },
|
|
560
|
+
|
|
561
|
+
i18n: {
|
|
562
|
+
enabled: true,
|
|
563
|
+
locales: ['en', 'tr', 'ar'], // Valid locales
|
|
564
|
+
defaultLocale: 'en', // Fallback when no locale in path
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// lib/api.ts - API client config
|
|
569
|
+
export const api = createApiClient({
|
|
570
|
+
apiBaseUrl: process.env.API_BASE_URL!,
|
|
571
|
+
cookies: { user: 'userToken', guest: 'guestToken' },
|
|
572
|
+
|
|
573
|
+
i18n: {
|
|
574
|
+
enabled: true,
|
|
575
|
+
paramName: 'lang', // Query param name (default: 'lang')
|
|
576
|
+
locales: ['en', 'tr', 'ar'],
|
|
577
|
+
defaultLocale: 'en',
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Request Flow
|
|
583
|
+
|
|
584
|
+
```
|
|
585
|
+
User visits: /tr/blog
|
|
586
|
+
↓
|
|
587
|
+
Middleware: Extracts 'tr' → Sets x-locale: tr header
|
|
588
|
+
↓
|
|
589
|
+
Route Handler: await api.get('posts')
|
|
590
|
+
↓
|
|
591
|
+
API Client: Reads x-locale header → Appends ?lang=tr
|
|
592
|
+
↓
|
|
593
|
+
Backend: GET /api/posts?lang=tr
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### With next-intl
|
|
597
|
+
|
|
598
|
+
When using with next-intl, use `afterAuth` hook to combine both middlewares:
|
|
599
|
+
|
|
600
|
+
```ts
|
|
601
|
+
import { createAuthProxy } from 'next-api-layer';
|
|
602
|
+
import createIntlMiddleware from 'next-intl/middleware';
|
|
603
|
+
import { routing } from './i18n/routing';
|
|
604
|
+
|
|
605
|
+
const intlMiddleware = createIntlMiddleware(routing);
|
|
606
|
+
|
|
607
|
+
export default createAuthProxy({
|
|
608
|
+
apiBaseUrl: process.env.API_URL!,
|
|
609
|
+
cookies: { user: 'userToken', guest: 'guestToken' },
|
|
610
|
+
|
|
611
|
+
i18n: {
|
|
612
|
+
enabled: true,
|
|
613
|
+
locales: ['en', 'tr', 'ar'],
|
|
614
|
+
defaultLocale: 'en',
|
|
615
|
+
},
|
|
616
|
+
|
|
617
|
+
afterAuth: async (req, response, _authResult) => {
|
|
618
|
+
if (req.nextUrl.pathname.startsWith('/api')) {
|
|
619
|
+
return response; // API routes keep auth response
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Page routes: run next-intl but preserve auth cookies
|
|
623
|
+
const intlResponse = intlMiddleware(req);
|
|
624
|
+
response.cookies.getAll().forEach(cookie => {
|
|
625
|
+
intlResponse.cookies.set(cookie.name, cookie.value, {
|
|
626
|
+
httpOnly: cookie.httpOnly,
|
|
627
|
+
secure: cookie.secure,
|
|
628
|
+
sameSite: cookie.sameSite,
|
|
629
|
+
path: cookie.path,
|
|
630
|
+
maxAge: cookie.maxAge,
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
return intlResponse;
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### AuthProvider
|
|
640
|
+
|
|
641
|
+
React context provider for client-side auth state.
|
|
642
|
+
|
|
643
|
+
```tsx
|
|
644
|
+
<AuthProvider
|
|
645
|
+
userEndpoint="/api/auth/me"
|
|
646
|
+
loginEndpoint="/api/auth/login"
|
|
647
|
+
logoutEndpoint="/api/auth/logout"
|
|
648
|
+
swrConfig={{ refreshInterval: 0 }}
|
|
649
|
+
onLogin={(user) => console.log('Logged in:', user)}
|
|
650
|
+
onLogout={() => console.log('Logged out')}
|
|
651
|
+
onError={(error) => console.error(error)}
|
|
652
|
+
>
|
|
653
|
+
{children}
|
|
654
|
+
</AuthProvider>
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### useAuth()
|
|
658
|
+
|
|
659
|
+
Hook to access authentication state.
|
|
660
|
+
|
|
661
|
+
```ts
|
|
662
|
+
const {
|
|
663
|
+
user, // UserData | null
|
|
664
|
+
isLoading, // boolean
|
|
665
|
+
isAuthenticated, // boolean (true for real users)
|
|
666
|
+
isGuest, // boolean (true for guest tokens)
|
|
667
|
+
error, // Error | null
|
|
668
|
+
login, // (credentials) => Promise<LoginResult>
|
|
669
|
+
logout, // () => Promise<void>
|
|
670
|
+
refresh, // () => Promise<void>
|
|
671
|
+
} = useAuth();
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### getServerUser(options)
|
|
675
|
+
|
|
676
|
+
Get user data in Server Components.
|
|
677
|
+
|
|
678
|
+
```ts
|
|
679
|
+
const { user, isAuthenticated, isGuest, token } = await getServerUser({
|
|
680
|
+
userCookie: 'userAuthToken',
|
|
681
|
+
guestCookie: 'guestAuthToken',
|
|
682
|
+
apiBaseUrl: process.env.API_BASE_URL,
|
|
683
|
+
});
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
## Public API / Skip Auth
|
|
687
|
+
|
|
688
|
+
Bazı endpoint'ler (haber siteleri, public içerikler) authentication gerektirmez. Bu durumlar için `skipAuth` özelliğini kullanabilirsiniz:
|
|
689
|
+
|
|
690
|
+
### Global Config
|
|
691
|
+
|
|
692
|
+
```ts
|
|
693
|
+
const api = createApiClient({
|
|
694
|
+
auth: {
|
|
695
|
+
// Bu pattern'lere uyan endpoint'ler token göndermez
|
|
696
|
+
publicEndpoints: ['news/*', 'categories', 'public/**'],
|
|
697
|
+
|
|
698
|
+
// Opsiyonel: Tüm endpoint'ler default olarak public olsun
|
|
699
|
+
// skipByDefault: true,
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### Per-Request Override
|
|
705
|
+
|
|
706
|
+
```ts
|
|
707
|
+
// Pattern'e uysa bile token gönder
|
|
708
|
+
await api.get('news/premium-article', { skipAuth: false });
|
|
709
|
+
|
|
710
|
+
// Pattern'e uymasa bile token gönderme
|
|
711
|
+
await api.get('some-endpoint', { skipAuth: true });
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### Route Handler (createProxyHandler)
|
|
715
|
+
|
|
716
|
+
API route handler'ınızda `createProxyHandler` kullanarak backend'e proxy yapabilirsiniz:
|
|
717
|
+
|
|
718
|
+
```ts
|
|
719
|
+
// app/api/[...path]/route.ts
|
|
720
|
+
import { createProxyHandler } from 'next-api-layer';
|
|
721
|
+
|
|
722
|
+
const handler = createProxyHandler({
|
|
723
|
+
apiBaseUrl: process.env.API_BASE_URL!,
|
|
724
|
+
publicEndpoints: ['news/*', 'categories', 'public/**'],
|
|
725
|
+
debug: process.env.NODE_ENV === 'development',
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
export const GET = handler;
|
|
729
|
+
export const POST = handler;
|
|
730
|
+
export const PUT = handler;
|
|
731
|
+
export const DELETE = handler;
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
## Peer Dependencies
|
|
735
|
+
|
|
736
|
+
- `next` >= 14.0.0
|
|
737
|
+
- `react` >= 18.0.0
|
|
738
|
+
- `swr` >= 2.0.0 (optional, for client module)
|
|
739
|
+
- `next-intl` >= 3.0.0 (optional, for i18n)
|
|
740
|
+
|
|
741
|
+
## License
|
|
742
|
+
|
|
743
|
+
MIT
|