@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 +259 -474
- package/dist/client/index.d.ts +17 -5
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +64 -32
- package/dist/client/index.js.map +1 -1
- package/dist/handlers/index.d.ts +88 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +359 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/server/index.d.ts +27 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +143 -32
- package/dist/server/index.js.map +1 -1
- package/package.json +6 -2
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. **
|
|
18
|
-
2. **
|
|
19
|
-
3. **JWT Public Key** (
|
|
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
|
-
|
|
82
|
+
## Quick Start
|
|
25
83
|
|
|
26
|
-
|
|
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.
|
|
89
|
+
### 1. Create API Route Handlers
|
|
29
90
|
|
|
30
|
-
Create `
|
|
91
|
+
Create `app/api/auth/[action]/route.ts`:
|
|
31
92
|
|
|
32
93
|
```typescript
|
|
33
|
-
import {
|
|
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
|
-
|
|
39
|
-
baseUrl: process.env.ACP_AUTH_URL
|
|
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
|
-
|
|
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 `
|
|
116
|
+
Create `lib/auth.ts`:
|
|
48
117
|
|
|
49
118
|
```typescript
|
|
50
|
-
import {
|
|
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
|
-
|
|
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
|
|
62
|
-
|
|
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
|
|
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
|
|
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 '
|
|
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 '
|
|
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
|
|
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={
|
|
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
|
-
|
|
295
|
+
## Middleware (Optional)
|
|
296
|
+
|
|
297
|
+
Create `middleware.ts` for route protection:
|
|
183
298
|
|
|
184
299
|
```typescript
|
|
185
|
-
import {
|
|
300
|
+
import { createACPAuthMiddleware } from '@artatol-acp/auth-nextjs/middleware';
|
|
301
|
+
import { readFileSync } from 'fs';
|
|
186
302
|
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
193
|
-
|
|
311
|
+
export const config = {
|
|
312
|
+
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
|
313
|
+
};
|
|
314
|
+
```
|
|
194
315
|
|
|
195
|
-
|
|
316
|
+
## Environment Variables
|
|
196
317
|
|
|
197
|
-
|
|
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
|
-
##
|
|
323
|
+
## API Reference
|
|
202
324
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
###
|
|
368
|
+
### API Handlers (`@artatol-acp/auth-nextjs/handlers`)
|
|
266
369
|
|
|
267
370
|
```typescript
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
setSent(true);
|
|
279
|
-
};
|
|
381
|
+
// Use combined handler
|
|
382
|
+
export const POST = handlers.authHandler;
|
|
280
383
|
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
When an unverified user tries to log in, they will receive an error:
|
|
388
|
+
## Error Handling
|
|
292
389
|
|
|
293
390
|
```typescript
|
|
294
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
421
|
+
## Password Requirements
|
|
363
422
|
|
|
364
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|