1- // src/auth/auth.controller.ts
2- import { Body , Controller , Post , UnauthorizedException } from "@nestjs/common" ;
1+ /* eslint-disable @typescript-eslint/no-unused-vars */
2+ import {
3+ Body ,
4+ Controller ,
5+ Post ,
6+ UnauthorizedException ,
7+ Req ,
8+ Res ,
9+ HttpCode ,
10+ } from "@nestjs/common" ;
311import { AuthService } from "./auth.service" ;
412import { RegisterDto } from "./dto/register.dto" ;
513import { LoginDto } from "./dto/login.dto" ;
14+ import type { Request , Response } from "express" ;
15+
16+ const REFRESH_COOKIE_NAME = "refresh_token" ;
17+ const COOKIE_MAX_AGE = 7 * 24 * 60 * 60 * 1000 ; // 7 days in ms
18+
19+ // Environment-aware cookie options
20+ const isProd = process . env . NODE_ENV === "production" ;
21+ const FE_URL =
22+ process . env . FE_URL || process . env . FRONTEND_ORIGIN || "http://localhost:3000" ;
23+ const BACKEND_ORIGIN =
24+ process . env . BACKEND_ORIGIN || `http://localhost:${ process . env . PORT || 8000 } ` ;
25+ const isCrossOrigin = FE_URL !== BACKEND_ORIGIN ;
26+
27+ // For cross-origin cookies in production you MUST use sameSite: 'none' AND secure: true.
28+ // For local dev over HTTP, browsers disallow SameSite=None without Secure, so we fall back to 'lax'.
29+ const COOKIE_OPTIONS = {
30+ httpOnly : true ,
31+ secure : isProd , // true in production (HTTPS)
32+ sameSite : isProd && isCrossOrigin ? ( "none" as const ) : ( "lax" as const ) ,
33+ path : "/" ,
34+ } ;
635
736@Controller ( "auth" )
837export class AuthController {
938 constructor ( private authService : AuthService ) { }
1039
1140 @Post ( "anon" )
12- async anonLogin ( @Body ( "userId" ) userId ?: string ) {
13- const { token, user } = await this . authService . createOrGetAnonymousUser (
14- userId
15- ) ;
16- return { token, userId : user . id } ; // kirim userId juga
41+ async anonLogin (
42+ @Body ( "userId" ) userId ?: string ,
43+ @Res ( { passthrough : true } ) res ?: Response
44+ ) {
45+ const result = await this . authService . createOrGetAnonymousUser ( userId ) ;
46+
47+ // if service returned a refreshToken, set it as httpOnly cookie
48+ if ( result ?. refreshToken && res ) {
49+ res . cookie ( REFRESH_COOKIE_NAME , result . refreshToken , {
50+ ...COOKIE_OPTIONS ,
51+ maxAge : COOKIE_MAX_AGE ,
52+ } ) ;
53+ }
54+
55+ // keep the same response shape you had before
56+ return { token : result . token , userId : result . user . id } ;
1757 }
1858
1959 @Post ( "register" )
20- async register ( @Body ( ) dto : RegisterDto ) {
60+ async register (
61+ @Body ( ) dto : RegisterDto ,
62+ @Res ( { passthrough : true } ) res ?: Response
63+ ) {
2164 const result = await this . authService . register ( dto ) ;
22- // return mapping so FE bisa replace optimistic client id with real teamMember.id
65+
66+ // if service returned a refreshToken, set cookie
67+ if ( result ?. refreshToken && res ) {
68+ res . cookie ( REFRESH_COOKIE_NAME , result . refreshToken , {
69+ ...COOKIE_OPTIONS ,
70+ maxAge : COOKIE_MAX_AGE ,
71+ } ) ;
72+ }
73+
74+ // keep your existing response mapping
2375 return {
2476 token : result . token ,
2577 userId : result . user . id ,
@@ -31,15 +83,78 @@ export class AuthController {
3183 }
3284
3385 @Post ( "login" )
34- async login ( @Body ( ) dto : LoginDto ) {
86+ async login (
87+ @Body ( ) dto : LoginDto ,
88+ @Res ( { passthrough : true } ) res ?: Response
89+ ) {
3590 const result = await this . authService . login ( dto ) ;
3691 if ( ! result ) throw new UnauthorizedException ( "Invalid credentials" ) ;
37- // return similar shape as register (token + user + optional member)
92+
93+ // set refresh cookie if present (service should ideally return refreshToken)
94+ if ( result . refreshToken && res ) {
95+ res . cookie ( REFRESH_COOKIE_NAME , result . refreshToken , {
96+ ...COOKIE_OPTIONS ,
97+ maxAge : COOKIE_MAX_AGE ,
98+ // set domain if backend is serving under a hostname different than localhost (optional)
99+ } ) ;
100+ }
101+
102+ // return similar shape as before (token + user + optional member)
38103 return {
39104 token : result . token ,
40105 userId : result ?. user ?. id ?? "" ,
41106 user : result . user ,
42107 teamMemberId : result ?. teamMemberId ,
43108 } ;
44109 }
110+
111+ // refresh endpoint: reads refresh_token cookie, verifies, rotates
112+ @HttpCode ( 200 )
113+ @Post ( "refresh" )
114+ async refresh (
115+ @Req ( ) req : Request ,
116+ @Res ( { passthrough : true } ) res : Response
117+ ) {
118+ const token = req . cookies ?. [ REFRESH_COOKIE_NAME ] ;
119+ if ( ! token ) throw new UnauthorizedException ( "No refresh token" ) ;
120+
121+ // attempt to verify and refresh via AuthService
122+ let payload : any ;
123+ try {
124+ payload = ( this . authService as any ) . jwtService . verify ( token ) ;
125+ } catch ( e : any ) {
126+ throw new UnauthorizedException ( "Invalid refresh token" ) ;
127+ }
128+
129+ const userId = payload . sub ;
130+ const newTokens = await this . authService . refreshTokens ( userId , token ) ;
131+ if ( ! newTokens ) throw new UnauthorizedException ( "Refresh failed" ) ;
132+
133+ // rotate cookie
134+ res . cookie ( REFRESH_COOKIE_NAME , newTokens . refreshToken , {
135+ ...COOKIE_OPTIONS ,
136+ maxAge : COOKIE_MAX_AGE ,
137+ } ) ;
138+
139+ // return access token
140+ return { token : newTokens . accessToken } ;
141+ }
142+
143+ @Post ( "logout" )
144+ async logout ( @Req ( ) req : Request , @Res ( { passthrough : true } ) res : Response ) {
145+ const token = req . cookies ?. [ REFRESH_COOKIE_NAME ] ;
146+ if ( token ) {
147+ try {
148+ const payload : any = ( this . authService as any ) . jwtService . verify ( token ) ;
149+ await this . authService . revokeRefreshToken ( payload . sub ) ;
150+ } catch ( e : any ) {
151+ // ignore verification errors during logout
152+ console . log ( e ) ;
153+ }
154+ }
155+
156+ // clear cookie with same attributes so browser accepts deletion
157+ res . clearCookie ( REFRESH_COOKIE_NAME , { ...COOKIE_OPTIONS , path : "/" } ) ;
158+ return { ok : true } ;
159+ }
45160}
0 commit comments