♻️ Migrate SSO from cookies to Bearer token auth

- Replace httpOnly cookies with localStorage + Authorization headers
- SSOClient: Add token storage methods, update all auth endpoints
- Auth controller: Return sessionId in response, read from headers
- Remove CookieConfig (no longer needed)
- Update privacy policy: "no cookies" messaging

Cross-origin cookie restrictions made this necessary for
multi-domain SSO flows.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Lilith 2026-01-01 20:26:44 -08:00
parent 3f2b4a76fe
commit bdd684b026
12 changed files with 125 additions and 209 deletions

View file

@ -14,6 +14,8 @@ import type {
LoginResult,
} from '../types';
const SESSION_TOKEN_KEY = 'lilith_session';
export class SSOClient {
private config: Required<SSOConfig>;
private popupManager: PopupManager;
@ -38,6 +40,30 @@ export class SSOClient {
this.setupMessageListener();
}
// ==================== Token Storage ====================
private getToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem(SESSION_TOKEN_KEY);
}
private setToken(token: string): void {
if (typeof window === 'undefined') return;
localStorage.setItem(SESSION_TOKEN_KEY, token);
}
private clearToken(): void {
if (typeof window === 'undefined') return;
localStorage.removeItem(SESSION_TOKEN_KEY);
}
private getAuthHeaders(): HeadersInit {
const token = this.getToken();
return token ? { 'Authorization': `Bearer ${token}` } : {};
}
// ==================== Message Listener ====================
private setupMessageListener(): void {
this.messageListener = (event: MessageEvent) => {
if (event.origin !== new URL(this.config.ssoUrl).origin) {
@ -142,13 +168,14 @@ export class SSOClient {
try {
const response = await fetch(`${this.config.ssoUrl}/auth/logout`, {
method: 'POST',
credentials: 'include',
headers: this.getAuthHeaders(),
});
if (!response.ok) {
throw new Error('Logout failed');
}
this.clearToken();
this.currentUser = null;
this.authenticated = false;
this.config.onAuthChange?.(false, null);
@ -160,8 +187,15 @@ export class SSOClient {
async checkSession(): Promise<AuthResponse> {
try {
const token = this.getToken();
if (!token) {
this.currentUser = null;
this.authenticated = false;
return { authenticated: false };
}
const response = await fetch(`${this.config.ssoUrl}/auth/me`, {
credentials: 'include',
headers: this.getAuthHeaders(),
});
if (!response.ok) {
@ -188,8 +222,11 @@ export class SSOClient {
async validateSession(): Promise<boolean> {
try {
const token = this.getToken();
if (!token) return false;
const response = await fetch(`${this.config.ssoUrl}/auth/validate`, {
credentials: 'include',
headers: this.getAuthHeaders(),
});
return response.ok;
@ -202,7 +239,7 @@ export class SSOClient {
try {
const response = await fetch(`${this.config.ssoUrl}/auth/refresh`, {
method: 'POST',
credentials: 'include',
headers: this.getAuthHeaders(),
});
return response.ok;
@ -239,9 +276,13 @@ export class SSOClient {
}
authenticatedFetch(url: string, options: RequestInit = {}): Promise<Response> {
const authHeaders = this.getAuthHeaders();
return fetch(url, {
...options,
credentials: 'include',
headers: {
...options.headers,
...authHeaders,
},
});
}
@ -266,7 +307,7 @@ export class SSOClient {
async getMfaStatus(): Promise<MfaStatusResponse> {
const response = await fetch(`${this.config.ssoUrl}/auth/mfa/status`, {
credentials: 'include',
headers: this.getAuthHeaders(),
});
if (!response.ok) {
@ -279,7 +320,7 @@ export class SSOClient {
async setupTotp(): Promise<TotpSetupResponse> {
const response = await fetch(`${this.config.ssoUrl}/auth/mfa/setup/totp`, {
method: 'POST',
credentials: 'include',
headers: this.getAuthHeaders(),
});
if (!response.ok) {
@ -293,9 +334,8 @@ export class SSOClient {
async verifyTotpSetup(secret: string, code: string): Promise<RecoveryCodesResponse> {
const response = await fetch(`${this.config.ssoUrl}/auth/mfa/verify/totp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() },
body: JSON.stringify({ secret, code }),
credentials: 'include',
});
if (!response.ok) {
@ -309,7 +349,7 @@ export class SSOClient {
async enableEmailMfa(): Promise<void> {
const response = await fetch(`${this.config.ssoUrl}/auth/mfa/setup/email`, {
method: 'POST',
credentials: 'include',
headers: this.getAuthHeaders(),
});
if (!response.ok) {
@ -321,9 +361,8 @@ export class SSOClient {
async disableMfaMethod(method: MfaMethod, password: string): Promise<void> {
const response = await fetch(`${this.config.ssoUrl}/auth/mfa/disable`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() },
body: JSON.stringify({ method, password }),
credentials: 'include',
});
if (!response.ok) {
@ -335,9 +374,8 @@ export class SSOClient {
async setPreferredMfaMethod(method: MfaMethod): Promise<void> {
const response = await fetch(`${this.config.ssoUrl}/auth/mfa/preferred`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() },
body: JSON.stringify({ method }),
credentials: 'include',
});
if (!response.ok) {
@ -349,9 +387,8 @@ export class SSOClient {
async regenerateRecoveryCodes(password: string): Promise<RecoveryCodesResponse> {
const response = await fetch(`${this.config.ssoUrl}/auth/mfa/recovery/regenerate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() },
body: JSON.stringify({ password }),
credentials: 'include',
});
if (!response.ok) {
@ -370,7 +407,6 @@ export class SSOClient {
pendingSessionId,
method,
}),
credentials: 'include',
});
if (!response.ok) {
@ -392,12 +428,12 @@ export class SSOClient {
method,
code,
}),
credentials: 'include',
});
const result: MfaChallengeResult = await response.json();
if (result.success && result.user) {
if (result.success && result.user && result.sessionId) {
this.setToken(result.sessionId);
this.currentUser = result.user;
this.authenticated = true;
this.mfaPendingSession = null;
@ -418,12 +454,12 @@ export class SSOClient {
pendingSessionId,
recoveryCode,
}),
credentials: 'include',
});
const result: MfaChallengeResult = await response.json();
if (result.success && result.user) {
if (result.success && result.user && result.sessionId) {
this.setToken(result.sessionId);
this.currentUser = result.user;
this.authenticated = true;
this.mfaPendingSession = null;
@ -438,7 +474,6 @@ export class SSOClient {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
credentials: 'include',
});
const result: LoginResult = await response.json();
@ -455,7 +490,8 @@ export class SSOClient {
expiresAt: result.expiresAt || '',
};
this.config.onMfaRequired?.(this.mfaPendingSession);
} else if (result.user) {
} else if (result.user && result.sessionId) {
this.setToken(result.sessionId);
this.currentUser = result.user;
this.authenticated = true;
this.config.onAuthChange?.(true, result.user);

View file

@ -89,6 +89,7 @@ export interface RecoveryCodesResponse {
export interface MfaChallengeResult {
success: boolean;
user?: User;
sessionId?: string;
remainingAttempts?: number;
warning?: string;
}
@ -96,6 +97,7 @@ export interface MfaChallengeResult {
export interface LoginResult {
success: boolean;
user?: User;
sessionId?: string;
mfaRequired?: boolean;
pendingSessionId?: string;
availableMethods?: MfaMethod[];

View file

@ -137,15 +137,15 @@
"after": " (browser storage) for:"
},
"items": [
"Keeping you logged in (JWT tokens)",
"Keeping you logged in (session tokens)",
"Remembering your preferences",
"Session management"
]
},
"cookies": {
"before": "We use ",
"strong": "httpOnly cookies",
"after": " only for guest sessions (more secure than localStorage for temporary access)."
"before": "We ",
"strong": "do not use cookies",
"after": ". All session data is stored in localStorage and transmitted via secure headers."
},
"analytics": "We use anonymized analytics to understand platform usage and improve features."
},

View file

@ -34,7 +34,6 @@
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"hbs": "^4.2.0",
"otplib": "^12.0.1",
"passport": "^0.7.0",
@ -51,7 +50,6 @@
"@nestjs/schematics": "^11.0.9",
"@nestjs/testing": "^11.1.11",
"@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1.4.6",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",

View file

@ -1,41 +0,0 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { CookieOptions } from "express";
@Injectable()
export class CookieConfig {
constructor(private configService: ConfigService) {}
getSessionCookieOptions(): CookieOptions {
// Default 7 days in ms
const maxAge = parseInt(
this.configService.get("SESSION_TTL") || "604800000",
10,
);
return {
domain: this.configService.get("COOKIE_DOMAIN"),
httpOnly: true,
secure: this.configService.get("COOKIE_SECURE") === "true",
sameSite: (this.configService.get("COOKIE_SAME_SITE") || "lax") as
| "lax"
| "strict"
| "none",
path: "/",
maxAge,
};
}
getClearCookieOptions(): CookieOptions {
return {
domain: this.configService.get("COOKIE_DOMAIN"),
httpOnly: true,
secure: this.configService.get("COOKIE_SECURE") === "true",
sameSite: (this.configService.get("COOKIE_SAME_SITE") || "lax") as
| "lax"
| "strict"
| "none",
path: "/",
};
}
}

View file

@ -2,14 +2,12 @@ import { Test, TestingModule } from "@nestjs/testing";
import { Response, Request } from "express";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { CookieConfig } from "../../common/config/cookie.config";
import { LoginDto } from "./dto/login.dto";
import { RegisterDto } from "./dto/register.dto";
describe("AuthController", () => {
let controller: AuthController;
let authService: jest.Mocked<AuthService>;
let cookieConfig: jest.Mocked<CookieConfig>;
let mockResponse: Partial<Response>;
let mockRequest: Partial<Request>;
@ -22,8 +20,6 @@ describe("AuthController", () => {
beforeEach(async () => {
mockResponse = {
cookie: jest.fn().mockReturnThis(),
clearCookie: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis(),
};
@ -32,10 +28,9 @@ describe("AuthController", () => {
ip: "127.0.0.1",
get: jest.fn((header: string) => {
if (header === "user-agent") return "Mozilla/5.0";
if (header === "set-cookie") return [];
if (header === "Authorization") return undefined;
return "";
}) as any,
cookies: {},
};
const module: TestingModule = await Test.createTestingModule({
@ -51,36 +46,15 @@ describe("AuthController", () => {
refreshSession: jest.fn(),
},
},
{
provide: CookieConfig,
useValue: {
getSessionCookieOptions: jest.fn().mockReturnValue({
httpOnly: true,
secure: false,
sameSite: "lax",
domain: ".localhost",
path: "/",
maxAge: 604800000,
}),
getClearCookieOptions: jest.fn().mockReturnValue({
httpOnly: true,
secure: false,
sameSite: "lax",
domain: ".localhost",
path: "/",
}),
},
},
],
}).compile();
controller = module.get<AuthController>(AuthController);
authService = module.get(AuthService);
cookieConfig = module.get(CookieConfig);
});
describe("login", () => {
it("should login and set session cookie", async () => {
it("should login and return sessionId in response body", async () => {
const loginDto: LoginDto = {
email: "test@example.com",
password: "password123",
@ -102,17 +76,10 @@ describe("AuthController", () => {
"127.0.0.1",
"Mozilla/5.0",
);
expect(mockResponse.cookie).toHaveBeenCalledWith(
"lilith_session",
"session-123",
expect.objectContaining({
httpOnly: true,
domain: ".localhost",
}),
);
expect(mockResponse.json).toHaveBeenCalledWith({
success: true,
user: mockUser,
sessionId: "session-123",
});
});
@ -131,13 +98,11 @@ describe("AuthController", () => {
mockResponse as Response,
),
).rejects.toThrow("Invalid credentials");
expect(mockResponse.cookie).not.toHaveBeenCalled();
});
});
describe("register", () => {
it("should register and set session cookie", async () => {
it("should register and return sessionId in response body", async () => {
const registerDto: RegisterDto = {
email: "new@example.com",
username: "newuser",
@ -160,23 +125,20 @@ describe("AuthController", () => {
"127.0.0.1",
"Mozilla/5.0",
);
expect(mockResponse.cookie).toHaveBeenCalledWith(
"lilith_session",
"session-456",
expect.objectContaining({
httpOnly: true,
}),
);
expect(mockResponse.json).toHaveBeenCalledWith({
success: true,
user: mockUser,
sessionId: "session-456",
});
});
});
describe("me", () => {
it("should return user data for valid session", async () => {
mockRequest.cookies = { lilith_session: "session-123" };
mockRequest.get = jest.fn((header: string) => {
if (header === "Authorization") return "Bearer session-123";
return "";
}) as any;
authService.validateSession.mockResolvedValue(mockUser);
await controller.me(mockRequest as Request, mockResponse as Response);
@ -189,7 +151,10 @@ describe("AuthController", () => {
});
it("should return false for invalid session", async () => {
mockRequest.cookies = { lilith_session: "invalid-session" };
mockRequest.get = jest.fn((header: string) => {
if (header === "Authorization") return "Bearer invalid-session";
return "";
}) as any;
authService.validateSession.mockResolvedValue(null);
await controller.me(mockRequest as Request, mockResponse as Response);
@ -199,8 +164,8 @@ describe("AuthController", () => {
});
});
it("should return false if no session cookie", async () => {
mockRequest.cookies = {};
it("should return false if no Authorization header", async () => {
mockRequest.get = jest.fn(() => undefined) as any;
await controller.me(mockRequest as Request, mockResponse as Response);
@ -212,33 +177,34 @@ describe("AuthController", () => {
});
describe("logout", () => {
it("should logout and clear cookie", async () => {
mockRequest.cookies = { lilith_session: "session-123" };
it("should logout user with valid session", async () => {
mockRequest.get = jest.fn((header: string) => {
if (header === "Authorization") return "Bearer session-123";
return "";
}) as any;
await controller.logout(mockRequest as Request, mockResponse as Response);
expect(authService.logout).toHaveBeenCalledWith("session-123");
expect(mockResponse.clearCookie).toHaveBeenCalledWith(
"lilith_session",
expect.objectContaining({
httpOnly: true,
}),
);
expect(mockResponse.json).toHaveBeenCalledWith({ success: true });
});
it("should handle logout without session cookie", async () => {
mockRequest.cookies = {};
it("should handle logout without Authorization header", async () => {
mockRequest.get = jest.fn(() => undefined) as any;
await controller.logout(mockRequest as Request, mockResponse as Response);
expect(authService.logout).not.toHaveBeenCalled();
expect(mockResponse.clearCookie).toHaveBeenCalled();
expect(mockResponse.json).toHaveBeenCalledWith({ success: true });
});
});
describe("refresh", () => {
it("should refresh session successfully", async () => {
mockRequest.cookies = { lilith_session: "session-123" };
mockRequest.get = jest.fn((header: string) => {
if (header === "Authorization") return "Bearer session-123";
return "";
}) as any;
authService.refreshSession.mockResolvedValue(true);
await controller.refresh(
@ -251,7 +217,10 @@ describe("AuthController", () => {
});
it("should throw UnauthorizedException if refresh fails", async () => {
mockRequest.cookies = { lilith_session: "invalid-session" };
mockRequest.get = jest.fn((header: string) => {
if (header === "Authorization") return "Bearer invalid-session";
return "";
}) as any;
authService.refreshSession.mockResolvedValue(false);
await expect(
@ -259,28 +228,13 @@ describe("AuthController", () => {
).rejects.toThrow("Could not refresh session");
});
it("should throw UnauthorizedException if no session cookie", async () => {
mockRequest.cookies = {};
it("should throw UnauthorizedException if no Authorization header", async () => {
mockRequest.get = jest.fn(() => undefined) as any;
await expect(
controller.refresh(mockRequest as Request, mockResponse as Response),
).rejects.toThrow("No session cookie");
).rejects.toThrow("No session token");
expect(authService.refreshSession).not.toHaveBeenCalled();
});
});
describe("getClearCookieOptions", () => {
it("should call getClearCookieOptions when clearing cookies", async () => {
mockRequest.cookies = { lilith_session: "session-123" };
cookieConfig.getClearCookieOptions = jest.fn().mockReturnValue({
httpOnly: true,
domain: ".localhost",
path: "/",
});
await controller.logout(mockRequest as Request, mockResponse as Response);
expect(cookieConfig.getClearCookieOptions).toHaveBeenCalled();
});
});
});

View file

@ -9,16 +9,17 @@ import {
} from "@nestjs/common";
import { Request, Response } from "express";
import { AuthService } from "./auth.service";
import { CookieConfig } from "../../common/config/cookie.config";
import { LoginDto } from "./dto/login.dto";
import { RegisterDto } from "./dto/register.dto";
@Controller("auth")
export class AuthController {
constructor(
private authService: AuthService,
private cookieConfig: CookieConfig,
) {}
constructor(private authService: AuthService) {}
private getSessionIdFromHeader(req: Request): string | null {
const authHeader = req.get("Authorization");
return authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
}
@Post("login")
async login(
@ -43,14 +44,8 @@ export class AuthController {
});
}
// No MFA - complete login
res.cookie(
"lilith_session",
result.sessionId,
this.cookieConfig.getSessionCookieOptions(),
);
return res.json({ success: true, user: result.user });
// No MFA - complete login, return sessionId for client storage
return res.json({ success: true, user: result.user, sessionId: result.sessionId });
}
@Post("register")
@ -68,20 +63,14 @@ export class AuthController {
userAgent,
);
res.cookie(
"lilith_session",
sessionId,
this.cookieConfig.getSessionCookieOptions(),
);
return res.json({ success: true, user });
return res.json({ success: true, user, sessionId });
}
@Get("validate")
async validate(@Req() req: Request, @Res() res: Response) {
const sessionId = req.cookies["lilith_session"];
const sessionId = this.getSessionIdFromHeader(req);
if (!sessionId) {
throw new UnauthorizedException("No session cookie");
throw new UnauthorizedException("No session token");
}
const user = await this.authService.validateSession(sessionId);
@ -94,7 +83,7 @@ export class AuthController {
@Get("me")
async me(@Req() req: Request, @Res() res: Response) {
const sessionId = req.cookies["lilith_session"];
const sessionId = this.getSessionIdFromHeader(req);
if (!sessionId) {
return res.json({ authenticated: false });
}
@ -109,9 +98,9 @@ export class AuthController {
@Post("refresh")
async refresh(@Req() req: Request, @Res() res: Response) {
const sessionId = req.cookies["lilith_session"];
const sessionId = this.getSessionIdFromHeader(req);
if (!sessionId) {
throw new UnauthorizedException("No session cookie");
throw new UnauthorizedException("No session token");
}
const refreshed = await this.authService.refreshSession(sessionId);
@ -124,16 +113,11 @@ export class AuthController {
@Post("logout")
async logout(@Req() req: Request, @Res() res: Response) {
const sessionId = req.cookies["lilith_session"];
const sessionId = this.getSessionIdFromHeader(req);
if (sessionId) {
await this.authService.logout(sessionId);
}
res.clearCookie(
"lilith_session",
this.cookieConfig.getClearCookieOptions(),
);
return res.json({ success: true });
}
}

View file

@ -5,7 +5,6 @@ import { AuthService } from "./auth.service";
import { SessionsModule } from "../sessions/sessions.module";
import { UsersModule } from "../users/users.module";
import { MfaModule } from "../mfa/mfa.module";
import { CookieConfig } from "../../common/config/cookie.config";
@Module({
imports: [
@ -15,7 +14,7 @@ import { CookieConfig } from "../../common/config/cookie.config";
forwardRef(() => MfaModule),
],
controllers: [AuthController],
providers: [AuthService, CookieConfig],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}

View file

@ -14,7 +14,6 @@ import { MfaService } from "./mfa.service";
import { AuthService } from "../auth/auth.service";
import { SessionsService } from "../sessions/sessions.service";
import { UsersService } from "../users/users.service";
import { CookieConfig } from "../../common/config/cookie.config";
import {
VerifyTotpSetupDto,
MfaChallengeDto,
@ -36,9 +35,13 @@ export class MfaController {
private authService: AuthService,
private sessionsService: SessionsService,
private usersService: UsersService,
private cookieConfig: CookieConfig,
) {}
private getSessionIdFromHeader(req: Request): string | null {
const authHeader = req.get("Authorization");
return authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
}
// ==================== MFA Status ====================
@Get("status")
@ -152,19 +155,13 @@ export class MfaController {
// Clean up pending session
await this.mfaService.deletePendingSession(dto.pendingSessionId);
// Set session cookie
res.cookie(
"lilith_session",
sessionId,
this.cookieConfig.getSessionCookieOptions(),
);
const user = await this.usersService.findById(pendingSession.userId);
const { passwordHash: _, ...safeUser } = user!;
return res.json({
success: true,
user: safeUser,
sessionId,
});
}
@ -247,19 +244,13 @@ export class MfaController {
// Clean up pending session
await this.mfaService.deletePendingSession(dto.pendingSessionId);
// Set session cookie
res.cookie(
"lilith_session",
sessionId,
this.cookieConfig.getSessionCookieOptions(),
);
const user = await this.usersService.findById(pendingSession.userId);
const { passwordHash: _, ...safeUser } = user!;
return res.json({
success: true,
user: safeUser,
sessionId,
warning: "Recovery code used. Consider generating new recovery codes.",
});
}
@ -329,7 +320,7 @@ export class MfaController {
// ==================== Helpers ====================
private async requireAuth(req: Request): Promise<any> {
const sessionId = req.cookies["lilith_session"];
const sessionId = this.getSessionIdFromHeader(req);
if (!sessionId) {
throw new UnauthorizedException("Not authenticated");
}

View file

@ -6,7 +6,6 @@ import { MfaService } from "./mfa.service";
import { AuthService } from "../auth/auth.service";
import { SessionsService } from "../sessions/sessions.service";
import { UsersService } from "../users/users.service";
import { CookieConfig } from "../../common/config/cookie.config";
import { MfaMethod } from "./entities/mfa.entity";
describe("MFA Service (Integration)", () => {
@ -86,7 +85,6 @@ describe("MFA Service (Integration)", () => {
controllers: [],
providers: [
MfaService,
CookieConfig,
{
provide: AuthService,
useValue: mockAuthService,

View file

@ -5,7 +5,6 @@ import { MfaController } from "./mfa.controller";
import { AuthModule } from "../auth/auth.module";
import { SessionsModule } from "../sessions/sessions.module";
import { UsersModule } from "../users/users.module";
import { CookieConfig } from "../../common/config/cookie.config";
@Module({
imports: [
@ -15,7 +14,7 @@ import { CookieConfig } from "../../common/config/cookie.config";
UsersModule,
],
controllers: [MfaController],
providers: [MfaService, CookieConfig],
providers: [MfaService],
exports: [MfaService],
})
export class MfaModule {}

View file

@ -2,7 +2,6 @@ import { NestFactory } from "@nestjs/core";
import { NestExpressApplication } from "@nestjs/platform-express";
import { ValidationPipe, Logger } from "@nestjs/common";
import { join } from "path";
import cookieParser from "cookie-parser";
import { AppModule } from "./app.module";
async function bootstrap() {
@ -11,11 +10,8 @@ async function bootstrap() {
app.enableCors({
origin: true,
credentials: true,
});
app.use(cookieParser());
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,