mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 13:05:44 +00:00
Throttling configuration from the backoffice added
This commit is contained in:
@@ -4,7 +4,8 @@ import {
|
|||||||
NestModule,
|
NestModule,
|
||||||
OnApplicationBootstrap,
|
OnApplicationBootstrap,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
import { ThrottlerModule } from '@nestjs/throttler';
|
||||||
|
import { DynamicThrottlerGuard } from './common/dynamic-throttler.guard';
|
||||||
import { APP_GUARD } from '@nestjs/core';
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
@@ -135,7 +136,8 @@ import { TasksModule } from './modules/tasks/tasks.module';
|
|||||||
TasksModule,
|
TasksModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: APP_GUARD, useClass: ThrottlerGuard },
|
{ provide: APP_GUARD, useClass: DynamicThrottlerGuard },
|
||||||
|
DynamicThrottlerGuard,
|
||||||
EdrPassengerOrgSeeder,
|
EdrPassengerOrgSeeder,
|
||||||
PassengerStaffUsersSeeder,
|
PassengerStaffUsersSeeder,
|
||||||
],
|
],
|
||||||
|
|||||||
34
apps/edr-passenger-api/src/common/dynamic-throttler.guard.ts
Normal file
34
apps/edr-passenger-api/src/common/dynamic-throttler.guard.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Injectable, ExecutionContext, Inject } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { ThrottlerGuard, ThrottlerStorage, getOptionsToken, getStorageToken } from '@nestjs/throttler';
|
||||||
|
import { SystemConfigService, CONFIG_KEYS } from '../modules/system-config/system-config.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DynamicThrottlerGuard extends ThrottlerGuard {
|
||||||
|
constructor(
|
||||||
|
@Inject(getOptionsToken()) options: any,
|
||||||
|
@Inject(getStorageToken()) storageService: ThrottlerStorage,
|
||||||
|
reflector: Reflector,
|
||||||
|
private readonly systemConfig: SystemConfigService,
|
||||||
|
) {
|
||||||
|
super(options, storageService, reflector);
|
||||||
|
}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const [authLimit, authTtl, strictLimit, strictTtl, defaultLimit, defaultTtl] =
|
||||||
|
await Promise.all([
|
||||||
|
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_AUTH_LIMIT),
|
||||||
|
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_AUTH_TTL_MS),
|
||||||
|
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_STRICT_LIMIT),
|
||||||
|
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_STRICT_TTL_MS),
|
||||||
|
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_DEFAULT_LIMIT),
|
||||||
|
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_DEFAULT_TTL_MS),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.throttlers = [
|
||||||
|
{ name: 'default', ttl: defaultTtl, limit: defaultLimit },
|
||||||
|
];
|
||||||
|
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
|||||||
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
|
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
|
||||||
import { PackagesService } from './packages.service';
|
import { PackagesService } from './packages.service';
|
||||||
import { CreatePackageDto, BookPackageDto, CreatePriceTierDto, UpdatePriceTierDto } from './packages.dto';
|
import { CreatePackageDto, BookPackageDto, CreatePriceTierDto, UpdatePriceTierDto } from './packages.dto';
|
||||||
|
import { IamGuard } from '../../common/iam-adapter';
|
||||||
import { JwtGuard } from '../../common/jwt.guard';
|
import { JwtGuard } from '../../common/jwt.guard';
|
||||||
import { OptionalJwtGuard } from '../verifayda/optional-jwt.guard';
|
import { OptionalJwtGuard } from '../verifayda/optional-jwt.guard';
|
||||||
|
|
||||||
@@ -19,8 +20,9 @@ export class PackagesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('all')
|
@Get('all')
|
||||||
@IsPublic()
|
@UseGuards(IamGuard)
|
||||||
@ApiOperation({ summary: 'List all packages' })
|
@ApiBearerAuth('IAM-auth')
|
||||||
|
@ApiOperation({ summary: 'List all packages (backoffice)' })
|
||||||
listAll(@Query('page') page?: string, @Query('pageSize') pageSize?: string) {
|
listAll(@Query('page') page?: string, @Query('pageSize') pageSize?: string) {
|
||||||
return this.service.listAll(page ? +page : 1, pageSize ? +pageSize : 20);
|
return this.service.listAll(page ? +page : 1, pageSize ? +pageSize : 20);
|
||||||
}
|
}
|
||||||
@@ -48,56 +50,64 @@ export class PackagesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(IamGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth('IAM-auth')
|
||||||
@ApiOperation({ summary: 'Create package (admin)' })
|
@ApiOperation({ summary: 'Create package (admin)' })
|
||||||
create(@Body() dto: CreatePackageDto) {
|
create(@Body() dto: CreatePackageDto) {
|
||||||
return this.service.create(dto);
|
return this.service.create(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(IamGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth('IAM-auth')
|
||||||
@ApiOperation({ summary: 'Update package (admin)' })
|
@ApiOperation({ summary: 'Update package (admin)' })
|
||||||
update(@Param('id') id: string, @Body() dto: Partial<CreatePackageDto>) {
|
update(@Param('id') id: string, @Body() dto: Partial<CreatePackageDto>) {
|
||||||
return this.service.update(id, dto);
|
return this.service.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(IamGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth('IAM-auth')
|
||||||
@ApiOperation({ summary: 'Delete package (admin)' })
|
@ApiOperation({ summary: 'Delete package (admin)' })
|
||||||
remove(@Param('id') id: string) {
|
remove(@Param('id') id: string) {
|
||||||
return this.service.remove(id);
|
return this.service.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id/activate')
|
@Patch(':id/activate')
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(IamGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth('IAM-auth')
|
||||||
@ApiOperation({ summary: 'Activate package (admin)' })
|
@ApiOperation({ summary: 'Activate package (admin)' })
|
||||||
activate(@Param('id') id: string) {
|
activate(@Param('id') id: string) {
|
||||||
return this.service.activate(id);
|
return this.service.activate(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch(':id/deactivate')
|
||||||
|
@UseGuards(IamGuard)
|
||||||
|
@ApiBearerAuth('IAM-auth')
|
||||||
|
@ApiOperation({ summary: 'Deactivate package (admin)' })
|
||||||
|
deactivate(@Param('id') id: string) {
|
||||||
|
return this.service.deactivate(id);
|
||||||
|
}
|
||||||
|
|
||||||
@Post(':id/tiers')
|
@Post(':id/tiers')
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(IamGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth('IAM-auth')
|
||||||
@ApiOperation({ summary: 'Add price tier to package (admin)' })
|
@ApiOperation({ summary: 'Add price tier to package (admin)' })
|
||||||
addTier(@Param('id') id: string, @Body() dto: CreatePriceTierDto) {
|
addTier(@Param('id') id: string, @Body() dto: CreatePriceTierDto) {
|
||||||
return this.service.addTier(id, dto);
|
return this.service.addTier(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('tiers/:tierId')
|
@Patch('tiers/:tierId')
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(IamGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth('IAM-auth')
|
||||||
@ApiOperation({ summary: 'Update price tier (admin)' })
|
@ApiOperation({ summary: 'Update price tier (admin)' })
|
||||||
updateTier(@Param('tierId') tierId: string, @Body() dto: UpdatePriceTierDto) {
|
updateTier(@Param('tierId') tierId: string, @Body() dto: UpdatePriceTierDto) {
|
||||||
return this.service.updateTier(tierId, dto);
|
return this.service.updateTier(tierId, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('tiers/:tierId')
|
@Delete('tiers/:tierId')
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(IamGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth('IAM-auth')
|
||||||
@ApiOperation({ summary: 'Delete price tier (admin)' })
|
@ApiOperation({ summary: 'Delete price tier (admin)' })
|
||||||
deleteTier(@Param('tierId') tierId: string) {
|
deleteTier(@Param('tierId') tierId: string) {
|
||||||
return this.service.deleteTier(tierId);
|
return this.service.deleteTier(tierId);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class PackagesService {
|
|||||||
listActive() {
|
listActive() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return this.prisma.travelPackage.findMany({
|
return this.prisma.travelPackage.findMany({
|
||||||
where: { status: 'ACTIVE', validFrom: { lte: now }, validUntil: { gte: now } },
|
where: { status: 'ACTIVE', validUntil: { gte: now } },
|
||||||
include: {
|
include: {
|
||||||
priceTiers: true,
|
priceTiers: true,
|
||||||
outboundSchedule: { include: { originStation: true, destinationStation: true } },
|
outboundSchedule: { include: { originStation: true, destinationStation: true } },
|
||||||
@@ -145,6 +145,12 @@ export class PackagesService {
|
|||||||
return this.prisma.travelPackage.update({ where: { id }, data: { status: 'ACTIVE' } });
|
return this.prisma.travelPackage.update({ where: { id }, data: { status: 'ACTIVE' } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deactivate(id: string) {
|
||||||
|
const pkg = await this.prisma.travelPackage.findUnique({ where: { id } });
|
||||||
|
if (!pkg) throw new NotFoundException('Package not found');
|
||||||
|
return this.prisma.travelPackage.update({ where: { id }, data: { status: 'DRAFT' } });
|
||||||
|
}
|
||||||
|
|
||||||
async book(dto: BookPackageDto, passengerId?: string) {
|
async book(dto: BookPackageDto, passengerId?: string) {
|
||||||
const pkg = await this.prisma.travelPackage.findUnique({
|
const pkg = await this.prisma.travelPackage.findUnique({
|
||||||
where: { id: dto.packageId },
|
where: { id: dto.packageId },
|
||||||
@@ -152,7 +158,6 @@ export class PackagesService {
|
|||||||
});
|
});
|
||||||
if (!pkg) throw new NotFoundException('Package not found');
|
if (!pkg) throw new NotFoundException('Package not found');
|
||||||
if (pkg.status !== 'ACTIVE') throw new BadRequestException('Package is not available for booking');
|
if (pkg.status !== 'ACTIVE') throw new BadRequestException('Package is not available for booking');
|
||||||
if (new Date() > pkg.validUntil) throw new BadRequestException('Package has expired');
|
|
||||||
|
|
||||||
const tier = pkg.priceTiers.find((t) => t.id === dto.priceTierId);
|
const tier = pkg.priceTiers.find((t) => t.id === dto.priceTierId);
|
||||||
if (!tier) throw new NotFoundException('Price tier not found');
|
if (!tier) throw new NotFoundException('Price tier not found');
|
||||||
|
|||||||
@@ -4,11 +4,23 @@ import { PrismaService } from '../../common/prisma.service';
|
|||||||
export const CONFIG_KEYS = {
|
export const CONFIG_KEYS = {
|
||||||
SEAT_HOLD_DURATION_MINUTES: 'seat_hold_duration_minutes',
|
SEAT_HOLD_DURATION_MINUTES: 'seat_hold_duration_minutes',
|
||||||
HOLD_CUTOFF_HOURS_BEFORE_DEPARTURE: 'hold_cutoff_hours_before_departure',
|
HOLD_CUTOFF_HOURS_BEFORE_DEPARTURE: 'hold_cutoff_hours_before_departure',
|
||||||
|
THROTTLE_AUTH_LIMIT: 'throttle_auth_limit',
|
||||||
|
THROTTLE_AUTH_TTL_MS: 'throttle_auth_ttl_ms',
|
||||||
|
THROTTLE_STRICT_LIMIT: 'throttle_strict_limit',
|
||||||
|
THROTTLE_STRICT_TTL_MS: 'throttle_strict_ttl_ms',
|
||||||
|
THROTTLE_DEFAULT_LIMIT: 'throttle_default_limit',
|
||||||
|
THROTTLE_DEFAULT_TTL_MS: 'throttle_default_ttl_ms',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const DEFAULTS: Record<string, string> = {
|
const DEFAULTS: Record<string, string> = {
|
||||||
[CONFIG_KEYS.SEAT_HOLD_DURATION_MINUTES]: '5',
|
[CONFIG_KEYS.SEAT_HOLD_DURATION_MINUTES]: '5',
|
||||||
[CONFIG_KEYS.HOLD_CUTOFF_HOURS_BEFORE_DEPARTURE]: '2',
|
[CONFIG_KEYS.HOLD_CUTOFF_HOURS_BEFORE_DEPARTURE]: '2',
|
||||||
|
[CONFIG_KEYS.THROTTLE_AUTH_LIMIT]: '5',
|
||||||
|
[CONFIG_KEYS.THROTTLE_AUTH_TTL_MS]: '60000',
|
||||||
|
[CONFIG_KEYS.THROTTLE_STRICT_LIMIT]: '20',
|
||||||
|
[CONFIG_KEYS.THROTTLE_STRICT_TTL_MS]: '60000',
|
||||||
|
[CONFIG_KEYS.THROTTLE_DEFAULT_LIMIT]: '100',
|
||||||
|
[CONFIG_KEYS.THROTTLE_DEFAULT_TTL_MS]: '60000',
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export default function PackagesPage() {
|
|||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [viewPackage, setViewPackage] = useState<any>(null);
|
const [viewPackage, setViewPackage] = useState<any>(null);
|
||||||
const [activateConfirm, setActivateConfirm] = useState<any>(null);
|
const [activateConfirm, setActivateConfirm] = useState<any>(null);
|
||||||
|
const [deactivateConfirm, setDeactivateConfirm] = useState<any>(null);
|
||||||
const [tiersPackage, setTiersPackage] = useState<any>(null);
|
const [tiersPackage, setTiersPackage] = useState<any>(null);
|
||||||
const [editingTier, setEditingTier] = useState<any>(null);
|
const [editingTier, setEditingTier] = useState<any>(null);
|
||||||
const [tierForm, setTierForm] = useState({ seatType: '', label: '', priceMinor: '', availableSeats: '' });
|
const [tierForm, setTierForm] = useState({ seatType: '', label: '', priceMinor: '', availableSeats: '' });
|
||||||
@@ -76,6 +77,11 @@ export default function PackagesPage() {
|
|||||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['packages'] }); setActivateConfirm(null); },
|
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['packages'] }); setActivateConfirm(null); },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const deactivateMutation = useMutation({
|
||||||
|
mutationFn: packagesApi.deactivate,
|
||||||
|
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['packages'] }); setDeactivateConfirm(null); },
|
||||||
|
});
|
||||||
|
|
||||||
const emptyTierForm = { seatType: '', label: '', priceMinor: '', availableSeats: '' };
|
const emptyTierForm = { seatType: '', label: '', priceMinor: '', availableSeats: '' };
|
||||||
|
|
||||||
const addTierMutation = useMutation({
|
const addTierMutation = useMutation({
|
||||||
@@ -261,7 +267,12 @@ export default function PackagesPage() {
|
|||||||
{
|
{
|
||||||
label: 'Activate', icon: CheckCircle, variant: 'primary' as const,
|
label: 'Activate', icon: CheckCircle, variant: 'primary' as const,
|
||||||
onClick: (p: any) => setActivateConfirm(p),
|
onClick: (p: any) => setActivateConfirm(p),
|
||||||
hidden: (p: any) => p.status === 'ACTIVE',
|
show: (p: any) => p.status !== 'ACTIVE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Deactivate', icon: CheckCircle, variant: 'secondary' as const,
|
||||||
|
onClick: (p: any) => setDeactivateConfirm(p),
|
||||||
|
show: (p: any) => p.status === 'ACTIVE',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Delete', icon: Trash2, variant: 'danger' as const,
|
label: 'Delete', icon: Trash2, variant: 'danger' as const,
|
||||||
@@ -343,6 +354,18 @@ export default function PackagesPage() {
|
|||||||
isDanger={false}
|
isDanger={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Deactivate Confirmation */}
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={!!deactivateConfirm}
|
||||||
|
onClose={() => setDeactivateConfirm(null)}
|
||||||
|
onConfirm={() => deactivateMutation.mutate(deactivateConfirm.id)}
|
||||||
|
title="Deactivate Package"
|
||||||
|
message={`Deactivate "${deactivateConfirm?.name}"? It will no longer be available for booking.`}
|
||||||
|
confirmText="Deactivate"
|
||||||
|
isDanger={false}
|
||||||
|
isLoading={deactivateMutation.isPending}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Tiers Modal */}
|
{/* Tiers Modal */}
|
||||||
<Modal isOpen={!!tiersPackage} onClose={() => { setTiersPackage(null); setEditingTier(null); setTierError(null); }} title={`Price Tiers — ${tiersPackage?.name ?? ''}`} size="lg">
|
<Modal isOpen={!!tiersPackage} onClose={() => { setTiersPackage(null); setEditingTier(null); setTierError(null); }} title={`Price Tiers — ${tiersPackage?.name ?? ''}`} size="lg">
|
||||||
{tiersPackage && (
|
{tiersPackage && (
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ export default function SettingsPage() {
|
|||||||
const [activeTab, setActiveTab] = useState<Tab>('general');
|
const [activeTab, setActiveTab] = useState<Tab>('general');
|
||||||
const [seatHoldMinutes, setSeatHoldMinutes] = useState('5');
|
const [seatHoldMinutes, setSeatHoldMinutes] = useState('5');
|
||||||
const [holdCutoffHours, setHoldCutoffHours] = useState('2');
|
const [holdCutoffHours, setHoldCutoffHours] = useState('2');
|
||||||
|
const [throttleAuthLimit, setThrottleAuthLimit] = useState('5');
|
||||||
|
const [throttleStrictLimit, setThrottleStrictLimit] = useState('20');
|
||||||
|
const [throttleDefaultLimit, setThrottleDefaultLimit] = useState('100');
|
||||||
const [configLoading, setConfigLoading] = useState(false);
|
const [configLoading, setConfigLoading] = useState(false);
|
||||||
const [configSaving, setConfigSaving] = useState(false);
|
const [configSaving, setConfigSaving] = useState(false);
|
||||||
const [configMessage, setConfigMessage] = useState('');
|
const [configMessage, setConfigMessage] = useState('');
|
||||||
@@ -21,6 +24,9 @@ export default function SettingsPage() {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data?.seat_hold_duration_minutes) setSeatHoldMinutes(data.seat_hold_duration_minutes);
|
if (data?.seat_hold_duration_minutes) setSeatHoldMinutes(data.seat_hold_duration_minutes);
|
||||||
if (data?.hold_cutoff_hours_before_departure) setHoldCutoffHours(data.hold_cutoff_hours_before_departure);
|
if (data?.hold_cutoff_hours_before_departure) setHoldCutoffHours(data.hold_cutoff_hours_before_departure);
|
||||||
|
if (data?.throttle_auth_limit) setThrottleAuthLimit(data.throttle_auth_limit);
|
||||||
|
if (data?.throttle_strict_limit) setThrottleStrictLimit(data.throttle_strict_limit);
|
||||||
|
if (data?.throttle_default_limit) setThrottleDefaultLimit(data.throttle_default_limit);
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => setConfigLoading(false));
|
.finally(() => setConfigLoading(false));
|
||||||
@@ -33,6 +39,9 @@ export default function SettingsPage() {
|
|||||||
await systemConfigApi.update({
|
await systemConfigApi.update({
|
||||||
seat_hold_duration_minutes: seatHoldMinutes,
|
seat_hold_duration_minutes: seatHoldMinutes,
|
||||||
hold_cutoff_hours_before_departure: holdCutoffHours,
|
hold_cutoff_hours_before_departure: holdCutoffHours,
|
||||||
|
throttle_auth_limit: throttleAuthLimit,
|
||||||
|
throttle_strict_limit: throttleStrictLimit,
|
||||||
|
throttle_default_limit: throttleDefaultLimit,
|
||||||
});
|
});
|
||||||
setConfigMessage('Saved successfully.');
|
setConfigMessage('Saved successfully.');
|
||||||
} catch {
|
} catch {
|
||||||
@@ -177,6 +186,41 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
{activeTab === 'configurations' && (
|
{activeTab === 'configurations' && (
|
||||||
<div className="card space-y-6">
|
<div className="card space-y-6">
|
||||||
|
<h3 className="text-lg font-semibold text-foreground">Rate Limiting (requests / minute / IP)</h3>
|
||||||
|
{!configLoading && (
|
||||||
|
<div className="max-w-sm space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="label" htmlFor="throttle-auth">Auth endpoints limit</label>
|
||||||
|
<input
|
||||||
|
id="throttle-auth"
|
||||||
|
type="number" min="1" className="input"
|
||||||
|
value={throttleAuthLimit}
|
||||||
|
onChange={(e) => setThrottleAuthLimit(e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">Login, register, OTP. Default: 5.</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="label" htmlFor="throttle-strict">Strict endpoints limit</label>
|
||||||
|
<input
|
||||||
|
id="throttle-strict"
|
||||||
|
type="number" min="1" className="input"
|
||||||
|
value={throttleStrictLimit}
|
||||||
|
onChange={(e) => setThrottleStrictLimit(e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">Sensitive operations. Default: 20.</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="label" htmlFor="throttle-default">Default endpoints limit</label>
|
||||||
|
<input
|
||||||
|
id="throttle-default"
|
||||||
|
type="number" min="1" className="input"
|
||||||
|
value={throttleDefaultLimit}
|
||||||
|
onChange={(e) => setThrottleDefaultLimit(e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">All other endpoints including search. Default: 100.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<h3 className="text-lg font-semibold text-foreground">Seat Booking</h3>
|
<h3 className="text-lg font-semibold text-foreground">Seat Booking</h3>
|
||||||
{configLoading ? (
|
{configLoading ? (
|
||||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||||
|
|||||||
@@ -390,6 +390,7 @@ export const packagesApi = {
|
|||||||
create: (data: any) => apiClient.post<any>('/packages', data),
|
create: (data: any) => apiClient.post<any>('/packages', data),
|
||||||
update: (id: string, data: any) => apiClient.patch<any>(`/packages/${id}`, data),
|
update: (id: string, data: any) => apiClient.patch<any>(`/packages/${id}`, data),
|
||||||
activate: (id: string) => apiClient.patch<any>(`/packages/${id}/activate`, {}),
|
activate: (id: string) => apiClient.patch<any>(`/packages/${id}/activate`, {}),
|
||||||
|
deactivate: (id: string) => apiClient.patch<any>(`/packages/${id}/deactivate`, {}),
|
||||||
remove: (id: string) => apiClient.delete(`/packages/${id}`),
|
remove: (id: string) => apiClient.delete(`/packages/${id}`),
|
||||||
addTier: (packageId: string, data: any) => apiClient.post<any>(`/packages/${packageId}/tiers`, data),
|
addTier: (packageId: string, data: any) => apiClient.post<any>(`/packages/${packageId}/tiers`, data),
|
||||||
updateTier: (tierId: string, data: any) => apiClient.patch<any>(`/packages/tiers/${tierId}`, data),
|
updateTier: (tierId: string, data: any) => apiClient.patch<any>(`/packages/tiers/${tierId}`, data),
|
||||||
|
|||||||
Reference in New Issue
Block a user