@kood/claude-code 0.3.6 → 0.3.7
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/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/nextjs/CLAUDE.md +228 -0
- package/templates/nextjs/docs/design.md +558 -0
- package/templates/nextjs/docs/guides/conventions.md +343 -0
- package/templates/nextjs/docs/guides/getting-started.md +367 -0
- package/templates/nextjs/docs/guides/routes.md +342 -0
- package/templates/nextjs/docs/library/better-auth/index.md +541 -0
- package/templates/nextjs/docs/library/nextjs/app-router.md +269 -0
- package/templates/nextjs/docs/library/nextjs/caching.md +351 -0
- package/templates/nextjs/docs/library/nextjs/index.md +291 -0
- package/templates/nextjs/docs/library/nextjs/middleware.md +391 -0
- package/templates/nextjs/docs/library/nextjs/route-handlers.md +382 -0
- package/templates/nextjs/docs/library/nextjs/server-actions.md +366 -0
- package/templates/nextjs/docs/library/prisma/cloudflare-d1.md +76 -0
- package/templates/nextjs/docs/library/prisma/config.md +77 -0
- package/templates/nextjs/docs/library/prisma/crud.md +90 -0
- package/templates/nextjs/docs/library/prisma/index.md +73 -0
- package/templates/nextjs/docs/library/prisma/relations.md +69 -0
- package/templates/nextjs/docs/library/prisma/schema.md +98 -0
- package/templates/nextjs/docs/library/prisma/setup.md +49 -0
- package/templates/nextjs/docs/library/prisma/transactions.md +50 -0
- package/templates/nextjs/docs/library/tanstack-query/index.md +66 -0
- package/templates/nextjs/docs/library/tanstack-query/invalidation.md +54 -0
- package/templates/nextjs/docs/library/tanstack-query/optimistic-updates.md +77 -0
- package/templates/nextjs/docs/library/tanstack-query/use-mutation.md +63 -0
- package/templates/nextjs/docs/library/tanstack-query/use-query.md +70 -0
- package/templates/nextjs/docs/library/zod/complex-types.md +61 -0
- package/templates/nextjs/docs/library/zod/index.md +56 -0
- package/templates/nextjs/docs/library/zod/transforms.md +51 -0
- package/templates/nextjs/docs/library/zod/validation.md +70 -0
- package/templates/tanstack-start/CLAUDE.md +7 -3
- package/templates/tanstack-start/docs/guides/hooks.md +28 -0
- package/templates/tanstack-start/docs/guides/routes.md +29 -10
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
# Better Auth
|
|
2
|
+
|
|
3
|
+
> TypeScript Authentication Library
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<quick_start>
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install better-auth
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Minimal Setup
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// src/lib/auth.ts - 서버
|
|
19
|
+
import { betterAuth } from "better-auth"
|
|
20
|
+
import { prismaAdapter } from "better-auth/adapters/prisma"
|
|
21
|
+
import { prisma } from "@/database/prisma"
|
|
22
|
+
|
|
23
|
+
export const auth = betterAuth({
|
|
24
|
+
database: prismaAdapter(prisma, { provider: "postgresql" }),
|
|
25
|
+
emailAndPassword: { enabled: true },
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// src/lib/auth-client.ts - 클라이언트
|
|
29
|
+
import { createAuthClient } from "better-auth/react"
|
|
30
|
+
|
|
31
|
+
export const authClient = createAuthClient({
|
|
32
|
+
baseURL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Next.js Route Handler
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// app/api/auth/[...all]/route.ts
|
|
40
|
+
import { auth } from "@/lib/auth"
|
|
41
|
+
|
|
42
|
+
export const GET = (request: Request) => auth.handler(request)
|
|
43
|
+
export const POST = (request: Request) => auth.handler(request)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
</quick_start>
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
<setup>
|
|
51
|
+
|
|
52
|
+
## Database Adapters
|
|
53
|
+
|
|
54
|
+
| Adapter | Import | Provider |
|
|
55
|
+
|---------|--------|----------|
|
|
56
|
+
| Prisma | `better-auth/adapters/prisma` | `postgresql`, `mysql`, `sqlite` |
|
|
57
|
+
| Drizzle | `better-auth/adapters/drizzle` | `pg`, `mysql2`, `better-sqlite3` |
|
|
58
|
+
| Kysely | `better-auth/adapters/kysely` | dialect 기반 |
|
|
59
|
+
|
|
60
|
+
## Auth Config
|
|
61
|
+
|
|
62
|
+
| 옵션 | 타입 | 기본값 | 설명 |
|
|
63
|
+
|------|------|--------|------|
|
|
64
|
+
| `database` | `Adapter` | 필수 | DB 어댑터 |
|
|
65
|
+
| `baseURL` | `string` | `http://localhost:3000` | 앱 URL |
|
|
66
|
+
| `basePath` | `string` | `/api/auth` | Auth 경로 |
|
|
67
|
+
| `secret` | `string` | 환경변수 | JWT 시크릿 |
|
|
68
|
+
| `trustedOrigins` | `string[]` | `[]` | CORS 허용 오리진 |
|
|
69
|
+
|
|
70
|
+
## Social Providers
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
export const auth = betterAuth({
|
|
74
|
+
database: prismaAdapter(prisma),
|
|
75
|
+
socialProviders: {
|
|
76
|
+
google: {
|
|
77
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
78
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
79
|
+
},
|
|
80
|
+
github: {
|
|
81
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
82
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Email & Password
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
export const auth = betterAuth({
|
|
92
|
+
emailAndPassword: {
|
|
93
|
+
enabled: true,
|
|
94
|
+
requireEmailVerification: false,
|
|
95
|
+
sendResetPassword: async ({ user, url }) => {
|
|
96
|
+
await sendEmail({
|
|
97
|
+
to: user.email,
|
|
98
|
+
subject: "Reset Password",
|
|
99
|
+
html: `<a href="${url}">Reset</a>`,
|
|
100
|
+
})
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
</setup>
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
<session>
|
|
111
|
+
|
|
112
|
+
## Session Config
|
|
113
|
+
|
|
114
|
+
| 옵션 | 타입 | 기본값 | 설명 |
|
|
115
|
+
|------|------|--------|------|
|
|
116
|
+
| `expiresIn` | `number` | `604800` (7일) | 세션 만료 시간 (초) |
|
|
117
|
+
| `updateAge` | `number` | `86400` (1일) | 세션 갱신 주기 (초) |
|
|
118
|
+
| `cookieCache.enabled` | `boolean` | `true` | 쿠키 캐시 활성화 |
|
|
119
|
+
| `cookieCache.maxAge` | `number` | `300` (5분) | 캐시 유효 시간 (초) |
|
|
120
|
+
| `cookieCache.strategy` | `'compact' \| 'jwt' \| 'jwe'` | `'compact'` | 캐시 전략 |
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
export const auth = betterAuth({
|
|
124
|
+
session: {
|
|
125
|
+
expiresIn: 604800, // 7일
|
|
126
|
+
updateAge: 86400, // 1일마다 갱신
|
|
127
|
+
cookieCache: {
|
|
128
|
+
enabled: true,
|
|
129
|
+
maxAge: 300, // 5분
|
|
130
|
+
strategy: "compact",
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Session Methods
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// ✅ 클라이언트
|
|
140
|
+
const session = await authClient.getSession()
|
|
141
|
+
const session = await authClient.getSession({ query: { disableCookieCache: true } })
|
|
142
|
+
|
|
143
|
+
// ✅ 서버 (Server Component)
|
|
144
|
+
import { auth } from "@/lib/auth"
|
|
145
|
+
import { headers } from "next/headers"
|
|
146
|
+
|
|
147
|
+
export default async function Page() {
|
|
148
|
+
const session = await auth.api.getSession({ headers: headers() })
|
|
149
|
+
if (!session?.user) redirect("/login")
|
|
150
|
+
|
|
151
|
+
return <div>Welcome {session.user.name}</div>
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ✅ 서버 (Server Action)
|
|
155
|
+
"use server"
|
|
156
|
+
|
|
157
|
+
import { auth } from "@/lib/auth"
|
|
158
|
+
import { headers } from "next/headers"
|
|
159
|
+
|
|
160
|
+
export async function getSession() {
|
|
161
|
+
const session = await auth.api.getSession({ headers: headers() })
|
|
162
|
+
return session
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ✅ 세션 업데이트
|
|
166
|
+
await authClient.updateUser({ name: "New Name" })
|
|
167
|
+
|
|
168
|
+
// ✅ 로그아웃
|
|
169
|
+
await authClient.signOut()
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Custom Session Fields
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { customSession } from "better-auth/plugins"
|
|
176
|
+
|
|
177
|
+
export const auth = betterAuth({
|
|
178
|
+
plugins: [
|
|
179
|
+
customSession({
|
|
180
|
+
schema: {
|
|
181
|
+
session: {
|
|
182
|
+
fields: {
|
|
183
|
+
ipAddress: { type: "string" },
|
|
184
|
+
userAgent: { type: "string" },
|
|
185
|
+
metadata: { type: "json" },
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
async onSessionCreate(session, context) {
|
|
190
|
+
return {
|
|
191
|
+
...session,
|
|
192
|
+
ipAddress: context.request.headers.get("x-forwarded-for") || "unknown",
|
|
193
|
+
userAgent: context.request.headers.get("user-agent") || "unknown",
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
],
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
</session>
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
<auth_methods>
|
|
206
|
+
|
|
207
|
+
## Email/Password
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
// ✅ 회원가입
|
|
211
|
+
await authClient.signUp.email({
|
|
212
|
+
email: "user@example.com",
|
|
213
|
+
password: "password123",
|
|
214
|
+
name: "User Name",
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// ✅ 로그인
|
|
218
|
+
await authClient.signIn.email({
|
|
219
|
+
email: "user@example.com",
|
|
220
|
+
password: "password123",
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// ✅ 비밀번호 재설정 요청
|
|
224
|
+
await authClient.forgetPassword({ email: "user@example.com" })
|
|
225
|
+
|
|
226
|
+
// ✅ 비밀번호 재설정
|
|
227
|
+
await authClient.resetPassword({ token, password: "newpassword" })
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Social Login
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// ✅ 소셜 로그인
|
|
234
|
+
await authClient.signIn.social({ provider: "google", callbackURL: "/dashboard" })
|
|
235
|
+
await authClient.signIn.social({ provider: "github", callbackURL: "/dashboard" })
|
|
236
|
+
|
|
237
|
+
// ✅ SSO
|
|
238
|
+
await authClient.signIn.sso({ providerId: "provider-id", callbackURL: "/dashboard" })
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Two-Factor Authentication
|
|
242
|
+
|
|
243
|
+
### Server Setup
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { twoFactor } from "better-auth/plugins"
|
|
247
|
+
|
|
248
|
+
export const auth = betterAuth({
|
|
249
|
+
plugins: [
|
|
250
|
+
twoFactor({
|
|
251
|
+
issuer: "My App",
|
|
252
|
+
otpOptions: {
|
|
253
|
+
async sendOTP({ user, otp }) {
|
|
254
|
+
await sendEmail({
|
|
255
|
+
to: user.email,
|
|
256
|
+
subject: "Your OTP Code",
|
|
257
|
+
html: `Your code: ${otp}`,
|
|
258
|
+
})
|
|
259
|
+
},
|
|
260
|
+
period: 300, // 5분
|
|
261
|
+
length: 6, // 6자리
|
|
262
|
+
},
|
|
263
|
+
backupCodeLength: 10,
|
|
264
|
+
backupCodeCount: 10,
|
|
265
|
+
}),
|
|
266
|
+
],
|
|
267
|
+
})
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Client Setup
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
import { twoFactorClient } from "better-auth/client/plugins"
|
|
274
|
+
|
|
275
|
+
export const authClient = createAuthClient({
|
|
276
|
+
plugins: [
|
|
277
|
+
twoFactorClient({
|
|
278
|
+
twoFactorPage: "/two-factor",
|
|
279
|
+
}),
|
|
280
|
+
],
|
|
281
|
+
})
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### 2FA Usage
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// ✅ TOTP 활성화
|
|
288
|
+
const { data } = await authClient.twoFactor.enable({ password: "current-password" })
|
|
289
|
+
// data: { totpURI: 'otpauth://...', backupCodes: ['ABCD-1234', ...] }
|
|
290
|
+
|
|
291
|
+
// ✅ TOTP 검증
|
|
292
|
+
await authClient.twoFactor.verifyTotp({ code: "123456" })
|
|
293
|
+
|
|
294
|
+
// ✅ OTP 전송
|
|
295
|
+
await authClient.twoFactor.sendOtp()
|
|
296
|
+
|
|
297
|
+
// ✅ OTP 검증
|
|
298
|
+
await authClient.twoFactor.verifyOtp({ code: "123456" })
|
|
299
|
+
|
|
300
|
+
// ✅ 백업 코드 사용
|
|
301
|
+
await authClient.twoFactor.useBackupCode({ code: "ABCD-1234" })
|
|
302
|
+
|
|
303
|
+
// ✅ 백업 코드 재생성
|
|
304
|
+
const { data } = await authClient.twoFactor.regenerateBackupCodes({ password: "current-password" })
|
|
305
|
+
|
|
306
|
+
// ✅ 2FA 비활성화
|
|
307
|
+
await authClient.twoFactor.disable({ password: "current-password" })
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
</auth_methods>
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
<plugins>
|
|
315
|
+
|
|
316
|
+
## Plugin System
|
|
317
|
+
|
|
318
|
+
| Plugin | Import | 기능 |
|
|
319
|
+
|--------|--------|------|
|
|
320
|
+
| `multiSession` | `better-auth/plugins` | 다중 세션 관리 |
|
|
321
|
+
| `customSession` | `better-auth/plugins` | 세션 필드 확장 |
|
|
322
|
+
| `twoFactor` | `better-auth/plugins` | 2단계 인증 |
|
|
323
|
+
| `captcha` | `better-auth/plugins` | CAPTCHA 검증 |
|
|
324
|
+
|
|
325
|
+
## Multi-Session
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// ✅ 서버
|
|
329
|
+
import { multiSession } from "better-auth/plugins"
|
|
330
|
+
|
|
331
|
+
export const auth = betterAuth({
|
|
332
|
+
plugins: [
|
|
333
|
+
multiSession({
|
|
334
|
+
maximumSessions: 5, // 최대 세션 수
|
|
335
|
+
}),
|
|
336
|
+
],
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
// ✅ 클라이언트
|
|
340
|
+
import { multiSessionClient } from "better-auth/client/plugins"
|
|
341
|
+
|
|
342
|
+
export const authClient = createAuthClient({
|
|
343
|
+
plugins: [multiSessionClient()],
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
// 사용
|
|
347
|
+
const sessions = await authClient.multiSession.listSessions()
|
|
348
|
+
await authClient.multiSession.revokeSession({ sessionId: "session-id" })
|
|
349
|
+
await authClient.multiSession.revokeOtherSessions()
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## CAPTCHA
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// ✅ 서버
|
|
356
|
+
import { captcha } from "better-auth/plugins"
|
|
357
|
+
|
|
358
|
+
export const auth = betterAuth({
|
|
359
|
+
plugins: [
|
|
360
|
+
captcha({
|
|
361
|
+
provider: "recaptcha",
|
|
362
|
+
siteKey: process.env.RECAPTCHA_SITE_KEY!,
|
|
363
|
+
secretKey: process.env.RECAPTCHA_SECRET_KEY!,
|
|
364
|
+
protectEndpoints: ["/sign-up/email", "/sign-in/email"],
|
|
365
|
+
}),
|
|
366
|
+
],
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
// ✅ 클라이언트
|
|
370
|
+
import { captchaClient } from "better-auth/client/plugins"
|
|
371
|
+
|
|
372
|
+
export const authClient = createAuthClient({
|
|
373
|
+
plugins: [
|
|
374
|
+
captchaClient({
|
|
375
|
+
siteKey: process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY!,
|
|
376
|
+
}),
|
|
377
|
+
],
|
|
378
|
+
})
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
</plugins>
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
<advanced>
|
|
386
|
+
|
|
387
|
+
## SIWE (Ethereum)
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
// 서버: siwe({ domain, uri })
|
|
391
|
+
// 클라이언트:
|
|
392
|
+
const { data } = await authClient.siwe.getNonce()
|
|
393
|
+
const message = await authClient.siwe.prepareMessage({ address: "0x...", nonce: data.nonce })
|
|
394
|
+
const signature = await signer.signMessage(message)
|
|
395
|
+
await authClient.siwe.signIn({ message, signature })
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Stateless Mode
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// ✅ DB 없이 소셜 로그인만
|
|
402
|
+
export const auth = betterAuth({
|
|
403
|
+
socialProviders: {
|
|
404
|
+
google: {
|
|
405
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
406
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
})
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Redis Secondary Storage
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
import { Redis } from "ioredis"
|
|
416
|
+
|
|
417
|
+
const redis = new Redis()
|
|
418
|
+
|
|
419
|
+
export const auth = betterAuth({
|
|
420
|
+
database: prismaAdapter(prisma),
|
|
421
|
+
secondaryStorage: {
|
|
422
|
+
get: async (key: string) => {
|
|
423
|
+
const value = await redis.get(key)
|
|
424
|
+
return value ? JSON.parse(value) : null
|
|
425
|
+
},
|
|
426
|
+
set: async (key: string, value: any, ttl: number) => {
|
|
427
|
+
await redis.set(key, JSON.stringify(value), "EX", ttl)
|
|
428
|
+
},
|
|
429
|
+
delete: async (key: string) => {
|
|
430
|
+
await redis.del(key)
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
})
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
</advanced>
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
<patterns>
|
|
441
|
+
|
|
442
|
+
## Next.js Integration
|
|
443
|
+
|
|
444
|
+
| 패턴 | 용도 |
|
|
445
|
+
|------|------|
|
|
446
|
+
| Route Handler | `app/api/auth/[...all]/route.ts` |
|
|
447
|
+
| Server Component | `auth.api.getSession({ headers })` |
|
|
448
|
+
| Server Action | `auth.api.getSession({ headers })` |
|
|
449
|
+
| Middleware | `auth.api.getSession({ headers })` + redirect |
|
|
450
|
+
|
|
451
|
+
### Server Component Pattern
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
// app/dashboard/page.tsx
|
|
455
|
+
import { auth } from "@/lib/auth"
|
|
456
|
+
import { headers } from "next/headers"
|
|
457
|
+
import { redirect } from "next/navigation"
|
|
458
|
+
|
|
459
|
+
export default async function DashboardPage() {
|
|
460
|
+
const session = await auth.api.getSession({ headers: headers() })
|
|
461
|
+
|
|
462
|
+
if (!session?.user) {
|
|
463
|
+
redirect("/login")
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return <div>Welcome {session.user.name}</div>
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Server Action Pattern
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
// actions/auth.ts
|
|
474
|
+
"use server"
|
|
475
|
+
|
|
476
|
+
import { auth } from "@/lib/auth"
|
|
477
|
+
import { headers } from "next/headers"
|
|
478
|
+
import { redirect } from "next/navigation"
|
|
479
|
+
|
|
480
|
+
export async function requireAuth() {
|
|
481
|
+
const session = await auth.api.getSession({ headers: headers() })
|
|
482
|
+
|
|
483
|
+
if (!session?.user) {
|
|
484
|
+
redirect("/login")
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return session.user
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export async function getProtectedData() {
|
|
491
|
+
const user = await requireAuth()
|
|
492
|
+
return prisma.data.findMany({ where: { userId: user.id } })
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Middleware Pattern
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
// middleware.ts
|
|
500
|
+
import { auth } from "@/lib/auth"
|
|
501
|
+
import { NextResponse } from "next/server"
|
|
502
|
+
import type { NextRequest } from "next/server"
|
|
503
|
+
|
|
504
|
+
export async function middleware(request: NextRequest) {
|
|
505
|
+
const session = await auth.api.getSession({ headers: request.headers })
|
|
506
|
+
|
|
507
|
+
if (!session?.user) {
|
|
508
|
+
return NextResponse.redirect(new URL("/login", request.url))
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return NextResponse.next()
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export const config = {
|
|
515
|
+
matcher: ["/dashboard/:path*", "/profile/:path*"],
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
## Common Patterns
|
|
520
|
+
|
|
521
|
+
| 작업 | 패턴 |
|
|
522
|
+
|------|------|
|
|
523
|
+
| 로그인 | `authClient.signIn.email({ email, password })` |
|
|
524
|
+
| 회원가입 | `authClient.signUp.email({ email, password, name })` |
|
|
525
|
+
| 로그아웃 | `authClient.signOut()` |
|
|
526
|
+
| 세션 조회 | `authClient.getSession()` (클라이언트) |
|
|
527
|
+
| 세션 조회 | `auth.api.getSession({ headers })` (서버) |
|
|
528
|
+
| 사용자 업데이트 | `authClient.updateUser({ name })` |
|
|
529
|
+
| 비밀번호 재설정 | `authClient.forgetPassword({ email })` → `authClient.resetPassword({ token, password })` |
|
|
530
|
+
|
|
531
|
+
## Do's & Don'ts
|
|
532
|
+
|
|
533
|
+
| ✅ Do | ❌ Don't |
|
|
534
|
+
|-------|----------|
|
|
535
|
+
| `auth.api.getSession()` 서버에서 사용 | 클라이언트에서 직접 세션 조작 |
|
|
536
|
+
| `authClient` 클라이언트에서 사용 | 하드코딩된 시크릿 |
|
|
537
|
+
| 환경변수로 시크릿 관리 | 세션 토큰 로컬스토리지 저장 |
|
|
538
|
+
| HTTPS 프로덕션 필수 | HTTP 프로덕션 배포 |
|
|
539
|
+
| `baseURL` 환경별 설정 | 절대 경로 하드코딩 |
|
|
540
|
+
|
|
541
|
+
</patterns>
|