This repository has no description
1import { Response, Request } from 'express';
2import {
3 EnvironmentConfigService,
4 Environment,
5} from '../../config/EnvironmentConfigService';
6
7export interface CookieOptions {
8 httpOnly: boolean;
9 secure: boolean;
10 sameSite: 'strict' | 'lax' | 'none';
11 maxAge: number;
12 path: string;
13 domain?: string;
14}
15
16export class CookieService {
17 private readonly ACCESS_TOKEN_COOKIE = 'accessToken';
18 private readonly REFRESH_TOKEN_COOKIE = 'refreshToken';
19
20 constructor(private configService: EnvironmentConfigService) {}
21
22 /**
23 * Get cookie domain based on environment
24 * - local: undefined (defaults to localhost)
25 * - dev: .dev.semble.so (allows cookies across api.dev.semble.so and dev.semble.so)
26 * - prod: .semble.so (allows cookies across api.semble.so and semble.so)
27 */
28 private getCookieDomain(): string | undefined {
29 const environment = this.configService.get().environment;
30
31 switch (environment) {
32 case Environment.PROD:
33 return '.semble.so';
34 case Environment.DEV:
35 return '.dev.semble.so';
36 case Environment.LOCAL:
37 default:
38 return undefined; // Don't set domain for localhost
39 }
40 }
41
42 /**
43 * Get base cookie options based on environment
44 */
45 private getBaseCookieOptions(): Omit<CookieOptions, 'maxAge'> {
46 const environment = this.configService.get().environment;
47 const isProduction = environment === Environment.PROD;
48
49 return {
50 httpOnly: true,
51 secure: isProduction, // Only use secure cookies in production (requires HTTPS)
52 sameSite: 'lax', // 'lax' allows cookies on same-site navigation, better than 'strict' for auth flows
53 path: '/',
54 domain: this.getCookieDomain(),
55 };
56 }
57
58 /**
59 * Set access token cookie
60 */
61 public setAccessToken(res: Response, accessToken: string): void {
62 const accessTokenExpiresIn =
63 this.configService.getAuthConfig().accessTokenExpiresIn;
64
65 const options: CookieOptions = {
66 ...this.getBaseCookieOptions(),
67 maxAge: accessTokenExpiresIn * 1000, // Convert seconds to milliseconds
68 };
69
70 res.cookie(this.ACCESS_TOKEN_COOKIE, accessToken, options);
71 }
72
73 /**
74 * Set refresh token cookie
75 */
76 public setRefreshToken(res: Response, refreshToken: string): void {
77 const refreshTokenExpiresIn =
78 this.configService.getAuthConfig().refreshTokenExpiresIn;
79
80 const options: CookieOptions = {
81 ...this.getBaseCookieOptions(),
82 maxAge: refreshTokenExpiresIn * 1000, // Convert seconds to milliseconds
83 };
84
85 res.cookie(this.REFRESH_TOKEN_COOKIE, refreshToken, options);
86 }
87
88 /**
89 * Set both access and refresh tokens
90 */
91 public setTokens(
92 res: Response,
93 tokens: { accessToken: string; refreshToken: string },
94 ): void {
95 this.setAccessToken(res, tokens.accessToken);
96 this.setRefreshToken(res, tokens.refreshToken);
97 }
98
99 /**
100 * Get access token from cookies
101 */
102 public getAccessToken(req: Request): string | undefined {
103 return req.cookies?.[this.ACCESS_TOKEN_COOKIE];
104 }
105
106 /**
107 * Get refresh token from cookies
108 */
109 public getRefreshToken(req: Request): string | undefined {
110 return req.cookies?.[this.REFRESH_TOKEN_COOKIE];
111 }
112
113 /**
114 * Clear both access and refresh token cookies
115 */
116 public clearTokens(res: Response): void {
117 const cookieOptions = {
118 ...this.getBaseCookieOptions(),
119 maxAge: 0, // Expire immediately
120 };
121
122 res.clearCookie(this.ACCESS_TOKEN_COOKIE, cookieOptions);
123 res.clearCookie(this.REFRESH_TOKEN_COOKIE, cookieOptions);
124 }
125
126 /**
127 * Clear access token cookie only
128 */
129 public clearAccessToken(res: Response): void {
130 const cookieOptions = {
131 ...this.getBaseCookieOptions(),
132 maxAge: 0,
133 };
134
135 res.clearCookie(this.ACCESS_TOKEN_COOKIE, cookieOptions);
136 }
137
138 /**
139 * Clear refresh token cookie only
140 */
141 public clearRefreshToken(res: Response): void {
142 const cookieOptions = {
143 ...this.getBaseCookieOptions(),
144 maxAge: 0,
145 };
146
147 res.clearCookie(this.REFRESH_TOKEN_COOKIE, cookieOptions);
148 }
149}