diff --git a/Dockerfile b/Dockerfile index 1f3b557..c7cc797 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,9 +22,6 @@ RUN cd apps/api && npx prisma generate RUN pnpm --filter @kooltab/api run build RUN pnpm --filter @kooltab/dashboard run build -# Compile seed script to JS (needed for production seeding) -RUN pnpm exec tsc apps/api/prisma/seed.ts --outDir apps/api/prisma --skipLibCheck --module commonjs --esModuleInterop true --resolveJsonModule true - # ---- Production API image ---- FROM node:20-alpine AS api RUN npm install -g pnpm diff --git a/apps/api/prisma/seed.js b/apps/api/prisma/seed.js new file mode 100644 index 0000000..86b76fe --- /dev/null +++ b/apps/api/prisma/seed.js @@ -0,0 +1,155 @@ +const bcrypt = require('bcrypt'); +const { PrismaClient } = require('@prisma/client'); +const { v4: uuidv4 } = require('uuid'); +const crypto = require('crypto'); + +const prisma = new PrismaClient(); + +async function main() { + console.log('Seeding database...'); + + await prisma.auditLog.deleteMany(); + await prisma.remoteCommand.deleteMany(); + await prisma.openTabSnapshot.deleteMany(); + await prisma.tabOwnership.deleteMany(); + await prisma.refreshToken.deleteMany(); + await prisma.registrationCode.deleteMany(); + await prisma.device.deleteMany(); + await prisma.clientUser.deleteMany(); + await prisma.venue.deleteMany(); + console.log('Cleared existing data'); + + const venueId = uuidv4(); + await prisma.venue.create({ + data: { id: venueId, name: 'KoolTab Demo Bar' }, + }); + console.log(`Created venue: KoolTab Demo Bar (${venueId})`); + + const adminHash = await bcrypt.hash('Admin123!', 10); + await prisma.clientUser.create({ + data: { + id: uuidv4(), + venueId, + email: 'admin@example.com', + passwordHash: adminHash, + name: 'Eigenaar', + role: 'admin', + }, + }); + console.log('Created admin user: admin@example.com / Admin123!'); + + const viewerHash = await bcrypt.hash('Viewer123!', 10); + await prisma.clientUser.create({ + data: { + id: uuidv4(), + venueId, + email: 'viewer@example.com', + passwordHash: viewerHash, + name: 'Bartender', + role: 'viewer', + }, + }); + console.log('Created viewer user: viewer@example.com / Viewer123!'); + + const device1Id = uuidv4(); + const device2Id = uuidv4(); + + const token1 = 'dev_' + crypto.randomBytes(48).toString('base64url'); + const token2 = 'dev_' + crypto.randomBytes(48).toString('base64url'); + + await prisma.device.create({ + data: { + id: device1Id, + venueId, + name: 'Bar terminal 1', + tokenHash: crypto.createHash('sha256').update(token1).digest('hex'), + tokenPrefix: token1.substring(0, 8), + tokenExpiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), + lastSeenAt: new Date(Date.now() - 1 * 60 * 1000), + }, + }); + + await prisma.device.create({ + data: { + id: device2Id, + venueId, + name: 'Bar terminal 2', + tokenHash: crypto.createHash('sha256').update(token2).digest('hex'), + tokenPrefix: token2.substring(0, 8), + tokenExpiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), + lastSeenAt: new Date(Date.now() - 15 * 60 * 1000), + }, + }); + + console.log(`Created device: Bar terminal 1 (${device1Id})`); + console.log(`Created device: Bar terminal 2 (${device2Id})`); + + const tabsData = [ + { tabId: uuidv4(), customerName: 'Jan', version: 5, totalInCents: 2450, itemCount: 3, reportedAt: new Date(Date.now() - 2 * 60 * 1000) }, + { tabId: uuidv4(), customerName: 'Marie', version: 3, totalInCents: 1800, itemCount: 2, reportedAt: new Date(Date.now() - 5 * 60 * 1000) }, + { tabId: uuidv4(), customerName: 'Thomas', version: 7, totalInCents: 4200, itemCount: 5, reportedAt: new Date(Date.now() - 3 * 60 * 1000) }, + { tabId: uuidv4(), customerName: 'Lisa', version: 2, totalInCents: 900, itemCount: 1, reportedAt: new Date(Date.now() - 4 * 60 * 1000) }, + ]; + + for (const tab of tabsData) { + await prisma.tabOwnership.create({ + data: { tabId: tab.tabId, venueId, deviceId: device1Id, lastReportedAt: tab.reportedAt }, + }); + await prisma.openTabSnapshot.create({ + data: { deviceId: device1Id, tabId: tab.tabId, customerName: tab.customerName, version: tab.version, totalInCents: tab.totalInCents, itemCount: tab.itemCount, reportedAt: tab.reportedAt }, + }); + } + + const staleTabs = [ + { tabId: uuidv4(), customerName: 'Emma', version: 4, totalInCents: 3200, itemCount: 4, reportedAt: new Date(Date.now() - 16 * 60 * 1000) }, + { tabId: uuidv4(), customerName: 'David', version: 1, totalInCents: 1500, itemCount: 2, reportedAt: new Date(Date.now() - 20 * 60 * 1000) }, + ]; + + for (const tab of staleTabs) { + await prisma.tabOwnership.create({ + data: { tabId: tab.tabId, venueId, deviceId: device2Id, lastReportedAt: tab.reportedAt }, + }); + await prisma.openTabSnapshot.create({ + data: { deviceId: device2Id, tabId: tab.tabId, customerName: tab.customerName, version: tab.version, totalInCents: tab.totalInCents, itemCount: tab.itemCount, reportedAt: tab.reportedAt }, + }); + } + + console.log('Created 6 open tabs across 2 devices'); + + const now = new Date(); + + await prisma.remoteCommand.create({ + data: { id: uuidv4(), venueId, deviceId: device1Id, targetTabId: tabsData[1].tabId, type: 'close_tab', expectedVersion: 3, paymentMethod: 'cash', requestedBy: 'Eigenaar', status: 'completed', result: 'Tab closed successfully', acknowledgedAt: new Date(now.getTime() - 30 * 60 * 1000), createdAt: new Date(now.getTime() - 35 * 60 * 1000), expiresAt: new Date(now.getTime() + 23 * 60 * 60 * 1000) }, + }); + + await prisma.remoteCommand.create({ + data: { id: uuidv4(), venueId, deviceId: device1Id, targetTabId: tabsData[0].tabId, type: 'close_tab', expectedVersion: 5, paymentMethod: 'payconiq', requestedBy: 'Eigenaar', status: 'pending', createdAt: new Date(now.getTime() - 5 * 60 * 1000), expiresAt: new Date(now.getTime() + 23 * 60 * 60 * 1000) }, + }); + + await prisma.remoteCommand.create({ + data: { id: uuidv4(), venueId, deviceId: device2Id, targetTabId: staleTabs[0].tabId, type: 'close_tab', expectedVersion: 4, paymentMethod: 'payment_pending', requestedBy: 'Bartender', status: 'failed', result: 'Tab could not be closed', acknowledgedAt: new Date(now.getTime() - 60 * 60 * 1000), createdAt: new Date(now.getTime() - 65 * 60 * 1000), expiresAt: new Date(now.getTime() + 23 * 60 * 60 * 1000) }, + }); + + await prisma.remoteCommand.create({ + data: { id: uuidv4(), venueId, deviceId: device1Id, targetTabId: tabsData[2].tabId, type: 'close_tab', expectedVersion: 7, paymentMethod: 'cash', requestedBy: 'Eigenaar', status: 'conflict', result: 'Version mismatch: expected 7, got 8', acknowledgedAt: new Date(now.getTime() - 2 * 60 * 60 * 1000), createdAt: new Date(now.getTime() - 2 * 60 * 60 * 1000 - 5 * 60 * 1000), expiresAt: new Date(now.getTime() + 22 * 60 * 60 * 1000) }, + }); + + console.log('Created sample commands: completed, pending, failed, conflict'); + + await prisma.auditLog.create({ + data: { id: uuidv4(), venueId, actorType: 'client_user', actorId: 'admin', action: 'command_created', details: JSON.stringify({ commandId: 'sample', tabId: tabsData[0].tabId }) }, + }); + + console.log('Seeding complete!'); + console.log(''); + console.log('Demo credentials:'); + console.log(' Admin: admin@example.com / Admin123!'); + console.log(' Viewer: viewer@example.com / Viewer123!'); + console.log(`\nDevice tokens (for simulator):`); + console.log(` Bar terminal 1: ${token1}`); + console.log(` Bar terminal 2: ${token2}`); +} + +main() + .catch(console.error) + .finally(() => prisma.$disconnect()); \ No newline at end of file