@kood/claude-code 0.1.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/dist/index.d.ts +2 -0
- package/dist/index.js +297 -0
- package/package.json +47 -0
- package/templates/hono/CLAUDE.md +376 -0
- package/templates/hono/docs/deployment/cloudflare.md +328 -0
- package/templates/hono/docs/deployment/index.md +291 -0
- package/templates/hono/docs/git/index.md +180 -0
- package/templates/hono/docs/library/hono/error-handling.md +400 -0
- package/templates/hono/docs/library/hono/index.md +241 -0
- package/templates/hono/docs/library/hono/middleware.md +334 -0
- package/templates/hono/docs/library/hono/rpc.md +454 -0
- package/templates/hono/docs/library/hono/validation.md +328 -0
- package/templates/hono/docs/library/prisma/index.md +427 -0
- package/templates/hono/docs/library/zod/index.md +413 -0
- package/templates/hono/docs/mcp/context7.md +106 -0
- package/templates/hono/docs/mcp/index.md +94 -0
- package/templates/hono/docs/mcp/sequential-thinking.md +101 -0
- package/templates/hono/docs/mcp/sgrep.md +105 -0
- package/templates/hono/docs/skills/gemini-review/SKILL.md +220 -0
- package/templates/hono/docs/skills/gemini-review/references/checklists.md +136 -0
- package/templates/hono/docs/skills/gemini-review/references/prompt-templates.md +303 -0
- package/templates/tanstack-start/CLAUDE.md +279 -0
- package/templates/tanstack-start/docs/architecture/architecture.md +547 -0
- package/templates/tanstack-start/docs/deployment/cloudflare.md +346 -0
- package/templates/tanstack-start/docs/deployment/index.md +102 -0
- package/templates/tanstack-start/docs/deployment/nitro.md +211 -0
- package/templates/tanstack-start/docs/deployment/railway.md +364 -0
- package/templates/tanstack-start/docs/deployment/vercel.md +287 -0
- package/templates/tanstack-start/docs/design/accessibility.md +433 -0
- package/templates/tanstack-start/docs/design/color.md +235 -0
- package/templates/tanstack-start/docs/design/components.md +409 -0
- package/templates/tanstack-start/docs/design/index.md +107 -0
- package/templates/tanstack-start/docs/design/safe-area.md +317 -0
- package/templates/tanstack-start/docs/design/spacing.md +341 -0
- package/templates/tanstack-start/docs/design/tailwind-setup.md +470 -0
- package/templates/tanstack-start/docs/design/typography.md +324 -0
- package/templates/tanstack-start/docs/git/index.md +203 -0
- package/templates/tanstack-start/docs/guides/best-practices.md +753 -0
- package/templates/tanstack-start/docs/guides/getting-started.md +304 -0
- package/templates/tanstack-start/docs/guides/husky-lint-staged.md +303 -0
- package/templates/tanstack-start/docs/guides/prettier.md +189 -0
- package/templates/tanstack-start/docs/guides/project-templates.md +710 -0
- package/templates/tanstack-start/docs/library/better-auth/2fa.md +136 -0
- package/templates/tanstack-start/docs/library/better-auth/advanced.md +138 -0
- package/templates/tanstack-start/docs/library/better-auth/index.md +83 -0
- package/templates/tanstack-start/docs/library/better-auth/plugins.md +111 -0
- package/templates/tanstack-start/docs/library/better-auth/session.md +127 -0
- package/templates/tanstack-start/docs/library/better-auth/setup.md +123 -0
- package/templates/tanstack-start/docs/library/prisma/crud.md +218 -0
- package/templates/tanstack-start/docs/library/prisma/index.md +165 -0
- package/templates/tanstack-start/docs/library/prisma/relations.md +191 -0
- package/templates/tanstack-start/docs/library/prisma/schema.md +177 -0
- package/templates/tanstack-start/docs/library/prisma/setup.md +156 -0
- package/templates/tanstack-start/docs/library/prisma/transactions.md +140 -0
- package/templates/tanstack-start/docs/library/tanstack-query/index.md +146 -0
- package/templates/tanstack-start/docs/library/tanstack-query/invalidation.md +146 -0
- package/templates/tanstack-start/docs/library/tanstack-query/optimistic-updates.md +196 -0
- package/templates/tanstack-start/docs/library/tanstack-query/setup.md +110 -0
- package/templates/tanstack-start/docs/library/tanstack-query/use-mutation.md +170 -0
- package/templates/tanstack-start/docs/library/tanstack-query/use-query.md +173 -0
- package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +171 -0
- package/templates/tanstack-start/docs/library/tanstack-start/index.md +114 -0
- package/templates/tanstack-start/docs/library/tanstack-start/middleware.md +142 -0
- package/templates/tanstack-start/docs/library/tanstack-start/routing.md +163 -0
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +128 -0
- package/templates/tanstack-start/docs/library/tanstack-start/setup.md +85 -0
- package/templates/tanstack-start/docs/library/zod/basic-types.md +186 -0
- package/templates/tanstack-start/docs/library/zod/complex-types.md +204 -0
- package/templates/tanstack-start/docs/library/zod/index.md +186 -0
- package/templates/tanstack-start/docs/library/zod/transforms.md +174 -0
- package/templates/tanstack-start/docs/library/zod/validation.md +208 -0
- package/templates/tanstack-start/docs/mcp/context7.md +204 -0
- package/templates/tanstack-start/docs/mcp/index.md +116 -0
- package/templates/tanstack-start/docs/mcp/sequential-thinking.md +180 -0
- package/templates/tanstack-start/docs/mcp/sgrep.md +174 -0
- package/templates/tanstack-start/docs/skills/gemini-review/SKILL.md +220 -0
- package/templates/tanstack-start/docs/skills/gemini-review/references/checklists.md +150 -0
- package/templates/tanstack-start/docs/skills/gemini-review/references/prompt-templates.md +293 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Better Auth - 2단계 인증 (2FA)
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [Better Auth](./index.md)
|
|
4
|
+
|
|
5
|
+
## 서버 설정
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { betterAuth } from 'better-auth'
|
|
9
|
+
import { twoFactor } from 'better-auth/plugins'
|
|
10
|
+
|
|
11
|
+
export const auth = betterAuth({
|
|
12
|
+
plugins: [
|
|
13
|
+
twoFactor({
|
|
14
|
+
issuer: 'My Application',
|
|
15
|
+
otpOptions: {
|
|
16
|
+
async sendOTP({ user, otp }) {
|
|
17
|
+
await sendEmail({
|
|
18
|
+
to: user.email,
|
|
19
|
+
subject: 'Your verification code',
|
|
20
|
+
html: `Your code is: ${otp}`,
|
|
21
|
+
})
|
|
22
|
+
},
|
|
23
|
+
period: 300, // 5분
|
|
24
|
+
length: 6,
|
|
25
|
+
},
|
|
26
|
+
backupCodeLength: 10,
|
|
27
|
+
backupCodeCount: 10,
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 클라이언트 설정
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { twoFactorClient } from 'better-auth/client/plugins'
|
|
37
|
+
|
|
38
|
+
const authClient = createAuthClient({
|
|
39
|
+
plugins: [
|
|
40
|
+
twoFactorClient({
|
|
41
|
+
twoFactorPage: '/two-factor',
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 2FA 활성화
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// 2FA 활성화
|
|
51
|
+
const { data } = await authClient.twoFactor.enable({
|
|
52
|
+
password: 'userPassword123',
|
|
53
|
+
})
|
|
54
|
+
console.log('TOTP URI:', data.totpURI)
|
|
55
|
+
console.log('Backup codes:', data.backupCodes)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## TOTP 코드 검증
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// TOTP 코드 검증
|
|
62
|
+
await authClient.twoFactor.verifyTotp({
|
|
63
|
+
code: '123456',
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## OTP 전송 및 검증
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// OTP 전송
|
|
71
|
+
await authClient.twoFactor.sendOtp()
|
|
72
|
+
|
|
73
|
+
// OTP 검증
|
|
74
|
+
await authClient.twoFactor.verifyOtp({
|
|
75
|
+
code: '123456',
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 백업 코드
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// 백업 코드 사용
|
|
83
|
+
await authClient.twoFactor.useBackupCode({
|
|
84
|
+
code: 'ABCD-1234-EFGH',
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// 백업 코드 재생성
|
|
88
|
+
const { data: newCodes } = await authClient.twoFactor.regenerateBackupCodes({
|
|
89
|
+
password: 'userPassword123',
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 2FA 비활성화
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
await authClient.twoFactor.disable({
|
|
97
|
+
password: 'userPassword123',
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## 2FA 페이지 구현
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
// pages/two-factor.tsx
|
|
105
|
+
import { useState } from 'react'
|
|
106
|
+
import { authClient } from '~/lib/auth-client'
|
|
107
|
+
|
|
108
|
+
export default function TwoFactorPage() {
|
|
109
|
+
const [code, setCode] = useState('')
|
|
110
|
+
const [error, setError] = useState('')
|
|
111
|
+
|
|
112
|
+
const handleVerify = async () => {
|
|
113
|
+
try {
|
|
114
|
+
await authClient.twoFactor.verifyTotp({ code })
|
|
115
|
+
// 성공 시 대시보드로 이동
|
|
116
|
+
window.location.href = '/dashboard'
|
|
117
|
+
} catch (e) {
|
|
118
|
+
setError('Invalid code')
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div>
|
|
124
|
+
<h1>2단계 인증</h1>
|
|
125
|
+
<input
|
|
126
|
+
type="text"
|
|
127
|
+
value={code}
|
|
128
|
+
onChange={(e) => setCode(e.target.value)}
|
|
129
|
+
placeholder="6자리 코드 입력"
|
|
130
|
+
/>
|
|
131
|
+
<button onClick={handleVerify}>확인</button>
|
|
132
|
+
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
```
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Better Auth - 고급 기능
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [Better Auth](./index.md)
|
|
4
|
+
|
|
5
|
+
## CAPTCHA 보호
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { betterAuth } from 'better-auth'
|
|
9
|
+
import { captcha } from 'better-auth/plugins'
|
|
10
|
+
|
|
11
|
+
export const auth = betterAuth({
|
|
12
|
+
plugins: [
|
|
13
|
+
captcha({
|
|
14
|
+
provider: 'recaptcha', // 'recaptcha' | 'hcaptcha'
|
|
15
|
+
siteKey: process.env.RECAPTCHA_SITE_KEY!,
|
|
16
|
+
secretKey: process.env.RECAPTCHA_SECRET_KEY!,
|
|
17
|
+
protectEndpoints: ['/sign-up/email', '/sign-in/email'],
|
|
18
|
+
}),
|
|
19
|
+
],
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// 클라이언트
|
|
23
|
+
import { captchaClient } from 'better-auth/client/plugins'
|
|
24
|
+
|
|
25
|
+
const authClient = createAuthClient({
|
|
26
|
+
plugins: [
|
|
27
|
+
captchaClient({
|
|
28
|
+
siteKey: process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY!,
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// CAPTCHA는 보호된 요청에 자동 포함됨
|
|
34
|
+
await authClient.signUp.email({
|
|
35
|
+
email: 'user@example.com',
|
|
36
|
+
password: 'SecurePassword123!',
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## SSO (Single Sign-On)
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// Provider ID로 SSO 로그인
|
|
44
|
+
const res = await authClient.signIn.sso({
|
|
45
|
+
providerId: 'example-provider-id',
|
|
46
|
+
callbackURL: '/dashboard',
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## SIWE (Sign-In with Ethereum)
|
|
51
|
+
|
|
52
|
+
### 서버 설정
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { betterAuth } from 'better-auth'
|
|
56
|
+
import { siwe } from 'better-auth/plugins'
|
|
57
|
+
|
|
58
|
+
export const auth = betterAuth({
|
|
59
|
+
plugins: [
|
|
60
|
+
siwe({
|
|
61
|
+
domain: 'example.com',
|
|
62
|
+
uri: 'https://example.com',
|
|
63
|
+
statement: 'Sign in with your Ethereum account',
|
|
64
|
+
}),
|
|
65
|
+
],
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 클라이언트 사용
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { siweClient } from 'better-auth/client/plugins'
|
|
73
|
+
import { ethers } from 'ethers'
|
|
74
|
+
|
|
75
|
+
const authClient = createAuthClient({
|
|
76
|
+
plugins: [siweClient()],
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Nonce 가져오기
|
|
80
|
+
const { data: nonce } = await authClient.siwe.getNonce()
|
|
81
|
+
|
|
82
|
+
// 메시지 생성 및 서명
|
|
83
|
+
const provider = new ethers.BrowserProvider(window.ethereum)
|
|
84
|
+
const signer = await provider.getSigner()
|
|
85
|
+
const address = await signer.getAddress()
|
|
86
|
+
|
|
87
|
+
const message = await authClient.siwe.prepareMessage({
|
|
88
|
+
address,
|
|
89
|
+
nonce: nonce.nonce,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const signature = await signer.signMessage(message)
|
|
93
|
+
|
|
94
|
+
// 검증 및 로그인
|
|
95
|
+
await authClient.siwe.signIn({
|
|
96
|
+
message,
|
|
97
|
+
signature,
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Stateless 모드
|
|
102
|
+
|
|
103
|
+
데이터베이스 없이 사용:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { betterAuth } from 'better-auth'
|
|
107
|
+
|
|
108
|
+
export const auth = betterAuth({
|
|
109
|
+
// 데이터베이스 설정 없음 - 자동으로 stateless 모드
|
|
110
|
+
socialProviders: {
|
|
111
|
+
google: {
|
|
112
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
113
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Redis와 함께 하이브리드 모드
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { betterAuth } from 'better-auth'
|
|
123
|
+
import { redis } from './redis'
|
|
124
|
+
|
|
125
|
+
export const auth = betterAuth({
|
|
126
|
+
secondaryStorage: {
|
|
127
|
+
get: async (key) => await redis.get(key),
|
|
128
|
+
set: async (key, value, ttl) => await redis.set(key, value, 'EX', ttl),
|
|
129
|
+
delete: async (key) => await redis.del(key),
|
|
130
|
+
},
|
|
131
|
+
session: {
|
|
132
|
+
cookieCache: {
|
|
133
|
+
maxAge: 5 * 60, // 5분
|
|
134
|
+
refreshCache: false,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Better Auth
|
|
2
|
+
|
|
3
|
+
> **Version**: Latest
|
|
4
|
+
> **Framework**: TypeScript Authentication Library
|
|
5
|
+
> **Source**: [Better Auth Documentation](https://www.better-auth.com)
|
|
6
|
+
|
|
7
|
+
Better Auth는 TypeScript를 위한 프레임워크 독립적인 인증 및 권한 부여 라이브러리입니다.
|
|
8
|
+
|
|
9
|
+
## 문서 구조
|
|
10
|
+
|
|
11
|
+
- [설치 및 설정](./setup.md) - 서버/클라이언트 설정, 소셜 로그인
|
|
12
|
+
- [세션 관리](./session.md) - 세션 설정, 쿠키 캐시
|
|
13
|
+
- [플러그인](./plugins.md) - Multi-Session, Custom Session
|
|
14
|
+
- [2단계 인증](./2fa.md) - TOTP, OTP, 백업 코드
|
|
15
|
+
- [고급 기능](./advanced.md) - CAPTCHA, SSO, SIWE, Stateless 모드
|
|
16
|
+
|
|
17
|
+
## 빠른 시작
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
yarn add better-auth
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 기본 서버 설정
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// lib/auth.ts
|
|
27
|
+
import { betterAuth } from 'better-auth'
|
|
28
|
+
import { prismaAdapter } from 'better-auth/adapters/prisma'
|
|
29
|
+
import { prisma } from './prisma'
|
|
30
|
+
|
|
31
|
+
export const auth = betterAuth({
|
|
32
|
+
database: prismaAdapter(prisma),
|
|
33
|
+
emailAndPassword: {
|
|
34
|
+
enabled: true,
|
|
35
|
+
requireEmailVerification: false,
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 기본 클라이언트 설정
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// lib/auth-client.ts
|
|
44
|
+
import { createAuthClient } from 'better-auth/react'
|
|
45
|
+
|
|
46
|
+
export const authClient = createAuthClient({
|
|
47
|
+
// 옵션
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 핵심 개념
|
|
52
|
+
|
|
53
|
+
### 이메일/비밀번호 인증
|
|
54
|
+
```typescript
|
|
55
|
+
// 로그인
|
|
56
|
+
await authClient.signIn.email({
|
|
57
|
+
email: 'user@example.com',
|
|
58
|
+
password: 'password123',
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// 회원가입
|
|
62
|
+
await authClient.signUp.email({
|
|
63
|
+
email: 'user@example.com',
|
|
64
|
+
password: 'password123',
|
|
65
|
+
name: 'John Doe',
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 세션 조회
|
|
70
|
+
```typescript
|
|
71
|
+
// 클라이언트
|
|
72
|
+
const session = await authClient.getSession()
|
|
73
|
+
|
|
74
|
+
// 서버
|
|
75
|
+
const session = await auth.api.getSession({
|
|
76
|
+
headers: request.headers,
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 참고 자료
|
|
81
|
+
|
|
82
|
+
- [Better Auth 공식 문서](https://www.better-auth.com)
|
|
83
|
+
- [Better Auth GitHub](https://github.com/better-auth/better-auth)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Better Auth - 플러그인
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [Better Auth](./index.md)
|
|
4
|
+
|
|
5
|
+
## Multi-Session Plugin
|
|
6
|
+
|
|
7
|
+
여러 계정으로 동시 로그인을 지원합니다.
|
|
8
|
+
|
|
9
|
+
### 서버 설정
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { betterAuth } from 'better-auth'
|
|
13
|
+
import { multiSession } from 'better-auth/plugins'
|
|
14
|
+
|
|
15
|
+
export const auth = betterAuth({
|
|
16
|
+
plugins: [
|
|
17
|
+
multiSession({
|
|
18
|
+
maximumSessions: 5, // 최대 동시 세션 수
|
|
19
|
+
}),
|
|
20
|
+
],
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 클라이언트 사용
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { createAuthClient } from 'better-auth/client'
|
|
28
|
+
import { multiSessionClient } from 'better-auth/client/plugins'
|
|
29
|
+
|
|
30
|
+
export const authClient = createAuthClient({
|
|
31
|
+
plugins: [multiSessionClient()],
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// 활성 세션 목록 조회
|
|
35
|
+
const { data: sessions } = await authClient.multiSession.listSessions()
|
|
36
|
+
|
|
37
|
+
sessions?.forEach((session) => {
|
|
38
|
+
console.log(`Device: ${session.userAgent}, Last active: ${session.updatedAt}`)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// 특정 세션 취소
|
|
42
|
+
await authClient.multiSession.revokeSession({
|
|
43
|
+
sessionId: 'session_456',
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// 다른 모든 세션 취소
|
|
47
|
+
await authClient.multiSession.revokeOtherSessions()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Polar Checkout Plugin
|
|
51
|
+
|
|
52
|
+
결제 통합을 위한 플러그인입니다.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { polar, checkout } from '@polar-sh/better-auth'
|
|
56
|
+
|
|
57
|
+
const auth = betterAuth({
|
|
58
|
+
plugins: [
|
|
59
|
+
polar({
|
|
60
|
+
use: [
|
|
61
|
+
checkout({
|
|
62
|
+
products: [{ productId: '123-456-789', slug: 'pro' }],
|
|
63
|
+
successUrl: '/success?checkout_id={CHECKOUT_ID}',
|
|
64
|
+
authenticatedUsersOnly: true,
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
}),
|
|
68
|
+
],
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// 클라이언트
|
|
72
|
+
import { polarClient } from '@polar-sh/better-auth'
|
|
73
|
+
|
|
74
|
+
export const authClient = createAuthClient({
|
|
75
|
+
plugins: [polarClient()],
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 플러그인 조합
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { betterAuth } from 'better-auth'
|
|
83
|
+
import {
|
|
84
|
+
multiSession,
|
|
85
|
+
customSession,
|
|
86
|
+
twoFactor,
|
|
87
|
+
captcha
|
|
88
|
+
} from 'better-auth/plugins'
|
|
89
|
+
|
|
90
|
+
export const auth = betterAuth({
|
|
91
|
+
database: prismaAdapter(prisma),
|
|
92
|
+
plugins: [
|
|
93
|
+
multiSession({ maximumSessions: 5 }),
|
|
94
|
+
customSession({
|
|
95
|
+
schema: {
|
|
96
|
+
session: {
|
|
97
|
+
fields: {
|
|
98
|
+
ipAddress: { type: 'string', required: false },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
twoFactor({ issuer: 'My App' }),
|
|
104
|
+
captcha({
|
|
105
|
+
provider: 'recaptcha',
|
|
106
|
+
siteKey: process.env.RECAPTCHA_SITE_KEY!,
|
|
107
|
+
secretKey: process.env.RECAPTCHA_SECRET_KEY!,
|
|
108
|
+
}),
|
|
109
|
+
],
|
|
110
|
+
})
|
|
111
|
+
```
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Better Auth - 세션 관리
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [Better Auth](./index.md)
|
|
4
|
+
|
|
5
|
+
## 세션 설정
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { betterAuth } from 'better-auth'
|
|
9
|
+
|
|
10
|
+
export const auth = betterAuth({
|
|
11
|
+
session: {
|
|
12
|
+
modelName: 'sessions',
|
|
13
|
+
fields: {
|
|
14
|
+
userId: 'user_id',
|
|
15
|
+
},
|
|
16
|
+
expiresIn: 604800, // 7일 (초 단위)
|
|
17
|
+
updateAge: 86400, // 1일마다 갱신
|
|
18
|
+
additionalFields: {
|
|
19
|
+
customField: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 쿠키 캐시
|
|
28
|
+
|
|
29
|
+
세션 조회 성능 최적화를 위한 쿠키 캐시:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
export const auth = betterAuth({
|
|
33
|
+
session: {
|
|
34
|
+
cookieCache: {
|
|
35
|
+
enabled: true,
|
|
36
|
+
maxAge: 5 * 60, // 5분
|
|
37
|
+
strategy: 'compact', // 'compact' | 'jwt' | 'jwe'
|
|
38
|
+
refreshCache: true,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 세션 가져오기
|
|
45
|
+
|
|
46
|
+
### 클라이언트
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// 기본
|
|
50
|
+
const session = await authClient.getSession()
|
|
51
|
+
|
|
52
|
+
// 쿠키 캐시 우회 (DB에서 직접 가져오기)
|
|
53
|
+
const session = await authClient.getSession({
|
|
54
|
+
query: {
|
|
55
|
+
disableCookieCache: true,
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 서버
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
await auth.api.getSession({
|
|
64
|
+
query: {
|
|
65
|
+
disableCookieCache: true,
|
|
66
|
+
},
|
|
67
|
+
headers: req.headers,
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Custom Session Plugin
|
|
72
|
+
|
|
73
|
+
세션 스키마 확장 및 커스텀 로직 구현:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { betterAuth } from 'better-auth'
|
|
77
|
+
import { customSession } from 'better-auth/plugins'
|
|
78
|
+
|
|
79
|
+
export const auth = betterAuth({
|
|
80
|
+
plugins: [
|
|
81
|
+
customSession({
|
|
82
|
+
// 세션 스키마 확장
|
|
83
|
+
schema: {
|
|
84
|
+
session: {
|
|
85
|
+
fields: {
|
|
86
|
+
ipAddress: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
required: false,
|
|
89
|
+
},
|
|
90
|
+
userAgent: {
|
|
91
|
+
type: 'string',
|
|
92
|
+
required: false,
|
|
93
|
+
},
|
|
94
|
+
metadata: {
|
|
95
|
+
type: 'json',
|
|
96
|
+
required: false,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// 세션 생성 시 커스터마이즈
|
|
103
|
+
async onSessionCreate(session, context) {
|
|
104
|
+
return {
|
|
105
|
+
...session,
|
|
106
|
+
ipAddress: context.request.headers.get('x-forwarded-for'),
|
|
107
|
+
userAgent: context.request.headers.get('user-agent'),
|
|
108
|
+
metadata: { loginMethod: 'email' },
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// 세션 조회 시 커스터마이즈
|
|
113
|
+
async onSessionGet(session, context) {
|
|
114
|
+
return {
|
|
115
|
+
...session,
|
|
116
|
+
activeDevices: await getActiveDevices(session.userId),
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
],
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// 타입 안전한 커스텀 세션 데이터 접근
|
|
124
|
+
const session = await auth.api.getSession({ headers })
|
|
125
|
+
console.log('IP Address:', session?.ipAddress)
|
|
126
|
+
console.log('Metadata:', session?.metadata)
|
|
127
|
+
```
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Better Auth - 설치 및 설정
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [Better Auth](./index.md)
|
|
4
|
+
|
|
5
|
+
## 설치
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn add better-auth
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 서버 설정
|
|
12
|
+
|
|
13
|
+
### 기본 설정
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// lib/auth.ts
|
|
17
|
+
import { betterAuth } from 'better-auth'
|
|
18
|
+
import { prismaAdapter } from 'better-auth/adapters/prisma'
|
|
19
|
+
import { prisma } from './prisma'
|
|
20
|
+
|
|
21
|
+
export const auth = betterAuth({
|
|
22
|
+
database: prismaAdapter(prisma),
|
|
23
|
+
|
|
24
|
+
// 이메일/비밀번호 인증 활성화
|
|
25
|
+
emailAndPassword: {
|
|
26
|
+
enabled: true,
|
|
27
|
+
requireEmailVerification: false,
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 소셜 로그인 설정
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { betterAuth } from 'better-auth'
|
|
36
|
+
|
|
37
|
+
export const auth = betterAuth({
|
|
38
|
+
database: prismaAdapter(prisma),
|
|
39
|
+
socialProviders: {
|
|
40
|
+
google: {
|
|
41
|
+
clientId: process.env.GOOGLE_CLIENT_ID as string,
|
|
42
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
|
43
|
+
},
|
|
44
|
+
github: {
|
|
45
|
+
clientId: process.env.GITHUB_CLIENT_ID as string,
|
|
46
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
|
|
47
|
+
},
|
|
48
|
+
spotify: {
|
|
49
|
+
clientId: process.env.SPOTIFY_CLIENT_ID as string,
|
|
50
|
+
clientSecret: process.env.SPOTIFY_CLIENT_SECRET as string,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 클라이언트 설정
|
|
57
|
+
|
|
58
|
+
### 기본 클라이언트
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// lib/auth-client.ts
|
|
62
|
+
import { createAuthClient } from 'better-auth/client'
|
|
63
|
+
|
|
64
|
+
export const authClient = createAuthClient({
|
|
65
|
+
// 옵션
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### React 클라이언트
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { createAuthClient } from 'better-auth/react'
|
|
73
|
+
|
|
74
|
+
export const authClient = createAuthClient({
|
|
75
|
+
// 옵션
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 플러그인 포함 클라이언트
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { createAuthClient } from 'better-auth/client'
|
|
83
|
+
import { twoFactorClient } from 'better-auth/client/plugins'
|
|
84
|
+
|
|
85
|
+
const authClient = createAuthClient({
|
|
86
|
+
plugins: [
|
|
87
|
+
twoFactorClient({
|
|
88
|
+
twoFactorPage: '/two-factor',
|
|
89
|
+
onTwoFactorRedirect() {
|
|
90
|
+
router.push('/two-factor')
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
],
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## TanStack Start 통합
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// lib/auth.ts
|
|
101
|
+
import { betterAuth } from 'better-auth'
|
|
102
|
+
import { prismaAdapter } from 'better-auth/adapters/prisma'
|
|
103
|
+
import { prisma } from './prisma'
|
|
104
|
+
|
|
105
|
+
export const auth = betterAuth({
|
|
106
|
+
database: prismaAdapter(prisma),
|
|
107
|
+
emailAndPassword: {
|
|
108
|
+
enabled: true,
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Server Function에서 사용
|
|
113
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
114
|
+
import { auth } from '~/lib/auth'
|
|
115
|
+
|
|
116
|
+
export const getSession = createServerFn({ method: 'GET' })
|
|
117
|
+
.handler(async ({ request }) => {
|
|
118
|
+
const session = await auth.api.getSession({
|
|
119
|
+
headers: request.headers,
|
|
120
|
+
})
|
|
121
|
+
return session
|
|
122
|
+
})
|
|
123
|
+
```
|