@opensaas/stack-auth 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/.turbo/turbo-build.log +4 -0
- package/INTEGRATION_SUMMARY.md +425 -0
- package/README.md +445 -0
- package/dist/client/index.d.ts +38 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +23 -0
- package/dist/client/index.js.map +1 -0
- package/dist/config/index.d.ts +50 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +115 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +160 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/lists/index.d.ts +46 -0
- package/dist/lists/index.d.ts.map +1 -0
- package/dist/lists/index.js +227 -0
- package/dist/lists/index.js.map +1 -0
- package/dist/server/index.d.ts +27 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +90 -0
- package/dist/server/index.js.map +1 -0
- package/dist/ui/components/ForgotPasswordForm.d.ts +36 -0
- package/dist/ui/components/ForgotPasswordForm.d.ts.map +1 -0
- package/dist/ui/components/ForgotPasswordForm.js +50 -0
- package/dist/ui/components/ForgotPasswordForm.js.map +1 -0
- package/dist/ui/components/SignInForm.d.ts +52 -0
- package/dist/ui/components/SignInForm.d.ts.map +1 -0
- package/dist/ui/components/SignInForm.js +66 -0
- package/dist/ui/components/SignInForm.js.map +1 -0
- package/dist/ui/components/SignUpForm.d.ts +56 -0
- package/dist/ui/components/SignUpForm.d.ts.map +1 -0
- package/dist/ui/components/SignUpForm.js +74 -0
- package/dist/ui/components/SignUpForm.js.map +1 -0
- package/dist/ui/index.d.ts +7 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +4 -0
- package/dist/ui/index.js.map +1 -0
- package/package.json +55 -0
- package/src/client/index.ts +44 -0
- package/src/config/index.ts +140 -0
- package/src/config/types.ts +166 -0
- package/src/index.ts +44 -0
- package/src/lists/index.ts +245 -0
- package/src/server/index.ts +120 -0
- package/src/ui/components/ForgotPasswordForm.tsx +120 -0
- package/src/ui/components/SignInForm.tsx +191 -0
- package/src/ui/components/SignUpForm.tsx +238 -0
- package/src/ui/index.ts +7 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { ExtendUserListConfig } from '../lists/index.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OAuth provider configuration
|
|
5
|
+
*/
|
|
6
|
+
export type OAuthProvider = {
|
|
7
|
+
clientId: string
|
|
8
|
+
clientSecret: string
|
|
9
|
+
enabled?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Social provider configurations
|
|
14
|
+
*/
|
|
15
|
+
export type SocialProvidersConfig = {
|
|
16
|
+
github?: OAuthProvider
|
|
17
|
+
google?: OAuthProvider
|
|
18
|
+
discord?: OAuthProvider
|
|
19
|
+
twitter?: OAuthProvider
|
|
20
|
+
[key: string]: OAuthProvider | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Email and password configuration
|
|
25
|
+
*/
|
|
26
|
+
export type EmailPasswordConfig = {
|
|
27
|
+
enabled: boolean
|
|
28
|
+
/**
|
|
29
|
+
* Minimum password length
|
|
30
|
+
* @default 8
|
|
31
|
+
*/
|
|
32
|
+
minPasswordLength?: number
|
|
33
|
+
/**
|
|
34
|
+
* Require password confirmation
|
|
35
|
+
* @default true
|
|
36
|
+
*/
|
|
37
|
+
requireConfirmation?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Email verification configuration
|
|
42
|
+
*/
|
|
43
|
+
export type EmailVerificationConfig = {
|
|
44
|
+
enabled: boolean
|
|
45
|
+
/**
|
|
46
|
+
* Send verification email on sign up
|
|
47
|
+
* @default true
|
|
48
|
+
*/
|
|
49
|
+
sendOnSignUp?: boolean
|
|
50
|
+
/**
|
|
51
|
+
* Token expiration in seconds
|
|
52
|
+
* @default 86400 (24 hours)
|
|
53
|
+
*/
|
|
54
|
+
tokenExpiration?: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Password reset configuration
|
|
59
|
+
*/
|
|
60
|
+
export type PasswordResetConfig = {
|
|
61
|
+
enabled: boolean
|
|
62
|
+
/**
|
|
63
|
+
* Token expiration in seconds
|
|
64
|
+
* @default 3600 (1 hour)
|
|
65
|
+
*/
|
|
66
|
+
tokenExpiration?: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Session configuration
|
|
71
|
+
*/
|
|
72
|
+
export type SessionConfig = {
|
|
73
|
+
/**
|
|
74
|
+
* Session expiration in seconds
|
|
75
|
+
* @default 604800 (7 days)
|
|
76
|
+
*/
|
|
77
|
+
expiresIn?: number
|
|
78
|
+
/**
|
|
79
|
+
* Update session expiration on each request
|
|
80
|
+
* @default true
|
|
81
|
+
*/
|
|
82
|
+
updateAge?: boolean
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Auth configuration options
|
|
87
|
+
*/
|
|
88
|
+
export type AuthConfig = {
|
|
89
|
+
/**
|
|
90
|
+
* Email and password authentication
|
|
91
|
+
*/
|
|
92
|
+
emailAndPassword?: EmailPasswordConfig | { enabled: true }
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Email verification
|
|
96
|
+
*/
|
|
97
|
+
emailVerification?: EmailVerificationConfig | { enabled: true }
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Password reset
|
|
101
|
+
*/
|
|
102
|
+
passwordReset?: PasswordResetConfig | { enabled: true }
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* OAuth/social providers
|
|
106
|
+
*/
|
|
107
|
+
socialProviders?: SocialProvidersConfig
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Session configuration
|
|
111
|
+
*/
|
|
112
|
+
session?: SessionConfig
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Which fields to include in the session object
|
|
116
|
+
* This determines what data is available in access control functions
|
|
117
|
+
* @default ['userId', 'email', 'name']
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* sessionFields: ['userId', 'email', 'name', 'role']
|
|
122
|
+
* // session will be: { userId: string, email: string, name: string, role: string }
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
sessionFields?: string[]
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Extend the auto-generated User list with custom fields
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* extendUserList: {
|
|
133
|
+
* fields: {
|
|
134
|
+
* role: text({ defaultValue: 'user' }),
|
|
135
|
+
* company: text(),
|
|
136
|
+
* }
|
|
137
|
+
* }
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
extendUserList?: ExtendUserListConfig
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Custom email sending function for verification and password reset
|
|
144
|
+
* If not provided, emails will be logged to console
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* sendEmail: async ({ to, subject, html }) => {
|
|
149
|
+
* await resend.emails.send({ to, subject, html })
|
|
150
|
+
* }
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
sendEmail?: (params: { to: string; subject: string; html: string }) => Promise<void>
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Internal normalized auth configuration
|
|
158
|
+
* Used after parsing user config
|
|
159
|
+
*/
|
|
160
|
+
export type NormalizedAuthConfig = Required<
|
|
161
|
+
Omit<AuthConfig, 'emailAndPassword' | 'emailVerification' | 'passwordReset'>
|
|
162
|
+
> & {
|
|
163
|
+
emailAndPassword: Required<EmailPasswordConfig>
|
|
164
|
+
emailVerification: Required<EmailVerificationConfig>
|
|
165
|
+
passwordReset: Required<PasswordResetConfig>
|
|
166
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @opensaas/stack-auth
|
|
3
|
+
*
|
|
4
|
+
* Better-auth integration for OpenSaas Stack
|
|
5
|
+
*
|
|
6
|
+
* This package provides:
|
|
7
|
+
* - Auto-generated User, Session, Account, Verification lists
|
|
8
|
+
* - Session integration with OpenSaas access control
|
|
9
|
+
* - Pre-built auth UI components (SignIn, SignUp, ForgotPassword)
|
|
10
|
+
* - Easy configuration with withAuth() wrapper
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // opensaas.config.ts
|
|
15
|
+
* import { config } from '@opensaas/stack-core'
|
|
16
|
+
* import { withAuth, authConfig } from '@opensaas/stack-auth'
|
|
17
|
+
*
|
|
18
|
+
* export default withAuth(
|
|
19
|
+
* config({
|
|
20
|
+
* db: { provider: 'sqlite', url: 'file:./dev.db' },
|
|
21
|
+
* lists: { ... }
|
|
22
|
+
* }),
|
|
23
|
+
* authConfig({
|
|
24
|
+
* emailAndPassword: { enabled: true },
|
|
25
|
+
* emailVerification: { enabled: true },
|
|
26
|
+
* })
|
|
27
|
+
* )
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// Config exports
|
|
32
|
+
export { withAuth, authConfig, normalizeAuthConfig } from './config/index.js'
|
|
33
|
+
export type { AuthConfig, NormalizedAuthConfig } from './config/index.js'
|
|
34
|
+
export type * from './config/types.js'
|
|
35
|
+
|
|
36
|
+
// List generators (for advanced use cases)
|
|
37
|
+
export {
|
|
38
|
+
getAuthLists,
|
|
39
|
+
createUserList,
|
|
40
|
+
createSessionList,
|
|
41
|
+
createAccountList,
|
|
42
|
+
createVerificationList,
|
|
43
|
+
} from './lists/index.js'
|
|
44
|
+
export type { ExtendUserListConfig } from './lists/index.js'
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { list } from '@opensaas/stack-core'
|
|
2
|
+
import { text, timestamp, checkbox, relationship } from '@opensaas/stack-core/fields'
|
|
3
|
+
import type { ListConfig, FieldConfig } from '@opensaas/stack-core'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for extending the auto-generated User list
|
|
7
|
+
*/
|
|
8
|
+
export type ExtendUserListConfig = {
|
|
9
|
+
/**
|
|
10
|
+
* Additional fields to add to the User list
|
|
11
|
+
* You can add custom fields beyond the basic better-auth fields
|
|
12
|
+
*/
|
|
13
|
+
fields?: Record<string, FieldConfig>
|
|
14
|
+
/**
|
|
15
|
+
* Access control for the User list
|
|
16
|
+
* If not provided, defaults to basic access control (users can update their own records)
|
|
17
|
+
*/
|
|
18
|
+
access?: ListConfig['access']
|
|
19
|
+
/**
|
|
20
|
+
* Hooks for the User list
|
|
21
|
+
*/
|
|
22
|
+
hooks?: ListConfig['hooks']
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create the base User list with better-auth required fields
|
|
27
|
+
* This matches the better-auth user schema
|
|
28
|
+
*/
|
|
29
|
+
export function createUserList(config?: ExtendUserListConfig): ListConfig {
|
|
30
|
+
return list({
|
|
31
|
+
fields: {
|
|
32
|
+
// Better-auth required fields
|
|
33
|
+
name: text({
|
|
34
|
+
validation: { isRequired: true },
|
|
35
|
+
}),
|
|
36
|
+
email: text({
|
|
37
|
+
validation: { isRequired: true },
|
|
38
|
+
isIndexed: 'unique',
|
|
39
|
+
}),
|
|
40
|
+
emailVerified: checkbox({
|
|
41
|
+
defaultValue: false,
|
|
42
|
+
}),
|
|
43
|
+
image: text(),
|
|
44
|
+
|
|
45
|
+
// Relationships to other auth tables
|
|
46
|
+
sessions: relationship({
|
|
47
|
+
ref: 'Session.user',
|
|
48
|
+
many: true,
|
|
49
|
+
}),
|
|
50
|
+
accounts: relationship({
|
|
51
|
+
ref: 'Account.user',
|
|
52
|
+
many: true,
|
|
53
|
+
}),
|
|
54
|
+
|
|
55
|
+
// Custom fields from user config
|
|
56
|
+
...(config?.fields || {}),
|
|
57
|
+
},
|
|
58
|
+
access: config?.access || {
|
|
59
|
+
operation: {
|
|
60
|
+
// Anyone can query users (for displaying names, etc.)
|
|
61
|
+
query: () => true,
|
|
62
|
+
// Anyone can create a user (sign up)
|
|
63
|
+
create: () => true,
|
|
64
|
+
// Only update your own user record
|
|
65
|
+
update: ({ session, item }) => {
|
|
66
|
+
if (!session) return false
|
|
67
|
+
const userId = (session as { userId?: string }).userId
|
|
68
|
+
const itemId = (item as { id?: string })?.id
|
|
69
|
+
return userId === itemId
|
|
70
|
+
},
|
|
71
|
+
// Only delete your own user record
|
|
72
|
+
delete: ({ session, item }) => {
|
|
73
|
+
if (!session) return false
|
|
74
|
+
const userId = (session as { userId?: string }).userId
|
|
75
|
+
const itemId = (item as { id?: string })?.id
|
|
76
|
+
return userId === itemId
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
hooks: config?.hooks,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create the Session list for better-auth
|
|
86
|
+
* Stores active user sessions
|
|
87
|
+
*/
|
|
88
|
+
export function createSessionList(): ListConfig {
|
|
89
|
+
return list({
|
|
90
|
+
fields: {
|
|
91
|
+
// Session token (stored in cookie, used as primary key)
|
|
92
|
+
token: text({
|
|
93
|
+
validation: { isRequired: true },
|
|
94
|
+
isIndexed: 'unique',
|
|
95
|
+
}),
|
|
96
|
+
// Expiration timestamp
|
|
97
|
+
expiresAt: timestamp(),
|
|
98
|
+
// Optional: IP address for security
|
|
99
|
+
ipAddress: text(),
|
|
100
|
+
// Optional: User agent for security
|
|
101
|
+
userAgent: text(),
|
|
102
|
+
// Relationship to user (userId will be auto-generated)
|
|
103
|
+
user: relationship({
|
|
104
|
+
ref: 'User.sessions',
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
access: {
|
|
108
|
+
operation: {
|
|
109
|
+
// Only the session owner can query their sessions
|
|
110
|
+
query: ({ session }) => {
|
|
111
|
+
if (!session) return false
|
|
112
|
+
const userId = (session as { userId?: string }).userId
|
|
113
|
+
if (!userId) return false
|
|
114
|
+
// Return Prisma filter for nested relationship
|
|
115
|
+
return {
|
|
116
|
+
user: {
|
|
117
|
+
id: { equals: userId },
|
|
118
|
+
},
|
|
119
|
+
} as Record<string, unknown>
|
|
120
|
+
},
|
|
121
|
+
// Better-auth handles session creation
|
|
122
|
+
create: () => true,
|
|
123
|
+
// No manual updates
|
|
124
|
+
update: () => false,
|
|
125
|
+
// Better-auth handles session deletion (logout)
|
|
126
|
+
delete: ({ session, item }) => {
|
|
127
|
+
if (!session) return false
|
|
128
|
+
const userId = (session as { userId?: string }).userId
|
|
129
|
+
const itemUserId = (item as { user?: { id?: string } })?.user?.id
|
|
130
|
+
return userId === itemUserId
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Create the Account list for better-auth
|
|
139
|
+
* Stores OAuth provider accounts and credentials
|
|
140
|
+
*/
|
|
141
|
+
export function createAccountList(): ListConfig {
|
|
142
|
+
return list({
|
|
143
|
+
fields: {
|
|
144
|
+
// Account identifier from provider
|
|
145
|
+
accountId: text({
|
|
146
|
+
validation: { isRequired: true },
|
|
147
|
+
}),
|
|
148
|
+
// Provider identifier (e.g., 'github', 'google', 'credentials')
|
|
149
|
+
providerId: text({
|
|
150
|
+
validation: { isRequired: true },
|
|
151
|
+
}),
|
|
152
|
+
// Relationship to user (userId will be auto-generated)
|
|
153
|
+
user: relationship({
|
|
154
|
+
ref: 'User.accounts',
|
|
155
|
+
}),
|
|
156
|
+
// OAuth tokens
|
|
157
|
+
accessToken: text(),
|
|
158
|
+
refreshToken: text(),
|
|
159
|
+
accessTokenExpiresAt: timestamp(),
|
|
160
|
+
refreshTokenExpiresAt: timestamp(),
|
|
161
|
+
scope: text(),
|
|
162
|
+
idToken: text(),
|
|
163
|
+
// Password hash for credential provider (better-auth stores in account table)
|
|
164
|
+
password: text(),
|
|
165
|
+
},
|
|
166
|
+
access: {
|
|
167
|
+
operation: {
|
|
168
|
+
// Only the account owner can query their accounts
|
|
169
|
+
query: ({ session }) => {
|
|
170
|
+
if (!session) return false
|
|
171
|
+
const userId = (session as { userId?: string }).userId
|
|
172
|
+
if (!userId) return false
|
|
173
|
+
// Return Prisma filter for nested relationship
|
|
174
|
+
return {
|
|
175
|
+
user: {
|
|
176
|
+
id: { equals: userId },
|
|
177
|
+
},
|
|
178
|
+
} as Record<string, unknown>
|
|
179
|
+
},
|
|
180
|
+
// Better-auth handles account creation
|
|
181
|
+
create: () => true,
|
|
182
|
+
// Better-auth handles account updates (token refresh)
|
|
183
|
+
update: ({ session, item }) => {
|
|
184
|
+
if (!session) return false
|
|
185
|
+
const userId = (session as { userId?: string }).userId
|
|
186
|
+
const itemUserId = (item as { user?: { id?: string } })?.user?.id
|
|
187
|
+
return userId === itemUserId
|
|
188
|
+
},
|
|
189
|
+
// Account owner can delete their accounts
|
|
190
|
+
delete: ({ session, item }) => {
|
|
191
|
+
if (!session) return false
|
|
192
|
+
const userId = (session as { userId?: string }).userId
|
|
193
|
+
const itemUserId = (item as { user?: { id?: string } })?.user?.id
|
|
194
|
+
return userId === itemUserId
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Create the Verification list for better-auth
|
|
203
|
+
* Stores email verification tokens, password reset tokens, etc.
|
|
204
|
+
*/
|
|
205
|
+
export function createVerificationList(): ListConfig {
|
|
206
|
+
return list({
|
|
207
|
+
fields: {
|
|
208
|
+
// Identifier (e.g., email address)
|
|
209
|
+
identifier: text({
|
|
210
|
+
validation: { isRequired: true },
|
|
211
|
+
}),
|
|
212
|
+
// Token value
|
|
213
|
+
value: text({
|
|
214
|
+
validation: { isRequired: true },
|
|
215
|
+
}),
|
|
216
|
+
// Expiration timestamp
|
|
217
|
+
expiresAt: timestamp(),
|
|
218
|
+
},
|
|
219
|
+
access: {
|
|
220
|
+
operation: {
|
|
221
|
+
// No public querying (better-auth handles verification internally)
|
|
222
|
+
query: () => false,
|
|
223
|
+
// Better-auth creates verification tokens
|
|
224
|
+
create: () => true,
|
|
225
|
+
// No updates
|
|
226
|
+
update: () => false,
|
|
227
|
+
// Better-auth deletes used/expired tokens
|
|
228
|
+
delete: () => true,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get all auth lists required by better-auth
|
|
236
|
+
* This is the main export used by withAuth()
|
|
237
|
+
*/
|
|
238
|
+
export function getAuthLists(userConfig?: ExtendUserListConfig): Record<string, ListConfig> {
|
|
239
|
+
return {
|
|
240
|
+
User: createUserList(userConfig),
|
|
241
|
+
Session: createSessionList(),
|
|
242
|
+
Account: createAccountList(),
|
|
243
|
+
Verification: createVerificationList(),
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { betterAuth } from 'better-auth'
|
|
2
|
+
import { prismaAdapter } from 'better-auth/adapters/prisma'
|
|
3
|
+
import type { BetterAuthOptions } from 'better-auth'
|
|
4
|
+
import type { OpenSaasConfig, DatabaseConfig, AccessContext } from '@opensaas/stack-core'
|
|
5
|
+
import type { NormalizedAuthConfig } from '../config/types.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get better-auth database configuration from OpenSaas config
|
|
9
|
+
*/
|
|
10
|
+
function getDatabaseConfig(
|
|
11
|
+
dbConfig: DatabaseConfig,
|
|
12
|
+
context: AccessContext,
|
|
13
|
+
): BetterAuthOptions['database'] {
|
|
14
|
+
return prismaAdapter(context.prisma, {
|
|
15
|
+
provider: dbConfig.provider,
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a better-auth instance from OpenSaas config
|
|
21
|
+
* This should be called once at app startup
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* // lib/auth.ts
|
|
26
|
+
* import { createAuth } from '@opensaas/stack-auth/server'
|
|
27
|
+
* import config from '../opensaas.config'
|
|
28
|
+
*
|
|
29
|
+
* export const auth = createAuth(config)
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function createAuth(
|
|
33
|
+
opensaasConfig: OpenSaasConfig & { __authConfig?: NormalizedAuthConfig },
|
|
34
|
+
context: AccessContext,
|
|
35
|
+
) {
|
|
36
|
+
// Extract auth config (added by withAuth)
|
|
37
|
+
const authConfig = opensaasConfig.__authConfig
|
|
38
|
+
|
|
39
|
+
if (!authConfig) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
'Auth config not found. Make sure to wrap your config with withAuth() in opensaas.config.ts',
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Build better-auth configuration
|
|
46
|
+
const betterAuthConfig: BetterAuthOptions = {
|
|
47
|
+
database: getDatabaseConfig(opensaasConfig.db, context),
|
|
48
|
+
|
|
49
|
+
// Enable email and password if configured
|
|
50
|
+
emailAndPassword: authConfig.emailAndPassword.enabled
|
|
51
|
+
? {
|
|
52
|
+
enabled: true,
|
|
53
|
+
requireEmailVerification: authConfig.emailVerification.enabled,
|
|
54
|
+
}
|
|
55
|
+
: undefined,
|
|
56
|
+
|
|
57
|
+
// Configure session
|
|
58
|
+
session: {
|
|
59
|
+
expiresIn: authConfig.session.expiresIn || 604800,
|
|
60
|
+
updateAge: authConfig.session.updateAge ? (authConfig.session.expiresIn || 604800) / 10 : 0,
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Trust host (required for production)
|
|
64
|
+
trustedOrigins: process.env.BETTER_AUTH_TRUSTED_ORIGINS?.split(',') || [],
|
|
65
|
+
|
|
66
|
+
// Social providers
|
|
67
|
+
socialProviders: Object.entries(authConfig.socialProviders)
|
|
68
|
+
.filter(([_, config]) => config?.enabled !== false)
|
|
69
|
+
.reduce(
|
|
70
|
+
(acc, [provider, config]) => {
|
|
71
|
+
if (config) {
|
|
72
|
+
acc[provider] = {
|
|
73
|
+
clientId: config.clientId,
|
|
74
|
+
clientSecret: config.clientSecret,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return acc
|
|
78
|
+
},
|
|
79
|
+
{} as Record<string, { clientId: string; clientSecret: string }>,
|
|
80
|
+
),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return betterAuth(betterAuthConfig)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get session from better-auth and transform it to OpenSaas session format
|
|
88
|
+
* This is used internally by the generated context
|
|
89
|
+
*/
|
|
90
|
+
export async function getSessionFromAuth(
|
|
91
|
+
auth: ReturnType<typeof betterAuth>,
|
|
92
|
+
sessionFields: string[],
|
|
93
|
+
) {
|
|
94
|
+
try {
|
|
95
|
+
const session = await auth.api.getSession({
|
|
96
|
+
headers: new Headers(),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
if (!session?.user) {
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Build session object with requested fields
|
|
104
|
+
const result: Record<string, unknown> = {}
|
|
105
|
+
|
|
106
|
+
for (const field of sessionFields) {
|
|
107
|
+
if (field === 'userId') {
|
|
108
|
+
result.userId = session.user.id
|
|
109
|
+
} else if (field in session.user) {
|
|
110
|
+
result[field] = session.user[field as keyof typeof session.user]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result
|
|
115
|
+
} catch {
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type { BetterAuthOptions }
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react'
|
|
4
|
+
import type { createAuthClient } from 'better-auth/react'
|
|
5
|
+
|
|
6
|
+
export type ForgotPasswordFormProps = {
|
|
7
|
+
/**
|
|
8
|
+
* Better-auth client instance
|
|
9
|
+
* Created with createAuthClient from better-auth/react
|
|
10
|
+
*/
|
|
11
|
+
authClient: ReturnType<typeof createAuthClient>
|
|
12
|
+
/**
|
|
13
|
+
* Custom CSS class for the form container
|
|
14
|
+
*/
|
|
15
|
+
className?: string
|
|
16
|
+
/**
|
|
17
|
+
* Callback when reset email is sent successfully
|
|
18
|
+
*/
|
|
19
|
+
onSuccess?: () => void
|
|
20
|
+
/**
|
|
21
|
+
* Callback when reset fails
|
|
22
|
+
*/
|
|
23
|
+
onError?: (error: Error) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Forgot password form component
|
|
28
|
+
* Allows users to request a password reset email
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* import { ForgotPasswordForm } from '@opensaas/stack-auth/ui'
|
|
33
|
+
* import { authClient } from '@/lib/auth-client'
|
|
34
|
+
*
|
|
35
|
+
* export default function ForgotPasswordPage() {
|
|
36
|
+
* return <ForgotPasswordForm authClient={authClient} />
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function ForgotPasswordForm({
|
|
41
|
+
authClient,
|
|
42
|
+
className = '',
|
|
43
|
+
onSuccess,
|
|
44
|
+
onError,
|
|
45
|
+
}: ForgotPasswordFormProps) {
|
|
46
|
+
const [email, setEmail] = useState('')
|
|
47
|
+
const [error, setError] = useState('')
|
|
48
|
+
const [success, setSuccess] = useState(false)
|
|
49
|
+
const [loading, setLoading] = useState(false)
|
|
50
|
+
|
|
51
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
52
|
+
e.preventDefault()
|
|
53
|
+
setError('')
|
|
54
|
+
setSuccess(false)
|
|
55
|
+
setLoading(true)
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const result = await authClient.forgetPassword({
|
|
59
|
+
email,
|
|
60
|
+
redirectTo: '/reset-password',
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
if (result.error) {
|
|
64
|
+
throw new Error(result.error.message)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setSuccess(true)
|
|
68
|
+
onSuccess?.()
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const message = err instanceof Error ? err.message : 'Failed to send reset email'
|
|
71
|
+
setError(message)
|
|
72
|
+
onError?.(err instanceof Error ? err : new Error(message))
|
|
73
|
+
} finally {
|
|
74
|
+
setLoading(false)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className={`w-full max-w-md mx-auto p-6 ${className}`}>
|
|
80
|
+
<h2 className="text-2xl font-bold mb-6">Forgot Password</h2>
|
|
81
|
+
|
|
82
|
+
{error && (
|
|
83
|
+
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-4">
|
|
84
|
+
{error}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{success && (
|
|
89
|
+
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded mb-4">
|
|
90
|
+
Password reset email sent! Check your inbox for instructions.
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
95
|
+
<div>
|
|
96
|
+
<label htmlFor="email" className="block text-sm font-medium mb-2">
|
|
97
|
+
Email
|
|
98
|
+
</label>
|
|
99
|
+
<input
|
|
100
|
+
id="email"
|
|
101
|
+
type="email"
|
|
102
|
+
value={email}
|
|
103
|
+
onChange={(e) => setEmail((e.target as HTMLInputElement).value)}
|
|
104
|
+
required
|
|
105
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
106
|
+
disabled={loading || success}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<button
|
|
111
|
+
type="submit"
|
|
112
|
+
disabled={loading || success}
|
|
113
|
+
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
|
114
|
+
>
|
|
115
|
+
{loading ? 'Sending...' : success ? 'Email Sent' : 'Send Reset Link'}
|
|
116
|
+
</button>
|
|
117
|
+
</form>
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|