b6499eb7b3
New domain mirroring the Feedback module's conventions: single-use voucher codes (wildcard + personalized), redemption with race-safe capacity and deadline enforcement, admin CRUD for vouchers/redemptions/event settings with an audit trail, and reuse of the existing Calendar session auth. Backend pieces: - Tickets domain (public redeem flow, admin vouchers/redemptions/events) - Calendar events.service.ts: add getEventById - Mailer: attachment/HTML support, fix a shared-mutable-state race - Per-event capacity/deadline/address settings, with an "address required" option on top of "address collected" - Email format validation on redeem and admin voucher/redemption input - Docker Compose dev stack (MariaDB + seed data) for local testing Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
39 lines
1.4 KiB
TypeScript
39 lines
1.4 KiB
TypeScript
import * as crypto from 'crypto';
|
|
|
|
// Excludes 0/O, 1/I/L to avoid look-alike confusion when a code is
|
|
// hand-written, read aloud, or typed from a printed fallback under a QR
|
|
// code. 31 symbols * 8 chars ≈ 39.6 bits of entropy - effectively
|
|
// unguessable combined with rate-limiting on the redeem endpoint.
|
|
const CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
|
|
const CODE_LENGTH = 8;
|
|
|
|
/**
|
|
* Generates a single random code. Uses crypto.randomInt (uniform, no
|
|
* modulo bias) rather than Math.random() since these gate a real-world
|
|
* concert invitation.
|
|
*/
|
|
export const generateCode = (): string => {
|
|
let code = '';
|
|
for (let i = 0; i < CODE_LENGTH; i++) {
|
|
code += CODE_ALPHABET[crypto.randomInt(CODE_ALPHABET.length)];
|
|
}
|
|
return code;
|
|
};
|
|
|
|
/**
|
|
* Generates a code guaranteed not to collide with any row `existingCodes`
|
|
* already contains, tries up to `maxAttempts` times before giving up.
|
|
* Collisions are astronomically unlikely at this entropy - this exists as
|
|
* a correctness backstop, not because collisions are expected.
|
|
*/
|
|
export const generateUniqueCode = (existingCodes: Set<string>, maxAttempts = 20): string => {
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
const candidate = generateCode();
|
|
if (!existingCodes.has(candidate)) {
|
|
existingCodes.add(candidate);
|
|
return candidate;
|
|
}
|
|
}
|
|
throw new Error('Could not generate a unique voucher code after ' + maxAttempts + ' attempts');
|
|
};
|