11import { cookies } from "next/headers" ;
2- import { NextResponse } from "next/server" ;
2+ import { NextRequest , NextResponse } from "next/server" ;
33import crypto from "crypto" ;
44import connectDb from "@/lib/db/connection" ;
55import PBCTFRefreshToken from "@/lib/db/models/CTFRefreshToken" ;
66import generateAccessToken from "@/lib/pbctf/generateAccessToken" ;
77import generateRefreshToken from "@/lib/pbctf/generateRefreshToken" ;
88
9- export async function POST ( ) {
10- try {
11- await connectDb ( ) ;
12- const cookieStore = await cookies ( ) ;
13- const refreshToken =
14- cookieStore . get ( "pbctf_refresh" ) ?. value ;
15-
16- if ( ! refreshToken ) {
17- return NextResponse . json (
18- {
19- error : "Refresh token missing" ,
20- } ,
21- {
22- status : 401 ,
23- }
24- ) ;
9+ type RefreshResult =
10+ | {
11+ success : true ;
12+ accessToken : string ;
13+ refreshToken : string ;
2514 }
15+ | {
16+ success : false ;
17+ status : number ;
18+ error : string ;
19+ } ;
20+
21+ const cookieOptions = {
22+ httpOnly : true ,
23+ secure : process . env . NODE_ENV === "production" ,
24+ sameSite : "strict" as const ,
25+ path : "/" ,
26+ } ;
27+
28+ function clearAuthCookies ( response : NextResponse ) {
29+ response . cookies . set ( "pbctf_access" , "" , {
30+ ...cookieOptions ,
31+ expires : new Date ( 0 ) ,
32+ } ) ;
33+
34+ response . cookies . set ( "pbctf_refresh" , "" , {
35+ ...cookieOptions ,
36+ expires : new Date ( 0 ) ,
37+ } ) ;
2638
27- const tokenHash = crypto
28- . createHash ( "sha256" )
29- . update ( refreshToken )
30- . digest ( "hex" ) ;
31-
32- const matchedToken =
33- await PBCTFRefreshToken . findOne ( {
34- tokenHash,
35- } ) ;
36-
37- if ( ! matchedToken ) {
38- const response =
39- NextResponse . json (
40- {
41- error :
42- "Invalid refresh token" ,
43- } ,
44- {
45- status : 401 ,
46- }
47- ) ;
39+ return response ;
40+ }
4841
49- response . cookies . set (
50- "pbctf_access" ,
51- "" ,
52- {
53- expires : new Date ( 0 ) ,
54- path : "/" ,
55- httpOnly : true ,
56- sameSite : "strict" ,
57- secure :
58- process . env . NODE_ENV ===
59- "production" ,
60- }
61- ) ;
42+ function setAuthCookies (
43+ response : NextResponse ,
44+ accessToken : string ,
45+ refreshToken : string
46+ ) {
47+ response . cookies . set ( "pbctf_access" , accessToken , {
48+ ...cookieOptions ,
49+ maxAge : 60 * 10 ,
50+ } ) ;
6251
63- response . cookies . set (
64- "pbctf_refresh" ,
65- "" ,
66- {
67- expires : new Date ( 0 ) ,
68- path : "/" ,
69- httpOnly : true ,
70- sameSite : "strict" ,
71- secure :
72- process . env . NODE_ENV ===
73- "production" ,
74- }
75- ) ;
52+ response . cookies . set ( "pbctf_refresh" , refreshToken , {
53+ ...cookieOptions ,
54+ maxAge : 24 * 60 * 60 ,
55+ } ) ;
7656
7757 return response ;
7858}
7959
80- if (
81- matchedToken . expiresAt <
82- new Date ( )
83- ) {
84- await matchedToken . deleteOne ( ) ;
60+ async function rotateRefreshToken ( ) : Promise < RefreshResult > {
61+ await connectDb ( ) ;
8562
86- const response =
87- NextResponse . json (
88- {
89- error :
90- "Refresh token expired" ,
91- } ,
92- {
93- status : 401 ,
94- }
95- ) ;
63+ const cookieStore = await cookies ( ) ;
64+ const refreshToken =
65+ cookieStore . get ( "pbctf_refresh" ) ?. value ;
9666
97- response . cookies . set (
98- "pbctf_access" ,
99- "" ,
100- {
101- expires : new Date ( 0 ) ,
102- path : "/" ,
103- httpOnly : true ,
104- sameSite : "strict" ,
105- secure :
106- process . env . NODE_ENV ===
107- "production" ,
108- }
109- ) ;
67+ if ( ! refreshToken ) {
68+ return {
69+ success : false ,
70+ status : 401 ,
71+ error : "Refresh token missing" ,
72+ } ;
73+ }
11074
111- response . cookies . set (
112- "pbctf_refresh" ,
113- "" ,
114- {
115- expires : new Date ( 0 ) ,
116- path : "/" ,
117- httpOnly : true ,
118- sameSite : "strict" ,
119- secure :
120- process . env . NODE_ENV ===
121- "production" ,
122- }
75+ const tokenHash = crypto
76+ . createHash ( "sha256" )
77+ . update ( refreshToken )
78+ . digest ( "hex" ) ;
79+
80+ const matchedToken =
81+ await PBCTFRefreshToken . findOne ( {
82+ tokenHash,
83+ } ) ;
84+
85+ if ( ! matchedToken ) {
86+ return {
87+ success : false ,
88+ status : 401 ,
89+ error : "Invalid refresh token" ,
90+ } ;
91+ }
92+
93+ if ( matchedToken . expiresAt < new Date ( ) ) {
94+ await matchedToken . deleteOne ( ) ;
95+
96+ return {
97+ success : false ,
98+ status : 401 ,
99+ error : "Refresh token expired" ,
100+ } ;
101+ }
102+
103+ const newAccessToken =
104+ generateAccessToken ( matchedToken . email ) ;
105+ const newRefreshToken =
106+ generateRefreshToken ( ) ;
107+
108+ const newRefreshHash = crypto
109+ . createHash ( "sha256" )
110+ . update ( newRefreshToken )
111+ . digest ( "hex" ) ;
112+
113+ matchedToken . tokenHash = newRefreshHash ;
114+ matchedToken . expiresAt = new Date (
115+ Date . now ( ) + 24 * 60 * 60 * 1000
123116 ) ;
124117
125- return response ;
118+ await matchedToken . save ( ) ;
119+
120+ return {
121+ success : true ,
122+ accessToken : newAccessToken ,
123+ refreshToken : newRefreshToken ,
124+ } ;
126125}
127126
128- const newAccessToken =
129- generateAccessToken (
130- matchedToken . email
131- ) ;
127+ function getSafeNextUrl ( request : NextRequest ) {
128+ const next =
129+ request . nextUrl . searchParams . get ( "next" ) ||
130+ "/pbctf/dashboard" ;
132131
133- const newRefreshToken =
134- generateRefreshToken ( ) ;
132+ if ( ! next . startsWith ( "/" ) || next . startsWith ( "//" ) ) {
133+ return "/pbctf/dashboard" ;
134+ }
135135
136- const newRefreshHash = crypto
137- . createHash ( "sha256" )
138- . update ( newRefreshToken )
139- . digest ( "hex" ) ;
136+ return next ;
137+ }
140138
141- matchedToken . tokenHash =
142- newRefreshHash ;
139+ export async function GET ( request : NextRequest ) {
140+ try {
141+ const result = await rotateRefreshToken ( ) ;
143142
144- matchedToken . expiresAt =
145- new Date (
146- Date . now ( ) + 24 * 60 * 60 * 1000
143+ if ( ! result . success ) {
144+ const response = NextResponse . redirect (
145+ new URL ( "/pbctf/login" , request . url )
147146 ) ;
148147
149- await matchedToken . save ( ) ;
148+ return clearAuthCookies ( response ) ;
149+ }
150150
151- const response =
152- NextResponse . json ( {
153- success : true ,
154- } ) ;
151+ const response = NextResponse . redirect (
152+ new URL ( getSafeNextUrl ( request ) , request . url )
153+ ) ;
155154
156- response . cookies . set (
157- "pbctf_access" ,
158- newAccessToken ,
159- {
160- httpOnly : true ,
161- secure :
162- process . env . NODE_ENV ===
163- "production" ,
164- sameSite : "strict" ,
165- path : "/" ,
166- maxAge : 60 * 10 ,
167- }
155+ return setAuthCookies (
156+ response ,
157+ result . accessToken ,
158+ result . refreshToken
168159 ) ;
160+ } catch ( error ) {
161+ console . error ( error ) ;
169162
170- response . cookies . set (
171- "pbctf_refresh" ,
172- newRefreshToken ,
173- {
174- httpOnly : true ,
175- secure :
176- process . env . NODE_ENV ===
177- "production" ,
178- sameSite : "strict" ,
179- path : "/" ,
180- maxAge :
181- 24 * 60 * 60 ,
182- }
163+ return NextResponse . redirect (
164+ new URL ( "/pbctf/login" , request . url )
183165 ) ;
166+ }
167+ }
184168
185- return response ;
169+ export async function POST ( ) {
170+ try {
171+ const result = await rotateRefreshToken ( ) ;
172+
173+ if ( ! result . success ) {
174+ const response = NextResponse . json (
175+ {
176+ error : result . error ,
177+ } ,
178+ {
179+ status : result . status ,
180+ }
181+ ) ;
182+
183+ return clearAuthCookies ( response ) ;
184+ }
185+
186+ const response = NextResponse . json ( {
187+ success : true ,
188+ } ) ;
189+
190+ return setAuthCookies (
191+ response ,
192+ result . accessToken ,
193+ result . refreshToken
194+ ) ;
186195 } catch ( error ) {
187196 console . error ( error ) ;
188197
189198 return NextResponse . json (
190199 {
191- error :
192- "Internal server error" ,
200+ error : "Internal server error" ,
193201 } ,
194202 {
195203 status : 500 ,
196204 }
197205 ) ;
198206 }
199- }
207+ }
0 commit comments