mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
78 lines
3 KiB
JavaScript
78 lines
3 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const { db } = require('../db/database');
|
|
const { canAdminWorkspace } = require('../lib/permissions');
|
|
|
|
// Workspace management routes. Operates on a target workspace specified by
|
|
// URL param, NOT the caller's currently active workspace - so this router
|
|
// does NOT use resolveTenancy. Permission is gated via canAdminWorkspace()
|
|
// which evaluates against the target workspace, not req.workspaceRole.
|
|
|
|
const NAME_MAX = 80;
|
|
const SLUG_MAX = 60;
|
|
const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
|
|
// Rename a workspace. MVP scope: name + slug only. Permission: platform_admin,
|
|
// org_owner/admin of the parent org, or workspace_admin of the target ws.
|
|
router.patch('/:id', (req, res) => {
|
|
const ws = db.prepare('SELECT * FROM workspaces WHERE id = ?').get(req.params.id);
|
|
if (!ws) return res.status(404).json({ error: 'Workspace not found' });
|
|
if (!canAdminWorkspace(db, req.user, ws)) {
|
|
return res.status(403).json({ error: 'Admin access required' });
|
|
}
|
|
|
|
// Stamp the target workspace_id so activityLogger captures the right
|
|
// tenant attribution. This route doesn't use resolveTenancy (operates on
|
|
// a URL-param target, not the caller's active workspace), so req.workspaceId
|
|
// would otherwise be undefined and the audit row would have NULL workspace.
|
|
req.workspaceId = ws.id;
|
|
|
|
const updates = [];
|
|
const values = [];
|
|
|
|
if (req.body.name !== undefined) {
|
|
const name = String(req.body.name).trim();
|
|
if (!name) return res.status(400).json({ error: 'Name cannot be empty' });
|
|
if (name.length > NAME_MAX) return res.status(400).json({ error: `Name must be ${NAME_MAX} characters or fewer` });
|
|
updates.push('name = ?');
|
|
values.push(name);
|
|
}
|
|
|
|
if (req.body.slug !== undefined) {
|
|
// Empty string -> NULL (workspace has no slug). Otherwise normalize +
|
|
// validate against the URL-safe segment pattern.
|
|
const raw = String(req.body.slug || '').trim().toLowerCase();
|
|
if (raw === '') {
|
|
updates.push('slug = NULL');
|
|
} else {
|
|
if (raw.length > SLUG_MAX) return res.status(400).json({ error: `Slug must be ${SLUG_MAX} characters or fewer` });
|
|
if (!SLUG_RE.test(raw)) {
|
|
return res.status(400).json({ error: 'Slug must be lowercase letters, digits, and hyphens (no leading/trailing/double hyphens)' });
|
|
}
|
|
updates.push('slug = ?');
|
|
values.push(raw);
|
|
}
|
|
}
|
|
|
|
if (updates.length === 0) {
|
|
return res.status(400).json({ error: 'No fields to update' });
|
|
}
|
|
|
|
updates.push("updated_at = strftime('%s','now')");
|
|
values.push(req.params.id);
|
|
|
|
try {
|
|
db.prepare(`UPDATE workspaces SET ${updates.join(', ')} WHERE id = ?`).run(...values);
|
|
} catch (e) {
|
|
if (e.code === 'SQLITE_CONSTRAINT_UNIQUE' || /UNIQUE/i.test(e.message)) {
|
|
return res.status(409).json({ error: 'Slug already used in this organization' });
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
const updated = db.prepare('SELECT id, name, slug, organization_id FROM workspaces WHERE id = ?').get(req.params.id);
|
|
res.json(updated);
|
|
});
|
|
|
|
module.exports = router;
|