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:
@@ -129,33 +129,46 @@ export class AuthService {
|
||||
throw new UnauthorizedException({ code: 'venue_mismatch', message: 'Registration code does not match venue' });
|
||||
}
|
||||
|
||||
const { rawToken, hash, prefix, expiresAt } = await this.generateDeviceToken();
|
||||
const deviceId = uuidv4();
|
||||
|
||||
// One device per venue — revoke any existing device
|
||||
const existing = await this.prisma.device.findFirst({ where: { venueId, revokedAt: null } });
|
||||
if (existing) {
|
||||
await this.prisma.device.update({
|
||||
where: { id: existing.id },
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
await this.prisma.device.create({
|
||||
data: {
|
||||
id: deviceId,
|
||||
venueId,
|
||||
name,
|
||||
tokenHash: hash,
|
||||
tokenPrefix: prefix,
|
||||
tokenExpiresAt: expiresAt,
|
||||
},
|
||||
});
|
||||
|
||||
await this.prisma.registrationCode.update({
|
||||
where: { id: code.id },
|
||||
// Claim the code atomically so it can only ever be used once
|
||||
const claimed = await this.prisma.registrationCode.updateMany({
|
||||
where: { id: code.id, usedAt: null },
|
||||
data: { usedAt: new Date() },
|
||||
});
|
||||
if (claimed.count === 0) {
|
||||
throw new UnauthorizedException({ code: 'invalid_registration_code', message: 'Invalid or expired registration code' });
|
||||
}
|
||||
|
||||
const { rawToken, hash, prefix, expiresAt } = await this.generateDeviceToken();
|
||||
let deviceId: string;
|
||||
|
||||
// One device per venue — update existing or create new
|
||||
const existing = await this.prisma.device.findFirst({ where: { venueId } });
|
||||
if (existing) {
|
||||
deviceId = existing.id;
|
||||
await this.prisma.device.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
name,
|
||||
tokenHash: hash,
|
||||
tokenPrefix: prefix,
|
||||
tokenExpiresAt: expiresAt,
|
||||
revokedAt: null,
|
||||
lastSeenAt: new Date(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
deviceId = uuidv4();
|
||||
await this.prisma.device.create({
|
||||
data: {
|
||||
id: deviceId,
|
||||
venueId,
|
||||
name,
|
||||
tokenHash: hash,
|
||||
tokenPrefix: prefix,
|
||||
tokenExpiresAt: expiresAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await this.auditService.log(venueId, 'device', deviceId, 'device_registered', { deviceName: name });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user