@artatol-acp/auth-nextjs 0.3.8 → 0.4.1

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 CHANGED
@@ -2,11 +2,69 @@
2
2
 
3
3
  Next.js SDK for Artatol Cloud Platform Authentication with support for App Router, Server Actions, Middleware, and automatic token refresh.
4
4
 
5
+ ## Changelog
6
+
7
+ ### v0.4.1
8
+
9
+ **Bug Fixes:**
10
+ - **Fixed refresh token extraction**: Auth server returns `refresh_token` only in `Set-Cookie` header, not in JSON body. SDK now correctly extracts it from the response header.
11
+
12
+ ### v0.4.0
13
+
14
+ **Breaking Changes:**
15
+ - `ACPAuthProvider` no longer requires `baseUrl` prop - it now uses local API routes
16
+ - `initACPAuth()` no longer returns an `ACPAuthClient` instance
17
+
18
+ **New Features:**
19
+ - **New `handlers` export**: Pre-built API route handlers for auth operations
20
+ ```typescript
21
+ import { createAuthHandlers } from '@artatol-acp/auth-nextjs/handlers';
22
+ ```
23
+ - **SSR-first architecture**: `ACPAuthProvider` now accepts `initialUser` prop for SSR hydration
24
+ - **New `verify2FALogin()` server function**: Complete 2FA login flow on the server
25
+ - **New `verify2FA()` client method**: Complete 2FA login from client components
26
+
27
+ **Improvements:**
28
+ - **httpOnly cookies**: All tokens are now stored in httpOnly cookies (XSS protection)
29
+ - **Proper cookie handling**: `login()` now automatically sets both `access_token` and `refresh_token` cookies
30
+ - **Server-side refresh**: `refreshAccessToken()` now properly forwards cookies to auth server
31
+ - **Better error messages**: Clear error when `baseUrl` is missing
32
+ - **Configurable cookies**: Cookie options (path, secure, sameSite) are now configurable
33
+
34
+ **Migration Guide:**
35
+
36
+ 1. Create API route handlers:
37
+ ```typescript
38
+ // app/api/auth/[action]/route.ts
39
+ import { createAuthHandlers } from '@artatol-acp/auth-nextjs/handlers';
40
+
41
+ const { authHandler } = createAuthHandlers({
42
+ baseUrl: process.env.ACP_AUTH_URL!,
43
+ });
44
+
45
+ export const POST = authHandler;
46
+ ```
47
+
48
+ 2. Update `ACPAuthProvider`:
49
+ ```typescript
50
+ // Before
51
+ <ACPAuthProvider baseUrl={process.env.NEXT_PUBLIC_ACP_AUTH_URL}>
52
+
53
+ // After
54
+ <ACPAuthProvider initialUser={user}>
55
+ ```
56
+
57
+ 3. Handle 2FA in login:
58
+ ```typescript
59
+ const result = await login(email, password);
60
+ if (result.requiresTwoFactor) {
61
+ // Show 2FA form, then call verify2FA(tempToken, code)
62
+ }
63
+ ```
64
+
5
65
  ## Installation
6
66
 
7
67
  ```bash
8
- npm install @artatol-acp/auth-nextjs
9
- # or
10
68
  pnpm add @artatol-acp/auth-nextjs
11
69
  ```
12
70
 
@@ -14,67 +72,94 @@ pnpm add @artatol-acp/auth-nextjs
14
72
 
15
73
  Before using this SDK, you need to obtain from the ACP AUTH service:
16
74
 
17
- 1. **API Key** (required) - Contact your system administrator
18
- 2. **Base URL** (required) - The auth service URL (e.g., `https://sso.artatol.net`)
19
- 3. **JWT Public Key** (optional) - Only needed for local JWT verification with `getUser()`. Download it from:
75
+ 1. **Base URL** (required) - The auth service URL (e.g., `https://sso.artatol.net`)
76
+ 2. **API Key** (optional) - Contact your system administrator if required
77
+ 3. **JWT Public Key** (required for `getUser()`) - For local JWT verification. Download it from:
20
78
  ```bash
21
79
  curl https://sso.artatol.net/public-key > public.pem
22
80
  ```
23
81
 
24
- **Note:** Without the public key, you can still use all auth operations (login, register, logout, 2FA, password reset, etc.), but you must use the `me()` function instead of `getUser()` for user verification, which makes an API call to the auth service.
82
+ ## Quick Start
25
83
 
26
- ## Setup
84
+ The SDK uses **httpOnly cookies** for secure token storage. This means:
85
+ - Tokens are NOT accessible from JavaScript (XSS protection)
86
+ - All auth operations must go through your Next.js server
87
+ - The SDK provides API route handlers to proxy requests to the auth server
27
88
 
28
- ### 1. Initialize Server-side
89
+ ### 1. Create API Route Handlers
29
90
 
30
- Create `lib/auth.ts`:
91
+ Create `app/api/auth/[action]/route.ts`:
31
92
 
32
93
  ```typescript
33
- import { initACPAuth } from '@artatol-acp/auth-nextjs/server';
34
- import { readFileSync } from 'fs';
35
-
36
- const publicKey = readFileSync('./keys/public.pem', 'utf-8');
94
+ import { createAuthHandlers } from '@artatol-acp/auth-nextjs/handlers';
37
95
 
38
- export const auth = initACPAuth({
39
- baseUrl: process.env.ACP_AUTH_URL || 'https://sso.artatol.com',
40
- apiKey: process.env.ACP_AUTH_API_KEY!,
41
- jwtPublicKey: publicKey,
96
+ const { authHandler } = createAuthHandlers({
97
+ baseUrl: process.env.ACP_AUTH_URL!,
98
+ apiKey: process.env.ACP_AUTH_API_KEY,
42
99
  });
100
+
101
+ export const POST = authHandler;
43
102
  ```
44
103
 
45
- ### 2. Add Middleware (Optional)
104
+ This creates the following endpoints:
105
+ - `POST /api/auth/login` - Login
106
+ - `POST /api/auth/logout` - Logout
107
+ - `POST /api/auth/session` - Refresh session & get user
108
+ - `POST /api/auth/register` - Register
109
+ - `POST /api/auth/verify-2fa` - Complete 2FA login
110
+ - `POST /api/auth/resend-verification` - Resend verification email
111
+ - `POST /api/auth/forgot-password` - Request password reset
112
+ - `POST /api/auth/reset-password` - Reset password
113
+
114
+ ### 2. Initialize Server-side Auth
46
115
 
47
- Create `middleware.ts`:
116
+ Create `lib/auth.ts`:
48
117
 
49
118
  ```typescript
50
- import { createACPAuthMiddleware } from '@artatol-acp/auth-nextjs/middleware';
119
+ import { initACPAuth } from '@artatol-acp/auth-nextjs/server';
51
120
  import { readFileSync } from 'fs';
52
121
 
53
122
  const publicKey = readFileSync('./keys/public.pem', 'utf-8');
54
123
 
55
- export const middleware = createACPAuthMiddleware({
124
+ initACPAuth({
125
+ baseUrl: process.env.ACP_AUTH_URL!,
126
+ apiKey: process.env.ACP_AUTH_API_KEY,
56
127
  jwtPublicKey: publicKey,
57
- publicPaths: ['/login', '/register', '/forgot-password'],
58
- loginPath: '/login',
59
128
  });
60
129
 
61
- export const config = {
62
- matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
63
- };
130
+ // Re-export server functions
131
+ export {
132
+ getUser,
133
+ me,
134
+ login,
135
+ logout,
136
+ register,
137
+ verify2FALogin,
138
+ verifyEmail,
139
+ resendVerificationEmail,
140
+ forgotPassword,
141
+ resetPassword,
142
+ deleteAccount,
143
+ refreshAccessToken,
144
+ } from '@artatol-acp/auth-nextjs/server';
64
145
  ```
65
146
 
66
- ### 3. Add Client Provider (Optional)
147
+ ### 3. Add Client Provider
67
148
 
68
149
  In your root layout (`app/layout.tsx`):
69
150
 
70
151
  ```typescript
71
152
  import { ACPAuthProvider } from '@artatol-acp/auth-nextjs/client';
153
+ import { getUser } from '@/lib/auth';
154
+
155
+ export default async function RootLayout({ children }) {
156
+ // SSR: Get initial user on server
157
+ const user = await getUser();
72
158
 
73
- export default function RootLayout({ children }) {
74
159
  return (
75
160
  <html>
76
161
  <body>
77
- <ACPAuthProvider baseUrl={process.env.NEXT_PUBLIC_ACP_AUTH_URL}>
162
+ <ACPAuthProvider initialUser={user}>
78
163
  {children}
79
164
  </ACPAuthProvider>
80
165
  </body>
@@ -85,14 +170,18 @@ export default function RootLayout({ children }) {
85
170
 
86
171
  ## Usage
87
172
 
88
- ### Server Components
173
+ ### Server Components (SSR)
89
174
 
90
175
  ```typescript
91
- import { getUser } from '@artatol-acp/auth-nextjs/server';
176
+ import { getUser, me } from '@/lib/auth';
92
177
 
93
178
  export default async function ProfilePage() {
179
+ // Fast local JWT verification (returns { id, email })
94
180
  const user = await getUser();
95
181
 
182
+ // Or fetch full user from API (returns { id, email, twoFactorEnabled })
183
+ const fullUser = await me();
184
+
96
185
  if (!user) {
97
186
  return <div>Not logged in</div>;
98
187
  }
@@ -110,7 +199,7 @@ export default async function ProfilePage() {
110
199
  ```typescript
111
200
  'use server';
112
201
 
113
- import { login, register, logout } from '@artatol-acp/auth-nextjs/server';
202
+ import { login, register, logout, verify2FALogin } from '@/lib/auth';
114
203
  import { redirect } from 'next/navigation';
115
204
 
116
205
  export async function loginAction(formData: FormData) {
@@ -120,19 +209,22 @@ export async function loginAction(formData: FormData) {
120
209
  const result = await login(email, password);
121
210
 
122
211
  if ('requiresTwoFactor' in result) {
123
- // Handle 2FA
124
212
  return { requires2FA: true, tempToken: result.tempToken };
125
213
  }
126
214
 
127
215
  redirect('/dashboard');
128
216
  }
129
217
 
218
+ export async function verify2FAAction(tempToken: string, code: string) {
219
+ await verify2FALogin(tempToken, code);
220
+ redirect('/dashboard');
221
+ }
222
+
130
223
  export async function registerAction(formData: FormData) {
131
224
  const email = formData.get('email') as string;
132
225
  const password = formData.get('password') as string;
133
226
 
134
227
  await register(email, password);
135
- // User will receive verification email
136
228
  redirect('/check-email');
137
229
  }
138
230
 
@@ -148,29 +240,50 @@ export async function logoutAction() {
148
240
  'use client';
149
241
 
150
242
  import { useAuth } from '@artatol-acp/auth-nextjs/client';
243
+ import { useState } from 'react';
151
244
 
152
245
  export function LoginForm() {
153
- const { login, user, isLoading } = useAuth();
246
+ const { login, verify2FA, user, isLoading } = useAuth();
247
+ const [tempToken, setTempToken] = useState<string | null>(null);
154
248
 
155
- const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
249
+ const handleLogin = async (e: React.FormEvent<HTMLFormElement>) => {
156
250
  e.preventDefault();
157
251
  const formData = new FormData(e.currentTarget);
158
252
 
159
253
  try {
160
- await login(
254
+ const result = await login(
161
255
  formData.get('email') as string,
162
256
  formData.get('password') as string
163
257
  );
258
+
259
+ if (result.requiresTwoFactor) {
260
+ setTempToken(result.tempToken!);
261
+ }
164
262
  } catch (error) {
165
263
  console.error('Login failed:', error);
166
264
  }
167
265
  };
168
266
 
267
+ const handle2FA = async (e: React.FormEvent<HTMLFormElement>) => {
268
+ e.preventDefault();
269
+ const code = new FormData(e.currentTarget).get('code') as string;
270
+ await verify2FA(tempToken!, code);
271
+ };
272
+
169
273
  if (isLoading) return <div>Loading...</div>;
170
274
  if (user) return <div>Welcome {user.email}</div>;
171
275
 
276
+ if (tempToken) {
277
+ return (
278
+ <form onSubmit={handle2FA}>
279
+ <input name="code" placeholder="2FA Code" required />
280
+ <button type="submit">Verify</button>
281
+ </form>
282
+ );
283
+ }
284
+
172
285
  return (
173
- <form onSubmit={handleSubmit}>
286
+ <form onSubmit={handleLogin}>
174
287
  <input name="email" type="email" required />
175
288
  <input name="password" type="password" required />
176
289
  <button type="submit">Login</button>
@@ -179,505 +292,177 @@ export function LoginForm() {
179
292
  }
180
293
  ```
181
294
 
182
- ### API Routes
295
+ ## Middleware (Optional)
296
+
297
+ Create `middleware.ts` for route protection:
183
298
 
184
299
  ```typescript
185
- import { ACPAuthClient } from '@artatol-acp/auth-nextjs/server';
300
+ import { createACPAuthMiddleware } from '@artatol-acp/auth-nextjs/middleware';
301
+ import { readFileSync } from 'fs';
186
302
 
187
- const client = new ACPAuthClient({
188
- baseUrl: process.env.ACP_AUTH_URL!,
189
- apiKey: process.env.ACP_AUTH_API_KEY!,
303
+ const publicKey = readFileSync('./keys/public.pem', 'utf-8');
304
+
305
+ export const middleware = createACPAuthMiddleware({
306
+ jwtPublicKey: publicKey,
307
+ publicPaths: ['/login', '/register', '/forgot-password'],
308
+ loginPath: '/login',
190
309
  });
191
310
 
192
- export async function POST(request: Request) {
193
- const { email, password } = await request.json();
311
+ export const config = {
312
+ matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
313
+ };
314
+ ```
194
315
 
195
- const result = await client.login({ email, password });
316
+ ## Environment Variables
196
317
 
197
- return Response.json(result);
198
- }
318
+ ```env
319
+ ACP_AUTH_URL=https://sso.artatol.net
320
+ ACP_AUTH_API_KEY=your-api-key-here
199
321
  ```
200
322
 
201
- ## Password Requirements
323
+ ## API Reference
202
324
 
203
- Passwords must meet the following requirements:
204
- - Minimum 10 characters
205
- - At least one lowercase letter (a-z)
206
- - At least one uppercase letter (A-Z)
207
- - At least one number (0-9)
325
+ ### Server Functions (`@artatol-acp/auth-nextjs/server`)
326
+
327
+ | Function | Description |
328
+ |----------|-------------|
329
+ | `initACPAuth(options)` | Initialize auth configuration |
330
+ | `getUser()` | Get user from JWT (local, fast) → `{ id, email }` |
331
+ | `me()` | Get user from API (full data) → `{ id, email, twoFactorEnabled }` |
332
+ | `login(email, password)` | Login user, sets httpOnly cookies |
333
+ | `verify2FALogin(tempToken, code)` | Complete 2FA login |
334
+ | `logout()` | Logout user, clears cookies |
335
+ | `register(email, password)` | Register new user |
336
+ | `verifyEmail(token)` | Verify email address |
337
+ | `resendVerificationEmail(email)` | Resend verification email |
338
+ | `forgotPassword(email)` | Request password reset |
339
+ | `resetPassword(token, password)` | Reset password |
340
+ | `deleteAccount(password, confirmation)` | Delete account |
341
+ | `refreshAccessToken()` | Refresh access token |
342
+
343
+ ### Client Hook (`@artatol-acp/auth-nextjs/client`)
208
344
 
209
345
  ```typescript
210
- // Example validation on the client side
211
- function validatePassword(password: string): string[] {
212
- const errors: string[] = [];
213
-
214
- if (password.length < 10) {
215
- errors.push('Password must be at least 10 characters');
216
- }
217
- if (!/[a-z]/.test(password)) {
218
- errors.push('Password must contain at least one lowercase letter');
219
- }
220
- if (!/[A-Z]/.test(password)) {
221
- errors.push('Password must contain at least one uppercase letter');
222
- }
223
- if (!/[0-9]/.test(password)) {
224
- errors.push('Password must contain at least one number');
225
- }
226
-
227
- return errors;
228
- }
346
+ const {
347
+ user, // Current user or null
348
+ isLoading, // True during initial load
349
+ login, // (email, password) => Promise<{ requiresTwoFactor?, tempToken? }>
350
+ verify2FA, // (tempToken, code) => Promise<void>
351
+ logout, // () => Promise<void>
352
+ refresh, // () => Promise<boolean>
353
+ resendVerification, // (email) => Promise<void>
354
+ } = useAuth();
229
355
  ```
230
356
 
231
- ## Email Verification
232
-
233
- After registration, users must verify their email address before they can log in. The auth service automatically sends a verification email upon registration.
234
-
235
- ### Verification Flow
236
-
237
- 1. User registers → receives verification email
238
- 2. User clicks link in email → email is verified
239
- 3. User can now log in
240
-
241
- ### Server Actions
357
+ ### ACPAuthProvider Props
242
358
 
243
359
  ```typescript
244
- 'use server';
245
-
246
- import { verifyEmail, resendVerificationEmail } from '@artatol-acp/auth-nextjs/server';
247
- import { redirect } from 'next/navigation';
248
-
249
- export async function verifyEmailAction(token: string) {
250
- try {
251
- await verifyEmail(token);
252
- redirect('/login?verified=true');
253
- } catch (error) {
254
- redirect('/verification-error');
255
- }
256
- }
257
-
258
- export async function resendVerificationAction(email: string) {
259
- await resendVerificationEmail(email);
260
- // Always succeeds to prevent email enumeration
261
- return { message: 'If the email exists, a verification link has been sent' };
262
- }
360
+ <ACPAuthProvider
361
+ apiBasePath="/api/auth" // Optional, default: "/api/auth"
362
+ initialUser={user} // Optional, SSR user data
363
+ >
364
+ {children}
365
+ </ACPAuthProvider>
263
366
  ```
264
367
 
265
- ### Client Component Example
368
+ ### API Handlers (`@artatol-acp/auth-nextjs/handlers`)
266
369
 
267
370
  ```typescript
268
- 'use client';
269
-
270
- import { useAuth } from '@artatol-acp/auth-nextjs/client';
271
-
272
- export function ResendVerificationButton({ email }: { email: string }) {
273
- const { resendVerification } = useAuth();
274
- const [sent, setSent] = useState(false);
371
+ const handlers = createAuthHandlers({
372
+ baseUrl: string,
373
+ apiKey?: string,
374
+ cookies?: {
375
+ path?: string, // Default: "/"
376
+ secure?: boolean, // Default: NODE_ENV === "production"
377
+ sameSite?: 'strict' | 'lax' | 'none', // Default: "lax"
378
+ },
379
+ });
275
380
 
276
- const handleResend = async () => {
277
- await resendVerification(email);
278
- setSent(true);
279
- };
381
+ // Use combined handler
382
+ export const POST = handlers.authHandler;
280
383
 
281
- return (
282
- <button onClick={handleResend} disabled={sent}>
283
- {sent ? 'Email Sent' : 'Resend Verification Email'}
284
- </button>
285
- );
286
- }
384
+ // Or use individual handlers
385
+ export const POST = handlers.loginHandler;
287
386
  ```
288
387
 
289
- ### Handling Unverified Users
290
-
291
- When an unverified user tries to log in, they will receive an error:
388
+ ## Error Handling
292
389
 
293
390
  ```typescript
294
- export async function loginAction(formData: FormData) {
295
- const email = formData.get('email') as string;
296
- const password = formData.get('password') as string;
391
+ import { ACPAuthError } from '@artatol-acp/auth-nextjs/server';
297
392
 
298
- try {
299
- const result = await login(email, password);
300
-
301
- if ('requiresTwoFactor' in result) {
302
- return { requires2FA: true, tempToken: result.tempToken };
303
- }
304
-
305
- redirect('/dashboard');
306
- } catch (error: any) {
307
- if (error.message?.includes('Email not verified')) {
308
- return { error: 'Please verify your email before logging in' };
393
+ try {
394
+ await login(email, password);
395
+ } catch (error) {
396
+ if (error instanceof ACPAuthError) {
397
+ console.error('Message:', error.message);
398
+ console.error('Status:', error.statusCode);
399
+ console.error('Code:', error.code);
400
+
401
+ if (error.isAuthError()) {
402
+ // 401 - Invalid credentials
403
+ } else if (error.isValidationError()) {
404
+ // 400 - Validation error
405
+ } else if (error.isNetworkError()) {
406
+ // Network/connection error
309
407
  }
310
- throw error;
311
408
  }
312
409
  }
313
410
  ```
314
411
 
315
- ## Environment Variables
316
-
317
- ```env
318
- ACP_AUTH_URL=https://sso.artatol.com
319
- ACP_AUTH_API_KEY=your-api-key-here
320
- NEXT_PUBLIC_ACP_AUTH_URL=https://sso.artatol.com
321
- ```
322
-
323
- ## API Reference
324
-
325
- ### Server Functions
326
-
327
- - `initACPAuth(options)` - Initialize the auth client
328
- - `getUser()` - Get current user from JWT (local verification, returns `{ id, email }`)
329
- - `me()` - Get current user from API (returns full `User` with `twoFactorEnabled`)
330
- - `verifyAccessToken(token)` - Verify and decode access token (returns `{ id, email }`)
331
-
332
- ```typescript
333
- // getUser() returns JWTUser (fast, local)
334
- type JWTUser = {
335
- id: string;
336
- email: string;
337
- };
338
-
339
- // me() returns User (API call, complete data)
340
- type User = {
341
- id: string;
342
- email: string;
343
- twoFactorEnabled: boolean;
344
- };
345
- ```
346
- - `refreshAccessToken()` - Refresh access token using refresh token cookie
347
- - `login(email, password)` - Login user
348
- - `logout()` - Logout user
349
- - `register(email, password)` - Register new user (sends verification email)
350
- - `verifyEmail(token)` - Verify user's email address
351
- - `resendVerificationEmail(email)` - Resend verification email
352
- - `forgotPassword(email)` - Request password reset email
353
- - `resetPassword(token, newPassword)` - Reset password using token from email
354
- - `deleteAccount(password, confirmation)` - Delete authenticated user's account
355
-
356
- ### Client Hooks
357
-
358
- - `useAuth()` - Access auth context (user, isLoading, login, logout, refresh)
412
+ ### Common Error Codes
359
413
 
360
- ### Middleware
414
+ | Status | Meaning |
415
+ |--------|---------|
416
+ | 400 | Validation error (bad input) |
417
+ | 401 | Unauthorized (invalid credentials/token) |
418
+ | 403 | Forbidden (email not verified, account locked) |
419
+ | 429 | Too Many Requests (rate limited) |
361
420
 
362
- - `createACPAuthMiddleware(options)` - Create middleware for route protection
421
+ ## Password Requirements
363
422
 
364
- ## 2FA (Two-Factor Authentication)
423
+ - Minimum 10 characters
424
+ - At least one lowercase letter (a-z)
425
+ - At least one uppercase letter (A-Z)
426
+ - At least one number (0-9)
365
427
 
366
- ### Setup 2FA
428
+ ## 2FA Setup
367
429
 
368
430
  ```typescript
369
431
  'use server';
370
432
 
371
433
  import { ACPAuthClient } from '@artatol-acp/auth-nextjs/server';
434
+ import { cookies } from 'next/headers';
372
435
 
373
436
  export async function setup2FAAction(password: string) {
437
+ const cookieStore = await cookies();
438
+ const accessToken = cookieStore.get('access_token')?.value;
439
+
374
440
  const client = new ACPAuthClient({
375
441
  baseUrl: process.env.ACP_AUTH_URL!,
376
- apiKey: process.env.ACP_AUTH_API_KEY!,
442
+ apiKey: process.env.ACP_AUTH_API_KEY,
377
443
  });
378
444
 
379
- const accessToken = // ... get from session/cookie
380
-
381
- const { secret, qrCodeUrl, recoveryCodes } = await client.setup2FA(
445
+ const { qrCodeUrl, recoveryCodes } = await client.setup2FA(
382
446
  { password },
383
447
  accessToken
384
448
  );
385
449
 
386
450
  return { qrCodeUrl, recoveryCodes };
387
451
  }
388
- ```
389
452
 
390
- ### Verify 2FA Setup
453
+ export async function verify2FASetupAction(code: string) {
454
+ const cookieStore = await cookies();
455
+ const accessToken = cookieStore.get('access_token')?.value;
391
456
 
392
- ```typescript
393
- 'use server';
394
-
395
- export async function verify2FAAction(code: string) {
396
457
  const client = new ACPAuthClient({
397
458
  baseUrl: process.env.ACP_AUTH_URL!,
398
- apiKey: process.env.ACP_AUTH_API_KEY!,
459
+ apiKey: process.env.ACP_AUTH_API_KEY,
399
460
  });
400
461
 
401
- const accessToken = // ... get from session/cookie
402
-
403
462
  await client.verify2FA({ code }, accessToken);
404
463
  }
405
464
  ```
406
465
 
407
- ### Complete 2FA Login Flow
408
-
409
- ```typescript
410
- 'use server';
411
-
412
- import { login } from '@artatol-acp/auth-nextjs/server';
413
-
414
- export async function loginAction(email: string, password: string) {
415
- const result = await login(email, password);
416
-
417
- if ('requiresTwoFactor' in result) {
418
- // User has 2FA enabled
419
- return {
420
- requires2FA: true,
421
- tempToken: result.tempToken,
422
- };
423
- }
424
-
425
- // Regular login successful
426
- return { success: true };
427
- }
428
-
429
- export async function complete2FALogin(tempToken: string, code: string) {
430
- const client = new ACPAuthClient({
431
- baseUrl: process.env.ACP_AUTH_URL!,
432
- apiKey: process.env.ACP_AUTH_API_KEY!,
433
- });
434
-
435
- const result = await client.verify2FALogin({ tempToken, code });
436
- // Set cookies, etc.
437
- return result;
438
- }
439
- ```
440
-
441
- ### Disable 2FA
442
-
443
- ```typescript
444
- 'use server';
445
-
446
- export async function disable2FAAction(password: string, code: string) {
447
- const client = new ACPAuthClient({
448
- baseUrl: process.env.ACP_AUTH_URL!,
449
- apiKey: process.env.ACP_AUTH_API_KEY!,
450
- });
451
-
452
- const accessToken = // ... get from session/cookie
453
-
454
- await client.disable2FA({ password, code }, accessToken);
455
- }
456
- ```
457
-
458
- ## Password Reset
459
-
460
- ### Forgot Password
461
-
462
- Request a password reset email:
463
-
464
- ```typescript
465
- 'use server';
466
-
467
- import { forgotPassword } from '@artatol-acp/auth-nextjs/server';
468
-
469
- export async function forgotPasswordAction(formData: FormData) {
470
- const email = formData.get('email') as string;
471
-
472
- await forgotPassword(email);
473
- // Always succeeds to prevent email enumeration
474
- return { message: 'If the email exists, a reset link has been sent' };
475
- }
476
- ```
477
-
478
- ### Reset Password
479
-
480
- Reset password using the token from the email:
481
-
482
- ```typescript
483
- 'use server';
484
-
485
- import { resetPassword } from '@artatol-acp/auth-nextjs/server';
486
- import { redirect } from 'next/navigation';
487
-
488
- export async function resetPasswordAction(formData: FormData) {
489
- const token = formData.get('token') as string;
490
- const newPassword = formData.get('password') as string;
491
-
492
- try {
493
- await resetPassword(token, newPassword);
494
- redirect('/login?reset=success');
495
- } catch (error) {
496
- return { error: 'Invalid or expired reset token' };
497
- }
498
- }
499
- ```
500
-
501
- ### Client Component Example
502
-
503
- ```typescript
504
- 'use client';
505
-
506
- import { useState } from 'react';
507
- import { forgotPasswordAction } from './actions';
508
-
509
- export function ForgotPasswordForm() {
510
- const [submitted, setSubmitted] = useState(false);
511
-
512
- const handleSubmit = async (formData: FormData) => {
513
- await forgotPasswordAction(formData);
514
- setSubmitted(true);
515
- };
516
-
517
- if (submitted) {
518
- return <p>Check your email for a password reset link.</p>;
519
- }
520
-
521
- return (
522
- <form action={handleSubmit}>
523
- <input name="email" type="email" placeholder="Email" required />
524
- <button type="submit">Send Reset Link</button>
525
- </form>
526
- );
527
- }
528
- ```
529
-
530
- ## Delete Account
531
-
532
- Delete the authenticated user's account:
533
-
534
- ```typescript
535
- 'use server';
536
-
537
- import { deleteAccount } from '@artatol-acp/auth-nextjs/server';
538
- import { redirect } from 'next/navigation';
539
-
540
- export async function deleteAccountAction(formData: FormData) {
541
- const password = formData.get('password') as string;
542
- const confirmation = formData.get('confirmation') as string;
543
-
544
- try {
545
- await deleteAccount(password, confirmation);
546
- redirect('/goodbye');
547
- } catch (error) {
548
- return { error: 'Failed to delete account. Check your password.' };
549
- }
550
- }
551
- ```
552
-
553
- **Note:** The `confirmation` parameter must be the string `"DELETE"` to confirm account deletion.
554
-
555
- ## Health Check
556
-
557
- To check if the auth service is available:
558
-
559
- ```typescript
560
- import { ACPAuthClient } from '@artatol-acp/auth-nextjs/server';
561
-
562
- const client = new ACPAuthClient({
563
- baseUrl: process.env.ACP_AUTH_URL!,
564
- apiKey: process.env.ACP_AUTH_API_KEY!,
565
- });
566
-
567
- const health = await client.health();
568
- console.log(health); // { status: 'ok', timestamp: '...' }
569
- ```
570
-
571
- ## Automatic Token Refresh
572
-
573
- The Next.js SDK includes automatic token refresh functionality on the client side via the `ACPAuthProvider`.
574
-
575
- ### How It Works
576
-
577
- 1. When you use `ACPAuthProvider`, the SDK automatically manages access tokens
578
- 2. After successful login, a background interval starts
579
- 3. **Every 4 minutes**, the token is automatically refreshed (tokens expire after 5 minutes)
580
- 4. If refresh fails, the user is automatically logged out
581
- 5. The interval stops when the user logs out or the component unmounts
582
-
583
- ### Client-Side Usage
584
-
585
- ```typescript
586
- 'use client';
587
-
588
- import { ACPAuthProvider } from '@artatol-acp/auth-nextjs/client';
589
-
590
- export default function RootLayout({ children }) {
591
- return (
592
- <html>
593
- <body>
594
- <ACPAuthProvider
595
- baseUrl={process.env.NEXT_PUBLIC_ACP_AUTH_URL}
596
- >
597
- {children}
598
- </ACPAuthProvider>
599
- </body>
600
- </html>
601
- );
602
- }
603
- ```
604
-
605
- The `ACPAuthProvider` internally creates an `ACPAuthClient` with auto-refresh enabled:
606
- - `autoRefresh: true` (enabled by default)
607
- - `refreshThresholdSeconds: 60` (refreshes 60 seconds before token expires)
608
-
609
- ### Manual Token Management
610
-
611
- If you're using the client directly without the provider:
612
-
613
- ```typescript
614
- import { ACPAuthClient } from '@artatol-acp/auth-js';
615
-
616
- const client = new ACPAuthClient({
617
- baseUrl: 'https://sso.artatol.com',
618
- apiKey: '',
619
- autoRefresh: true,
620
- refreshThresholdSeconds: 60
621
- });
622
-
623
- // After login, token is automatically managed
624
- const result = await client.login({ email: '...', password: '...' });
625
-
626
- // Subsequent calls automatically refresh if needed
627
- const user = await client.me(); // No manual token management required
628
- ```
629
-
630
- ### Server-Side Token Refresh
631
-
632
- For server-side token refresh, use the `refreshAccessToken()` function:
633
-
634
- ```typescript
635
- 'use server';
636
-
637
- import { refreshAccessToken } from '@artatol-acp/auth-nextjs/server';
638
-
639
- export async function refreshTokenAction() {
640
- try {
641
- await refreshAccessToken();
642
- return { success: true };
643
- } catch (error) {
644
- return { success: false, error: 'Failed to refresh token' };
645
- }
646
- }
647
- ```
648
-
649
- ## Error Handling
650
-
651
- ```typescript
652
- import { ACPAuthError } from '@artatol-acp/auth-js';
653
-
654
- try {
655
- await login(email, password);
656
- } catch (error) {
657
- if (error instanceof ACPAuthError) {
658
- console.error('Auth error:', error.message);
659
- console.error('Status code:', error.statusCode);
660
-
661
- if (error.statusCode === 401) {
662
- // Invalid credentials
663
- } else if (error.statusCode === 429) {
664
- // Rate limited
665
- }
666
- } else {
667
- console.error('Unexpected error:', error);
668
- }
669
- }
670
- ```
671
-
672
- ### Common Error Codes
673
-
674
- | Status Code | Meaning |
675
- |-------------|---------|
676
- | 401 | Unauthorized (invalid credentials or token) |
677
- | 403 | Forbidden (email not verified, account locked) |
678
- | 429 | Too Many Requests (rate limited) |
679
- | 500 | Internal Server Error |
680
-
681
466
  ## License
682
467
 
683
468
  MIT