Compare commits
2 Commits
5f4c474e37
...
934464bf8e
| Author | SHA1 | Date | |
|---|---|---|---|
| 934464bf8e | |||
| 8e88880838 |
@@ -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"
|
||||||
|
|||||||
39
backend/package-lock.json
generated
39
backend/package-lock.json
generated
@@ -11,7 +11,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.72.1",
|
"@anthropic-ai/sdk": "^0.72.1",
|
||||||
"@casl/ability": "^6.8.0",
|
"@casl/ability": "^6.8.0",
|
||||||
"@casl/prisma": "^1.6.1",
|
|
||||||
"@nestjs/axios": "^4.0.1",
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/common": "^10.3.0",
|
"@nestjs/common": "^10.3.0",
|
||||||
"@nestjs/config": "^3.1.1",
|
"@nestjs/config": "^3.1.1",
|
||||||
@@ -21,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",
|
||||||
@@ -783,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"
|
||||||
},
|
},
|
||||||
@@ -791,20 +791,6 @@
|
|||||||
"url": "https://github.com/stalniy/casl/blob/master/BACKERS.md"
|
"url": "https://github.com/stalniy/casl/blob/master/BACKERS.md"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@casl/prisma": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@casl/prisma/-/prisma-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-VSAzfTMOZvP3Atj3F0qwJItOm1ixIiumjbBz21PL/gLUIDwoktyAx2dB7dPwjH9AQvzZPE629ee7fVU5K2hpzg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ucast/core": "^1.10.0",
|
|
||||||
"@ucast/js": "^3.0.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@casl/ability": "^5.3.0 || ^6.0.0",
|
|
||||||
"@prisma/client": "^2.14.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@colors/colors": {
|
"node_modules/@colors/colors": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||||
@@ -2025,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",
|
||||||
@@ -2134,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"
|
||||||
},
|
},
|
||||||
@@ -5866,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",
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.72.1",
|
"@anthropic-ai/sdk": "^0.72.1",
|
||||||
"@casl/ability": "^6.8.0",
|
"@casl/ability": "^6.8.0",
|
||||||
"@casl/prisma": "^1.6.1",
|
|
||||||
"@nestjs/axios": "^4.0.1",
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/common": "^10.3.0",
|
"@nestjs/common": "^10.3.0",
|
||||||
"@nestjs/config": "^3.1.1",
|
"@nestjs/config": "^3.1.1",
|
||||||
@@ -36,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",
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AbilityBuilder, PureAbility, AbilityClass, ExtractSubjectType, InferSubjects } from '@casl/ability';
|
import { AbilityBuilder, PureAbility, AbilityClass, ExtractSubjectType } from '@casl/ability';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Role, User, VIP, Driver, ScheduleEvent, Flight, Vehicle } from '@prisma/client';
|
import { Role, User, VIP, Driver, ScheduleEvent, Flight, Vehicle } from '@prisma/client';
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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' }));
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@@ -11,7 +11,6 @@
|
|||||||
"@auth0/auth0-react": "^2.2.4",
|
"@auth0/auth0-react": "^2.2.4",
|
||||||
"@casl/ability": "^6.8.0",
|
"@casl/ability": "^6.8.0",
|
||||||
"@casl/react": "^5.0.1",
|
"@casl/react": "^5.0.1",
|
||||||
"@heroicons/react": "^2.2.0",
|
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
"@tanstack/react-query": "^5.17.19",
|
"@tanstack/react-query": "^5.17.19",
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.6.5",
|
||||||
@@ -912,15 +911,6 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@heroicons/react": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">= 16 || ^19.0.0-rc"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
"@auth0/auth0-react": "^2.2.4",
|
"@auth0/auth0-react": "^2.2.4",
|
||||||
"@casl/ability": "^6.8.0",
|
"@casl/ability": "^6.8.0",
|
||||||
"@casl/react": "^5.0.1",
|
"@casl/react": "^5.0.1",
|
||||||
"@heroicons/react": "^2.2.0",
|
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
"@tanstack/react-query": "^5.17.19",
|
"@tanstack/react-query": "^5.17.19",
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.6.5",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
/**
|
|
||||||
* TypeScript interfaces for VIP Coordinator
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface VIP {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
organization?: string;
|
|
||||||
department: 'OFFICE_OF_DEVELOPMENT' | 'ADMIN';
|
|
||||||
arrivalMode: 'FLIGHT' | 'SELF_DRIVING';
|
|
||||||
expectedArrival?: string;
|
|
||||||
airportPickup: boolean;
|
|
||||||
venueTransport: boolean;
|
|
||||||
notes?: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Driver {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
phone: string;
|
|
||||||
department?: 'OFFICE_OF_DEVELOPMENT' | 'ADMIN';
|
|
||||||
userId?: string;
|
|
||||||
shiftStartTime?: string;
|
|
||||||
shiftEndTime?: string;
|
|
||||||
isAvailable: boolean;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScheduleEvent {
|
|
||||||
id: string;
|
|
||||||
vipId: string;
|
|
||||||
vip?: VIP;
|
|
||||||
title: string;
|
|
||||||
pickupLocation?: string;
|
|
||||||
dropoffLocation?: string;
|
|
||||||
location?: string;
|
|
||||||
startTime: string;
|
|
||||||
endTime: string;
|
|
||||||
actualStartTime?: string;
|
|
||||||
actualEndTime?: string;
|
|
||||||
description?: string;
|
|
||||||
type: 'TRANSPORT' | 'MEETING' | 'EVENT' | 'MEAL' | 'ACCOMMODATION';
|
|
||||||
status: 'SCHEDULED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED';
|
|
||||||
driverId?: string;
|
|
||||||
driver?: Driver;
|
|
||||||
vehicleId?: string;
|
|
||||||
eventId?: string;
|
|
||||||
event?: Event;
|
|
||||||
notes?: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EventTemplate {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
defaultDuration: number; // in minutes
|
|
||||||
location?: string;
|
|
||||||
type: 'TRANSPORT' | 'MEETING' | 'EVENT' | 'MEAL' | 'ACCOMMODATION';
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt?: string;
|
|
||||||
_count?: {
|
|
||||||
events: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Event {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
startTime: string;
|
|
||||||
endTime: string;
|
|
||||||
location: string;
|
|
||||||
type: 'TRANSPORT' | 'MEETING' | 'EVENT' | 'MEAL' | 'ACCOMMODATION';
|
|
||||||
templateId?: string;
|
|
||||||
template?: EventTemplate;
|
|
||||||
attendees: EventAttendance[];
|
|
||||||
scheduleTasks?: ScheduleEvent[];
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt?: string;
|
|
||||||
_count?: {
|
|
||||||
attendees: number;
|
|
||||||
scheduleTasks: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EventAttendance {
|
|
||||||
id: string;
|
|
||||||
eventId: string;
|
|
||||||
vipId: string;
|
|
||||||
vip: VIP;
|
|
||||||
addedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateEventTemplateDto {
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
defaultDuration: number;
|
|
||||||
location?: string;
|
|
||||||
type: 'TRANSPORT' | 'MEETING' | 'EVENT' | 'MEAL' | 'ACCOMMODATION';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateEventDto {
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
startTime: string;
|
|
||||||
endTime: string;
|
|
||||||
location: string;
|
|
||||||
type: 'TRANSPORT' | 'MEETING' | 'EVENT' | 'MEAL' | 'ACCOMMODATION';
|
|
||||||
templateId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AddVipsToEventDto {
|
|
||||||
vipIds: string[];
|
|
||||||
pickupMinutesBeforeEvent?: number;
|
|
||||||
pickupLocationOverride?: string;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { Users, Car, Calendar, Plane, Clock } from 'lucide-react';
|
import { Users, Car, Plane, Clock } from 'lucide-react';
|
||||||
import { VIP, Driver, ScheduleEvent } from '@/types';
|
import { VIP, Driver, ScheduleEvent } from '@/types';
|
||||||
import { formatDateTime } from '@/lib/utils';
|
import { formatDateTime } from '@/lib/utils';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user