1
1
import type { User } from '@prisma/client'
2
2
import type { OAuth2Tokens } from 'arctic'
3
+ import type { NextRequest } from 'next/server'
3
4
import { cookies } from 'next/headers'
4
5
import { redirect } from 'next/navigation'
5
- import { generateCodeVerifier , generateState } from 'arctic'
6
+ import { NextResponse } from 'next/server'
7
+ import { generateCodeVerifier , generateState , OAuth2RequestError } from 'arctic'
6
8
import { z } from 'zod'
7
9
8
10
import { db } from '@yuki/db'
@@ -17,14 +19,12 @@ export interface AuthOptions {
17
19
providers : Providers
18
20
}
19
21
20
- type SignInType = 'credentials' | 'discord' | 'google'
21
-
22
22
class AuthClass {
23
23
private readonly db : typeof db
24
24
private readonly session : Session
25
25
private readonly password : Password
26
- private readonly COOKIE_KEY : string
27
26
27
+ private readonly COOKIE_KEY : string
28
28
private readonly providers : Providers
29
29
30
30
constructor ( options : AuthOptions ) {
@@ -36,79 +36,87 @@ class AuthClass {
36
36
this . password = new Password ( )
37
37
}
38
38
39
- public async auth ( req ?: Request ) : Promise < SessionResult > {
39
+ public async auth ( req ?: NextRequest ) : Promise < SessionResult > {
40
40
let authToken : string | undefined
41
41
42
- if ( req ) {
43
- const cookies = this . parsedCookies ( req . headers )
42
+ if ( req )
44
43
authToken =
45
- cookies [ this . COOKIE_KEY ] ??
44
+ req . cookies . get ( this . COOKIE_KEY ) ?. value ??
46
45
req . headers . get ( 'Authorization' ) ?. replace ( 'Bearer ' , '' )
47
- } else {
48
- authToken = ( await cookies ( ) ) . get ( this . COOKIE_KEY ) ?. value
49
- }
46
+ else authToken = ( await cookies ( ) ) . get ( this . COOKIE_KEY ) ?. value
50
47
51
48
if ( ! authToken ) return { expires : new Date ( ) }
52
49
53
50
return await this . session . validateSessionToken ( authToken )
54
51
}
55
52
56
- public async handlers ( req : Request ) : Promise < Response > {
57
- const url = new URL ( req . url )
53
+ public async handlers ( req : NextRequest ) : Promise < Response > {
54
+ const url = new URL ( req . nextUrl )
58
55
59
- let response : Response = new Response ( 'Not found' , { status : 404 } )
56
+ let response : NextResponse = NextResponse . json (
57
+ { error : 'Not found' } ,
58
+ { status : 404 } ,
59
+ )
60
60
61
61
switch ( req . method ) {
62
62
case 'OPTIONS' :
63
- response = new Response ( '' , { status : 204 } )
63
+ response = NextResponse . json ( '' , { status : 204 } )
64
64
break
65
65
case 'GET' :
66
66
if ( url . pathname === '/api/auth' ) {
67
67
const session = await this . auth ( req )
68
- response = new Response ( JSON . stringify ( session ) )
68
+ response = NextResponse . json ( session )
69
69
} else if ( url . pathname . startsWith ( '/api/auth/oauth' ) ) {
70
70
const isCallback = url . pathname . endsWith ( '/callback' )
71
71
72
72
if ( ! isCallback ) {
73
- const provider = String ( url . pathname . split ( '/' ) . pop ( ) )
73
+ const provider =
74
+ this . providers [ String ( url . pathname . split ( '/' ) . pop ( ) ) ]
75
+
76
+ if ( ! provider ) {
77
+ response = NextResponse . json (
78
+ { error : 'Provider not supported' } ,
79
+ { status : 404 } ,
80
+ )
81
+ break
82
+ }
74
83
const state = generateState ( )
75
84
const codeVerifier = generateCodeVerifier ( )
76
- const authorizationUrl = this . providers [
77
- provider
78
- ] ?. createAuthorizationURL ( state , codeVerifier )
79
-
80
- if ( ! authorizationUrl ) {
81
- response = new Response ( 'Provider not found' , { status : 404 } )
82
- } else {
83
- response = new Response ( '' , { status : 302 } )
84
- response . headers . set ( 'Location' , authorizationUrl . toString ( ) )
85
- response . headers . set ( 'Set-Cookie' , `oauth_state=${ state } ; Path=/` )
86
-
87
- this . setCookie ( response , 'oauth_state' , state , { maxAge : 600 } )
88
- this . setCookie ( response , 'code_verifier' , codeVerifier , {
89
- maxAge : 600 ,
90
- } )
91
- }
85
+ const authorizationUrl = provider . createAuthorizationURL (
86
+ state ,
87
+ codeVerifier ,
88
+ )
89
+
90
+ response = NextResponse . redirect (
91
+ new URL ( authorizationUrl , req . nextUrl ) ,
92
+ )
93
+ response . cookies . set ( 'code_verifier' , codeVerifier )
94
+ response . cookies . set ( 'oauth_state' , state )
92
95
} else {
93
- const cookies = this . parsedCookies ( req . headers )
96
+ const provider =
97
+ this . providers [ String ( url . pathname . split ( '/' ) . slice ( - 2 ) [ 0 ] ) ]
98
+ if ( ! provider ) {
99
+ response = NextResponse . json (
100
+ { error : 'Provider not supported' } ,
101
+ { status : 404 } ,
102
+ )
103
+ break
104
+ }
94
105
95
- const provider = String ( url . pathname . split ( '/' ) . slice ( - 2 ) [ 0 ] )
96
106
const code = url . searchParams . get ( 'code' )
97
107
const state = url . searchParams . get ( 'state' )
98
- const storedState = cookies . oauth_state
99
- const codeVerifier = cookies . code_verifier ?? ''
108
+ const storedState = req . cookies . get ( ' oauth_state' ) ?. value ?? ''
109
+ const codeVerifier = req . cookies . get ( ' code_verifier' ) ?. value ?? ''
100
110
101
111
try {
102
112
if ( ! code || ! state || state !== storedState )
103
113
throw new Error ( 'Invalid state' )
104
114
105
115
const { validateAuthorizationCode, fetchUserUrl, mapUser } =
106
- this . providers [ provider ] ?? { }
107
- if ( ! validateAuthorizationCode || ! fetchUserUrl || ! mapUser )
108
- throw new Error ( 'Provider not found' )
116
+ provider
109
117
110
118
const verifiedCode = await validateAuthorizationCode (
111
- code ,
119
+ 'dsadasd' ,
112
120
codeVerifier ,
113
121
)
114
122
@@ -117,41 +125,38 @@ class AuthClass {
117
125
const res = await fetch ( fetchUserUrl , {
118
126
headers : { Authorization : `Bearer ${ token } ` } ,
119
127
} )
120
- if ( ! res . ok )
121
- throw new Error ( `Failed to fetch user data from ${ provider } ` )
128
+ if ( ! res . ok ) throw new Error ( 'Failed to fetch user data' )
122
129
123
130
const user = await this . createUser (
124
131
mapUser ( ( await res . json ( ) ) as never ) ,
125
132
)
126
133
const session = await this . session . createSession ( user . id )
127
134
128
- response = new Response ( '' , { status : 302 } )
129
-
130
- response . headers . set ( 'Location' , '/' )
131
- this . setCookie ( response , this . COOKIE_KEY , session . sessionToken , {
135
+ response = NextResponse . redirect ( new URL ( '/' , req . nextUrl ) )
136
+ response . cookies . set ( this . COOKIE_KEY , session . sessionToken , {
137
+ httpOnly : true ,
138
+ path : '/' ,
139
+ secure : env . NODE_ENV === 'production' ,
140
+ sameSite : 'lax' as const ,
132
141
expires : session . expires ,
133
- replace : true ,
134
- } )
135
-
136
- const pastDate = new Date ( 0 )
137
- this . setCookie ( response , 'oauth_state' , '' , { expires : pastDate } )
138
- this . setCookie ( response , 'code_verifier' , '' , {
139
- expires : pastDate ,
140
142
} )
143
+ response . cookies . delete ( 'oauth_state' )
144
+ response . cookies . delete ( 'code_verifier' )
141
145
} catch ( error ) {
142
- if ( error instanceof Error )
143
- response = new Response (
144
- JSON . stringify ( { error : error . message } ) ,
145
- {
146
- status : 400 ,
147
- } ,
146
+ if ( error instanceof OAuth2RequestError ) {
147
+ response = NextResponse . json (
148
+ { error : error . message , description : error . description } ,
149
+ { status : 400 } ,
150
+ )
151
+ } else if ( error instanceof Error )
152
+ response = NextResponse . json (
153
+ { error : error . message } ,
154
+ { status : 400 } ,
148
155
)
149
156
else
150
- response = new Response (
151
- JSON . stringify ( { error : 'An error occurred' } ) ,
152
- {
153
- status : 500 ,
154
- } ,
157
+ response = NextResponse . json (
158
+ { error : 'An unknown error occurred' } ,
159
+ { status : 400 } ,
155
160
)
156
161
}
157
162
}
@@ -160,11 +165,8 @@ class AuthClass {
160
165
case 'POST' :
161
166
if ( url . pathname === '/api/auth/sign-out' ) {
162
167
await this . signOut ( req )
163
- response = new Response ( '' , { status : 302 } )
164
- response . headers . set ( 'Location' , '/' )
165
- this . setCookie ( response , this . COOKIE_KEY , '' , {
166
- expires : new Date ( 0 ) ,
167
- } )
168
+ response = NextResponse . redirect ( new URL ( '/' , req . url ) )
169
+ response . cookies . delete ( this . COOKIE_KEY )
168
170
}
169
171
break
170
172
}
@@ -178,7 +180,7 @@ class AuthClass {
178
180
data ?: z . infer < typeof credentialsSchema > ,
179
181
) : Promise <
180
182
| { success : false ; fieldErrors : Record < string , string [ ] > }
181
- | { success : true ; session : { sessionToken : string ; expires : Date } }
183
+ | { success : true ; message : string }
182
184
| undefined
183
185
> {
184
186
if ( type === 'credentials' ) {
@@ -198,63 +200,34 @@ class AuthClass {
198
200
const passwordMatch = this . password . verify ( password , user . password )
199
201
if ( ! passwordMatch ) throw new Error ( 'Invalid password' )
200
202
201
- return {
202
- success : true ,
203
- session : await this . session . createSession ( user . id ) ,
204
- }
203
+ const session = await this . session . createSession ( user . id )
204
+ ; ( await cookies ( ) ) . set ( 'auth_token' , session . sessionToken , {
205
+ httpOnly : true ,
206
+ path : '/' ,
207
+ secure : env . NODE_ENV === 'production' ,
208
+ sameSite : 'lax' as const ,
209
+ expires : session . expires ,
210
+ } )
211
+
212
+ return { success : true , message : 'Signed in' }
205
213
} else {
206
214
redirect ( `/api/auth/oauth/${ type } ` )
207
215
}
208
216
}
209
217
210
- public async signOut ( req ?: Request ) : Promise < void > {
211
- let token : string
212
-
213
- if ( req ) {
214
- const cookies = this . parsedCookies ( req . headers )
215
- token =
216
- cookies [ this . COOKIE_KEY ] ??
217
- req . headers . get ( 'Authorization' ) ?. replace ( 'Bearer ' , '' ) ??
218
- ''
219
- } else {
220
- token = ( await cookies ( ) ) . get ( this . COOKIE_KEY ) ?. value ?? ''
221
- }
222
-
218
+ public async signOut ( req ?: NextRequest ) : Promise < void > {
219
+ const token = await this . getToken ( req )
223
220
await this . session . invalidateSessionToken ( token )
224
- if ( ! req ) ( await cookies ( ) ) . delete ( this . COOKIE_KEY )
225
221
}
226
222
227
- private parsedCookies ( headers : Headers ) : Record < string , string > {
228
- const cookiesHeader = headers . get ( 'cookie' )
229
- if ( ! cookiesHeader ) return { }
230
-
231
- return cookiesHeader . split ( ';' ) . reduce ( ( acc , cookie ) => {
232
- const [ name , value ] = cookie . split ( '=' ) as [ string , string ]
233
- return { ...acc , [ name . trim ( ) ] : value }
234
- } , { } )
235
- }
236
-
237
- private setCookie (
238
- response : Response ,
239
- name : string ,
240
- value : string ,
241
- options : {
242
- expires ?: Date
243
- maxAge ?: number
244
- replace ?: boolean
245
- } = { } ,
246
- ) : void {
247
- const { expires, maxAge, replace = false } = options
248
-
249
- let cookieValue = `${ name } =${ value } ; HttpOnly; Path=/; SameSite=Lax`
250
-
251
- if ( expires ) cookieValue += `; Expires=${ expires . toUTCString ( ) } `
252
- else if ( maxAge !== undefined ) cookieValue += `; Max-Age=${ maxAge } `
253
-
254
- if ( env . NODE_ENV === 'production' ) cookieValue += '; Secure'
255
-
256
- if ( replace ) response . headers . set ( 'Set-Cookie' , cookieValue )
257
- else response . headers . append ( 'Set-Cookie' , cookieValue )
223
+ private async getToken ( req ?: NextRequest ) : Promise < string > {
224
+ if ( req )
225
+ return (
226
+ req . cookies . get ( this . COOKIE_KEY ) ?. value ??
227
+ req . headers . get ( 'Authorization' ) ?. replace ( 'Bearer ' , '' ) ??
228
+ ''
229
+ )
230
+ return ( await cookies ( ) ) . get ( this . COOKIE_KEY ) ?. value ?? ''
258
231
}
259
232
260
233
private async createUser ( data : {
@@ -298,14 +271,37 @@ class AuthClass {
298
271
}
299
272
300
273
private setCorsHeaders ( res : Response ) : void {
301
- res . headers . set ( 'Content-Type' , 'application/json' )
302
274
res . headers . set ( 'Access-Control-Allow-Origin' , '*' )
303
275
res . headers . set ( 'Access-Control-Request-Method' , '*' )
304
276
res . headers . set ( 'Access-Control-Allow-Methods' , 'OPTIONS, GET, POST' )
305
277
res . headers . set ( 'Access-Control-Allow-Headers' , '*' )
306
278
}
307
279
}
308
280
281
+ export const Auth = ( options : AuthOptions ) => {
282
+ const authInstance = new AuthClass ( options )
283
+
284
+ return {
285
+ auth : ( req ?: NextRequest ) => authInstance . auth ( req ) ,
286
+ signIn : ( type : SignInType , data ?: z . infer < typeof credentialsSchema > ) =>
287
+ authInstance . signIn ( type , data ) ,
288
+ signOut : ( req ?: NextRequest ) => authInstance . signOut ( req ) ,
289
+ handlers : ( req : NextRequest ) => authInstance . handlers ( req ) ,
290
+ }
291
+ }
292
+
293
+ const credentialsSchema = z . object ( {
294
+ email : z . string ( ) . email ( ) ,
295
+ password : z
296
+ . string ( )
297
+ . min ( 8 , 'Password must be at least 8 characters' )
298
+ . regex (
299
+ / ^ (? = .* [ a - z ] ) (? = .* [ A - Z ] ) (? = .* \d ) (? = .* [ ^ A - Z a - z 0 - 9 ] ) .{ 8 , } $ / ,
300
+ 'Password must contain at least one lowercase letter, one uppercase letter, one number, and one special character' ,
301
+ ) ,
302
+ } )
303
+
304
+ type SignInType = 'credentials' | 'discord' | 'google'
309
305
type Providers = Record <
310
306
string ,
311
307
{
@@ -324,26 +320,3 @@ type Providers = Record<
324
320
}
325
321
}
326
322
>
327
-
328
- const credentialsSchema = z . object ( {
329
- email : z . string ( ) . email ( ) ,
330
- password : z
331
- . string ( )
332
- . min ( 8 , 'Password must be at least 8 characters' )
333
- . regex (
334
- / ^ (? = .* [ a - z ] ) (? = .* [ A - Z ] ) (? = .* \d ) (? = .* [ ^ A - Z a - z 0 - 9 ] ) .{ 8 , } $ / ,
335
- 'Password must contain at least one lowercase letter, one uppercase letter, one number, and one special character' ,
336
- ) ,
337
- } )
338
-
339
- export const Auth = ( options : AuthOptions ) => {
340
- const authInstance = new AuthClass ( options )
341
-
342
- return {
343
- auth : ( req ?: Request ) => authInstance . auth ( req ) ,
344
- signIn : ( type : SignInType , data ?: z . infer < typeof credentialsSchema > ) =>
345
- authInstance . signIn ( type , data ) ,
346
- signOut : ( req ?: Request ) => authInstance . signOut ( req ) ,
347
- handlers : ( req : Request ) => authInstance . handlers ( req ) ,
348
- }
349
- }
0 commit comments