feat(topology): add personal/global scope to lab templates
Labs can now be marked as Personal or Global when creating or editing. Personal topologies are visible only to the owner and admins; others cannot see, book, or edit them. Global topologies are visible to all but editable only by the creator, admins, or legacy (migrated) labs. - DB: idempotent ALTER TABLE adds scope + ownerId columns to labs - API: POST sets ownerId from JWT; PUT/DELETE enforce ownership (403 for unauthorized edits; legacy ownerId='' remains freely editable) - Types: LabTemplate extended with scope and ownerId fields - LabTemplates UI: sectioned list (My / Global / Others' Personal), Personal/Global toggle in form, Lock/Globe badges on cards, edit+delete buttons hidden for non-owners - BookingCalendar: personal labs filtered from selects/quick booking, optgroup grouping for Global vs Personal in topology dropdown - Light mode: add missing bg-slate-950/50 and border-slate-800/50 overrides so the Global badge renders correctly
This commit is contained in:
42
server.ts
42
server.ts
@ -526,6 +526,8 @@ async function startServer() {
|
||||
deviceIds: JSON.parse(r.deviceIds), topology: JSON.parse(r.topology),
|
||||
semaphoreSetupTemplateId: r.semaphoreSetupTemplateId || '',
|
||||
semaphoreTeardownTemplateId: r.semaphoreTeardownTemplateId || '',
|
||||
scope: (r.scope === 'personal' ? 'personal' : 'global') as 'global' | 'personal',
|
||||
ownerId: r.ownerId ?? '',
|
||||
}));
|
||||
res.json(labs);
|
||||
} catch (err: any) {
|
||||
@ -535,40 +537,54 @@ async function startServer() {
|
||||
|
||||
app.post('/api/labs', requireAuth, (req, res) => {
|
||||
try {
|
||||
const { name, description, contactPerson, location, deviceIds, topology, semaphoreSetupTemplateId, semaphoreTeardownTemplateId } = req.body;
|
||||
const { name, description, contactPerson, location, deviceIds, topology, semaphoreSetupTemplateId, semaphoreTeardownTemplateId, scope } = req.body;
|
||||
if (!name || !deviceIds || !Array.isArray(deviceIds)) {
|
||||
return res.status(400).json({ error: 'Missing name or associated device configurations.' });
|
||||
}
|
||||
|
||||
const safeScope: 'global' | 'personal' = scope === 'personal' ? 'personal' : 'global';
|
||||
const ownerId = req.user!.userId;
|
||||
const id = uid("lab");
|
||||
db.prepare(`INSERT INTO labs (id, name, description, contactPerson, location, deviceIds, topology, semaphoreSetupTemplateId, semaphoreTeardownTemplateId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
||||
.run(id, name, description || '', contactPerson || '', location || '', JSON.stringify(deviceIds), JSON.stringify(topology || []), semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId || '');
|
||||
db.prepare(`INSERT INTO labs (id, name, description, contactPerson, location, deviceIds, topology, semaphoreSetupTemplateId, semaphoreTeardownTemplateId, scope, ownerId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
||||
.run(id, name, description || '', contactPerson || '', location || '', JSON.stringify(deviceIds), JSON.stringify(topology || []), semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId || '', safeScope, ownerId);
|
||||
|
||||
addLog('maintenance',
|
||||
`Released a new lab template profile "${name}" (${location || 'Staging Area'}) for engineering teams.`,
|
||||
{ userId: req.user!.userId });
|
||||
|
||||
const r = db.prepare('SELECT * FROM labs WHERE id = ?').get(id) as any;
|
||||
res.status(201).json({ id: r.id, name: r.name, description: r.description, contactPerson: r.contactPerson, location: r.location, deviceIds: JSON.parse(r.deviceIds), topology: JSON.parse(r.topology), semaphoreSetupTemplateId: r.semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId: r.semaphoreTeardownTemplateId || '' });
|
||||
res.status(201).json({ id: r.id, name: r.name, description: r.description, contactPerson: r.contactPerson, location: r.location, deviceIds: JSON.parse(r.deviceIds), topology: JSON.parse(r.topology), semaphoreSetupTemplateId: r.semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId: r.semaphoreTeardownTemplateId || '', scope: r.scope, ownerId: r.ownerId });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/labs/:id', requireAuth, (req, res) => {
|
||||
app.put('/api/labs/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const { name, description, contactPerson, location, deviceIds, topology, semaphoreSetupTemplateId, semaphoreTeardownTemplateId } = req.body;
|
||||
const { name, description, contactPerson, location, deviceIds, topology, semaphoreSetupTemplateId, semaphoreTeardownTemplateId, scope } = req.body;
|
||||
|
||||
db.prepare(`UPDATE labs SET name = ?, description = ?, contactPerson = ?, location = ?, deviceIds = ?, topology = ?, semaphoreSetupTemplateId = ?, semaphoreTeardownTemplateId = ? WHERE id = ?`)
|
||||
.run(name, description, contactPerson, location, JSON.stringify(deviceIds), JSON.stringify(topology), semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId || '', id);
|
||||
const existing = db.prepare('SELECT ownerId, scope FROM labs WHERE id = ?').get(id) as any;
|
||||
if (!existing) return res.status(404).json({ error: 'Lab template not found.' });
|
||||
|
||||
const reqUser = db.prepare('SELECT role FROM users WHERE id = ?').get(req.user!.userId) as any;
|
||||
const isAdmin = reqUser?.role?.toLowerCase() === 'admin';
|
||||
const isOwner = existing.ownerId === req.user!.userId;
|
||||
const isLegacy = existing.ownerId === '';
|
||||
if (!isOwner && !isAdmin && !isLegacy) {
|
||||
return res.status(403).json({ error: 'You do not have permission to edit this topology.' });
|
||||
}
|
||||
|
||||
const safeScope: 'global' | 'personal' = scope === 'personal' ? 'personal' : 'global';
|
||||
db.prepare(`UPDATE labs SET name = ?, description = ?, contactPerson = ?, location = ?, deviceIds = ?, topology = ?, semaphoreSetupTemplateId = ?, semaphoreTeardownTemplateId = ?, scope = ? WHERE id = ?`)
|
||||
.run(name, description, contactPerson, location, JSON.stringify(deviceIds), JSON.stringify(topology), semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId || '', safeScope, id);
|
||||
|
||||
addLog('maintenance',
|
||||
`Modified the active topology mapping schema for the "${name}" lab template.`,
|
||||
{ userId: req.user!.userId });
|
||||
|
||||
const r = db.prepare('SELECT * FROM labs WHERE id = ?').get(id) as any;
|
||||
res.json({ id: r.id, name: r.name, description: r.description, contactPerson: r.contactPerson, location: r.location, deviceIds: JSON.parse(r.deviceIds), topology: JSON.parse(r.topology), semaphoreSetupTemplateId: r.semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId: r.semaphoreTeardownTemplateId || '' });
|
||||
res.json({ id: r.id, name: r.name, description: r.description, contactPerson: r.contactPerson, location: r.location, deviceIds: JSON.parse(r.deviceIds), topology: JSON.parse(r.topology), semaphoreSetupTemplateId: r.semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId: r.semaphoreTeardownTemplateId || '', scope: r.scope, ownerId: r.ownerId });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
@ -580,6 +596,14 @@ async function startServer() {
|
||||
const lab = db.prepare('SELECT * FROM labs WHERE id = ?').get(id) as any;
|
||||
if (!lab) return res.status(404).json({ error: 'Lab template not found.' });
|
||||
|
||||
const reqUser = db.prepare('SELECT role FROM users WHERE id = ?').get(req.user!.userId) as any;
|
||||
const isAdmin = reqUser?.role?.toLowerCase() === 'admin';
|
||||
const isOwner = lab.ownerId === req.user!.userId;
|
||||
const isLegacy = lab.ownerId === '';
|
||||
if (!isOwner && !isAdmin && !isLegacy) {
|
||||
return res.status(403).json({ error: 'You do not have permission to delete this topology.' });
|
||||
}
|
||||
|
||||
db.prepare('DELETE FROM labs WHERE id = ?').run(id);
|
||||
db.prepare(`UPDATE bookings SET status = 'cancelled' WHERE labId = ? AND status = 'upcoming'`).run(id);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user