omgkit 2.2.0 → 2.3.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 +3 -3
- package/package.json +1 -1
- package/plugin/skills/databases/database-management/SKILL.md +288 -0
- package/plugin/skills/databases/database-migration/SKILL.md +285 -0
- package/plugin/skills/databases/database-schema-design/SKILL.md +195 -0
- package/plugin/skills/databases/mongodb/SKILL.md +60 -776
- package/plugin/skills/databases/prisma/SKILL.md +53 -744
- package/plugin/skills/databases/redis/SKILL.md +53 -860
- package/plugin/skills/databases/supabase/SKILL.md +283 -0
- package/plugin/skills/devops/aws/SKILL.md +68 -672
- package/plugin/skills/devops/github-actions/SKILL.md +54 -657
- package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +87 -853
- package/plugin/skills/frameworks/express/SKILL.md +95 -1301
- package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
- package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
- package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
- package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
- package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
- package/plugin/skills/frontend/responsive/SKILL.md +76 -799
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
- package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
- package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
- package/plugin/skills/languages/javascript/SKILL.md +106 -849
- package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
- package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
- package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
- package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
- package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
- package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
- package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
- package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
- package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
- package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
- package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
- package/plugin/skills/security/better-auth/SKILL.md +46 -1034
- package/plugin/skills/security/oauth/SKILL.md +80 -934
- package/plugin/skills/security/owasp/SKILL.md +78 -862
- package/plugin/skills/testing/playwright/SKILL.md +77 -700
- package/plugin/skills/testing/pytest/SKILL.md +73 -811
- package/plugin/skills/testing/vitest/SKILL.md +60 -920
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
- package/plugin/skills/SKILL_STANDARDS.md +0 -743
|
@@ -1,1090 +1,102 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: Better Auth
|
|
4
|
-
category: security
|
|
5
|
-
triggers:
|
|
6
|
-
- better-auth
|
|
7
|
-
- authentication
|
|
8
|
-
- auth library
|
|
9
|
-
- typescript auth
|
|
10
|
-
- session management
|
|
2
|
+
name: Implementing Better Auth
|
|
3
|
+
description: Claude implements enterprise TypeScript authentication with Better Auth. Use when building auth systems, adding OAuth providers, enabling MFA, or managing sessions in TypeScript/Next.js projects.
|
|
11
4
|
---
|
|
12
5
|
|
|
13
|
-
# Better Auth
|
|
6
|
+
# Implementing Better Auth
|
|
14
7
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
## Purpose
|
|
18
|
-
|
|
19
|
-
Build secure authentication systems:
|
|
20
|
-
|
|
21
|
-
- Implement email/password authentication
|
|
22
|
-
- Configure social OAuth providers
|
|
23
|
-
- Set up multi-factor authentication (MFA)
|
|
24
|
-
- Manage sessions securely
|
|
25
|
-
- Implement role-based access control
|
|
26
|
-
- Handle password reset flows
|
|
27
|
-
- Integrate with popular frameworks
|
|
28
|
-
|
|
29
|
-
## Features
|
|
30
|
-
|
|
31
|
-
### 1. Core Configuration
|
|
8
|
+
## Quick Start
|
|
32
9
|
|
|
33
10
|
```typescript
|
|
34
11
|
// lib/auth.ts
|
|
35
12
|
import { betterAuth } from "better-auth";
|
|
36
13
|
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
37
|
-
import { twoFactor } from "better-auth/plugins/two-factor";
|
|
38
|
-
import { admin } from "better-auth/plugins/admin";
|
|
39
|
-
import { organization } from "better-auth/plugins/organization";
|
|
40
|
-
import { prisma } from "./prisma";
|
|
41
14
|
|
|
42
15
|
export const auth = betterAuth({
|
|
43
|
-
database: prismaAdapter(prisma, {
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Email and password authentication
|
|
48
|
-
emailAndPassword: {
|
|
49
|
-
enabled: true,
|
|
50
|
-
requireEmailVerification: true,
|
|
51
|
-
minPasswordLength: 12,
|
|
52
|
-
maxPasswordLength: 128,
|
|
53
|
-
passwordValidation: {
|
|
54
|
-
requireUppercase: true,
|
|
55
|
-
requireLowercase: true,
|
|
56
|
-
requireNumber: true,
|
|
57
|
-
requireSpecialChar: true,
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
// Email verification settings
|
|
62
|
-
emailVerification: {
|
|
63
|
-
sendOnSignUp: true,
|
|
64
|
-
autoSignInAfterVerification: true,
|
|
65
|
-
expiresIn: 60 * 60 * 24, // 24 hours
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
// Session configuration
|
|
69
|
-
session: {
|
|
70
|
-
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
|
71
|
-
updateAge: 60 * 60 * 24, // Update session every 24 hours
|
|
72
|
-
cookieCache: {
|
|
73
|
-
enabled: true,
|
|
74
|
-
maxAge: 5 * 60, // 5 minutes
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
// Cookie settings
|
|
79
|
-
cookie: {
|
|
80
|
-
secure: process.env.NODE_ENV === "production",
|
|
81
|
-
sameSite: "lax",
|
|
82
|
-
httpOnly: true,
|
|
83
|
-
path: "/",
|
|
84
|
-
domain: process.env.COOKIE_DOMAIN,
|
|
85
|
-
},
|
|
86
|
-
|
|
87
|
-
// Rate limiting
|
|
88
|
-
rateLimit: {
|
|
89
|
-
enabled: true,
|
|
90
|
-
window: 60, // 1 minute
|
|
91
|
-
max: 10, // 10 requests per window
|
|
92
|
-
customRules: {
|
|
93
|
-
signIn: { window: 300, max: 5 }, // 5 attempts per 5 minutes
|
|
94
|
-
signUp: { window: 3600, max: 3 }, // 3 signups per hour
|
|
95
|
-
forgotPassword: { window: 3600, max: 3 }, // 3 requests per hour
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
// Account settings
|
|
100
|
-
account: {
|
|
101
|
-
accountLinking: {
|
|
102
|
-
enabled: true,
|
|
103
|
-
trustedProviders: ["google", "github"],
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
|
|
107
|
-
// Social OAuth providers
|
|
16
|
+
database: prismaAdapter(prisma, { provider: "postgresql" }),
|
|
17
|
+
emailAndPassword: { enabled: true, requireEmailVerification: true },
|
|
18
|
+
session: { expiresIn: 60 * 60 * 24 * 7 },
|
|
108
19
|
socialProviders: {
|
|
109
|
-
google: {
|
|
110
|
-
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
111
|
-
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
112
|
-
scope: ["openid", "email", "profile"],
|
|
113
|
-
},
|
|
114
|
-
github: {
|
|
115
|
-
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
116
|
-
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
117
|
-
scope: ["read:user", "user:email"],
|
|
118
|
-
},
|
|
119
|
-
discord: {
|
|
120
|
-
clientId: process.env.DISCORD_CLIENT_ID!,
|
|
121
|
-
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
|
|
122
|
-
scope: ["identify", "email"],
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
// Plugins
|
|
127
|
-
plugins: [
|
|
128
|
-
twoFactor({
|
|
129
|
-
issuer: "MyApp",
|
|
130
|
-
otpOptions: {
|
|
131
|
-
digits: 6,
|
|
132
|
-
period: 30,
|
|
133
|
-
},
|
|
134
|
-
backupCodes: {
|
|
135
|
-
enabled: true,
|
|
136
|
-
count: 10,
|
|
137
|
-
length: 10,
|
|
138
|
-
},
|
|
139
|
-
}),
|
|
140
|
-
admin({
|
|
141
|
-
impersonationSessionDuration: 60 * 60, // 1 hour
|
|
142
|
-
}),
|
|
143
|
-
organization({
|
|
144
|
-
allowUserToCreateOrganization: true,
|
|
145
|
-
creatorRole: "owner",
|
|
146
|
-
memberRole: "member",
|
|
147
|
-
}),
|
|
148
|
-
],
|
|
149
|
-
|
|
150
|
-
// User fields
|
|
151
|
-
user: {
|
|
152
|
-
additionalFields: {
|
|
153
|
-
role: {
|
|
154
|
-
type: "string",
|
|
155
|
-
required: false,
|
|
156
|
-
defaultValue: "user",
|
|
157
|
-
},
|
|
158
|
-
displayName: {
|
|
159
|
-
type: "string",
|
|
160
|
-
required: false,
|
|
161
|
-
},
|
|
162
|
-
},
|
|
20
|
+
google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET! },
|
|
163
21
|
},
|
|
22
|
+
});
|
|
23
|
+
```
|
|
164
24
|
|
|
165
|
-
|
|
166
|
-
advanced: {
|
|
167
|
-
generateId: () => crypto.randomUUID(),
|
|
168
|
-
crossSubDomainCookies: {
|
|
169
|
-
enabled: process.env.NODE_ENV === "production",
|
|
170
|
-
domain: process.env.COOKIE_DOMAIN,
|
|
171
|
-
},
|
|
172
|
-
},
|
|
25
|
+
## Features
|
|
173
26
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
27
|
+
| Feature | Description | Reference |
|
|
28
|
+
|---------|-------------|-----------|
|
|
29
|
+
| Email/Password Auth | Secure registration, login, password validation | [Auth Guide](https://www.better-auth.com/docs/authentication/email-password) |
|
|
30
|
+
| Social OAuth | Google, GitHub, Discord provider integration | [Social Providers](https://www.better-auth.com/docs/authentication/social-providers) |
|
|
31
|
+
| Two-Factor Auth | TOTP-based MFA with backup codes | [2FA Plugin](https://www.better-auth.com/docs/plugins/two-factor) |
|
|
32
|
+
| Session Management | Secure cookie-based sessions with refresh | [Sessions](https://www.better-auth.com/docs/concepts/sessions) |
|
|
33
|
+
| Rate Limiting | Configurable limits per endpoint | [Rate Limiting](https://www.better-auth.com/docs/concepts/rate-limit) |
|
|
34
|
+
| Organizations | Multi-tenant support with roles | [Organizations Plugin](https://www.better-auth.com/docs/plugins/organization) |
|
|
180
35
|
|
|
181
|
-
|
|
182
|
-
```
|
|
36
|
+
## Common Patterns
|
|
183
37
|
|
|
184
|
-
###
|
|
38
|
+
### Client Setup with Plugins
|
|
185
39
|
|
|
186
40
|
```typescript
|
|
187
41
|
// lib/auth-client.ts
|
|
188
42
|
import { createAuthClient } from "better-auth/client";
|
|
189
|
-
import { twoFactorClient } from "better-auth/client/plugins";
|
|
190
|
-
import { organizationClient } from "better-auth/client/plugins";
|
|
43
|
+
import { twoFactorClient, organizationClient } from "better-auth/client/plugins";
|
|
191
44
|
|
|
192
45
|
export const authClient = createAuthClient({
|
|
193
46
|
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
|
194
|
-
plugins: [
|
|
195
|
-
twoFactorClient(),
|
|
196
|
-
organizationClient(),
|
|
197
|
-
],
|
|
47
|
+
plugins: [twoFactorClient(), organizationClient()],
|
|
198
48
|
});
|
|
199
49
|
|
|
200
|
-
|
|
201
|
-
export const {
|
|
202
|
-
signIn,
|
|
203
|
-
signUp,
|
|
204
|
-
signOut,
|
|
205
|
-
useSession,
|
|
206
|
-
getSession,
|
|
207
|
-
// OAuth
|
|
208
|
-
signIn: { social: socialSignIn },
|
|
209
|
-
// Two-factor
|
|
210
|
-
twoFactor,
|
|
211
|
-
// Organizations
|
|
212
|
-
organization,
|
|
213
|
-
} = authClient;
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
### 3. Email/Password Authentication
|
|
217
|
-
|
|
218
|
-
```typescript
|
|
219
|
-
// components/auth/SignUpForm.tsx
|
|
220
|
-
"use client";
|
|
221
|
-
|
|
222
|
-
import { useState } from "react";
|
|
223
|
-
import { authClient } from "@/lib/auth-client";
|
|
224
|
-
import { useRouter } from "next/navigation";
|
|
225
|
-
|
|
226
|
-
interface SignUpFormData {
|
|
227
|
-
name: string;
|
|
228
|
-
email: string;
|
|
229
|
-
password: string;
|
|
230
|
-
confirmPassword: string;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export function SignUpForm() {
|
|
234
|
-
const router = useRouter();
|
|
235
|
-
const [formData, setFormData] = useState<SignUpFormData>({
|
|
236
|
-
name: "",
|
|
237
|
-
email: "",
|
|
238
|
-
password: "",
|
|
239
|
-
confirmPassword: "",
|
|
240
|
-
});
|
|
241
|
-
const [error, setError] = useState<string | null>(null);
|
|
242
|
-
const [loading, setLoading] = useState(false);
|
|
243
|
-
|
|
244
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
245
|
-
e.preventDefault();
|
|
246
|
-
setError(null);
|
|
247
|
-
|
|
248
|
-
if (formData.password !== formData.confirmPassword) {
|
|
249
|
-
setError("Passwords do not match");
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
setLoading(true);
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
const { data, error } = await authClient.signUp.email({
|
|
257
|
-
email: formData.email,
|
|
258
|
-
password: formData.password,
|
|
259
|
-
name: formData.name,
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
if (error) {
|
|
263
|
-
setError(error.message);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Redirect to verification page
|
|
268
|
-
router.push("/verify-email");
|
|
269
|
-
} catch (err) {
|
|
270
|
-
setError("An unexpected error occurred");
|
|
271
|
-
} finally {
|
|
272
|
-
setLoading(false);
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
return (
|
|
277
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
278
|
-
<div>
|
|
279
|
-
<label htmlFor="name">Name</label>
|
|
280
|
-
<input
|
|
281
|
-
id="name"
|
|
282
|
-
type="text"
|
|
283
|
-
value={formData.name}
|
|
284
|
-
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
285
|
-
required
|
|
286
|
-
/>
|
|
287
|
-
</div>
|
|
288
|
-
|
|
289
|
-
<div>
|
|
290
|
-
<label htmlFor="email">Email</label>
|
|
291
|
-
<input
|
|
292
|
-
id="email"
|
|
293
|
-
type="email"
|
|
294
|
-
value={formData.email}
|
|
295
|
-
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
296
|
-
required
|
|
297
|
-
/>
|
|
298
|
-
</div>
|
|
299
|
-
|
|
300
|
-
<div>
|
|
301
|
-
<label htmlFor="password">Password</label>
|
|
302
|
-
<input
|
|
303
|
-
id="password"
|
|
304
|
-
type="password"
|
|
305
|
-
value={formData.password}
|
|
306
|
-
onChange={(e) =>
|
|
307
|
-
setFormData({ ...formData, password: e.target.value })
|
|
308
|
-
}
|
|
309
|
-
minLength={12}
|
|
310
|
-
required
|
|
311
|
-
/>
|
|
312
|
-
<PasswordStrengthIndicator password={formData.password} />
|
|
313
|
-
</div>
|
|
314
|
-
|
|
315
|
-
<div>
|
|
316
|
-
<label htmlFor="confirmPassword">Confirm Password</label>
|
|
317
|
-
<input
|
|
318
|
-
id="confirmPassword"
|
|
319
|
-
type="password"
|
|
320
|
-
value={formData.confirmPassword}
|
|
321
|
-
onChange={(e) =>
|
|
322
|
-
setFormData({ ...formData, confirmPassword: e.target.value })
|
|
323
|
-
}
|
|
324
|
-
required
|
|
325
|
-
/>
|
|
326
|
-
</div>
|
|
327
|
-
|
|
328
|
-
{error && <div className="text-red-500">{error}</div>}
|
|
329
|
-
|
|
330
|
-
<button type="submit" disabled={loading}>
|
|
331
|
-
{loading ? "Creating account..." : "Sign Up"}
|
|
332
|
-
</button>
|
|
333
|
-
</form>
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// components/auth/SignInForm.tsx
|
|
338
|
-
"use client";
|
|
339
|
-
|
|
340
|
-
import { useState } from "react";
|
|
341
|
-
import { authClient } from "@/lib/auth-client";
|
|
342
|
-
import { useRouter } from "next/navigation";
|
|
343
|
-
|
|
344
|
-
export function SignInForm() {
|
|
345
|
-
const router = useRouter();
|
|
346
|
-
const [email, setEmail] = useState("");
|
|
347
|
-
const [password, setPassword] = useState("");
|
|
348
|
-
const [error, setError] = useState<string | null>(null);
|
|
349
|
-
const [loading, setLoading] = useState(false);
|
|
350
|
-
const [requiresTwoFactor, setRequiresTwoFactor] = useState(false);
|
|
351
|
-
const [twoFactorCode, setTwoFactorCode] = useState("");
|
|
352
|
-
|
|
353
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
354
|
-
e.preventDefault();
|
|
355
|
-
setError(null);
|
|
356
|
-
setLoading(true);
|
|
357
|
-
|
|
358
|
-
try {
|
|
359
|
-
const { data, error } = await authClient.signIn.email({
|
|
360
|
-
email,
|
|
361
|
-
password,
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
if (error) {
|
|
365
|
-
if (error.code === "TWO_FACTOR_REQUIRED") {
|
|
366
|
-
setRequiresTwoFactor(true);
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
setError(error.message);
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
router.push("/dashboard");
|
|
374
|
-
} catch (err) {
|
|
375
|
-
setError("An unexpected error occurred");
|
|
376
|
-
} finally {
|
|
377
|
-
setLoading(false);
|
|
378
|
-
}
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
const handleTwoFactorSubmit = async (e: React.FormEvent) => {
|
|
382
|
-
e.preventDefault();
|
|
383
|
-
setLoading(true);
|
|
384
|
-
|
|
385
|
-
try {
|
|
386
|
-
const { data, error } = await authClient.twoFactor.verify({
|
|
387
|
-
code: twoFactorCode,
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
if (error) {
|
|
391
|
-
setError(error.message);
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
router.push("/dashboard");
|
|
396
|
-
} finally {
|
|
397
|
-
setLoading(false);
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
if (requiresTwoFactor) {
|
|
402
|
-
return (
|
|
403
|
-
<form onSubmit={handleTwoFactorSubmit} className="space-y-4">
|
|
404
|
-
<div>
|
|
405
|
-
<label htmlFor="code">Two-Factor Code</label>
|
|
406
|
-
<input
|
|
407
|
-
id="code"
|
|
408
|
-
type="text"
|
|
409
|
-
inputMode="numeric"
|
|
410
|
-
pattern="[0-9]*"
|
|
411
|
-
maxLength={6}
|
|
412
|
-
value={twoFactorCode}
|
|
413
|
-
onChange={(e) => setTwoFactorCode(e.target.value)}
|
|
414
|
-
placeholder="Enter 6-digit code"
|
|
415
|
-
required
|
|
416
|
-
/>
|
|
417
|
-
</div>
|
|
418
|
-
|
|
419
|
-
{error && <div className="text-red-500">{error}</div>}
|
|
420
|
-
|
|
421
|
-
<button type="submit" disabled={loading}>
|
|
422
|
-
{loading ? "Verifying..." : "Verify"}
|
|
423
|
-
</button>
|
|
424
|
-
|
|
425
|
-
<button
|
|
426
|
-
type="button"
|
|
427
|
-
onClick={() => setRequiresTwoFactor(false)}
|
|
428
|
-
className="text-sm text-gray-500"
|
|
429
|
-
>
|
|
430
|
-
Use a different account
|
|
431
|
-
</button>
|
|
432
|
-
</form>
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return (
|
|
437
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
438
|
-
<div>
|
|
439
|
-
<label htmlFor="email">Email</label>
|
|
440
|
-
<input
|
|
441
|
-
id="email"
|
|
442
|
-
type="email"
|
|
443
|
-
value={email}
|
|
444
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
445
|
-
required
|
|
446
|
-
/>
|
|
447
|
-
</div>
|
|
448
|
-
|
|
449
|
-
<div>
|
|
450
|
-
<label htmlFor="password">Password</label>
|
|
451
|
-
<input
|
|
452
|
-
id="password"
|
|
453
|
-
type="password"
|
|
454
|
-
value={password}
|
|
455
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
456
|
-
required
|
|
457
|
-
/>
|
|
458
|
-
</div>
|
|
459
|
-
|
|
460
|
-
{error && <div className="text-red-500">{error}</div>}
|
|
461
|
-
|
|
462
|
-
<button type="submit" disabled={loading}>
|
|
463
|
-
{loading ? "Signing in..." : "Sign In"}
|
|
464
|
-
</button>
|
|
465
|
-
|
|
466
|
-
<a href="/forgot-password" className="text-sm">
|
|
467
|
-
Forgot password?
|
|
468
|
-
</a>
|
|
469
|
-
</form>
|
|
470
|
-
);
|
|
471
|
-
}
|
|
50
|
+
export const { signIn, signUp, signOut, useSession } = authClient;
|
|
472
51
|
```
|
|
473
52
|
|
|
474
|
-
###
|
|
475
|
-
|
|
476
|
-
```typescript
|
|
477
|
-
// components/auth/SocialAuth.tsx
|
|
478
|
-
"use client";
|
|
479
|
-
|
|
480
|
-
import { authClient } from "@/lib/auth-client";
|
|
481
|
-
|
|
482
|
-
interface SocialAuthProps {
|
|
483
|
-
mode: "signin" | "signup";
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
export function SocialAuth({ mode }: SocialAuthProps) {
|
|
487
|
-
const handleGoogleAuth = async () => {
|
|
488
|
-
await authClient.signIn.social({
|
|
489
|
-
provider: "google",
|
|
490
|
-
callbackURL: "/dashboard",
|
|
491
|
-
errorCallbackURL: "/auth/error",
|
|
492
|
-
});
|
|
493
|
-
};
|
|
494
|
-
|
|
495
|
-
const handleGitHubAuth = async () => {
|
|
496
|
-
await authClient.signIn.social({
|
|
497
|
-
provider: "github",
|
|
498
|
-
callbackURL: "/dashboard",
|
|
499
|
-
errorCallbackURL: "/auth/error",
|
|
500
|
-
});
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
const handleDiscordAuth = async () => {
|
|
504
|
-
await authClient.signIn.social({
|
|
505
|
-
provider: "discord",
|
|
506
|
-
callbackURL: "/dashboard",
|
|
507
|
-
errorCallbackURL: "/auth/error",
|
|
508
|
-
});
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
return (
|
|
512
|
-
<div className="space-y-3">
|
|
513
|
-
<button
|
|
514
|
-
onClick={handleGoogleAuth}
|
|
515
|
-
className="w-full flex items-center justify-center gap-2 btn-outline"
|
|
516
|
-
>
|
|
517
|
-
<GoogleIcon />
|
|
518
|
-
{mode === "signin" ? "Sign in with Google" : "Sign up with Google"}
|
|
519
|
-
</button>
|
|
520
|
-
|
|
521
|
-
<button
|
|
522
|
-
onClick={handleGitHubAuth}
|
|
523
|
-
className="w-full flex items-center justify-center gap-2 btn-outline"
|
|
524
|
-
>
|
|
525
|
-
<GitHubIcon />
|
|
526
|
-
{mode === "signin" ? "Sign in with GitHub" : "Sign up with GitHub"}
|
|
527
|
-
</button>
|
|
528
|
-
|
|
529
|
-
<button
|
|
530
|
-
onClick={handleDiscordAuth}
|
|
531
|
-
className="w-full flex items-center justify-center gap-2 btn-outline"
|
|
532
|
-
>
|
|
533
|
-
<DiscordIcon />
|
|
534
|
-
{mode === "signin" ? "Sign in with Discord" : "Sign up with Discord"}
|
|
535
|
-
</button>
|
|
536
|
-
</div>
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// app/api/auth/callback/[provider]/route.ts
|
|
541
|
-
import { auth } from "@/lib/auth";
|
|
542
|
-
import { NextRequest } from "next/server";
|
|
543
|
-
|
|
544
|
-
export async function GET(
|
|
545
|
-
request: NextRequest,
|
|
546
|
-
{ params }: { params: { provider: string } }
|
|
547
|
-
) {
|
|
548
|
-
return auth.handler(request);
|
|
549
|
-
}
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
### 5. Two-Factor Authentication
|
|
553
|
-
|
|
554
|
-
```typescript
|
|
555
|
-
// components/auth/TwoFactorSetup.tsx
|
|
556
|
-
"use client";
|
|
557
|
-
|
|
558
|
-
import { useState } from "react";
|
|
559
|
-
import { authClient } from "@/lib/auth-client";
|
|
560
|
-
import QRCode from "qrcode.react";
|
|
561
|
-
|
|
562
|
-
export function TwoFactorSetup() {
|
|
563
|
-
const [step, setStep] = useState<"start" | "verify" | "backup" | "complete">(
|
|
564
|
-
"start"
|
|
565
|
-
);
|
|
566
|
-
const [totpUri, setTotpUri] = useState<string | null>(null);
|
|
567
|
-
const [backupCodes, setBackupCodes] = useState<string[]>([]);
|
|
568
|
-
const [verificationCode, setVerificationCode] = useState("");
|
|
569
|
-
const [error, setError] = useState<string | null>(null);
|
|
570
|
-
|
|
571
|
-
const handleEnable = async () => {
|
|
572
|
-
try {
|
|
573
|
-
const { data, error } = await authClient.twoFactor.enable();
|
|
574
|
-
|
|
575
|
-
if (error) {
|
|
576
|
-
setError(error.message);
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
setTotpUri(data.totpURI);
|
|
581
|
-
setStep("verify");
|
|
582
|
-
} catch (err) {
|
|
583
|
-
setError("Failed to enable 2FA");
|
|
584
|
-
}
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
const handleVerify = async (e: React.FormEvent) => {
|
|
588
|
-
e.preventDefault();
|
|
589
|
-
|
|
590
|
-
try {
|
|
591
|
-
const { data, error } = await authClient.twoFactor.verifySetup({
|
|
592
|
-
code: verificationCode,
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
if (error) {
|
|
596
|
-
setError(error.message);
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
setBackupCodes(data.backupCodes);
|
|
601
|
-
setStep("backup");
|
|
602
|
-
} catch (err) {
|
|
603
|
-
setError("Failed to verify code");
|
|
604
|
-
}
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
const handleComplete = () => {
|
|
608
|
-
setStep("complete");
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
if (step === "start") {
|
|
612
|
-
return (
|
|
613
|
-
<div className="space-y-4">
|
|
614
|
-
<h2>Enable Two-Factor Authentication</h2>
|
|
615
|
-
<p>Add an extra layer of security to your account.</p>
|
|
616
|
-
<button onClick={handleEnable}>Enable 2FA</button>
|
|
617
|
-
</div>
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
if (step === "verify") {
|
|
622
|
-
return (
|
|
623
|
-
<div className="space-y-4">
|
|
624
|
-
<h2>Scan QR Code</h2>
|
|
625
|
-
<p>Scan this QR code with your authenticator app:</p>
|
|
626
|
-
|
|
627
|
-
{totpUri && (
|
|
628
|
-
<div className="flex justify-center">
|
|
629
|
-
<QRCode value={totpUri} size={200} />
|
|
630
|
-
</div>
|
|
631
|
-
)}
|
|
632
|
-
|
|
633
|
-
<form onSubmit={handleVerify} className="space-y-4">
|
|
634
|
-
<div>
|
|
635
|
-
<label htmlFor="code">Verification Code</label>
|
|
636
|
-
<input
|
|
637
|
-
id="code"
|
|
638
|
-
type="text"
|
|
639
|
-
inputMode="numeric"
|
|
640
|
-
pattern="[0-9]*"
|
|
641
|
-
maxLength={6}
|
|
642
|
-
value={verificationCode}
|
|
643
|
-
onChange={(e) => setVerificationCode(e.target.value)}
|
|
644
|
-
placeholder="Enter 6-digit code"
|
|
645
|
-
required
|
|
646
|
-
/>
|
|
647
|
-
</div>
|
|
648
|
-
|
|
649
|
-
{error && <div className="text-red-500">{error}</div>}
|
|
650
|
-
|
|
651
|
-
<button type="submit">Verify</button>
|
|
652
|
-
</form>
|
|
653
|
-
</div>
|
|
654
|
-
);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
if (step === "backup") {
|
|
658
|
-
return (
|
|
659
|
-
<div className="space-y-4">
|
|
660
|
-
<h2>Save Backup Codes</h2>
|
|
661
|
-
<p>
|
|
662
|
-
Save these backup codes in a secure place. You can use them to access
|
|
663
|
-
your account if you lose your authenticator device.
|
|
664
|
-
</p>
|
|
665
|
-
|
|
666
|
-
<div className="grid grid-cols-2 gap-2 font-mono bg-gray-100 p-4 rounded">
|
|
667
|
-
{backupCodes.map((code, index) => (
|
|
668
|
-
<div key={index} className="text-center">
|
|
669
|
-
{code}
|
|
670
|
-
</div>
|
|
671
|
-
))}
|
|
672
|
-
</div>
|
|
673
|
-
|
|
674
|
-
<button onClick={handleComplete}>I've saved my backup codes</button>
|
|
675
|
-
</div>
|
|
676
|
-
);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
return (
|
|
680
|
-
<div className="space-y-4">
|
|
681
|
-
<h2>Two-Factor Authentication Enabled</h2>
|
|
682
|
-
<p>Your account is now protected with 2FA.</p>
|
|
683
|
-
</div>
|
|
684
|
-
);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// components/auth/TwoFactorDisable.tsx
|
|
688
|
-
"use client";
|
|
689
|
-
|
|
690
|
-
import { useState } from "react";
|
|
691
|
-
import { authClient } from "@/lib/auth-client";
|
|
692
|
-
|
|
693
|
-
export function TwoFactorDisable() {
|
|
694
|
-
const [password, setPassword] = useState("");
|
|
695
|
-
const [error, setError] = useState<string | null>(null);
|
|
696
|
-
const [loading, setLoading] = useState(false);
|
|
697
|
-
|
|
698
|
-
const handleDisable = async (e: React.FormEvent) => {
|
|
699
|
-
e.preventDefault();
|
|
700
|
-
setLoading(true);
|
|
701
|
-
|
|
702
|
-
try {
|
|
703
|
-
const { error } = await authClient.twoFactor.disable({
|
|
704
|
-
password,
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
if (error) {
|
|
708
|
-
setError(error.message);
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Refresh page or update state
|
|
713
|
-
window.location.reload();
|
|
714
|
-
} finally {
|
|
715
|
-
setLoading(false);
|
|
716
|
-
}
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
return (
|
|
720
|
-
<form onSubmit={handleDisable} className="space-y-4">
|
|
721
|
-
<h2>Disable Two-Factor Authentication</h2>
|
|
722
|
-
<p className="text-yellow-600">
|
|
723
|
-
Warning: This will make your account less secure.
|
|
724
|
-
</p>
|
|
725
|
-
|
|
726
|
-
<div>
|
|
727
|
-
<label htmlFor="password">Confirm Password</label>
|
|
728
|
-
<input
|
|
729
|
-
id="password"
|
|
730
|
-
type="password"
|
|
731
|
-
value={password}
|
|
732
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
733
|
-
required
|
|
734
|
-
/>
|
|
735
|
-
</div>
|
|
736
|
-
|
|
737
|
-
{error && <div className="text-red-500">{error}</div>}
|
|
738
|
-
|
|
739
|
-
<button type="submit" disabled={loading} className="btn-danger">
|
|
740
|
-
{loading ? "Disabling..." : "Disable 2FA"}
|
|
741
|
-
</button>
|
|
742
|
-
</form>
|
|
743
|
-
);
|
|
744
|
-
}
|
|
745
|
-
```
|
|
746
|
-
|
|
747
|
-
### 6. Session Management
|
|
53
|
+
### Protected Routes Middleware
|
|
748
54
|
|
|
749
55
|
```typescript
|
|
750
56
|
// middleware.ts
|
|
751
57
|
import { auth } from "@/lib/auth";
|
|
752
58
|
import { NextRequest, NextResponse } from "next/server";
|
|
753
59
|
|
|
754
|
-
const publicRoutes = ["/", "/login", "/signup", "/forgot-password"];
|
|
755
|
-
const authRoutes = ["/login", "/signup"];
|
|
756
|
-
|
|
757
60
|
export async function middleware(request: NextRequest) {
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
// Get session
|
|
761
|
-
const session = await auth.api.getSession({
|
|
762
|
-
headers: request.headers,
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
// Redirect authenticated users away from auth pages
|
|
766
|
-
if (authRoutes.includes(pathname) && session) {
|
|
767
|
-
return NextResponse.redirect(new URL("/dashboard", request.url));
|
|
768
|
-
}
|
|
61
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
769
62
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const loginUrl = new URL("/login", request.url);
|
|
773
|
-
loginUrl.searchParams.set("callbackUrl", pathname);
|
|
774
|
-
return NextResponse.redirect(loginUrl);
|
|
63
|
+
if (!session && !request.nextUrl.pathname.startsWith("/login")) {
|
|
64
|
+
return NextResponse.redirect(new URL("/login", request.url));
|
|
775
65
|
}
|
|
776
|
-
|
|
777
66
|
return NextResponse.next();
|
|
778
67
|
}
|
|
779
|
-
|
|
780
|
-
export const config = {
|
|
781
|
-
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
|
|
782
|
-
};
|
|
783
|
-
|
|
784
|
-
// hooks/useAuth.ts
|
|
785
|
-
"use client";
|
|
786
|
-
|
|
787
|
-
import { authClient } from "@/lib/auth-client";
|
|
788
|
-
import { useRouter } from "next/navigation";
|
|
789
|
-
import { useCallback } from "react";
|
|
790
|
-
|
|
791
|
-
export function useAuth() {
|
|
792
|
-
const router = useRouter();
|
|
793
|
-
const { data: session, isPending, error, refetch } = authClient.useSession();
|
|
794
|
-
|
|
795
|
-
const signOut = useCallback(async () => {
|
|
796
|
-
await authClient.signOut();
|
|
797
|
-
router.push("/login");
|
|
798
|
-
}, [router]);
|
|
799
|
-
|
|
800
|
-
const refreshSession = useCallback(async () => {
|
|
801
|
-
await refetch();
|
|
802
|
-
}, [refetch]);
|
|
803
|
-
|
|
804
|
-
return {
|
|
805
|
-
user: session?.user ?? null,
|
|
806
|
-
session: session?.session ?? null,
|
|
807
|
-
isAuthenticated: !!session?.user,
|
|
808
|
-
isLoading: isPending,
|
|
809
|
-
error,
|
|
810
|
-
signOut,
|
|
811
|
-
refreshSession,
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
// components/auth/SessionInfo.tsx
|
|
816
|
-
"use client";
|
|
817
|
-
|
|
818
|
-
import { useAuth } from "@/hooks/useAuth";
|
|
819
|
-
import { formatDistanceToNow } from "date-fns";
|
|
820
|
-
|
|
821
|
-
export function SessionInfo() {
|
|
822
|
-
const { session, user, signOut } = useAuth();
|
|
823
|
-
|
|
824
|
-
if (!session || !user) return null;
|
|
825
|
-
|
|
826
|
-
return (
|
|
827
|
-
<div className="p-4 bg-gray-100 rounded">
|
|
828
|
-
<h3>Session Information</h3>
|
|
829
|
-
<dl className="space-y-2">
|
|
830
|
-
<div>
|
|
831
|
-
<dt className="text-sm text-gray-500">User</dt>
|
|
832
|
-
<dd>{user.email}</dd>
|
|
833
|
-
</div>
|
|
834
|
-
<div>
|
|
835
|
-
<dt className="text-sm text-gray-500">Session created</dt>
|
|
836
|
-
<dd>
|
|
837
|
-
{formatDistanceToNow(new Date(session.createdAt), {
|
|
838
|
-
addSuffix: true,
|
|
839
|
-
})}
|
|
840
|
-
</dd>
|
|
841
|
-
</div>
|
|
842
|
-
<div>
|
|
843
|
-
<dt className="text-sm text-gray-500">Expires</dt>
|
|
844
|
-
<dd>
|
|
845
|
-
{formatDistanceToNow(new Date(session.expiresAt), {
|
|
846
|
-
addSuffix: true,
|
|
847
|
-
})}
|
|
848
|
-
</dd>
|
|
849
|
-
</div>
|
|
850
|
-
</dl>
|
|
851
|
-
<button onClick={signOut} className="mt-4 btn-danger">
|
|
852
|
-
Sign Out
|
|
853
|
-
</button>
|
|
854
|
-
</div>
|
|
855
|
-
);
|
|
856
|
-
}
|
|
857
68
|
```
|
|
858
69
|
|
|
859
|
-
###
|
|
70
|
+
### Two-Factor Authentication Flow
|
|
860
71
|
|
|
861
72
|
```typescript
|
|
862
|
-
//
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
import { useState } from "react";
|
|
866
|
-
import { authClient } from "@/lib/auth-client";
|
|
867
|
-
|
|
868
|
-
export function ForgotPassword() {
|
|
869
|
-
const [email, setEmail] = useState("");
|
|
870
|
-
const [submitted, setSubmitted] = useState(false);
|
|
871
|
-
const [error, setError] = useState<string | null>(null);
|
|
872
|
-
const [loading, setLoading] = useState(false);
|
|
873
|
-
|
|
874
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
875
|
-
e.preventDefault();
|
|
876
|
-
setLoading(true);
|
|
877
|
-
setError(null);
|
|
878
|
-
|
|
879
|
-
try {
|
|
880
|
-
const { error } = await authClient.forgetPassword({
|
|
881
|
-
email,
|
|
882
|
-
redirectTo: "/reset-password",
|
|
883
|
-
});
|
|
884
|
-
|
|
885
|
-
if (error) {
|
|
886
|
-
setError(error.message);
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
setSubmitted(true);
|
|
891
|
-
} finally {
|
|
892
|
-
setLoading(false);
|
|
893
|
-
}
|
|
894
|
-
};
|
|
895
|
-
|
|
896
|
-
if (submitted) {
|
|
897
|
-
return (
|
|
898
|
-
<div className="text-center">
|
|
899
|
-
<h2>Check your email</h2>
|
|
900
|
-
<p>
|
|
901
|
-
We've sent a password reset link to <strong>{email}</strong>
|
|
902
|
-
</p>
|
|
903
|
-
</div>
|
|
904
|
-
);
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
return (
|
|
908
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
909
|
-
<h2>Forgot Password</h2>
|
|
910
|
-
<p>Enter your email to receive a password reset link.</p>
|
|
911
|
-
|
|
912
|
-
<div>
|
|
913
|
-
<label htmlFor="email">Email</label>
|
|
914
|
-
<input
|
|
915
|
-
id="email"
|
|
916
|
-
type="email"
|
|
917
|
-
value={email}
|
|
918
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
919
|
-
required
|
|
920
|
-
/>
|
|
921
|
-
</div>
|
|
922
|
-
|
|
923
|
-
{error && <div className="text-red-500">{error}</div>}
|
|
924
|
-
|
|
925
|
-
<button type="submit" disabled={loading}>
|
|
926
|
-
{loading ? "Sending..." : "Send Reset Link"}
|
|
927
|
-
</button>
|
|
928
|
-
</form>
|
|
929
|
-
);
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// components/auth/ResetPassword.tsx
|
|
933
|
-
"use client";
|
|
934
|
-
|
|
935
|
-
import { useState } from "react";
|
|
936
|
-
import { authClient } from "@/lib/auth-client";
|
|
937
|
-
import { useRouter, useSearchParams } from "next/navigation";
|
|
938
|
-
|
|
939
|
-
export function ResetPassword() {
|
|
940
|
-
const router = useRouter();
|
|
941
|
-
const searchParams = useSearchParams();
|
|
942
|
-
const token = searchParams.get("token");
|
|
943
|
-
|
|
944
|
-
const [password, setPassword] = useState("");
|
|
945
|
-
const [confirmPassword, setConfirmPassword] = useState("");
|
|
946
|
-
const [error, setError] = useState<string | null>(null);
|
|
947
|
-
const [loading, setLoading] = useState(false);
|
|
948
|
-
|
|
949
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
950
|
-
e.preventDefault();
|
|
951
|
-
|
|
952
|
-
if (password !== confirmPassword) {
|
|
953
|
-
setError("Passwords do not match");
|
|
954
|
-
return;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
if (!token) {
|
|
958
|
-
setError("Invalid reset token");
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
setLoading(true);
|
|
963
|
-
setError(null);
|
|
964
|
-
|
|
965
|
-
try {
|
|
966
|
-
const { error } = await authClient.resetPassword({
|
|
967
|
-
newPassword: password,
|
|
968
|
-
token,
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
if (error) {
|
|
972
|
-
setError(error.message);
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
73
|
+
// Handle 2FA during sign-in
|
|
74
|
+
const { data, error } = await authClient.signIn.email({ email, password });
|
|
975
75
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
}
|
|
980
|
-
};
|
|
981
|
-
|
|
982
|
-
return (
|
|
983
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
984
|
-
<h2>Reset Password</h2>
|
|
985
|
-
|
|
986
|
-
<div>
|
|
987
|
-
<label htmlFor="password">New Password</label>
|
|
988
|
-
<input
|
|
989
|
-
id="password"
|
|
990
|
-
type="password"
|
|
991
|
-
value={password}
|
|
992
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
993
|
-
minLength={12}
|
|
994
|
-
required
|
|
995
|
-
/>
|
|
996
|
-
</div>
|
|
997
|
-
|
|
998
|
-
<div>
|
|
999
|
-
<label htmlFor="confirmPassword">Confirm Password</label>
|
|
1000
|
-
<input
|
|
1001
|
-
id="confirmPassword"
|
|
1002
|
-
type="password"
|
|
1003
|
-
value={confirmPassword}
|
|
1004
|
-
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
1005
|
-
required
|
|
1006
|
-
/>
|
|
1007
|
-
</div>
|
|
1008
|
-
|
|
1009
|
-
{error && <div className="text-red-500">{error}</div>}
|
|
1010
|
-
|
|
1011
|
-
<button type="submit" disabled={loading}>
|
|
1012
|
-
{loading ? "Resetting..." : "Reset Password"}
|
|
1013
|
-
</button>
|
|
1014
|
-
</form>
|
|
1015
|
-
);
|
|
76
|
+
if (error?.code === "TWO_FACTOR_REQUIRED") {
|
|
77
|
+
// Show 2FA input
|
|
78
|
+
const { data: verified } = await authClient.twoFactor.verify({ code: totpCode });
|
|
1016
79
|
}
|
|
1017
|
-
```
|
|
1018
|
-
|
|
1019
|
-
## Use Cases
|
|
1020
|
-
|
|
1021
|
-
### Next.js API Route Handler
|
|
1022
|
-
|
|
1023
|
-
```typescript
|
|
1024
|
-
// app/api/[...auth]/route.ts
|
|
1025
|
-
import { auth } from "@/lib/auth";
|
|
1026
|
-
import { toNextJsHandler } from "better-auth/next-js";
|
|
1027
|
-
|
|
1028
|
-
export const { GET, POST } = toNextJsHandler(auth.handler);
|
|
1029
|
-
```
|
|
1030
|
-
|
|
1031
|
-
### Server Component Auth Check
|
|
1032
|
-
|
|
1033
|
-
```typescript
|
|
1034
|
-
// app/dashboard/page.tsx
|
|
1035
|
-
import { auth } from "@/lib/auth";
|
|
1036
|
-
import { headers } from "next/headers";
|
|
1037
|
-
import { redirect } from "next/navigation";
|
|
1038
|
-
|
|
1039
|
-
export default async function DashboardPage() {
|
|
1040
|
-
const session = await auth.api.getSession({
|
|
1041
|
-
headers: headers(),
|
|
1042
|
-
});
|
|
1043
80
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
return (
|
|
1049
|
-
<div>
|
|
1050
|
-
<h1>Welcome, {session.user.name}</h1>
|
|
1051
|
-
</div>
|
|
1052
|
-
);
|
|
1053
|
-
}
|
|
81
|
+
// Enable 2FA for user
|
|
82
|
+
const { data } = await authClient.twoFactor.enable();
|
|
83
|
+
// data.totpURI contains QR code data
|
|
84
|
+
// data.backupCodes contains recovery codes
|
|
1054
85
|
```
|
|
1055
86
|
|
|
1056
87
|
## Best Practices
|
|
1057
88
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
- Set appropriate session expiration
|
|
1067
|
-
- Log authentication events
|
|
1068
|
-
- Use HTTPS in production
|
|
1069
|
-
- Implement account lockout policies
|
|
1070
|
-
|
|
1071
|
-
### Don'ts
|
|
1072
|
-
|
|
1073
|
-
- Don't store plain-text passwords
|
|
1074
|
-
- Don't use predictable session tokens
|
|
1075
|
-
- Don't expose detailed error messages
|
|
1076
|
-
- Don't allow unlimited login attempts
|
|
1077
|
-
- Don't skip CSRF protection
|
|
1078
|
-
- Don't use weak password policies
|
|
1079
|
-
- Don't store sensitive data in JWT
|
|
1080
|
-
- Don't ignore session fixation
|
|
1081
|
-
- Don't disable security headers
|
|
1082
|
-
- Don't trust client-side validation alone
|
|
89
|
+
| Do | Avoid |
|
|
90
|
+
|----|-------|
|
|
91
|
+
| Require email verification for new accounts | Storing plain-text passwords |
|
|
92
|
+
| Enable rate limiting on auth endpoints | Exposing detailed error messages |
|
|
93
|
+
| Use httpOnly, secure cookies | Using predictable session tokens |
|
|
94
|
+
| Set strong password requirements (12+ chars) | Allowing unlimited login attempts |
|
|
95
|
+
| Implement proper session expiration | Storing sensitive data in JWT |
|
|
96
|
+
| Log authentication events for auditing | Skipping CSRF protection |
|
|
1083
97
|
|
|
1084
98
|
## References
|
|
1085
99
|
|
|
1086
100
|
- [Better Auth Documentation](https://www.better-auth.com/docs)
|
|
1087
101
|
- [Better Auth Plugins](https://www.better-auth.com/docs/plugins)
|
|
1088
102
|
- [OWASP Authentication Guidelines](https://owasp.org/www-project-web-security-testing-guide/)
|
|
1089
|
-
- [OAuth 2.0 Best Practices](https://oauth.net/2/)
|
|
1090
|
-
- [NIST Password Guidelines](https://pages.nist.gov/800-63-3/)
|