@artatol-acp/auth-nextjs 0.3.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +254 -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 +321 -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 +95 -32
- package/dist/server/index.js.map +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -2,11 +2,64 @@
|
|
|
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.0
|
|
8
|
+
|
|
9
|
+
**Breaking Changes:**
|
|
10
|
+
- `ACPAuthProvider` no longer requires `baseUrl` prop - it now uses local API routes
|
|
11
|
+
- `initACPAuth()` no longer returns an `ACPAuthClient` instance
|
|
12
|
+
|
|
13
|
+
**New Features:**
|
|
14
|
+
- **New `handlers` export**: Pre-built API route handlers for auth operations
|
|
15
|
+
```typescript
|
|
16
|
+
import { createAuthHandlers } from '@artatol-acp/auth-nextjs/handlers';
|
|
17
|
+
```
|
|
18
|
+
- **SSR-first architecture**: `ACPAuthProvider` now accepts `initialUser` prop for SSR hydration
|
|
19
|
+
- **New `verify2FALogin()` server function**: Complete 2FA login flow on the server
|
|
20
|
+
- **New `verify2FA()` client method**: Complete 2FA login from client components
|
|
21
|
+
|
|
22
|
+
**Improvements:**
|
|
23
|
+
- **httpOnly cookies**: All tokens are now stored in httpOnly cookies (XSS protection)
|
|
24
|
+
- **Proper cookie handling**: `login()` now automatically sets both `access_token` and `refresh_token` cookies
|
|
25
|
+
- **Server-side refresh**: `refreshAccessToken()` now properly forwards cookies to auth server
|
|
26
|
+
- **Better error messages**: Clear error when `baseUrl` is missing
|
|
27
|
+
- **Configurable cookies**: Cookie options (path, secure, sameSite) are now configurable
|
|
28
|
+
|
|
29
|
+
**Migration Guide:**
|
|
30
|
+
|
|
31
|
+
1. Create API route handlers:
|
|
32
|
+
```typescript
|
|
33
|
+
// app/api/auth/[action]/route.ts
|
|
34
|
+
import { createAuthHandlers } from '@artatol-acp/auth-nextjs/handlers';
|
|
35
|
+
|
|
36
|
+
const { authHandler } = createAuthHandlers({
|
|
37
|
+
baseUrl: process.env.ACP_AUTH_URL!,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const POST = authHandler;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
2. Update `ACPAuthProvider`:
|
|
44
|
+
```typescript
|
|
45
|
+
// Before
|
|
46
|
+
<ACPAuthProvider baseUrl={process.env.NEXT_PUBLIC_ACP_AUTH_URL}>
|
|
47
|
+
|
|
48
|
+
// After
|
|
49
|
+
<ACPAuthProvider initialUser={user}>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
3. Handle 2FA in login:
|
|
53
|
+
```typescript
|
|
54
|
+
const result = await login(email, password);
|
|
55
|
+
if (result.requiresTwoFactor) {
|
|
56
|
+
// Show 2FA form, then call verify2FA(tempToken, code)
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
5
60
|
## Installation
|
|
6
61
|
|
|
7
62
|
```bash
|
|
8
|
-
npm install @artatol-acp/auth-nextjs
|
|
9
|
-
# or
|
|
10
63
|
pnpm add @artatol-acp/auth-nextjs
|
|
11
64
|
```
|
|
12
65
|
|
|
@@ -14,67 +67,94 @@ pnpm add @artatol-acp/auth-nextjs
|
|
|
14
67
|
|
|
15
68
|
Before using this SDK, you need to obtain from the ACP AUTH service:
|
|
16
69
|
|
|
17
|
-
1. **
|
|
18
|
-
2. **
|
|
19
|
-
3. **JWT Public Key** (
|
|
70
|
+
1. **Base URL** (required) - The auth service URL (e.g., `https://sso.artatol.net`)
|
|
71
|
+
2. **API Key** (optional) - Contact your system administrator if required
|
|
72
|
+
3. **JWT Public Key** (required for `getUser()`) - For local JWT verification. Download it from:
|
|
20
73
|
```bash
|
|
21
74
|
curl https://sso.artatol.net/public-key > public.pem
|
|
22
75
|
```
|
|
23
76
|
|
|
24
|
-
|
|
77
|
+
## Quick Start
|
|
25
78
|
|
|
26
|
-
|
|
79
|
+
The SDK uses **httpOnly cookies** for secure token storage. This means:
|
|
80
|
+
- Tokens are NOT accessible from JavaScript (XSS protection)
|
|
81
|
+
- All auth operations must go through your Next.js server
|
|
82
|
+
- The SDK provides API route handlers to proxy requests to the auth server
|
|
27
83
|
|
|
28
|
-
### 1.
|
|
84
|
+
### 1. Create API Route Handlers
|
|
29
85
|
|
|
30
|
-
Create `
|
|
86
|
+
Create `app/api/auth/[action]/route.ts`:
|
|
31
87
|
|
|
32
88
|
```typescript
|
|
33
|
-
import {
|
|
34
|
-
import { readFileSync } from 'fs';
|
|
89
|
+
import { createAuthHandlers } from '@artatol-acp/auth-nextjs/handlers';
|
|
35
90
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
baseUrl: process.env.ACP_AUTH_URL || 'https://sso.artatol.com',
|
|
40
|
-
apiKey: process.env.ACP_AUTH_API_KEY!,
|
|
41
|
-
jwtPublicKey: publicKey,
|
|
91
|
+
const { authHandler } = createAuthHandlers({
|
|
92
|
+
baseUrl: process.env.ACP_AUTH_URL!,
|
|
93
|
+
apiKey: process.env.ACP_AUTH_API_KEY,
|
|
42
94
|
});
|
|
95
|
+
|
|
96
|
+
export const POST = authHandler;
|
|
43
97
|
```
|
|
44
98
|
|
|
45
|
-
|
|
99
|
+
This creates the following endpoints:
|
|
100
|
+
- `POST /api/auth/login` - Login
|
|
101
|
+
- `POST /api/auth/logout` - Logout
|
|
102
|
+
- `POST /api/auth/session` - Refresh session & get user
|
|
103
|
+
- `POST /api/auth/register` - Register
|
|
104
|
+
- `POST /api/auth/verify-2fa` - Complete 2FA login
|
|
105
|
+
- `POST /api/auth/resend-verification` - Resend verification email
|
|
106
|
+
- `POST /api/auth/forgot-password` - Request password reset
|
|
107
|
+
- `POST /api/auth/reset-password` - Reset password
|
|
46
108
|
|
|
47
|
-
|
|
109
|
+
### 2. Initialize Server-side Auth
|
|
110
|
+
|
|
111
|
+
Create `lib/auth.ts`:
|
|
48
112
|
|
|
49
113
|
```typescript
|
|
50
|
-
import {
|
|
114
|
+
import { initACPAuth } from '@artatol-acp/auth-nextjs/server';
|
|
51
115
|
import { readFileSync } from 'fs';
|
|
52
116
|
|
|
53
117
|
const publicKey = readFileSync('./keys/public.pem', 'utf-8');
|
|
54
118
|
|
|
55
|
-
|
|
119
|
+
initACPAuth({
|
|
120
|
+
baseUrl: process.env.ACP_AUTH_URL!,
|
|
121
|
+
apiKey: process.env.ACP_AUTH_API_KEY,
|
|
56
122
|
jwtPublicKey: publicKey,
|
|
57
|
-
publicPaths: ['/login', '/register', '/forgot-password'],
|
|
58
|
-
loginPath: '/login',
|
|
59
123
|
});
|
|
60
124
|
|
|
61
|
-
export
|
|
62
|
-
|
|
63
|
-
|
|
125
|
+
// Re-export server functions
|
|
126
|
+
export {
|
|
127
|
+
getUser,
|
|
128
|
+
me,
|
|
129
|
+
login,
|
|
130
|
+
logout,
|
|
131
|
+
register,
|
|
132
|
+
verify2FALogin,
|
|
133
|
+
verifyEmail,
|
|
134
|
+
resendVerificationEmail,
|
|
135
|
+
forgotPassword,
|
|
136
|
+
resetPassword,
|
|
137
|
+
deleteAccount,
|
|
138
|
+
refreshAccessToken,
|
|
139
|
+
} from '@artatol-acp/auth-nextjs/server';
|
|
64
140
|
```
|
|
65
141
|
|
|
66
|
-
### 3. Add Client Provider
|
|
142
|
+
### 3. Add Client Provider
|
|
67
143
|
|
|
68
144
|
In your root layout (`app/layout.tsx`):
|
|
69
145
|
|
|
70
146
|
```typescript
|
|
71
147
|
import { ACPAuthProvider } from '@artatol-acp/auth-nextjs/client';
|
|
148
|
+
import { getUser } from '@/lib/auth';
|
|
149
|
+
|
|
150
|
+
export default async function RootLayout({ children }) {
|
|
151
|
+
// SSR: Get initial user on server
|
|
152
|
+
const user = await getUser();
|
|
72
153
|
|
|
73
|
-
export default function RootLayout({ children }) {
|
|
74
154
|
return (
|
|
75
155
|
<html>
|
|
76
156
|
<body>
|
|
77
|
-
<ACPAuthProvider
|
|
157
|
+
<ACPAuthProvider initialUser={user}>
|
|
78
158
|
{children}
|
|
79
159
|
</ACPAuthProvider>
|
|
80
160
|
</body>
|
|
@@ -85,14 +165,18 @@ export default function RootLayout({ children }) {
|
|
|
85
165
|
|
|
86
166
|
## Usage
|
|
87
167
|
|
|
88
|
-
### Server Components
|
|
168
|
+
### Server Components (SSR)
|
|
89
169
|
|
|
90
170
|
```typescript
|
|
91
|
-
import { getUser } from '
|
|
171
|
+
import { getUser, me } from '@/lib/auth';
|
|
92
172
|
|
|
93
173
|
export default async function ProfilePage() {
|
|
174
|
+
// Fast local JWT verification (returns { id, email })
|
|
94
175
|
const user = await getUser();
|
|
95
176
|
|
|
177
|
+
// Or fetch full user from API (returns { id, email, twoFactorEnabled })
|
|
178
|
+
const fullUser = await me();
|
|
179
|
+
|
|
96
180
|
if (!user) {
|
|
97
181
|
return <div>Not logged in</div>;
|
|
98
182
|
}
|
|
@@ -110,7 +194,7 @@ export default async function ProfilePage() {
|
|
|
110
194
|
```typescript
|
|
111
195
|
'use server';
|
|
112
196
|
|
|
113
|
-
import { login, register, logout } from '
|
|
197
|
+
import { login, register, logout, verify2FALogin } from '@/lib/auth';
|
|
114
198
|
import { redirect } from 'next/navigation';
|
|
115
199
|
|
|
116
200
|
export async function loginAction(formData: FormData) {
|
|
@@ -120,19 +204,22 @@ export async function loginAction(formData: FormData) {
|
|
|
120
204
|
const result = await login(email, password);
|
|
121
205
|
|
|
122
206
|
if ('requiresTwoFactor' in result) {
|
|
123
|
-
// Handle 2FA
|
|
124
207
|
return { requires2FA: true, tempToken: result.tempToken };
|
|
125
208
|
}
|
|
126
209
|
|
|
127
210
|
redirect('/dashboard');
|
|
128
211
|
}
|
|
129
212
|
|
|
213
|
+
export async function verify2FAAction(tempToken: string, code: string) {
|
|
214
|
+
await verify2FALogin(tempToken, code);
|
|
215
|
+
redirect('/dashboard');
|
|
216
|
+
}
|
|
217
|
+
|
|
130
218
|
export async function registerAction(formData: FormData) {
|
|
131
219
|
const email = formData.get('email') as string;
|
|
132
220
|
const password = formData.get('password') as string;
|
|
133
221
|
|
|
134
222
|
await register(email, password);
|
|
135
|
-
// User will receive verification email
|
|
136
223
|
redirect('/check-email');
|
|
137
224
|
}
|
|
138
225
|
|
|
@@ -148,29 +235,50 @@ export async function logoutAction() {
|
|
|
148
235
|
'use client';
|
|
149
236
|
|
|
150
237
|
import { useAuth } from '@artatol-acp/auth-nextjs/client';
|
|
238
|
+
import { useState } from 'react';
|
|
151
239
|
|
|
152
240
|
export function LoginForm() {
|
|
153
|
-
const { login, user, isLoading } = useAuth();
|
|
241
|
+
const { login, verify2FA, user, isLoading } = useAuth();
|
|
242
|
+
const [tempToken, setTempToken] = useState<string | null>(null);
|
|
154
243
|
|
|
155
|
-
const
|
|
244
|
+
const handleLogin = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
156
245
|
e.preventDefault();
|
|
157
246
|
const formData = new FormData(e.currentTarget);
|
|
158
247
|
|
|
159
248
|
try {
|
|
160
|
-
await login(
|
|
249
|
+
const result = await login(
|
|
161
250
|
formData.get('email') as string,
|
|
162
251
|
formData.get('password') as string
|
|
163
252
|
);
|
|
253
|
+
|
|
254
|
+
if (result.requiresTwoFactor) {
|
|
255
|
+
setTempToken(result.tempToken!);
|
|
256
|
+
}
|
|
164
257
|
} catch (error) {
|
|
165
258
|
console.error('Login failed:', error);
|
|
166
259
|
}
|
|
167
260
|
};
|
|
168
261
|
|
|
262
|
+
const handle2FA = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
263
|
+
e.preventDefault();
|
|
264
|
+
const code = new FormData(e.currentTarget).get('code') as string;
|
|
265
|
+
await verify2FA(tempToken!, code);
|
|
266
|
+
};
|
|
267
|
+
|
|
169
268
|
if (isLoading) return <div>Loading...</div>;
|
|
170
269
|
if (user) return <div>Welcome {user.email}</div>;
|
|
171
270
|
|
|
271
|
+
if (tempToken) {
|
|
272
|
+
return (
|
|
273
|
+
<form onSubmit={handle2FA}>
|
|
274
|
+
<input name="code" placeholder="2FA Code" required />
|
|
275
|
+
<button type="submit">Verify</button>
|
|
276
|
+
</form>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
172
280
|
return (
|
|
173
|
-
<form onSubmit={
|
|
281
|
+
<form onSubmit={handleLogin}>
|
|
174
282
|
<input name="email" type="email" required />
|
|
175
283
|
<input name="password" type="password" required />
|
|
176
284
|
<button type="submit">Login</button>
|
|
@@ -179,505 +287,177 @@ export function LoginForm() {
|
|
|
179
287
|
}
|
|
180
288
|
```
|
|
181
289
|
|
|
182
|
-
|
|
290
|
+
## Middleware (Optional)
|
|
291
|
+
|
|
292
|
+
Create `middleware.ts` for route protection:
|
|
183
293
|
|
|
184
294
|
```typescript
|
|
185
|
-
import {
|
|
295
|
+
import { createACPAuthMiddleware } from '@artatol-acp/auth-nextjs/middleware';
|
|
296
|
+
import { readFileSync } from 'fs';
|
|
186
297
|
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
298
|
+
const publicKey = readFileSync('./keys/public.pem', 'utf-8');
|
|
299
|
+
|
|
300
|
+
export const middleware = createACPAuthMiddleware({
|
|
301
|
+
jwtPublicKey: publicKey,
|
|
302
|
+
publicPaths: ['/login', '/register', '/forgot-password'],
|
|
303
|
+
loginPath: '/login',
|
|
190
304
|
});
|
|
191
305
|
|
|
192
|
-
export
|
|
193
|
-
|
|
306
|
+
export const config = {
|
|
307
|
+
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
|
308
|
+
};
|
|
309
|
+
```
|
|
194
310
|
|
|
195
|
-
|
|
311
|
+
## Environment Variables
|
|
196
312
|
|
|
197
|
-
|
|
198
|
-
|
|
313
|
+
```env
|
|
314
|
+
ACP_AUTH_URL=https://sso.artatol.net
|
|
315
|
+
ACP_AUTH_API_KEY=your-api-key-here
|
|
199
316
|
```
|
|
200
317
|
|
|
201
|
-
##
|
|
318
|
+
## API Reference
|
|
202
319
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
320
|
+
### Server Functions (`@artatol-acp/auth-nextjs/server`)
|
|
321
|
+
|
|
322
|
+
| Function | Description |
|
|
323
|
+
|----------|-------------|
|
|
324
|
+
| `initACPAuth(options)` | Initialize auth configuration |
|
|
325
|
+
| `getUser()` | Get user from JWT (local, fast) → `{ id, email }` |
|
|
326
|
+
| `me()` | Get user from API (full data) → `{ id, email, twoFactorEnabled }` |
|
|
327
|
+
| `login(email, password)` | Login user, sets httpOnly cookies |
|
|
328
|
+
| `verify2FALogin(tempToken, code)` | Complete 2FA login |
|
|
329
|
+
| `logout()` | Logout user, clears cookies |
|
|
330
|
+
| `register(email, password)` | Register new user |
|
|
331
|
+
| `verifyEmail(token)` | Verify email address |
|
|
332
|
+
| `resendVerificationEmail(email)` | Resend verification email |
|
|
333
|
+
| `forgotPassword(email)` | Request password reset |
|
|
334
|
+
| `resetPassword(token, password)` | Reset password |
|
|
335
|
+
| `deleteAccount(password, confirmation)` | Delete account |
|
|
336
|
+
| `refreshAccessToken()` | Refresh access token |
|
|
337
|
+
|
|
338
|
+
### Client Hook (`@artatol-acp/auth-nextjs/client`)
|
|
208
339
|
|
|
209
340
|
```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
|
-
}
|
|
341
|
+
const {
|
|
342
|
+
user, // Current user or null
|
|
343
|
+
isLoading, // True during initial load
|
|
344
|
+
login, // (email, password) => Promise<{ requiresTwoFactor?, tempToken? }>
|
|
345
|
+
verify2FA, // (tempToken, code) => Promise<void>
|
|
346
|
+
logout, // () => Promise<void>
|
|
347
|
+
refresh, // () => Promise<boolean>
|
|
348
|
+
resendVerification, // (email) => Promise<void>
|
|
349
|
+
} = useAuth();
|
|
229
350
|
```
|
|
230
351
|
|
|
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
|
|
352
|
+
### ACPAuthProvider Props
|
|
242
353
|
|
|
243
354
|
```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
|
-
}
|
|
355
|
+
<ACPAuthProvider
|
|
356
|
+
apiBasePath="/api/auth" // Optional, default: "/api/auth"
|
|
357
|
+
initialUser={user} // Optional, SSR user data
|
|
358
|
+
>
|
|
359
|
+
{children}
|
|
360
|
+
</ACPAuthProvider>
|
|
263
361
|
```
|
|
264
362
|
|
|
265
|
-
###
|
|
363
|
+
### API Handlers (`@artatol-acp/auth-nextjs/handlers`)
|
|
266
364
|
|
|
267
365
|
```typescript
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
366
|
+
const handlers = createAuthHandlers({
|
|
367
|
+
baseUrl: string,
|
|
368
|
+
apiKey?: string,
|
|
369
|
+
cookies?: {
|
|
370
|
+
path?: string, // Default: "/"
|
|
371
|
+
secure?: boolean, // Default: NODE_ENV === "production"
|
|
372
|
+
sameSite?: 'strict' | 'lax' | 'none', // Default: "lax"
|
|
373
|
+
},
|
|
374
|
+
});
|
|
271
375
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const [sent, setSent] = useState(false);
|
|
376
|
+
// Use combined handler
|
|
377
|
+
export const POST = handlers.authHandler;
|
|
275
378
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
setSent(true);
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
return (
|
|
282
|
-
<button onClick={handleResend} disabled={sent}>
|
|
283
|
-
{sent ? 'Email Sent' : 'Resend Verification Email'}
|
|
284
|
-
</button>
|
|
285
|
-
);
|
|
286
|
-
}
|
|
379
|
+
// Or use individual handlers
|
|
380
|
+
export const POST = handlers.loginHandler;
|
|
287
381
|
```
|
|
288
382
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
When an unverified user tries to log in, they will receive an error:
|
|
383
|
+
## Error Handling
|
|
292
384
|
|
|
293
385
|
```typescript
|
|
294
|
-
|
|
295
|
-
const email = formData.get('email') as string;
|
|
296
|
-
const password = formData.get('password') as string;
|
|
386
|
+
import { ACPAuthError } from '@artatol-acp/auth-nextjs/server';
|
|
297
387
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
388
|
+
try {
|
|
389
|
+
await login(email, password);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (error instanceof ACPAuthError) {
|
|
392
|
+
console.error('Message:', error.message);
|
|
393
|
+
console.error('Status:', error.statusCode);
|
|
394
|
+
console.error('Code:', error.code);
|
|
395
|
+
|
|
396
|
+
if (error.isAuthError()) {
|
|
397
|
+
// 401 - Invalid credentials
|
|
398
|
+
} else if (error.isValidationError()) {
|
|
399
|
+
// 400 - Validation error
|
|
400
|
+
} else if (error.isNetworkError()) {
|
|
401
|
+
// Network/connection error
|
|
309
402
|
}
|
|
310
|
-
throw error;
|
|
311
403
|
}
|
|
312
404
|
}
|
|
313
405
|
```
|
|
314
406
|
|
|
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)
|
|
407
|
+
### Common Error Codes
|
|
359
408
|
|
|
360
|
-
|
|
409
|
+
| Status | Meaning |
|
|
410
|
+
|--------|---------|
|
|
411
|
+
| 400 | Validation error (bad input) |
|
|
412
|
+
| 401 | Unauthorized (invalid credentials/token) |
|
|
413
|
+
| 403 | Forbidden (email not verified, account locked) |
|
|
414
|
+
| 429 | Too Many Requests (rate limited) |
|
|
361
415
|
|
|
362
|
-
|
|
416
|
+
## Password Requirements
|
|
363
417
|
|
|
364
|
-
|
|
418
|
+
- Minimum 10 characters
|
|
419
|
+
- At least one lowercase letter (a-z)
|
|
420
|
+
- At least one uppercase letter (A-Z)
|
|
421
|
+
- At least one number (0-9)
|
|
365
422
|
|
|
366
|
-
|
|
423
|
+
## 2FA Setup
|
|
367
424
|
|
|
368
425
|
```typescript
|
|
369
426
|
'use server';
|
|
370
427
|
|
|
371
428
|
import { ACPAuthClient } from '@artatol-acp/auth-nextjs/server';
|
|
429
|
+
import { cookies } from 'next/headers';
|
|
372
430
|
|
|
373
431
|
export async function setup2FAAction(password: string) {
|
|
432
|
+
const cookieStore = await cookies();
|
|
433
|
+
const accessToken = cookieStore.get('access_token')?.value;
|
|
434
|
+
|
|
374
435
|
const client = new ACPAuthClient({
|
|
375
436
|
baseUrl: process.env.ACP_AUTH_URL!,
|
|
376
|
-
apiKey: process.env.ACP_AUTH_API_KEY
|
|
437
|
+
apiKey: process.env.ACP_AUTH_API_KEY,
|
|
377
438
|
});
|
|
378
439
|
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
const { secret, qrCodeUrl, recoveryCodes } = await client.setup2FA(
|
|
440
|
+
const { qrCodeUrl, recoveryCodes } = await client.setup2FA(
|
|
382
441
|
{ password },
|
|
383
442
|
accessToken
|
|
384
443
|
);
|
|
385
444
|
|
|
386
445
|
return { qrCodeUrl, recoveryCodes };
|
|
387
446
|
}
|
|
388
|
-
```
|
|
389
447
|
|
|
390
|
-
|
|
448
|
+
export async function verify2FASetupAction(code: string) {
|
|
449
|
+
const cookieStore = await cookies();
|
|
450
|
+
const accessToken = cookieStore.get('access_token')?.value;
|
|
391
451
|
|
|
392
|
-
```typescript
|
|
393
|
-
'use server';
|
|
394
|
-
|
|
395
|
-
export async function verify2FAAction(code: string) {
|
|
396
452
|
const client = new ACPAuthClient({
|
|
397
453
|
baseUrl: process.env.ACP_AUTH_URL!,
|
|
398
|
-
apiKey: process.env.ACP_AUTH_API_KEY
|
|
454
|
+
apiKey: process.env.ACP_AUTH_API_KEY,
|
|
399
455
|
});
|
|
400
456
|
|
|
401
|
-
const accessToken = // ... get from session/cookie
|
|
402
|
-
|
|
403
457
|
await client.verify2FA({ code }, accessToken);
|
|
404
458
|
}
|
|
405
459
|
```
|
|
406
460
|
|
|
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
461
|
## License
|
|
682
462
|
|
|
683
463
|
MIT
|