security: add helmet, rate limiting, webhook auth, fix token storage, restrict hard deletes

- Add helmet for HTTP security headers (CSP, HSTS, X-Frame-Options, etc.)
- Add @nestjs/throttler for rate limiting (100 req/60s per IP)
- Add shared secret validation on Signal webhook endpoint
- Remove JWT token from localStorage, use Auth0 SDK memory cache
  with async getAccessTokenSilently() in API interceptor
- Restrict hard delete (?hard=true) to ADMINISTRATOR role in service layer
- Replace exposed Anthropic API key with placeholder in .env

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 18:30:14 +01:00
parent 8e88880838
commit 934464bf8e
18 changed files with 132 additions and 55 deletions

View File

@@ -37,4 +37,4 @@ AVIATIONSTACK_API_KEY="your-aviationstack-api-key"
# ============================================ # ============================================
# Get API key from: https://console.anthropic.com/ # Get API key from: https://console.anthropic.com/
# Cost: ~$3 per million tokens # Cost: ~$3 per million tokens
ANTHROPIC_API_KEY="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" ANTHROPIC_API_KEY="your-anthropic-api-key"

View File

@@ -20,11 +20,13 @@
"@nestjs/passport": "^10.0.3", "@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.3.0", "@nestjs/platform-express": "^10.3.0",
"@nestjs/schedule": "^4.1.2", "@nestjs/schedule": "^4.1.2",
"@nestjs/throttler": "^6.5.0",
"@prisma/client": "^5.8.1", "@prisma/client": "^5.8.1",
"@types/pdfkit": "^0.17.4", "@types/pdfkit": "^0.17.4",
"axios": "^1.6.5", "axios": "^1.6.5",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"helmet": "^8.1.0",
"ics": "^3.8.1", "ics": "^3.8.1",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"jwks-rsa": "^3.1.0", "jwks-rsa": "^3.1.0",
@@ -782,7 +784,6 @@
"resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.8.0.tgz", "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.8.0.tgz",
"integrity": "sha512-Ipt4mzI4gSgnomFdaPjaLgY2MWuXqAEZLrU6qqWBB7khGiBBuuEp6ytYDnq09bRXqcjaeeHiaCvCGFbBA2SpvA==", "integrity": "sha512-Ipt4mzI4gSgnomFdaPjaLgY2MWuXqAEZLrU6qqWBB7khGiBBuuEp6ytYDnq09bRXqcjaeeHiaCvCGFbBA2SpvA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ucast/mongo2js": "^1.3.0" "@ucast/mongo2js": "^1.3.0"
}, },
@@ -2010,6 +2011,17 @@
} }
} }
}, },
"node_modules/@nestjs/throttler": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz",
"integrity": "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"reflect-metadata": "^0.1.13 || ^0.2.0"
}
},
"node_modules/@noble/hashes": { "node_modules/@noble/hashes": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
@@ -2119,7 +2131,6 @@
"integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"engines": { "engines": {
"node": ">=16.13" "node": ">=16.13"
}, },
@@ -5851,6 +5862,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/helmet": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/html-escaper": { "node_modules/html-escaper": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",

View File

@@ -35,11 +35,13 @@
"@nestjs/passport": "^10.0.3", "@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.3.0", "@nestjs/platform-express": "^10.3.0",
"@nestjs/schedule": "^4.1.2", "@nestjs/schedule": "^4.1.2",
"@nestjs/throttler": "^6.5.0",
"@prisma/client": "^5.8.1", "@prisma/client": "^5.8.1",
"@types/pdfkit": "^0.17.4", "@types/pdfkit": "^0.17.4",
"axios": "^1.6.5", "axios": "^1.6.5",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"helmet": "^8.1.0",
"ics": "^3.8.1", "ics": "^3.8.1",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"jwks-rsa": "^3.1.0", "jwks-rsa": "^3.1.0",

View File

@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core'; import { APP_GUARD } from '@nestjs/core';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { PrismaModule } from './prisma/prisma.module'; import { PrismaModule } from './prisma/prisma.module';
@@ -26,6 +27,12 @@ import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
envFilePath: '.env', envFilePath: '.env',
}), }),
// Rate limiting: 100 requests per 60 seconds per IP
ThrottlerModule.forRoot([{
ttl: 60000,
limit: 100,
}]),
// Core modules // Core modules
PrismaModule, PrismaModule,
AuthModule, AuthModule,
@@ -51,6 +58,11 @@ import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
provide: APP_GUARD, provide: APP_GUARD,
useClass: JwtAuthGuard, useClass: JwtAuthGuard,
}, },
// Apply rate limiting globally
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
], ],
}) })
export class AppModule {} export class AppModule {}

View File

@@ -220,8 +220,9 @@ export class DriversController {
remove( remove(
@Param('id') id: string, @Param('id') id: string,
@Query('hard') hard?: string, @Query('hard') hard?: string,
@CurrentUser() user?: any,
) { ) {
const isHardDelete = hard === 'true'; const isHardDelete = hard === 'true';
return this.driversService.remove(id, isHardDelete); return this.driversService.remove(id, isHardDelete, user?.role);
} }
} }

View File

@@ -1,4 +1,4 @@
import { Injectable, NotFoundException, Logger } from '@nestjs/common'; import { Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { CreateDriverDto, UpdateDriverDto } from './dto'; import { CreateDriverDto, UpdateDriverDto } from './dto';
@@ -78,7 +78,11 @@ export class DriversService {
}); });
} }
async remove(id: string, hardDelete = false) { async remove(id: string, hardDelete = false, userRole?: string) {
if (hardDelete && userRole !== 'ADMINISTRATOR') {
throw new ForbiddenException('Only administrators can permanently delete records');
}
const driver = await this.findOne(id); const driver = await this.findOne(id);
if (hardDelete) { if (hardDelete) {

View File

@@ -13,6 +13,7 @@ import { EventsService } from './events.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard'; import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator'; import { Roles } from '../auth/decorators/roles.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Role } from '@prisma/client'; import { Role } from '@prisma/client';
import { CreateEventDto, UpdateEventDto, UpdateEventStatusDto } from './dto'; import { CreateEventDto, UpdateEventDto, UpdateEventStatusDto } from './dto';
@@ -59,8 +60,9 @@ export class EventsController {
remove( remove(
@Param('id') id: string, @Param('id') id: string,
@Query('hard') hard?: string, @Query('hard') hard?: string,
@CurrentUser() user?: any,
) { ) {
const isHardDelete = hard === 'true'; const isHardDelete = hard === 'true';
return this.eventsService.remove(id, isHardDelete); return this.eventsService.remove(id, isHardDelete, user?.role);
} }
} }

View File

@@ -2,6 +2,7 @@ import {
Injectable, Injectable,
NotFoundException, NotFoundException,
BadRequestException, BadRequestException,
ForbiddenException,
Logger, Logger,
} from '@nestjs/common'; } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
@@ -248,7 +249,11 @@ export class EventsService {
return this.enrichEventWithVips(updatedEvent); return this.enrichEventWithVips(updatedEvent);
} }
async remove(id: string, hardDelete = false) { async remove(id: string, hardDelete = false, userRole?: string) {
if (hardDelete && userRole !== 'ADMINISTRATOR') {
throw new ForbiddenException('Only administrators can permanently delete records');
}
const event = await this.findOne(id); const event = await this.findOne(id);
if (hardDelete) { if (hardDelete) {

View File

@@ -1,6 +1,7 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { ValidationPipe, Logger } from '@nestjs/common'; import { ValidationPipe, Logger } from '@nestjs/common';
import { json, urlencoded } from 'express'; import { json, urlencoded } from 'express';
import helmet from 'helmet';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { AllExceptionsFilter, HttpExceptionFilter } from './common/filters'; import { AllExceptionsFilter, HttpExceptionFilter } from './common/filters';
@@ -9,6 +10,9 @@ async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
// Security headers
app.use(helmet());
// Increase body size limit for PDF attachments (base64 encoded) // Increase body size limit for PDF attachments (base64 encoded)
app.use(json({ limit: '5mb' })); app.use(json({ limit: '5mb' }));
app.use(urlencoded({ extended: true, limit: '5mb' })); app.use(urlencoded({ extended: true, limit: '5mb' }));

View File

@@ -6,11 +6,12 @@ import {
Body, Body,
Param, Param,
Query, Query,
Req,
UseGuards, UseGuards,
Logger, Logger,
Res, Res,
} from '@nestjs/common'; } from '@nestjs/common';
import { Response } from 'express'; import { Request, Response } from 'express';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard'; import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator'; import { Roles } from '../auth/decorators/roles.decorator';
@@ -105,7 +106,14 @@ export class MessagesController {
*/ */
@Public() @Public()
@Post('webhook') @Post('webhook')
async handleWebhook(@Body() payload: SignalWebhookPayload) { async handleWebhook(@Body() payload: SignalWebhookPayload, @Req() req: Request) {
// Validate webhook secret if configured
const secret = process.env.SIGNAL_WEBHOOK_SECRET;
if (secret && req.headers['x-webhook-secret'] !== secret) {
this.logger.warn('Webhook rejected: invalid or missing secret');
return { success: false, error: 'Unauthorized' };
}
this.logger.debug('Received Signal webhook:', JSON.stringify(payload)); this.logger.debug('Received Signal webhook:', JSON.stringify(payload));
try { try {

View File

@@ -13,6 +13,7 @@ import { VehiclesService } from './vehicles.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard'; import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator'; import { Roles } from '../auth/decorators/roles.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Role } from '@prisma/client'; import { Role } from '@prisma/client';
import { CreateVehicleDto, UpdateVehicleDto } from './dto'; import { CreateVehicleDto, UpdateVehicleDto } from './dto';
@@ -56,8 +57,12 @@ export class VehiclesController {
@Delete(':id') @Delete(':id')
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR) @Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
remove(@Param('id') id: string, @Query('hard') hard?: string) { remove(
@Param('id') id: string,
@Query('hard') hard?: string,
@CurrentUser() user?: any,
) {
const isHardDelete = hard === 'true'; const isHardDelete = hard === 'true';
return this.vehiclesService.remove(id, isHardDelete); return this.vehiclesService.remove(id, isHardDelete, user?.role);
} }
} }

View File

@@ -1,4 +1,4 @@
import { Injectable, NotFoundException, Logger } from '@nestjs/common'; import { Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { CreateVehicleDto, UpdateVehicleDto } from './dto'; import { CreateVehicleDto, UpdateVehicleDto } from './dto';
@@ -89,7 +89,11 @@ export class VehiclesService {
}); });
} }
async remove(id: string, hardDelete = false) { async remove(id: string, hardDelete = false, userRole?: string) {
if (hardDelete && userRole !== 'ADMINISTRATOR') {
throw new ForbiddenException('Only administrators can permanently delete records');
}
const vehicle = await this.findOne(id); const vehicle = await this.findOne(id);
if (hardDelete) { if (hardDelete) {

View File

@@ -13,6 +13,7 @@ import { VipsService } from './vips.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { AbilitiesGuard } from '../auth/guards/abilities.guard'; import { AbilitiesGuard } from '../auth/guards/abilities.guard';
import { CanCreate, CanRead, CanUpdate, CanDelete } from '../auth/decorators/check-ability.decorator'; import { CanCreate, CanRead, CanUpdate, CanDelete } from '../auth/decorators/check-ability.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { CreateVipDto, UpdateVipDto } from './dto'; import { CreateVipDto, UpdateVipDto } from './dto';
@Controller('vips') @Controller('vips')
@@ -49,9 +50,10 @@ export class VipsController {
remove( remove(
@Param('id') id: string, @Param('id') id: string,
@Query('hard') hard?: string, @Query('hard') hard?: string,
@CurrentUser() user?: any,
) { ) {
// Only administrators can hard delete // Only administrators can hard delete
const isHardDelete = hard === 'true'; const isHardDelete = hard === 'true';
return this.vipsService.remove(id, isHardDelete); return this.vipsService.remove(id, isHardDelete, user?.role);
} }
} }

View File

@@ -1,4 +1,4 @@
import { Injectable, NotFoundException, Logger } from '@nestjs/common'; import { Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { CreateVipDto, UpdateVipDto } from './dto'; import { CreateVipDto, UpdateVipDto } from './dto';
@@ -58,7 +58,11 @@ export class VipsService {
}); });
} }
async remove(id: string, hardDelete = false) { async remove(id: string, hardDelete = false, userRole?: string) {
if (hardDelete && userRole !== 'ADMINISTRATOR') {
throw new ForbiddenException('Only administrators can permanently delete records');
}
const vip = await this.findOne(id); const vip = await this.findOne(id);
if (hardDelete) { if (hardDelete) {

View File

@@ -80,6 +80,7 @@ services:
DATABASE_URL: postgresql://postgres:changeme@postgres:5432/vip_coordinator DATABASE_URL: postgresql://postgres:changeme@postgres:5432/vip_coordinator
REDIS_URL: redis://redis:6379 REDIS_URL: redis://redis:6379
SIGNAL_API_URL: http://signal-api:8080 SIGNAL_API_URL: http://signal-api:8080
SIGNAL_WEBHOOK_SECRET: ${SIGNAL_WEBHOOK_SECRET:-}
TRACCAR_API_URL: http://traccar:8082 TRACCAR_API_URL: http://traccar:8082
TRACCAR_DEVICE_PORT: 5055 TRACCAR_DEVICE_PORT: 5055
AUTH0_DOMAIN: ${AUTH0_DOMAIN} AUTH0_DOMAIN: ${AUTH0_DOMAIN}

View File

@@ -62,7 +62,7 @@ function App() {
scope: 'openid profile email offline_access', scope: 'openid profile email offline_access',
}} }}
useRefreshTokens={true} useRefreshTokens={true}
cacheLocation="localstorage" cacheLocation="memory"
> >
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<AuthProvider> <AuthProvider>

View File

@@ -1,6 +1,6 @@
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { useAuth0 } from '@auth0/auth0-react'; import { useAuth0 } from '@auth0/auth0-react';
import { api } from '@/lib/api'; import { api, setTokenGetter } from '@/lib/api';
interface BackendUser { interface BackendUser {
id: string; id: string;
@@ -40,9 +40,16 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const [fetchingUser, setFetchingUser] = useState(false); const [fetchingUser, setFetchingUser] = useState(false);
const [authError, setAuthError] = useState<string | null>(null); const [authError, setAuthError] = useState<string | null>(null);
// Set up token and fetch backend user profile // Wire up the API token getter so axios can fetch fresh tokens
useEffect(() => {
if (isAuthenticated) {
setTokenGetter(() => getAccessTokenSilently());
}
return () => setTokenGetter(null);
}, [isAuthenticated, getAccessTokenSilently]);
// Fetch backend user profile after authentication
useEffect(() => { useEffect(() => {
// Wait for Auth0 to finish loading before fetching token
if (isAuthenticated && !isLoading && !fetchingUser && !backendUser) { if (isAuthenticated && !isLoading && !fetchingUser && !backendUser) {
setFetchingUser(true); setFetchingUser(true);
setAuthError(null); setAuthError(null);
@@ -53,42 +60,28 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setFetchingUser(false); setFetchingUser(false);
}, 10000); // 10 second timeout }, 10000); // 10 second timeout
getAccessTokenSilently() // Fetch backend user profile (api interceptor handles token automatically)
.then(async (token) => { api.get('/auth/profile')
.then((response) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
console.log('[AUTH] Got access token, fetching user profile'); console.log('[AUTH] User profile fetched successfully:', response.data.email);
localStorage.setItem('auth0_token', token); setBackendUser(response.data);
setAuthError(null);
// Fetch backend user profile
try {
const response = await api.get('/auth/profile');
console.log('[AUTH] User profile fetched successfully:', response.data.email);
setBackendUser(response.data);
setAuthError(null);
} catch (error: any) {
console.error('[AUTH] Failed to fetch user profile:', error);
setBackendUser(null);
// Set specific error message
if (error.response?.status === 401) {
setAuthError('Your account is pending approval or your session has expired');
} else {
setAuthError('Failed to load user profile - please try logging in again');
}
}
}) })
.catch((error) => { .catch((error: any) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
console.error('[AUTH] Failed to get token:', error); console.error('[AUTH] Failed to fetch user profile:', error);
setBackendUser(null); setBackendUser(null);
// Handle specific Auth0 errors // Handle specific errors
if (error.error === 'missing_refresh_token' || error.message?.includes('Missing Refresh Token')) { if (error.response?.status === 401) {
setAuthError('Your account is pending approval or your session has expired');
} else if (error.error === 'missing_refresh_token' || error.message?.includes('Missing Refresh Token')) {
setAuthError('Session expired - please log in again'); setAuthError('Session expired - please log in again');
} else if (error.error === 'login_required') { } else if (error.error === 'login_required') {
setAuthError('Login required'); setAuthError('Login required');
} else { } else {
setAuthError('Authentication failed - please try logging in again'); setAuthError('Failed to load user profile - please try logging in again');
} }
}) })
.finally(() => { .finally(() => {
@@ -99,7 +92,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}, [isAuthenticated, isLoading]); }, [isAuthenticated, isLoading]);
const handleLogout = () => { const handleLogout = () => {
localStorage.removeItem('auth0_token'); setTokenGetter(null);
auth0Logout({ logoutParams: { returnTo: window.location.origin } }); auth0Logout({ logoutParams: { returnTo: window.location.origin } });
}; };

View File

@@ -20,11 +20,22 @@ export const copilotApi = axios.create({
timeout: 120000, // 2 minute timeout for AI requests timeout: 120000, // 2 minute timeout for AI requests
}); });
// Shared request interceptor function // Token getter function - set by AuthContext when authenticated
const requestInterceptor = (config: any) => { let getToken: (() => Promise<string>) | null = null;
const token = localStorage.getItem('auth0_token');
if (token) { export function setTokenGetter(getter: (() => Promise<string>) | null) {
config.headers.Authorization = `Bearer ${token}`; getToken = getter;
}
// Shared request interceptor function (async to support token refresh)
const requestInterceptor = async (config: any) => {
if (getToken) {
try {
const token = await getToken();
config.headers.Authorization = `Bearer ${token}`;
} catch {
// Token fetch failed - request goes without auth header
}
} }
if (DEBUG_MODE) { if (DEBUG_MODE) {
@@ -69,7 +80,6 @@ const responseErrorInterceptor = (error: any) => {
status: response.status, status: response.status,
statusText: response.statusText, statusText: response.statusText,
data: response.data, data: response.data,
requestData: config?.data,
} }
); );