mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
Phase 2.2f: white-label.js scoped to workspace_id; requireWorkspaceAdmin gate; status.js bundle
This commit is contained in:
parent
806c931e43
commit
0d642e4d80
|
|
@ -61,11 +61,13 @@ router.get('/export', (req, res) => {
|
||||||
if (!token) return res.status(401).json({ error: 'Token required' });
|
if (!token) return res.status(401).json({ error: 'Token required' });
|
||||||
|
|
||||||
let userId;
|
let userId;
|
||||||
|
let workspaceId;
|
||||||
try {
|
try {
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const decoded = jwt.verify(token, config.jwtSecret);
|
const decoded = jwt.verify(token, config.jwtSecret);
|
||||||
userId = decoded.id;
|
userId = decoded.id;
|
||||||
|
workspaceId = decoded.current_workspace_id || null;
|
||||||
if (!userId) return res.status(401).json({ error: 'Invalid token' });
|
if (!userId) return res.status(401).json({ error: 'Invalid token' });
|
||||||
} catch {
|
} catch {
|
||||||
return res.status(401).json({ error: 'Invalid token' });
|
return res.status(401).json({ error: 'Invalid token' });
|
||||||
|
|
@ -74,6 +76,17 @@ router.get('/export', (req, res) => {
|
||||||
const user = db.prepare('SELECT id, email, name, role, auth_provider, plan_id, created_at FROM users WHERE id = ?').get(userId);
|
const user = db.prepare('SELECT id, email, name, role, auth_provider, plan_id, created_at FROM users WHERE id = ?').get(userId);
|
||||||
if (!user) return res.status(404).json({ error: 'User not found' });
|
if (!user) return res.status(404).json({ error: 'User not found' });
|
||||||
|
|
||||||
|
// Phase 2.2f: export workspace-scoped branding. Fall back to first-accessible
|
||||||
|
// workspace if the JWT didn't carry one.
|
||||||
|
if (!workspaceId) {
|
||||||
|
const w = db.prepare(`
|
||||||
|
SELECT w.id FROM workspaces w
|
||||||
|
JOIN workspace_members wm ON wm.workspace_id = w.id
|
||||||
|
WHERE wm.user_id = ? ORDER BY wm.joined_at ASC LIMIT 1
|
||||||
|
`).get(userId);
|
||||||
|
workspaceId = w?.id || null;
|
||||||
|
}
|
||||||
|
|
||||||
const devices = db.prepare('SELECT id, name, status, ip_address, android_version, app_version, screen_width, screen_height, created_at FROM devices WHERE user_id = ?').all(userId);
|
const devices = db.prepare('SELECT id, name, status, ip_address, android_version, app_version, screen_width, screen_height, created_at FROM devices WHERE user_id = ?').all(userId);
|
||||||
const deviceIds = devices.map(d => d.id);
|
const deviceIds = devices.map(d => d.id);
|
||||||
const devicePlaceholders = deviceIds.map(() => '?').join(',') || "'__none__'";
|
const devicePlaceholders = deviceIds.map(() => '?').join(',') || "'__none__'";
|
||||||
|
|
@ -102,7 +115,7 @@ router.get('/export', (req, res) => {
|
||||||
const groupPlaceholders = groupIds.map(() => '?').join(',') || "'__none__'";
|
const groupPlaceholders = groupIds.map(() => '?').join(',') || "'__none__'";
|
||||||
const groupMembers = groupIds.length ? db.prepare(`SELECT * FROM device_group_members WHERE group_id IN (${groupPlaceholders})`).all(...groupIds) : [];
|
const groupMembers = groupIds.length ? db.prepare(`SELECT * FROM device_group_members WHERE group_id IN (${groupPlaceholders})`).all(...groupIds) : [];
|
||||||
const alertConfigs = db.prepare('SELECT id, alert_type, enabled, config, created_at FROM alert_configs WHERE user_id = ?').all(userId);
|
const alertConfigs = db.prepare('SELECT id, alert_type, enabled, config, created_at FROM alert_configs WHERE user_id = ?').all(userId);
|
||||||
const whiteLabel = db.prepare('SELECT * FROM white_labels WHERE user_id = ?').get(userId);
|
const whiteLabel = workspaceId ? db.prepare('SELECT * FROM white_labels WHERE workspace_id = ?').get(workspaceId) : null;
|
||||||
|
|
||||||
const exportData = {
|
const exportData = {
|
||||||
format: 'screentinker-export-v2',
|
format: 'screentinker-export-v2',
|
||||||
|
|
@ -425,14 +438,14 @@ router.post('/import', importUpload.single('file'), async (req, res) => {
|
||||||
db.prepare(`INSERT INTO alert_configs (id, user_id, alert_type, enabled, config, created_at) VALUES (?, ?, ?, ?, ?, ?)`).run(newId, userId, a.alert_type, a.enabled !== undefined ? a.enabled : 1, config, a.created_at || Math.floor(Date.now() / 1000));
|
db.prepare(`INSERT INTO alert_configs (id, user_id, alert_type, enabled, config, created_at) VALUES (?, ?, ?, ?, ?, ?)`).run(newId, userId, a.alert_type, a.enabled !== undefined ? a.enabled : 1, config, a.created_at || Math.floor(Date.now() / 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import white label
|
// Import white label - UPSERT into the importer's current workspace.
|
||||||
if (data.white_label) {
|
if (data.white_label && workspaceId) {
|
||||||
const wl = data.white_label;
|
const wl = data.white_label;
|
||||||
const existing = db.prepare('SELECT id FROM white_labels WHERE user_id = ?').get(userId);
|
const existing = db.prepare('SELECT id FROM white_labels WHERE workspace_id = ?').get(workspaceId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
db.prepare(`UPDATE white_labels SET brand_name=?, logo_url=?, favicon_url=?, primary_color=?, bg_color=?, custom_domain=?, custom_css=?, hide_branding=?, updated_at=strftime('%s','now') WHERE user_id=?`).run(wl.brand_name || 'ScreenTinker', wl.logo_url || null, wl.favicon_url || null, wl.primary_color || '#3B82F6', wl.bg_color || '#111827', wl.custom_domain || null, wl.custom_css || null, wl.hide_branding || 0, userId);
|
db.prepare(`UPDATE white_labels SET brand_name=?, logo_url=?, favicon_url=?, primary_color=?, bg_color=?, custom_domain=?, custom_css=?, hide_branding=?, updated_at=strftime('%s','now') WHERE workspace_id=?`).run(wl.brand_name || 'ScreenTinker', wl.logo_url || null, wl.favicon_url || null, wl.primary_color || '#3B82F6', wl.bg_color || '#111827', wl.custom_domain || null, wl.custom_css || null, wl.hide_branding || 0, workspaceId);
|
||||||
} else {
|
} else {
|
||||||
db.prepare(`INSERT INTO white_labels (id, user_id, brand_name, logo_url, favicon_url, primary_color, bg_color, custom_domain, custom_css, hide_branding) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(uuid.v4(), userId, wl.brand_name || 'ScreenTinker', wl.logo_url || null, wl.favicon_url || null, wl.primary_color || '#3B82F6', wl.bg_color || '#111827', wl.custom_domain || null, wl.custom_css || null, wl.hide_branding || 0);
|
db.prepare(`INSERT INTO white_labels (id, user_id, workspace_id, brand_name, logo_url, favicon_url, primary_color, bg_color, custom_domain, custom_css, hide_branding) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(uuid.v4(), userId, workspaceId, wl.brand_name || 'ScreenTinker', wl.logo_url || null, wl.favicon_url || null, wl.primary_color || '#3B82F6', wl.bg_color || '#111827', wl.custom_domain || null, wl.custom_css || null, wl.hide_branding || 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,30 +2,40 @@ const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const { db } = require('../db/database');
|
const { db } = require('../db/database');
|
||||||
|
// Phase 2.2f: workspace-scoped branding. POST gated by requireWorkspaceAdmin
|
||||||
|
// per the design doc (branding is a workspace_admin power, not editor).
|
||||||
|
const { requireWorkspaceAdmin } = require('../lib/permissions');
|
||||||
|
|
||||||
// Get current user's white-label config
|
// Get current workspace's white-label config.
|
||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
let wl = db.prepare('SELECT * FROM white_labels WHERE user_id = ?').get(req.user.id);
|
if (!req.workspaceId) {
|
||||||
|
return res.json({ brand_name: 'ScreenTinker', primary_color: '#3B82F6', secondary_color: '#1E293B', bg_color: '#111827', hide_branding: 0 });
|
||||||
|
}
|
||||||
|
let wl = db.prepare('SELECT * FROM white_labels WHERE workspace_id = ?').get(req.workspaceId);
|
||||||
if (!wl) {
|
if (!wl) {
|
||||||
// Return default branding
|
|
||||||
wl = { brand_name: 'ScreenTinker', primary_color: '#3B82F6', secondary_color: '#1E293B', bg_color: '#111827', hide_branding: 0 };
|
wl = { brand_name: 'ScreenTinker', primary_color: '#3B82F6', secondary_color: '#1E293B', bg_color: '#111827', hide_branding: 0 };
|
||||||
}
|
}
|
||||||
res.json(wl);
|
res.json(wl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get branding by domain (public, for white-label domains)
|
// Get branding by custom domain (public, unauthenticated - used pre-login by
|
||||||
|
// white-label frontends to resolve their hostname's branding). Keyed by the
|
||||||
|
// globally-unique custom_domain column; no scope check.
|
||||||
router.get('/domain/:domain', (req, res) => {
|
router.get('/domain/:domain', (req, res) => {
|
||||||
const wl = db.prepare('SELECT * FROM white_labels WHERE custom_domain = ?').get(req.params.domain);
|
const wl = db.prepare('SELECT * FROM white_labels WHERE custom_domain = ?').get(req.params.domain);
|
||||||
if (!wl) return res.json({ brand_name: 'ScreenTinker', primary_color: '#3B82F6' });
|
if (!wl) return res.json({ brand_name: 'ScreenTinker', primary_color: '#3B82F6' });
|
||||||
res.json(wl);
|
res.json(wl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create or update white-label config
|
// Create or update the current workspace's white-label config. Restricted to
|
||||||
router.post('/', (req, res) => {
|
// workspace_admin / org_owner / org_admin / platform_admin.
|
||||||
|
router.post('/', requireWorkspaceAdmin, (req, res) => {
|
||||||
|
if (!req.workspaceId) return res.status(403).json({ error: 'No workspace context. Switch to a workspace before configuring branding.' });
|
||||||
|
|
||||||
const { brand_name, logo_url, favicon_url, primary_color, secondary_color, bg_color,
|
const { brand_name, logo_url, favicon_url, primary_color, secondary_color, bg_color,
|
||||||
custom_domain, custom_css, hide_branding } = req.body;
|
custom_domain, custom_css, hide_branding } = req.body;
|
||||||
|
|
||||||
let wl = db.prepare('SELECT * FROM white_labels WHERE user_id = ?').get(req.user.id);
|
let wl = db.prepare('SELECT * FROM white_labels WHERE workspace_id = ?').get(req.workspaceId);
|
||||||
|
|
||||||
if (wl) {
|
if (wl) {
|
||||||
const fields = { brand_name, logo_url, favicon_url, primary_color, secondary_color, bg_color, custom_domain, custom_css, hide_branding };
|
const fields = { brand_name, logo_url, favicon_url, primary_color, secondary_color, bg_color, custom_domain, custom_css, hide_branding };
|
||||||
|
|
@ -36,19 +46,19 @@ router.post('/', (req, res) => {
|
||||||
});
|
});
|
||||||
if (updates.length) {
|
if (updates.length) {
|
||||||
updates.push("updated_at = strftime('%s','now')");
|
updates.push("updated_at = strftime('%s','now')");
|
||||||
values.push(req.user.id);
|
values.push(req.workspaceId);
|
||||||
db.prepare(`UPDATE white_labels SET ${updates.join(', ')} WHERE user_id = ?`).run(...values);
|
db.prepare(`UPDATE white_labels SET ${updates.join(', ')} WHERE workspace_id = ?`).run(...values);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
db.prepare(`INSERT INTO white_labels (id, user_id, brand_name, logo_url, favicon_url, primary_color, secondary_color, bg_color, custom_domain, custom_css, hide_branding)
|
db.prepare(`INSERT INTO white_labels (id, user_id, workspace_id, brand_name, logo_url, favicon_url, primary_color, secondary_color, bg_color, custom_domain, custom_css, hide_branding)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
||||||
id, req.user.id, brand_name || 'ScreenTinker', logo_url || null, favicon_url || null,
|
id, req.user.id, req.workspaceId, brand_name || 'ScreenTinker', logo_url || null, favicon_url || null,
|
||||||
primary_color || '#3B82F6', secondary_color || '#1E293B', bg_color || '#111827',
|
primary_color || '#3B82F6', secondary_color || '#1E293B', bg_color || '#111827',
|
||||||
custom_domain || null, custom_css || null, hide_branding ? 1 : 0);
|
custom_domain || null, custom_css || null, hide_branding ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(db.prepare('SELECT * FROM white_labels WHERE user_id = ?').get(req.user.id));
|
res.json(db.prepare('SELECT * FROM white_labels WHERE workspace_id = ?').get(req.workspaceId));
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue