omgkit 2.1.1 → 2.2.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/package.json +1 -1
- package/plugin/skills/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,53 +1,1090 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: better-auth
|
|
3
|
-
description: Better Auth library
|
|
3
|
+
description: Better Auth library for TypeScript with email/password, OAuth, MFA, sessions, and security best practices
|
|
4
|
+
category: security
|
|
5
|
+
triggers:
|
|
6
|
+
- better-auth
|
|
7
|
+
- authentication
|
|
8
|
+
- auth library
|
|
9
|
+
- typescript auth
|
|
10
|
+
- session management
|
|
4
11
|
---
|
|
5
12
|
|
|
6
|
-
# Better Auth
|
|
13
|
+
# Better Auth
|
|
14
|
+
|
|
15
|
+
Enterprise-grade **TypeScript authentication library** following industry best practices. This skill covers email/password auth, social OAuth, multi-factor authentication, session management, role-based access control, and security patterns used by top engineering teams.
|
|
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
|
|
7
32
|
|
|
8
|
-
## Setup
|
|
9
33
|
```typescript
|
|
10
|
-
|
|
34
|
+
// lib/auth.ts
|
|
35
|
+
import { betterAuth } from "better-auth";
|
|
36
|
+
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";
|
|
11
41
|
|
|
12
42
|
export const auth = betterAuth({
|
|
13
|
-
database: {
|
|
14
|
-
provider:
|
|
15
|
-
|
|
16
|
-
|
|
43
|
+
database: prismaAdapter(prisma, {
|
|
44
|
+
provider: "postgresql",
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
// Email and password authentication
|
|
17
48
|
emailAndPassword: {
|
|
18
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
|
|
19
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
|
|
20
108
|
socialProviders: {
|
|
21
109
|
google: {
|
|
22
|
-
clientId: process.env.GOOGLE_CLIENT_ID
|
|
23
|
-
clientSecret: process.env.GOOGLE_CLIENT_SECRET
|
|
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"],
|
|
24
123
|
},
|
|
25
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
|
+
},
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// Advanced options
|
|
166
|
+
advanced: {
|
|
167
|
+
generateId: () => crypto.randomUUID(),
|
|
168
|
+
crossSubDomainCookies: {
|
|
169
|
+
enabled: process.env.NODE_ENV === "production",
|
|
170
|
+
domain: process.env.COOKIE_DOMAIN,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// Trusted origins for CORS
|
|
175
|
+
trustedOrigins: [
|
|
176
|
+
process.env.FRONTEND_URL!,
|
|
177
|
+
process.env.ADMIN_URL!,
|
|
178
|
+
].filter(Boolean),
|
|
26
179
|
});
|
|
180
|
+
|
|
181
|
+
export type Auth = typeof auth;
|
|
27
182
|
```
|
|
28
183
|
|
|
29
|
-
|
|
30
|
-
```typescript
|
|
31
|
-
import { createAuthClient } from 'better-auth/client';
|
|
184
|
+
### 2. Client Setup
|
|
32
185
|
|
|
33
|
-
|
|
186
|
+
```typescript
|
|
187
|
+
// lib/auth-client.ts
|
|
188
|
+
import { createAuthClient } from "better-auth/client";
|
|
189
|
+
import { twoFactorClient } from "better-auth/client/plugins";
|
|
190
|
+
import { organizationClient } from "better-auth/client/plugins";
|
|
34
191
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
192
|
+
export const authClient = createAuthClient({
|
|
193
|
+
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
|
194
|
+
plugins: [
|
|
195
|
+
twoFactorClient(),
|
|
196
|
+
organizationClient(),
|
|
197
|
+
],
|
|
40
198
|
});
|
|
41
199
|
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
200
|
+
// Type-safe hooks
|
|
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
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### 4. Social OAuth Authentication
|
|
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
|
|
748
|
+
|
|
749
|
+
```typescript
|
|
750
|
+
// middleware.ts
|
|
751
|
+
import { auth } from "@/lib/auth";
|
|
752
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
753
|
+
|
|
754
|
+
const publicRoutes = ["/", "/login", "/signup", "/forgot-password"];
|
|
755
|
+
const authRoutes = ["/login", "/signup"];
|
|
756
|
+
|
|
757
|
+
export async function middleware(request: NextRequest) {
|
|
758
|
+
const { pathname } = request.nextUrl;
|
|
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
|
+
}
|
|
769
|
+
|
|
770
|
+
// Protect private routes
|
|
771
|
+
if (!publicRoutes.includes(pathname) && !session) {
|
|
772
|
+
const loginUrl = new URL("/login", request.url);
|
|
773
|
+
loginUrl.searchParams.set("callbackUrl", pathname);
|
|
774
|
+
return NextResponse.redirect(loginUrl);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return NextResponse.next();
|
|
778
|
+
}
|
|
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
|
+
```
|
|
858
|
+
|
|
859
|
+
### 7. Password Reset Flow
|
|
860
|
+
|
|
861
|
+
```typescript
|
|
862
|
+
// components/auth/ForgotPassword.tsx
|
|
863
|
+
"use client";
|
|
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
|
+
}
|
|
975
|
+
|
|
976
|
+
router.push("/login?message=password-reset");
|
|
977
|
+
} finally {
|
|
978
|
+
setLoading(false);
|
|
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
|
+
);
|
|
1016
|
+
}
|
|
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
|
+
|
|
1044
|
+
if (!session) {
|
|
1045
|
+
redirect("/login");
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return (
|
|
1049
|
+
<div>
|
|
1050
|
+
<h1>Welcome, {session.user.name}</h1>
|
|
1051
|
+
</div>
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
47
1054
|
```
|
|
48
1055
|
|
|
49
1056
|
## Best Practices
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
-
|
|
1057
|
+
|
|
1058
|
+
### Do's
|
|
1059
|
+
|
|
1060
|
+
- Require email verification for new accounts
|
|
1061
|
+
- Implement rate limiting on auth endpoints
|
|
1062
|
+
- Use secure, httpOnly cookies
|
|
1063
|
+
- Enable two-factor authentication option
|
|
1064
|
+
- Hash passwords with strong algorithms
|
|
1065
|
+
- Validate password strength on signup
|
|
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
|
|
1083
|
+
|
|
1084
|
+
## References
|
|
1085
|
+
|
|
1086
|
+
- [Better Auth Documentation](https://www.better-auth.com/docs)
|
|
1087
|
+
- [Better Auth Plugins](https://www.better-auth.com/docs/plugins)
|
|
1088
|
+
- [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/)
|