feat: add superadmin
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
2026-10-26 11:58:10 +01:00
parent 072cda111c
commit 4e43521da7
45 changed files with 3531 additions and 65 deletions
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ClientUser" ADD COLUMN "sessionsRevokedAt" TIMESTAMP(3);
+4 -1
View File
@@ -56,9 +56,12 @@ model ClientUser {
email String @unique
passwordHash String
name String
// admin (the owner: users and terminals) | manager (runs the venue: products, stock, tabs, reminders)
// admin (the owner: users and terminals) | manager (runs the venue: products, stock, tabs, reminders) |
// superadmin (runs KoolTap itself: every venue, and the admin page)
role String @default("manager")
createdAt DateTime @default(now())
// Access tokens issued before this are refused, so a forced logout applies at once instead of when they expire
sessionsRevokedAt DateTime?
venue Venue @relation(fields: [venueId], references: [id])
+30
View File
@@ -61,6 +61,35 @@ async function main() {
});
console.log('Created viewer user: viewer@example.com / Viewer123!');
// Runs KoolTap itself: every venue, and the admin page
const superadminHash = await bcrypt.hash('Super123!', 10);
await prisma.clientUser.create({
data: {
id: uuidv4(),
venueId,
email: 'superadmin@example.com',
passwordHash: superadminHash,
name: 'KoolTap',
role: 'superadmin',
},
});
console.log('Created superadmin user: superadmin@example.com / Super123!');
// A second venue, so the admin page has more than one
const secondVenueId = uuidv4();
await prisma.venue.create({ data: { id: secondVenueId, name: 'KoolTap Festival Bar' } });
await prisma.clientUser.create({
data: {
id: uuidv4(),
venueId: secondVenueId,
email: 'festival@example.com',
passwordHash: adminHash,
name: 'Festival',
role: 'admin',
},
});
console.log('Created venue: KoolTap Festival Bar, admin festival@example.com / Admin123!');
// Create one device
const deviceId = uuidv4();
const token = 'dev_' + crypto.randomBytes(48).toString('base64url');
@@ -208,6 +237,7 @@ async function main() {
console.log('Demo credentials:');
console.log(' Admin: admin@example.com / Admin123!');
console.log(' Viewer: viewer@example.com / Viewer123!');
console.log(' Superadmin: superadmin@example.com / Super123!');
console.log(`\nDevice token (for simulator): ${token}`);
}
+31
View File
@@ -0,0 +1,31 @@
import { PrismaClient } from '@prisma/client';
/**
* Makes an existing dashboard user a superadmin, or an admin again with --revoke. There is no other way to become
* the first superadmin: the dashboard only lets a superadmin hand out the role.
*
* pnpm --filter @kooltap/api run superadmin user@example.com [--revoke]
*/
async function main() {
const [email, flag] = process.argv.slice(2);
if (!email) {
console.error('Usage: superadmin <email> [--revoke]');
process.exit(1);
}
const role = flag === '--revoke' ? 'admin' : 'superadmin';
const prisma = new PrismaClient();
try {
const user = await prisma.clientUser.findUnique({ where: { email } });
if (!user) {
console.error(`No user with email ${email}`);
process.exit(1);
}
await prisma.clientUser.update({ where: { id: user.id }, data: { role } });
console.log(`${email} is now ${role === 'superadmin' ? 'a superadmin' : 'an admin'}`);
} finally {
await prisma.$disconnect();
}
}
main();