feat: reminders, Excel customer import and security hardening
Features - Payment reminders via Resend (candidates, send, history, test email) - Bulk import of the customer list from Excel/CSV in the dashboard - Dashboard i18n (en/nl) with language selector Security - Enforce venue isolation on all /venues/:venueId routes - Require admin for planned-tab changes and pairing codes - Rate-limit login and device registration; opt-in TRUST_PROXY - Require JWT_SECRET in production; stop leaking internal errors - Escape customer names in reminder emails Sync and commands - Remove tabs the terminal no longer reports; validate nested sync payload - Return all pending commands on every sync (fixes missed-command race) - First command ack wins; atomic registration code claim - Reject commands for revoked devices, stale versions or duplicates - Remind only unpaid balances, with a 7-day cooldown Dashboard - Share a single token refresh across concurrent 401s (random logouts) - Keep the session alive for the refresh token lifetime Deployment - Replace `db push --accept-data-loss` with migrations (baselines legacy DBs) - Set NODE_ENV=production; pass Resend and TRUST_PROXY env in prod compose
This commit is contained in:
@@ -2,6 +2,7 @@ import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
@@ -54,6 +55,33 @@ export class CommandsService {
|
||||
});
|
||||
}
|
||||
|
||||
if (device.revokedAt) {
|
||||
throw new ConflictException({
|
||||
code: 'device_revoked',
|
||||
message: 'The device owning this tab has been revoked',
|
||||
});
|
||||
}
|
||||
|
||||
const snapshot = await tx.openTabSnapshot.findUnique({
|
||||
where: { deviceId_tabId: { deviceId: device.id, tabId: dto.tabId } },
|
||||
});
|
||||
if (snapshot && snapshot.version !== dto.expectedVersion) {
|
||||
throw new ConflictException({
|
||||
code: 'version_mismatch',
|
||||
message: 'The tab has changed since it was loaded. Refresh and try again.',
|
||||
});
|
||||
}
|
||||
|
||||
const alreadyPending = await tx.remoteCommand.findFirst({
|
||||
where: { venueId, targetTabId: dto.tabId, status: 'pending' },
|
||||
});
|
||||
if (alreadyPending) {
|
||||
throw new ConflictException({
|
||||
code: 'command_already_pending',
|
||||
message: 'A command for this tab is already pending',
|
||||
});
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + COMMAND_EXPIRY_HOURS * 60 * 60 * 1000);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user