mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
Phase 2.1: tenancy middleware, permission helpers, JWT workspace context, frontend + backend role-rename compat
This commit is contained in:
parent
d8492f3720
commit
2954fd1a84
|
|
@ -20,6 +20,7 @@ import * as designer from './views/designer.js';
|
|||
import * as playlists from './views/playlists.js';
|
||||
import { applyBranding } from './branding.js';
|
||||
import { t } from './i18n.js';
|
||||
import { isPlatformAdmin } from './utils.js';
|
||||
|
||||
const app = document.getElementById('app');
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
|
|
@ -227,9 +228,9 @@ function updateSidebarUser() {
|
|||
const user = getCurrentUser();
|
||||
if (!user) return;
|
||||
|
||||
// Show admin nav only for superadmins
|
||||
// Show admin nav only for platform admins (legacy 'superadmin' or Phase 1 renamed 'platform_admin')
|
||||
const adminNav = document.getElementById('adminNavItem');
|
||||
if (adminNav) adminNav.style.display = user.role === 'superadmin' ? '' : 'none';
|
||||
if (adminNav) adminNav.style.display = isPlatformAdmin(user) ? '' : 'none';
|
||||
|
||||
let userEl = document.getElementById('sidebarUser');
|
||||
if (!userEl) {
|
||||
|
|
|
|||
|
|
@ -3,3 +3,11 @@ export function esc(str) {
|
|||
if (str == null) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||
}
|
||||
|
||||
// Phase 2.1: the Phase 1 schema migration renamed the legacy 'superadmin'
|
||||
// role to 'platform_admin'. Existing frontend checks still match the old
|
||||
// string; this helper accepts both so we don't have to splatter the array
|
||||
// at every call site. Use everywhere the UI gates on platform-level access.
|
||||
export function isPlatformAdmin(user) {
|
||||
return !!(user && (user.role === 'superadmin' || user.role === 'platform_admin'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { api } from '../api.js';
|
||||
import { showToast } from '../components/toast.js';
|
||||
import { esc } from '../utils.js';
|
||||
import { esc, isPlatformAdmin } from '../utils.js';
|
||||
import { t } from '../i18n.js';
|
||||
|
||||
const headers = () => ({ Authorization: `Bearer ${localStorage.getItem('token')}`, 'Content-Type': 'application/json' });
|
||||
|
|
@ -8,7 +8,7 @@ const API = (url, opts = {}) => fetch('/api' + url, { headers: headers(), ...opt
|
|||
|
||||
export async function render(container) {
|
||||
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
if (user.role !== 'superadmin') {
|
||||
if (!isPlatformAdmin(user)) {
|
||||
container.innerHTML = `<div class="empty-state"><h3>${t('admin.access_denied')}</h3><p>${t('admin.access_denied_desc')}</p></div>`;
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { api } from '../api.js';
|
||||
import { showToast } from '../components/toast.js';
|
||||
import { getLanguage, setLanguage, getAvailableLanguages, t, tn } from '../i18n.js';
|
||||
import { esc } from '../utils.js';
|
||||
import { esc, isPlatformAdmin } from '../utils.js';
|
||||
import { resetBranding } from '../branding.js';
|
||||
|
||||
export async function render(container) {
|
||||
|
|
@ -11,7 +11,7 @@ export async function render(container) {
|
|||
let user;
|
||||
try { user = await api.getMe(); localStorage.setItem('user', JSON.stringify(user)); }
|
||||
catch { user = JSON.parse(localStorage.getItem('user') || '{}'); }
|
||||
const isSuperAdmin = user.role === 'superadmin';
|
||||
const isSuperAdmin = isPlatformAdmin(user);
|
||||
const isAdmin = user.role === 'admin' || isSuperAdmin;
|
||||
|
||||
container.innerHTML = `
|
||||
|
|
@ -327,11 +327,11 @@ async function loadWhiteLabel() {
|
|||
const token = localStorage.getItem('token');
|
||||
const headers = { Authorization: `Bearer ${token}` };
|
||||
|
||||
// Only show white-label for enterprise/superadmin.
|
||||
// Only show white-label for enterprise plans or platform admins.
|
||||
// Use the fresh user cached by render() above, which called api.getMe().
|
||||
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
const section = document.getElementById('whiteLabelSection');
|
||||
if (section && user.plan_id !== 'enterprise' && user.role !== 'superadmin') {
|
||||
if (section && user.plan_id !== 'enterprise' && !isPlatformAdmin(user)) {
|
||||
section.innerHTML = `
|
||||
<h3>${t('settings.white_label')}</h3>
|
||||
<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:16px;text-align:center">
|
||||
|
|
|
|||
107
server/lib/permissions.js
Normal file
107
server/lib/permissions.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// Phase 2.1: permission helpers.
|
||||
//
|
||||
// Routes call these as Express middleware to gate access, or as predicate
|
||||
// functions to branch within a handler. They presume resolveTenancy has
|
||||
// already attached req.workspaceId / req.workspaceRole / req.orgRole /
|
||||
// req.isPlatformAdmin.
|
||||
//
|
||||
// Layering (top wins):
|
||||
// 1. req.isPlatformAdmin -> allow anything
|
||||
// 2. req.orgRole in {org_owner, org_admin} -> allow read/write/admin within the org
|
||||
// org_owner also has billing.write and org.delete (not exposed in 2.1)
|
||||
// 3. req.workspaceRole in {workspace_admin, workspace_editor, workspace_viewer}
|
||||
// gates resource access per the role's bands
|
||||
|
||||
'use strict';
|
||||
|
||||
function canRead(req) {
|
||||
if (req.isPlatformAdmin) return true;
|
||||
if (req.orgRole === 'org_owner' || req.orgRole === 'org_admin') return true;
|
||||
return !!req.workspaceRole; // any workspace_member can read
|
||||
}
|
||||
|
||||
function canWrite(req) {
|
||||
if (req.isPlatformAdmin) return true;
|
||||
if (req.orgRole === 'org_owner' || req.orgRole === 'org_admin') return true;
|
||||
return req.workspaceRole === 'workspace_admin' || req.workspaceRole === 'workspace_editor';
|
||||
}
|
||||
|
||||
function canAdmin(req) {
|
||||
if (req.isPlatformAdmin) return true;
|
||||
if (req.orgRole === 'org_owner' || req.orgRole === 'org_admin') return true;
|
||||
return req.workspaceRole === 'workspace_admin';
|
||||
}
|
||||
|
||||
function isOrgAdmin(req) {
|
||||
if (req.isPlatformAdmin) return true;
|
||||
return req.orgRole === 'org_owner' || req.orgRole === 'org_admin';
|
||||
}
|
||||
|
||||
function isOrgOwner(req) {
|
||||
if (req.isPlatformAdmin) return true;
|
||||
return req.orgRole === 'org_owner';
|
||||
}
|
||||
|
||||
// ---- middleware variants ----
|
||||
|
||||
function requireWorkspace(req, res, next) {
|
||||
if (!req.workspaceId) {
|
||||
return res.status(403).json({ error: 'No workspace context' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
function requireWorkspaceRead(req, res, next) {
|
||||
if (!canRead(req)) {
|
||||
return res.status(403).json({ error: 'Workspace access required' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
function requireWorkspaceWrite(req, res, next) {
|
||||
if (!canWrite(req)) {
|
||||
return res.status(403).json({ error: 'Workspace editor or admin required' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
function requireWorkspaceAdmin(req, res, next) {
|
||||
if (!canAdmin(req)) {
|
||||
return res.status(403).json({ error: 'Workspace admin required' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
function requireOrgAdmin(req, res, next) {
|
||||
if (!isOrgAdmin(req)) {
|
||||
return res.status(403).json({ error: 'Organization admin required' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
function requireOrgOwner(req, res, next) {
|
||||
if (!isOrgOwner(req)) {
|
||||
return res.status(403).json({ error: 'Organization owner required' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
function requirePlatformAdmin(req, res, next) {
|
||||
if (!req.user || req.user.role !== 'platform_admin') {
|
||||
return res.status(403).json({ error: 'Platform admin required' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
// boolean predicates
|
||||
canRead, canWrite, canAdmin, isOrgAdmin, isOrgOwner,
|
||||
// express middleware
|
||||
requireWorkspace,
|
||||
requireWorkspaceRead,
|
||||
requireWorkspaceWrite,
|
||||
requireWorkspaceAdmin,
|
||||
requireOrgAdmin,
|
||||
requireOrgOwner,
|
||||
requirePlatformAdmin,
|
||||
};
|
||||
148
server/lib/tenancy.js
Normal file
148
server/lib/tenancy.js
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
// Phase 2.1: per-request tenancy resolver.
|
||||
//
|
||||
// Runs after requireAuth (which sets req.user and req.jwtWorkspaceId).
|
||||
// Resolves the active workspace context for this request and attaches:
|
||||
//
|
||||
// req.workspaceId string | null the workspace this request operates in
|
||||
// req.workspace object | null the full workspaces row
|
||||
// req.organizationId string | null parent org of req.workspace
|
||||
// req.workspaceRole string | null 'workspace_admin' | 'workspace_editor' | 'workspace_viewer'
|
||||
// req.orgRole string | null 'org_owner' | 'org_admin'
|
||||
// req.isPlatformAdmin boolean shortcut for req.user.role === 'platform_admin'
|
||||
// req.actingAs boolean true when the user reached this workspace via
|
||||
// org-level or platform-level access rather than
|
||||
// a direct workspace_members row
|
||||
//
|
||||
// Resolution order, top wins:
|
||||
// 1. X-Workspace-Id header (for explicit per-request override)
|
||||
// 2. ?workspace_id= query param (same purpose, easier in browser dev)
|
||||
// 3. JWT current_workspace_id (the user's last switched-to workspace)
|
||||
// 4. First workspace_members row for user (sorted by joined_at ASC)
|
||||
// 5. For platform_admin only: any workspace
|
||||
//
|
||||
// Steps 1-3 are validated against access. If a stale value (e.g. user was
|
||||
// removed from the workspace) is found, it's discarded and we fall through.
|
||||
|
||||
'use strict';
|
||||
|
||||
const { db } = require('../db/database');
|
||||
|
||||
function membershipOf(userId, workspaceId) {
|
||||
return db.prepare(
|
||||
'SELECT role FROM workspace_members WHERE workspace_id = ? AND user_id = ?'
|
||||
).get(workspaceId, userId);
|
||||
}
|
||||
|
||||
function orgMembershipOf(userId, organizationId) {
|
||||
return db.prepare(
|
||||
'SELECT role FROM organization_members WHERE organization_id = ? AND user_id = ?'
|
||||
).get(organizationId, userId);
|
||||
}
|
||||
|
||||
function loadWorkspace(workspaceId) {
|
||||
if (!workspaceId) return null;
|
||||
return db.prepare('SELECT * FROM workspaces WHERE id = ?').get(workspaceId);
|
||||
}
|
||||
|
||||
function firstAccessibleWorkspace(userId) {
|
||||
return db.prepare(`
|
||||
SELECT w.* 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);
|
||||
}
|
||||
|
||||
// Check whether userId can access workspace via any path (member, org admin,
|
||||
// or platform admin). Returns the access context: { workspaceRole, actingAs }
|
||||
// or null if no access.
|
||||
function accessContext(userId, role, workspace) {
|
||||
const isPlatformAdmin = role === 'platform_admin';
|
||||
const wsMembership = membershipOf(userId, workspace.id);
|
||||
if (wsMembership) {
|
||||
return { workspaceRole: wsMembership.role, actingAs: false };
|
||||
}
|
||||
const orgMembership = orgMembershipOf(userId, workspace.organization_id);
|
||||
if (orgMembership && (orgMembership.role === 'org_owner' || orgMembership.role === 'org_admin')) {
|
||||
return { workspaceRole: null, actingAs: true };
|
||||
}
|
||||
if (isPlatformAdmin) {
|
||||
return { workspaceRole: null, actingAs: true };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveTenancy(req, res, next) {
|
||||
if (!req.user) {
|
||||
// Should not happen when chained after requireAuth, but tolerate optionalAuth flows.
|
||||
return next();
|
||||
}
|
||||
|
||||
const isPlatformAdmin = req.user.role === 'platform_admin';
|
||||
req.isPlatformAdmin = isPlatformAdmin;
|
||||
|
||||
// Build the ordered candidate list of workspace_ids to try.
|
||||
const candidates = [];
|
||||
const headerWs = (req.headers['x-workspace-id'] || '').trim();
|
||||
if (headerWs) candidates.push(headerWs);
|
||||
if (req.query && req.query.workspace_id) candidates.push(String(req.query.workspace_id));
|
||||
if (req.jwtWorkspaceId) candidates.push(req.jwtWorkspaceId);
|
||||
|
||||
let workspace = null;
|
||||
let context = null;
|
||||
for (const wsId of candidates) {
|
||||
const ws = loadWorkspace(wsId);
|
||||
if (!ws) continue;
|
||||
const ctx = accessContext(req.user.id, req.user.role, ws);
|
||||
if (!ctx) continue;
|
||||
workspace = ws;
|
||||
context = ctx;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!workspace) {
|
||||
// Fall back to the user's first workspace_members row.
|
||||
const first = firstAccessibleWorkspace(req.user.id);
|
||||
if (first) {
|
||||
workspace = first;
|
||||
const wm = membershipOf(req.user.id, first.id);
|
||||
context = { workspaceRole: wm.role, actingAs: false };
|
||||
} else if (isPlatformAdmin) {
|
||||
// Platform admin with no direct memberships: pick any workspace (acting-as).
|
||||
const any = db.prepare('SELECT * FROM workspaces LIMIT 1').get();
|
||||
if (any) {
|
||||
workspace = any;
|
||||
context = { workspaceRole: null, actingAs: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (workspace) {
|
||||
req.workspaceId = workspace.id;
|
||||
req.workspace = workspace;
|
||||
req.organizationId = workspace.organization_id;
|
||||
req.workspaceRole = context.workspaceRole;
|
||||
req.actingAs = context.actingAs;
|
||||
const orgMembership = orgMembershipOf(req.user.id, workspace.organization_id);
|
||||
req.orgRole = orgMembership ? orgMembership.role : null;
|
||||
} else {
|
||||
req.workspaceId = null;
|
||||
req.workspace = null;
|
||||
req.organizationId = null;
|
||||
req.workspaceRole = null;
|
||||
req.orgRole = null;
|
||||
req.actingAs = false;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resolveTenancy,
|
||||
// Exported for testing / direct use by routes that need ad-hoc checks.
|
||||
accessContext,
|
||||
membershipOf,
|
||||
orgMembershipOf,
|
||||
firstAccessibleWorkspace,
|
||||
};
|
||||
|
|
@ -2,9 +2,14 @@ const jwt = require('jsonwebtoken');
|
|||
const config = require('../config');
|
||||
const { db } = require('../db/database');
|
||||
|
||||
function generateToken(user) {
|
||||
// Phase 2.1: JWT now optionally carries the user's current workspace_id so
|
||||
// the tenancy middleware can resolve scope without an extra DB lookup on
|
||||
// every request. Callers that don't know the workspace yet (legacy paths,
|
||||
// recovery tokens) pass null and the tenancy resolver falls back to the
|
||||
// user's first accessible workspace.
|
||||
function generateToken(user, currentWorkspaceId) {
|
||||
return jwt.sign(
|
||||
{ id: user.id, email: user.email, role: user.role },
|
||||
{ id: user.id, email: user.email, role: user.role, current_workspace_id: currentWorkspaceId || null },
|
||||
config.jwtSecret,
|
||||
{ algorithm: 'HS256', expiresIn: config.jwtExpiry }
|
||||
);
|
||||
|
|
@ -40,11 +45,14 @@ function requireAuth(req, res, next) {
|
|||
const decoded = verifyToken(token);
|
||||
if (decoded.recovery) {
|
||||
req.user = recoveryUser(decoded);
|
||||
req.jwtWorkspaceId = null;
|
||||
return next();
|
||||
}
|
||||
const user = db.prepare('SELECT id, email, name, role, auth_provider, avatar_url, plan_id FROM users WHERE id = ?').get(decoded.id);
|
||||
if (!user) return res.status(401).json({ error: 'User not found' });
|
||||
req.user = user;
|
||||
// Tenancy middleware reads this on the resolver step.
|
||||
req.jwtWorkspaceId = decoded.current_workspace_id || null;
|
||||
next();
|
||||
} catch (err) {
|
||||
return res.status(401).json({ error: 'Invalid or expired token' });
|
||||
|
|
@ -61,6 +69,7 @@ function optionalAuth(req, res, next) {
|
|||
req.user = decoded.recovery
|
||||
? recoveryUser(decoded)
|
||||
: db.prepare('SELECT id, email, name, role, auth_provider, avatar_url, plan_id FROM users WHERE id = ?').get(decoded.id);
|
||||
req.jwtWorkspaceId = decoded.current_workspace_id || null;
|
||||
} catch (err) {
|
||||
// Token invalid, continue without user
|
||||
}
|
||||
|
|
@ -68,20 +77,30 @@ function optionalAuth(req, res, next) {
|
|||
next();
|
||||
}
|
||||
|
||||
// Require admin role (admin or superadmin)
|
||||
// Phase 2.1: role rename. Phase 1 renamed 'superadmin' to 'platform_admin' and
|
||||
// dropped the in-between 'admin' role. These two guards are widened to accept
|
||||
// either spelling so existing callers keep working without per-route edits.
|
||||
// New code should prefer requirePlatformAdmin / requireOrgAdmin / workspace
|
||||
// role guards from server/lib/permissions.js.
|
||||
|
||||
const PLATFORM_ROLES = ['superadmin', 'platform_admin'];
|
||||
const ELEVATED_ROLES = ['admin', 'superadmin', 'platform_admin'];
|
||||
|
||||
function requireAdmin(req, res, next) {
|
||||
if (!req.user || !['admin', 'superadmin'].includes(req.user.role)) {
|
||||
if (!req.user || !ELEVATED_ROLES.includes(req.user.role)) {
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// Require superadmin role (platform owner only)
|
||||
function requireSuperAdmin(req, res, next) {
|
||||
if (!req.user || req.user.role !== 'superadmin') {
|
||||
if (!req.user || !PLATFORM_ROLES.includes(req.user.role)) {
|
||||
return res.status(403).json({ error: 'Platform admin access required' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = { generateToken, verifyToken, requireAuth, optionalAuth, requireAdmin, requireSuperAdmin };
|
||||
// Preferred alias for new code.
|
||||
const requirePlatformAdmin = requireSuperAdmin;
|
||||
|
||||
module.exports = { generateToken, verifyToken, requireAuth, optionalAuth, requireAdmin, requireSuperAdmin, requirePlatformAdmin, PLATFORM_ROLES, ELEVATED_ROLES };
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getActivity, pruneActivityLog } = require('../services/activity');
|
||||
const { PLATFORM_ROLES, ELEVATED_ROLES } = require('../middleware/auth');
|
||||
|
||||
// Get activity log
|
||||
router.get('/', (req, res) => {
|
||||
const { device_id, limit, offset } = req.query;
|
||||
const isAdmin = req.user.role === 'superadmin';
|
||||
const isAdmin = PLATFORM_ROLES.includes(req.user.role);
|
||||
|
||||
const activity = getActivity({
|
||||
userId: isAdmin ? null : req.user.id,
|
||||
|
|
@ -19,7 +20,7 @@ router.get('/', (req, res) => {
|
|||
|
||||
// Prune old logs (admin only)
|
||||
router.delete('/prune', (req, res) => {
|
||||
if (!['admin','superadmin'].includes(req.user.role)) return res.status(403).json({ error: 'Admin only' });
|
||||
if (!ELEVATED_ROLES.includes(req.user.role)) return res.status(403).json({ error: 'Admin only' });
|
||||
pruneActivityLog();
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const express = require('express');
|
|||
const router = express.Router();
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { db } = require('../db/database');
|
||||
const { ELEVATED_ROLES } = require('../middleware/auth');
|
||||
|
||||
// Mark playlist as draft (called after any item mutation)
|
||||
function markDraft(playlistId) {
|
||||
|
|
@ -12,7 +13,7 @@ function markDraft(playlistId) {
|
|||
function checkDeviceAccess(req, res, paramName = 'deviceId') {
|
||||
const device = db.prepare('SELECT user_id FROM devices WHERE id = ?').get(req.params[paramName]);
|
||||
if (!device) { res.status(404).json({ error: 'Device not found' }); return false; }
|
||||
if (!['admin','superadmin'].includes(req.user.role) && device.user_id && device.user_id !== req.user.id) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && device.user_id && device.user_id !== req.user.id) {
|
||||
res.status(403).json({ error: 'Access denied' }); return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -65,7 +66,7 @@ router.post('/device/:deviceId', (req, res) => {
|
|||
if (content_id) {
|
||||
const content = db.prepare('SELECT id, user_id FROM content WHERE id = ?').get(content_id);
|
||||
if (!content) return res.status(404).json({ error: 'Content not found' });
|
||||
if (!['admin','superadmin'].includes(req.user.role) && content.user_id && content.user_id !== req.user.id) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && content.user_id && content.user_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Content not owned by you' });
|
||||
}
|
||||
}
|
||||
|
|
@ -105,7 +106,7 @@ router.post('/device/:deviceId', (req, res) => {
|
|||
router.put('/:id', (req, res) => {
|
||||
const item = db.prepare('SELECT pi.*, p.user_id FROM playlist_items pi JOIN playlists p ON pi.playlist_id = p.id WHERE pi.id = ?').get(req.params.id);
|
||||
if (!item) return res.status(404).json({ error: 'Item not found' });
|
||||
if (!['admin','superadmin'].includes(req.user.role) && item.user_id !== req.user.id) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && item.user_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +132,7 @@ router.put('/:id', (req, res) => {
|
|||
router.delete('/:id', (req, res) => {
|
||||
const item = db.prepare('SELECT pi.*, p.user_id FROM playlist_items pi JOIN playlists p ON pi.playlist_id = p.id WHERE pi.id = ?').get(req.params.id);
|
||||
if (!item) return res.status(404).json({ error: 'Item not found' });
|
||||
if (!['admin','superadmin'].includes(req.user.role) && item.user_id !== req.user.id) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && item.user_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,48 @@ const https = require('https');
|
|||
const { v4: uuidv4 } = require('uuid');
|
||||
const { OAuth2Client } = require('google-auth-library');
|
||||
const { db } = require('../db/database');
|
||||
const { generateToken, requireAuth, requireAdmin, requireSuperAdmin } = require('../middleware/auth');
|
||||
const { generateToken, requireAuth, requireAdmin, requireSuperAdmin, PLATFORM_ROLES } = require('../middleware/auth');
|
||||
const { resolveTenancy } = require('../lib/tenancy');
|
||||
const { logActivity, getClientIp } = require('../services/activity');
|
||||
const config = require('../config');
|
||||
|
||||
// Phase 2.1: find or create the user's default org+workspace. Returns the
|
||||
// workspace_id to embed in the JWT. Idempotent: if the user already has
|
||||
// memberships (e.g. migrated from Phase 1), returns the first one without
|
||||
// creating anything.
|
||||
function ensureDefaultOrgForUser(user) {
|
||||
const existing = 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(user.id);
|
||||
if (existing) return existing.id;
|
||||
|
||||
// No memberships -> mint a fresh org and Default workspace owned by user.
|
||||
const orgId = uuidv4();
|
||||
const wsId = uuidv4();
|
||||
const orgName = (user.name && user.name.trim())
|
||||
? `${user.name}'s organization`
|
||||
: `${user.email}'s organization`;
|
||||
const tx = db.transaction(() => {
|
||||
db.prepare(`INSERT INTO organizations (
|
||||
id, name, owner_user_id, plan_id,
|
||||
stripe_customer_id, stripe_subscription_id,
|
||||
subscription_status, subscription_ends
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
||||
orgId, orgName, user.id, user.plan_id || 'free',
|
||||
user.stripe_customer_id || null, user.stripe_subscription_id || null,
|
||||
user.subscription_status || 'active', user.subscription_ends || null
|
||||
);
|
||||
db.prepare(`INSERT INTO organization_members (organization_id, user_id, role) VALUES (?, ?, 'org_owner')`).run(orgId, user.id);
|
||||
db.prepare(`INSERT INTO workspaces (id, organization_id, name, created_by) VALUES (?, ?, 'Default', ?)`).run(wsId, orgId, user.id);
|
||||
db.prepare(`INSERT INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, 'workspace_admin')`).run(wsId, user.id);
|
||||
});
|
||||
tx();
|
||||
return wsId;
|
||||
}
|
||||
|
||||
function logFailedLogin(email, ip, reason) {
|
||||
try {
|
||||
db.prepare('INSERT INTO activity_log (user_id, action, details, ip_address) VALUES (NULL, ?, ?, ?)')
|
||||
|
|
@ -49,9 +87,10 @@ router.post('/register', (req, res) => {
|
|||
const id = uuidv4();
|
||||
const passwordHash = bcrypt.hashSync(password, 10);
|
||||
|
||||
// First user becomes admin with enterprise plan (self-hosted) or free plan with Pro trial
|
||||
// First user becomes platform_admin with enterprise plan (self-hosted) or free plan with Pro trial.
|
||||
// Phase 1 renamed the legacy 'superadmin' role to 'platform_admin'; new bootstrap users get the new name directly.
|
||||
const userCount = db.prepare('SELECT COUNT(*) as count FROM users').get().count;
|
||||
const role = userCount === 0 ? 'superadmin' : 'user';
|
||||
const role = userCount === 0 ? 'platform_admin' : 'user';
|
||||
const isFirstUser = userCount === 0;
|
||||
const plan = (isFirstUser && config.selfHosted) ? 'enterprise' : 'pro'; // Start on Pro trial
|
||||
const trialStarted = isFirstUser && config.selfHosted ? null : Math.floor(Date.now() / 1000);
|
||||
|
|
@ -61,10 +100,11 @@ router.post('/register', (req, res) => {
|
|||
VALUES (?, ?, ?, ?, 'local', ?, ?, ?, ?)
|
||||
`).run(id, email.toLowerCase(), name || email.split('@')[0], passwordHash, role, plan, trialStarted, trialStarted ? 'pro' : null);
|
||||
|
||||
const user = db.prepare('SELECT id, email, name, role, auth_provider, avatar_url, plan_id FROM users WHERE id = ?').get(id);
|
||||
const token = generateToken(user);
|
||||
const user = db.prepare('SELECT id, email, name, role, auth_provider, avatar_url, plan_id, stripe_customer_id, stripe_subscription_id, subscription_status, subscription_ends FROM users WHERE id = ?').get(id);
|
||||
const workspaceId = ensureDefaultOrgForUser(user);
|
||||
const token = generateToken(user, workspaceId);
|
||||
|
||||
res.status(201).json({ token, user });
|
||||
res.status(201).json({ token, user, current_workspace_id: workspaceId });
|
||||
});
|
||||
|
||||
// Login
|
||||
|
|
@ -84,9 +124,10 @@ router.post('/login', (req, res) => {
|
|||
}
|
||||
|
||||
logSuccessfulLogin(user.id, email, getClientIp(req));
|
||||
const token = generateToken(user);
|
||||
const workspaceId = ensureDefaultOrgForUser(user);
|
||||
const token = generateToken(user, workspaceId);
|
||||
const { password_hash, ...safeUser } = user;
|
||||
res.json({ token, user: safeUser });
|
||||
res.json({ token, user: safeUser, current_workspace_id: workspaceId });
|
||||
});
|
||||
|
||||
// ==================== Google OAuth ====================
|
||||
|
|
@ -111,7 +152,7 @@ router.post('/google', async (req, res) => {
|
|||
}
|
||||
const id = uuidv4();
|
||||
const userCount = db.prepare('SELECT COUNT(*) as count FROM users').get().count;
|
||||
const role = userCount === 0 ? 'superadmin' : 'user';
|
||||
const role = userCount === 0 ? 'platform_admin' : 'user';
|
||||
const isFirst = userCount === 0;
|
||||
const plan = (isFirst && config.selfHosted) ? 'enterprise' : 'pro';
|
||||
const trialStarted = isFirst && config.selfHosted ? null : Math.floor(Date.now() / 1000);
|
||||
|
|
@ -134,9 +175,10 @@ router.post('/google', async (req, res) => {
|
|||
user = db.prepare('SELECT * FROM users WHERE id = ?').get(user.id);
|
||||
}
|
||||
|
||||
const token = generateToken(user);
|
||||
const workspaceId = ensureDefaultOrgForUser(user);
|
||||
const token = generateToken(user, workspaceId);
|
||||
const { password_hash, ...safeUser } = user;
|
||||
res.json({ token, user: safeUser });
|
||||
res.json({ token, user: safeUser, current_workspace_id: workspaceId });
|
||||
} catch (err) {
|
||||
console.error('Google auth error:', err);
|
||||
res.status(401).json({ error: 'Google authentication failed' });
|
||||
|
|
@ -189,7 +231,7 @@ router.post('/microsoft', async (req, res) => {
|
|||
}
|
||||
const id = uuidv4();
|
||||
const userCount = db.prepare('SELECT COUNT(*) as count FROM users').get().count;
|
||||
const role = userCount === 0 ? 'superadmin' : 'user';
|
||||
const role = userCount === 0 ? 'platform_admin' : 'user';
|
||||
const isFirst = userCount === 0;
|
||||
const plan = (isFirst && config.selfHosted) ? 'enterprise' : 'pro';
|
||||
const trialStarted = isFirst && config.selfHosted ? null : Math.floor(Date.now() / 1000);
|
||||
|
|
@ -210,9 +252,10 @@ router.post('/microsoft', async (req, res) => {
|
|||
user = db.prepare('SELECT * FROM users WHERE id = ?').get(user.id);
|
||||
}
|
||||
|
||||
const token = generateToken(user);
|
||||
const workspaceId = ensureDefaultOrgForUser(user);
|
||||
const token = generateToken(user, workspaceId);
|
||||
const { password_hash, ...safeUser } = user;
|
||||
res.json({ token, user: safeUser });
|
||||
res.json({ token, user: safeUser, current_workspace_id: workspaceId });
|
||||
} catch (err) {
|
||||
console.error('Microsoft auth error:', err);
|
||||
res.status(401).json({ error: 'Microsoft authentication failed' });
|
||||
|
|
@ -238,9 +281,60 @@ function getMicrosoftProfile(accessToken) {
|
|||
|
||||
// ==================== User Management ====================
|
||||
|
||||
// Get current user
|
||||
router.get('/me', requireAuth, (req, res) => {
|
||||
res.json(req.user);
|
||||
// Get current user + tenancy context.
|
||||
// Phase 2.1: response shape extended with current_workspace, current_organization,
|
||||
// roles, and the list of accessible workspaces. Legacy fields (user object at
|
||||
// the top level) are preserved so existing frontend code continues to work.
|
||||
router.get('/me', requireAuth, resolveTenancy, (req, res) => {
|
||||
const accessible = db.prepare(`
|
||||
SELECT w.id, w.name, w.organization_id, o.name AS organization_name, wm.role AS workspace_role
|
||||
FROM workspace_members wm
|
||||
JOIN workspaces w ON w.id = wm.workspace_id
|
||||
JOIN organizations o ON o.id = w.organization_id
|
||||
WHERE wm.user_id = ?
|
||||
ORDER BY o.name, w.name
|
||||
`).all(req.user.id);
|
||||
|
||||
const currentOrg = req.organizationId
|
||||
? db.prepare('SELECT id, name FROM organizations WHERE id = ?').get(req.organizationId)
|
||||
: null;
|
||||
|
||||
res.json({
|
||||
...req.user,
|
||||
current_workspace_id: req.workspaceId,
|
||||
current_workspace: req.workspace ? { id: req.workspace.id, name: req.workspace.name, organization_id: req.workspace.organization_id } : null,
|
||||
current_organization: currentOrg,
|
||||
current_workspace_role: req.workspaceRole,
|
||||
current_org_role: req.orgRole,
|
||||
is_platform_admin: req.isPlatformAdmin,
|
||||
acting_as: req.actingAs,
|
||||
accessible_workspaces: accessible,
|
||||
});
|
||||
});
|
||||
|
||||
// Switch the active workspace. Validates the user has access (direct
|
||||
// workspace_member, org-level admin in the parent org, or platform_admin),
|
||||
// then mints a fresh JWT with the new current_workspace_id.
|
||||
router.post('/switch-workspace', requireAuth, (req, res) => {
|
||||
const { workspace_id } = req.body || {};
|
||||
if (!workspace_id) return res.status(400).json({ error: 'workspace_id required' });
|
||||
|
||||
const ws = db.prepare('SELECT * FROM workspaces WHERE id = ?').get(workspace_id);
|
||||
if (!ws) return res.status(404).json({ error: 'Workspace not found' });
|
||||
|
||||
const isPlatformAdmin = req.user.role === 'platform_admin' || req.user.role === 'superadmin';
|
||||
const wsMember = db.prepare('SELECT 1 FROM workspace_members WHERE workspace_id = ? AND user_id = ?').get(ws.id, req.user.id);
|
||||
const orgMember = db.prepare(`
|
||||
SELECT role FROM organization_members WHERE organization_id = ? AND user_id = ?
|
||||
`).get(ws.organization_id, req.user.id);
|
||||
const canAct = isPlatformAdmin
|
||||
|| !!wsMember
|
||||
|| (orgMember && (orgMember.role === 'org_owner' || orgMember.role === 'org_admin'));
|
||||
|
||||
if (!canAct) return res.status(403).json({ error: 'Access denied to that workspace' });
|
||||
|
||||
const token = generateToken(req.user, ws.id);
|
||||
res.json({ token, current_workspace_id: ws.id });
|
||||
});
|
||||
|
||||
// Update current user
|
||||
|
|
@ -270,9 +364,9 @@ router.put('/me', requireAuth, (req, res) => {
|
|||
res.json(user);
|
||||
});
|
||||
|
||||
// List users - superadmins see all, admins see team members only
|
||||
// List users - platform admins see all, admins see team members only
|
||||
router.get('/users', requireAuth, requireAdmin, (req, res) => {
|
||||
if (req.user.role === 'superadmin') {
|
||||
if (PLATFORM_ROLES.includes(req.user.role)) {
|
||||
const users = db.prepare('SELECT id, email, name, role, auth_provider, avatar_url, plan_id, created_at, last_login FROM users ORDER BY created_at ASC').all();
|
||||
res.json(users);
|
||||
} else {
|
||||
|
|
@ -322,9 +416,9 @@ router.put('/users/:id/password', requireAuth, requireAdmin, (req, res) => {
|
|||
return res.status(400).json({ error: `User signs in via ${target.auth_provider} — password reset does not apply` });
|
||||
}
|
||||
|
||||
if (req.user.role !== 'superadmin') {
|
||||
if (!PLATFORM_ROLES.includes(req.user.role)) {
|
||||
// Admin path: must own a team that includes the target, and target must
|
||||
// be a regular user (cannot reset another admin's or a superadmin's
|
||||
// be a regular user (cannot reset another admin's or a platform_admin's
|
||||
// password — that would be a lateral-takeover vector).
|
||||
if (target.role !== 'user') {
|
||||
return res.status(403).json({ error: 'Admins can only reset passwords for regular users' });
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const upload = require('../middleware/upload');
|
|||
const config = require('../config');
|
||||
const { checkStorageLimit, checkRemoteUrl } = require('../middleware/subscription');
|
||||
const { sanitizeString } = require('../middleware/sanitize');
|
||||
const { PLATFORM_ROLES, ELEVATED_ROLES } = require('../middleware/auth');
|
||||
|
||||
// Multer captures file.originalname directly from the multipart filename header,
|
||||
// bypassing sanitizeBody. Apply the same HTML-escape here so a filename like
|
||||
|
|
@ -42,7 +43,7 @@ function validateRemoteUrl(url) {
|
|||
// List content for current user (admins see all).
|
||||
// folder_id filter: omit for everything; "root" or "" for root-level only; <uuid> for that folder.
|
||||
router.get('/', (req, res) => {
|
||||
const isAdmin = req.user.role === 'superadmin';
|
||||
const isAdmin = PLATFORM_ROLES.includes(req.user.role);
|
||||
const folder = req.query.folder;
|
||||
const folderId = req.query.folder_id;
|
||||
let sql = `SELECT * FROM content ${isAdmin ? 'WHERE 1=1' : 'WHERE (user_id = ? OR user_id IS NULL)'}`;
|
||||
|
|
@ -64,7 +65,7 @@ router.get('/', (req, res) => {
|
|||
|
||||
// Get folders list
|
||||
router.get('/folders', (req, res) => {
|
||||
const isAdmin = req.user.role === 'superadmin';
|
||||
const isAdmin = PLATFORM_ROLES.includes(req.user.role);
|
||||
const folders = db.prepare(
|
||||
`SELECT folder, COUNT(*) as count FROM content WHERE folder IS NOT NULL ${isAdmin ? '' : 'AND (user_id = ? OR user_id IS NULL)'} GROUP BY folder ORDER BY folder`
|
||||
).all(...(isAdmin ? [] : [req.user.id]));
|
||||
|
|
@ -218,7 +219,7 @@ function extractYoutubeId(url) {
|
|||
function checkContentAccess(req, res) {
|
||||
const content = db.prepare('SELECT * FROM content WHERE id = ?').get(req.params.id);
|
||||
if (!content) { res.status(404).json({ error: 'Content not found' }); return null; }
|
||||
if (!['admin','superadmin'].includes(req.user.role) && content.user_id && content.user_id !== req.user.id) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && content.user_id && content.user_id !== req.user.id) {
|
||||
res.status(403).json({ error: 'Access denied' }); return null;
|
||||
}
|
||||
return content;
|
||||
|
|
@ -257,7 +258,7 @@ router.put('/:id', (req, res) => {
|
|||
if (folder_id) {
|
||||
const target = db.prepare('SELECT user_id FROM content_folders WHERE id = ?').get(folder_id);
|
||||
if (!target) return res.status(400).json({ error: 'Invalid folder_id' });
|
||||
const isSuperadmin = req.user.role === 'superadmin';
|
||||
const isSuperadmin = PLATFORM_ROLES.includes(req.user.role);
|
||||
if (!isSuperadmin && target.user_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Cannot move content to another user\'s folder' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const express = require('express');
|
|||
const router = express.Router();
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { db } = require('../db/database');
|
||||
const { ELEVATED_ROLES } = require('../middleware/auth');
|
||||
|
||||
const VALID_COLOR = /^#[0-9A-Fa-f]{6}$/;
|
||||
const ALLOWED_COMMANDS = ['screen_on', 'screen_off', 'launch', 'update', 'reboot', 'shutdown'];
|
||||
|
|
@ -117,7 +118,7 @@ router.post('/:id/devices', requireGroupOwnership, (req, res) => {
|
|||
if (!device_id) return res.status(400).json({ error: 'device_id required' });
|
||||
const device = db.prepare('SELECT user_id FROM devices WHERE id = ?').get(device_id);
|
||||
if (!device) return res.status(404).json({ error: 'Device not found' });
|
||||
if (!['admin','superadmin'].includes(req.user.role) && device.user_id && device.user_id !== req.user.id) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && device.user_id && device.user_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { db } = require('../db/database');
|
||||
const { PLATFORM_ROLES, ELEVATED_ROLES } = require('../middleware/auth');
|
||||
|
||||
// List devices for current user (admins see all)
|
||||
router.get('/', (req, res) => {
|
||||
const isAdmin = req.user.role === 'superadmin';
|
||||
const isAdmin = PLATFORM_ROLES.includes(req.user.role);
|
||||
const devices = db.prepare(`
|
||||
SELECT d.*,
|
||||
t.battery_level, t.battery_charging, t.storage_free_mb, t.storage_total_mb,
|
||||
|
|
@ -33,7 +34,7 @@ router.get('/', (req, res) => {
|
|||
|
||||
// List unclaimed provisioning devices (admin only)
|
||||
router.get('/unassigned', (req, res) => {
|
||||
if (!['admin', 'superadmin'].includes(req.user.role)) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role)) {
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
const devices = db.prepare(`
|
||||
|
|
@ -50,7 +51,7 @@ router.get('/:id', (req, res) => {
|
|||
const device = db.prepare('SELECT d.*, u.email as owner_email, u.name as owner_name FROM devices d LEFT JOIN users u ON d.user_id = u.id WHERE d.id = ?').get(req.params.id);
|
||||
if (!device) return res.status(404).json({ error: 'Device not found' });
|
||||
// Check access: admin, owner, or team member
|
||||
if (!['admin','superadmin'].includes(req.user.role) && device.user_id !== req.user.id) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && device.user_id !== req.user.id) {
|
||||
const teamAccess = device.team_id ? db.prepare('SELECT role FROM team_members WHERE team_id = ? AND user_id = ?').get(device.team_id, req.user.id) : null;
|
||||
if (!teamAccess) return res.status(403).json({ error: 'Access denied' });
|
||||
device._teamRole = teamAccess.role; // Pass team role for frontend to check
|
||||
|
|
@ -109,7 +110,7 @@ router.get('/:id', (req, res) => {
|
|||
function checkDeviceOwnership(req, res) {
|
||||
const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(req.params.id);
|
||||
if (!device) { res.status(404).json({ error: 'Device not found' }); return null; }
|
||||
if (!['admin','superadmin'].includes(req.user.role) && device.user_id && device.user_id !== req.user.id) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && device.user_id && device.user_id !== req.user.id) {
|
||||
// Check team membership
|
||||
const teamAccess = device.team_id ? db.prepare('SELECT role FROM team_members WHERE team_id = ? AND user_id = ?').get(device.team_id, req.user.id) : null;
|
||||
if (!teamAccess || teamAccess.role === 'viewer') {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const express = require('express');
|
|||
const router = express.Router();
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { db } = require('../db/database');
|
||||
const { PLATFORM_ROLES } = require('../middleware/auth');
|
||||
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
|
|
@ -22,7 +23,7 @@ function ownedFolder(req, folderId) {
|
|||
if (!UUID_RE.test(folderId)) return null;
|
||||
const row = db.prepare('SELECT * FROM content_folders WHERE id = ?').get(folderId);
|
||||
if (!row) return null;
|
||||
const isSuperadmin = req.user.role === 'superadmin';
|
||||
const isSuperadmin = PLATFORM_ROLES.includes(req.user.role);
|
||||
if (!isSuperadmin && row.user_id !== req.user.id) return null;
|
||||
return row;
|
||||
}
|
||||
|
|
@ -30,7 +31,7 @@ function ownedFolder(req, folderId) {
|
|||
// List folders for the current user. Returns the full tree as a flat array;
|
||||
// the client builds the hierarchy from parent_id.
|
||||
router.get('/', (req, res) => {
|
||||
const isAdmin = req.user.role === 'superadmin';
|
||||
const isAdmin = PLATFORM_ROLES.includes(req.user.role);
|
||||
const rows = isAdmin
|
||||
? db.prepare('SELECT * FROM content_folders ORDER BY name COLLATE NOCASE').all()
|
||||
: db.prepare('SELECT * FROM content_folders WHERE user_id = ? ORDER BY name COLLATE NOCASE').all(req.user.id);
|
||||
|
|
@ -43,7 +44,7 @@ router.post('/', (req, res) => {
|
|||
if (!name) return res.status(400).json({ error: 'name is required' });
|
||||
if (name.length > 100) return res.status(400).json({ error: 'name too long' });
|
||||
|
||||
const isSuperadmin = req.user.role === 'superadmin';
|
||||
const isSuperadmin = PLATFORM_ROLES.includes(req.user.role);
|
||||
if (!isSuperadmin) {
|
||||
const { count } = db.prepare('SELECT COUNT(*) AS count FROM content_folders WHERE user_id = ?').get(req.user.id);
|
||||
if (count >= MAX_FOLDERS_PER_USER) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const express = require('express');
|
|||
const router = express.Router();
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { db } = require('../db/database');
|
||||
const { PLATFORM_ROLES, ELEVATED_ROLES } = require('../middleware/auth');
|
||||
|
||||
// Escape HTML to prevent XSS
|
||||
function escapeHtml(str) {
|
||||
|
|
@ -24,7 +25,7 @@ function safeNumber(val, fallback) {
|
|||
|
||||
// List kiosk pages
|
||||
router.get('/', (req, res) => {
|
||||
const isAdmin = req.user.role === 'superadmin';
|
||||
const isAdmin = PLATFORM_ROLES.includes(req.user.role);
|
||||
const pages = db.prepare(
|
||||
`SELECT * FROM kiosk_pages ${isAdmin ? '' : 'WHERE user_id = ?'} ORDER BY created_at DESC`
|
||||
).all(...(isAdmin ? [] : [req.user.id]));
|
||||
|
|
@ -35,7 +36,7 @@ router.get('/', (req, res) => {
|
|||
function checkKioskAccess(req, res) {
|
||||
const page = db.prepare('SELECT * FROM kiosk_pages WHERE id = ?').get(req.params.id);
|
||||
if (!page) { res.status(404).json({ error: 'Page not found' }); return null; }
|
||||
if (req.user && !['admin','superadmin'].includes(req.user.role) && page.user_id !== req.user.id) {
|
||||
if (req.user && !ELEVATED_ROLES.includes(req.user.role) && page.user_id !== req.user.id) {
|
||||
res.status(403).json({ error: 'Access denied' }); return null;
|
||||
}
|
||||
return page;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ const express = require('express');
|
|||
const router = express.Router();
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { db } = require('../db/database');
|
||||
const { PLATFORM_ROLES, ELEVATED_ROLES } = require('../middleware/auth');
|
||||
|
||||
// List layouts (user's + templates)
|
||||
router.get('/', (req, res) => {
|
||||
const showTemplates = req.query.templates === 'true';
|
||||
const isAdmin = req.user.role === 'superadmin';
|
||||
const isAdmin = PLATFORM_ROLES.includes(req.user.role);
|
||||
|
||||
let layouts;
|
||||
if (showTemplates) {
|
||||
|
|
@ -28,7 +29,7 @@ router.get('/', (req, res) => {
|
|||
function checkLayoutAccess(req, res) {
|
||||
const layout = db.prepare('SELECT * FROM layouts WHERE id = ?').get(req.params.id);
|
||||
if (!layout) { res.status(404).json({ error: 'Layout not found' }); return null; }
|
||||
if (!layout.is_template && !['admin','superadmin'].includes(req.user.role) && layout.user_id !== req.user.id) {
|
||||
if (!layout.is_template && !ELEVATED_ROLES.includes(req.user.role) && layout.user_id !== req.user.id) {
|
||||
res.status(403).json({ error: 'Access denied' }); return null;
|
||||
}
|
||||
return layout;
|
||||
|
|
@ -74,7 +75,7 @@ router.post('/', (req, res) => {
|
|||
router.put('/:id', (req, res) => {
|
||||
const layout = checkLayoutAccess(req, res);
|
||||
if (!layout) return;
|
||||
if (layout.is_template && !['admin','superadmin'].includes(req.user.role)) return res.status(403).json({ error: 'Cannot edit templates' });
|
||||
if (layout.is_template && !ELEVATED_ROLES.includes(req.user.role)) return res.status(403).json({ error: 'Cannot edit templates' });
|
||||
|
||||
const { name, width, height } = req.body;
|
||||
if (name) db.prepare('UPDATE layouts SET name = ?, updated_at = strftime(\'%s\',\'now\') WHERE id = ?').run(name, req.params.id);
|
||||
|
|
@ -90,7 +91,7 @@ router.put('/:id', (req, res) => {
|
|||
router.delete('/:id', (req, res) => {
|
||||
const layout = checkLayoutAccess(req, res);
|
||||
if (!layout) return;
|
||||
if (layout.is_template && !['admin','superadmin'].includes(req.user.role)) return res.status(403).json({ error: 'Cannot delete templates' });
|
||||
if (layout.is_template && !ELEVATED_ROLES.includes(req.user.role)) return res.status(403).json({ error: 'Cannot delete templates' });
|
||||
|
||||
db.prepare('DELETE FROM layouts WHERE id = ?').run(req.params.id);
|
||||
res.json({ success: true });
|
||||
|
|
@ -182,7 +183,7 @@ router.post('/:id/duplicate', (req, res) => {
|
|||
router.put('/device/:deviceId', (req, res) => {
|
||||
const device = db.prepare('SELECT user_id FROM devices WHERE id = ?').get(req.params.deviceId);
|
||||
if (!device) return res.status(404).json({ error: 'Device not found' });
|
||||
if (!['admin','superadmin'].includes(req.user.role) && device.user_id !== req.user.id) return res.status(403).json({ error: 'Access denied' });
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && device.user_id !== req.user.id) return res.status(403).json({ error: 'Access denied' });
|
||||
const { layout_id } = req.body;
|
||||
db.prepare("UPDATE devices SET layout_id = ?, updated_at = strftime('%s','now') WHERE id = ?")
|
||||
.run(layout_id || null, req.params.deviceId);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const path = require('path');
|
|||
const { v4: uuidv4 } = require('uuid');
|
||||
const { db } = require('../db/database');
|
||||
const config = require('../config');
|
||||
const { ELEVATED_ROLES } = require('../middleware/auth');
|
||||
|
||||
// Re-probe video duration with ffprobe if content.duration_sec is missing
|
||||
async function probeAndUpdateDuration(content) {
|
||||
|
|
@ -239,7 +240,7 @@ router.post('/:id/items', requirePlaylistOwnership, async (req, res) => {
|
|||
if (content_id) {
|
||||
const content = db.prepare('SELECT id, user_id, duration_sec, mime_type, filepath FROM content WHERE id = ?').get(content_id);
|
||||
if (!content) return res.status(404).json({ error: 'Content not found' });
|
||||
if (!['admin', 'superadmin'].includes(req.user.role) && content.user_id && content.user_id !== req.user.id) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && content.user_id && content.user_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Content not owned by you' });
|
||||
}
|
||||
if (duration_sec === undefined || duration_sec === null) {
|
||||
|
|
@ -377,7 +378,7 @@ router.post('/:id/assign', requirePlaylistOwnership, (req, res) => {
|
|||
|
||||
const device = db.prepare('SELECT id, user_id FROM devices WHERE id = ?').get(device_id);
|
||||
if (!device) return res.status(404).json({ error: 'Device not found' });
|
||||
if (!['admin', 'superadmin'].includes(req.user.role) && device.user_id !== req.user.id) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && device.user_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Device not owned by you' });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { db } = require('../db/database');
|
||||
const { PLATFORM_ROLES, ELEVATED_ROLES } = require('../middleware/auth');
|
||||
|
||||
// Helper: scope reports to user's devices
|
||||
function getUserDeviceFilter(user) {
|
||||
if (user.role === 'superadmin') return { sql: '', params: [] };
|
||||
if (PLATFORM_ROLES.includes(user.role)) return { sql: '', params: [] };
|
||||
return { sql: ' AND d.user_id = ?', params: [user.id] };
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +39,7 @@ router.get('/summary', (req, res) => {
|
|||
let deviceFilter = '';
|
||||
const params = [startEpoch, endEpoch];
|
||||
// Scope to user's devices (non-admin)
|
||||
if (!['admin','superadmin'].includes(req.user.role)) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role)) {
|
||||
deviceFilter += ' AND device_id IN (SELECT id FROM devices WHERE user_id = ?)';
|
||||
params.push(req.user.id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const express = require('express');
|
|||
const router = express.Router();
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { db } = require('../db/database');
|
||||
const { ELEVATED_ROLES } = require('../middleware/auth');
|
||||
|
||||
// Helper: build the expanded schedule query for a device (device-level + group-level)
|
||||
function getDeviceSchedulesQuery() {
|
||||
|
|
@ -57,7 +58,7 @@ router.get('/', (req, res) => {
|
|||
router.get('/device/:deviceId', (req, res) => {
|
||||
const device = db.prepare('SELECT user_id FROM devices WHERE id = ?').get(req.params.deviceId);
|
||||
if (!device) return res.status(404).json({ error: 'Device not found' });
|
||||
if (!['admin','superadmin'].includes(req.user.role) && device.user_id !== req.user.id) return res.status(403).json({ error: 'Access denied' });
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && device.user_id !== req.user.id) return res.status(403).json({ error: 'Access denied' });
|
||||
|
||||
const schedules = db.prepare(getDeviceSchedulesQuery()).all(req.params.deviceId, req.params.deviceId);
|
||||
res.json(schedules);
|
||||
|
|
@ -71,7 +72,7 @@ router.get('/week', (req, res) => {
|
|||
// Verify device ownership
|
||||
const device = db.prepare('SELECT user_id FROM devices WHERE id = ?').get(device_id);
|
||||
if (!device) return res.status(404).json({ error: 'Device not found' });
|
||||
if (!['admin','superadmin'].includes(req.user.role) && device.user_id !== req.user.id) return res.status(403).json({ error: 'Access denied' });
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && device.user_id !== req.user.id) return res.status(403).json({ error: 'Access denied' });
|
||||
|
||||
const weekStart = date ? new Date(date) : new Date();
|
||||
weekStart.setHours(0, 0, 0, 0);
|
||||
|
|
@ -111,14 +112,14 @@ router.post('/', (req, res) => {
|
|||
if (device_id) {
|
||||
const device = db.prepare('SELECT user_id FROM devices WHERE id = ?').get(device_id);
|
||||
if (!device) return res.status(404).json({ error: 'Device not found' });
|
||||
if (!['admin','superadmin'].includes(req.user.role) && device.user_id !== req.user.id) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && device.user_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
}
|
||||
if (group_id) {
|
||||
const group = db.prepare('SELECT user_id FROM device_groups WHERE id = ?').get(group_id);
|
||||
if (!group) return res.status(404).json({ error: 'Group not found' });
|
||||
if (!['admin','superadmin'].includes(req.user.role) && group.user_id !== req.user.id) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && group.user_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
}
|
||||
|
|
@ -140,7 +141,7 @@ router.post('/', (req, res) => {
|
|||
router.put('/:id', (req, res) => {
|
||||
const schedule = db.prepare('SELECT * FROM schedules WHERE id = ?').get(req.params.id);
|
||||
if (!schedule) return res.status(404).json({ error: 'Schedule not found' });
|
||||
const isAdmin = ['admin','superadmin'].includes(req.user.role);
|
||||
const isAdmin = ELEVATED_ROLES.includes(req.user.role);
|
||||
if (!isAdmin && schedule.user_id !== req.user.id) return res.status(403).json({ error: 'Access denied' });
|
||||
|
||||
// If changing target, enforce mutual exclusion
|
||||
|
|
@ -206,7 +207,7 @@ router.put('/:id', (req, res) => {
|
|||
router.delete('/:id', (req, res) => {
|
||||
const schedule = db.prepare('SELECT * FROM schedules WHERE id = ?').get(req.params.id);
|
||||
if (!schedule) return res.status(404).json({ error: 'Schedule not found' });
|
||||
if (!['admin','superadmin'].includes(req.user.role) && schedule.user_id !== req.user.id) return res.status(403).json({ error: 'Access denied' });
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && schedule.user_id !== req.user.id) return res.status(403).json({ error: 'Access denied' });
|
||||
db.prepare('DELETE FROM schedules WHERE id = ?').run(req.params.id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const os = require('os');
|
|||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const config = require('../config');
|
||||
const { PLATFORM_ROLES } = require('../middleware/auth');
|
||||
|
||||
// Public status page
|
||||
router.get('/', (req, res) => {
|
||||
|
|
@ -45,7 +46,7 @@ router.get('/backup', (req, res) => {
|
|||
const config = require('../config');
|
||||
const decoded = jwt.verify(token, config.jwtSecret);
|
||||
const user = db.prepare('SELECT role FROM users WHERE id = ?').get(decoded.id);
|
||||
if (!user || user.role !== 'superadmin') return res.status(403).json({ error: 'Superadmin only' });
|
||||
if (!user || !PLATFORM_ROLES.includes(user.role)) return res.status(403).json({ error: 'Platform admin only' });
|
||||
} catch {
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const express = require('express');
|
|||
const router = express.Router();
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { db } = require('../db/database');
|
||||
const { ELEVATED_ROLES } = require('../middleware/auth');
|
||||
|
||||
// List user's teams
|
||||
router.get('/', (req, res) => {
|
||||
|
|
@ -35,7 +36,7 @@ router.get('/:id', (req, res) => {
|
|||
|
||||
const membership = db.prepare('SELECT * FROM team_members WHERE team_id = ? AND user_id = ?')
|
||||
.get(req.params.id, req.user.id);
|
||||
if (!membership && !['admin','superadmin'].includes(req.user.role)) return res.status(403).json({ error: 'Not a member' });
|
||||
if (!membership && !ELEVATED_ROLES.includes(req.user.role)) return res.status(403).json({ error: 'Not a member' });
|
||||
|
||||
team.members = db.prepare(`
|
||||
SELECT tm.*, u.email, u.name as user_name, u.avatar_url
|
||||
|
|
@ -54,7 +55,7 @@ router.get('/:id', (req, res) => {
|
|||
router.put('/:id', (req, res) => {
|
||||
const team = db.prepare('SELECT * FROM teams WHERE id = ?').get(req.params.id);
|
||||
if (!team) return res.status(404).json({ error: 'Team not found' });
|
||||
if (team.owner_id !== req.user.id && !['admin','superadmin'].includes(req.user.role)) return res.status(403).json({ error: 'Owner only' });
|
||||
if (team.owner_id !== req.user.id && !ELEVATED_ROLES.includes(req.user.role)) return res.status(403).json({ error: 'Owner only' });
|
||||
|
||||
if (req.body.name) {
|
||||
db.prepare('UPDATE teams SET name = ? WHERE id = ?').run(req.body.name, req.params.id);
|
||||
|
|
@ -66,7 +67,7 @@ router.put('/:id', (req, res) => {
|
|||
router.delete('/:id', (req, res) => {
|
||||
const team = db.prepare('SELECT * FROM teams WHERE id = ?').get(req.params.id);
|
||||
if (!team) return res.status(404).json({ error: 'Team not found' });
|
||||
if (team.owner_id !== req.user.id && !['admin','superadmin'].includes(req.user.role)) return res.status(403).json({ error: 'Owner only' });
|
||||
if (team.owner_id !== req.user.id && !ELEVATED_ROLES.includes(req.user.role)) return res.status(403).json({ error: 'Owner only' });
|
||||
|
||||
db.prepare('DELETE FROM teams WHERE id = ?').run(req.params.id);
|
||||
res.json({ success: true });
|
||||
|
|
@ -127,7 +128,7 @@ router.put('/:id/members/:userId', (req, res) => {
|
|||
|
||||
// Only team owner or admin can change roles
|
||||
const membership = db.prepare('SELECT * FROM team_members WHERE team_id = ? AND user_id = ?').get(req.params.id, req.user.id);
|
||||
if (!['admin','superadmin'].includes(req.user.role) && (!membership || membership.role !== 'owner')) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && (!membership || membership.role !== 'owner')) {
|
||||
return res.status(403).json({ error: 'Only team owner can change roles' });
|
||||
}
|
||||
|
||||
|
|
@ -143,7 +144,7 @@ router.delete('/:id/members/:userId', (req, res) => {
|
|||
if (team.owner_id === req.params.userId) return res.status(400).json({ error: 'Cannot remove owner' });
|
||||
|
||||
const membership = db.prepare('SELECT * FROM team_members WHERE team_id = ? AND user_id = ?').get(req.params.id, req.user.id);
|
||||
if (!['admin','superadmin'].includes(req.user.role) && (!membership || membership.role !== 'owner')) {
|
||||
if (!ELEVATED_ROLES.includes(req.user.role) && (!membership || membership.role !== 'owner')) {
|
||||
return res.status(403).json({ error: 'Only team owner can remove members' });
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +157,7 @@ router.delete('/:id/members/:userId', (req, res) => {
|
|||
function checkTeamAccess(req, res) {
|
||||
const membership = db.prepare('SELECT * FROM team_members WHERE team_id = ? AND user_id = ?')
|
||||
.get(req.params.id, req.user.id);
|
||||
if (!membership && !['admin','superadmin'].includes(req.user.role)) {
|
||||
if (!membership && !ELEVATED_ROLES.includes(req.user.role)) {
|
||||
res.status(403).json({ error: 'Not a team member' });
|
||||
return false;
|
||||
}
|
||||
|
|
@ -173,7 +174,7 @@ router.post('/:id/devices', (req, res) => {
|
|||
|
||||
const device = db.prepare('SELECT user_id FROM devices WHERE id = ?').get(device_id);
|
||||
if (!device) return res.status(404).json({ error: 'Device not found' });
|
||||
const isAdmin = ['admin', 'superadmin'].includes(req.user.role);
|
||||
const isAdmin = ELEVATED_ROLES.includes(req.user.role);
|
||||
if (!isAdmin && device.user_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'You do not own this device' });
|
||||
}
|
||||
|
|
@ -188,7 +189,7 @@ router.delete('/:id/devices/:deviceId', (req, res) => {
|
|||
if (!checkTeamAccess(req, res)) return;
|
||||
const device = db.prepare('SELECT user_id FROM devices WHERE id = ?').get(req.params.deviceId);
|
||||
if (!device) return res.status(404).json({ error: 'Device not found' });
|
||||
const isAdmin = ['admin', 'superadmin'].includes(req.user.role);
|
||||
const isAdmin = ELEVATED_ROLES.includes(req.user.role);
|
||||
if (!isAdmin && device.user_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'You do not own this device' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ const express = require('express');
|
|||
const router = express.Router();
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { db } = require('../db/database');
|
||||
const { PLATFORM_ROLES } = require('../middleware/auth');
|
||||
|
||||
// Visibility model (matches widgets/users):
|
||||
// superadmin: all walls
|
||||
// admin: own + walls owned by members of teams this admin owns
|
||||
// user: own only
|
||||
function listVisibleWalls(user) {
|
||||
if (user.role === 'superadmin') {
|
||||
if (PLATFORM_ROLES.includes(user.role)) {
|
||||
return db.prepare('SELECT * FROM video_walls ORDER BY created_at DESC').all();
|
||||
}
|
||||
if (user.role === 'admin') {
|
||||
|
|
@ -28,7 +29,7 @@ function listVisibleWalls(user) {
|
|||
}
|
||||
|
||||
function userCanAccessWall(user, wall) {
|
||||
if (user.role === 'superadmin') return true;
|
||||
if (PLATFORM_ROLES.includes(user.role)) return true;
|
||||
if (wall.user_id === user.id) return true;
|
||||
if (user.role === 'admin') {
|
||||
const ownsTeamWithOwner = db.prepare(`
|
||||
|
|
@ -195,7 +196,7 @@ router.put('/:id/devices', (req, res) => {
|
|||
// Without this a user could attach another tenant's devices to their own
|
||||
// wall and silently take over the playlist + wall_id on those rows.
|
||||
// Mirrors the per-device check in device-groups.js.
|
||||
if (!['superadmin'].includes(req.user.role)) {
|
||||
if (!PLATFORM_ROLES.includes(req.user.role)) {
|
||||
const isAdmin = req.user.role === 'admin';
|
||||
for (const d of devices) {
|
||||
const dev = db.prepare('SELECT user_id, team_id FROM devices WHERE id = ?').get(d.device_id);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const path = require('path');
|
|||
const { v4: uuidv4 } = require('uuid');
|
||||
const { db } = require('../db/database');
|
||||
const appConfig = require('../config');
|
||||
const { PLATFORM_ROLES, ELEVATED_ROLES } = require('../middleware/auth');
|
||||
|
||||
// For preview only: inline /api/content/:id/file and /thumbnail URLs as data URIs,
|
||||
// scoped to the current user. Lets the srcdoc preview iframe show logos/bg images
|
||||
|
|
@ -64,7 +65,7 @@ function safeUrl(url) {
|
|||
// this admin owns (matches /auth/users visibility)
|
||||
// user: own + public (null owner)
|
||||
router.get('/', (req, res) => {
|
||||
if (req.user.role === 'superadmin') {
|
||||
if (PLATFORM_ROLES.includes(req.user.role)) {
|
||||
const widgets = db.prepare('SELECT * FROM widgets ORDER BY created_at DESC').all();
|
||||
return res.json(widgets);
|
||||
}
|
||||
|
|
@ -106,7 +107,7 @@ function checkWidgetAccess(req, res) {
|
|||
const widget = db.prepare('SELECT * FROM widgets WHERE id = ?').get(req.params.id);
|
||||
if (!widget) { res.status(404).json({ error: 'Widget not found' }); return null; }
|
||||
// Allow access if: admin, owner, no owner (public), or render route (no req.user)
|
||||
if (req.user && !['admin','superadmin'].includes(req.user.role) && widget.user_id && widget.user_id !== req.user.id) {
|
||||
if (req.user && !ELEVATED_ROLES.includes(req.user.role) && widget.user_id && widget.user_id !== req.user.id) {
|
||||
res.status(403).json({ error: 'Access denied' }); return null;
|
||||
}
|
||||
return widget;
|
||||
|
|
|
|||
|
|
@ -294,27 +294,32 @@ app.get('/api/content/:id/thumbnail', (req, res) => {
|
|||
res.sendFile(safePath);
|
||||
});
|
||||
|
||||
// Protected API Routes
|
||||
// Protected API Routes.
|
||||
// Phase 2.1: resolveTenancy runs right after requireAuth on every resource
|
||||
// route. It attaches req.workspaceId, req.workspaceRole, req.orgRole,
|
||||
// req.isPlatformAdmin, req.actingAs. Route handlers in 2.1 don't read these
|
||||
// yet (they still filter by user_id); 2.2 will migrate them one route at a time.
|
||||
const { requireAuth } = require('./middleware/auth');
|
||||
app.use('/api/devices', requireAuth, require('./routes/devices'));
|
||||
app.use('/api/content', requireAuth, require('./routes/content'));
|
||||
app.use('/api/folders', requireAuth, require('./routes/folders'));
|
||||
app.use('/api/assignments', requireAuth, require('./routes/assignments'));
|
||||
app.use('/api/provision', requireAuth, require('./routes/provisioning'));
|
||||
app.use('/api/layouts', requireAuth, require('./routes/layouts'));
|
||||
const { resolveTenancy } = require('./lib/tenancy');
|
||||
app.use('/api/devices', requireAuth, resolveTenancy, require('./routes/devices'));
|
||||
app.use('/api/content', requireAuth, resolveTenancy, require('./routes/content'));
|
||||
app.use('/api/folders', requireAuth, resolveTenancy, require('./routes/folders'));
|
||||
app.use('/api/assignments', requireAuth, resolveTenancy, require('./routes/assignments'));
|
||||
app.use('/api/provision', requireAuth, resolveTenancy, require('./routes/provisioning'));
|
||||
app.use('/api/layouts', requireAuth, resolveTenancy, require('./routes/layouts'));
|
||||
// Widget render is public (accessed by devices)
|
||||
app.get('/api/widgets/:id/render', (req, res, next) => { req._skipAuth = true; next(); });
|
||||
// Rate limit preview endpoint — it inlines user content as base64 which is memory-intensive
|
||||
app.use('/api/widgets/preview', rateLimit(60000, 30));
|
||||
app.use('/api/widgets', (req, res, next) => { if (req._skipAuth) return next(); requireAuth(req, res, next); }, require('./routes/widgets'));
|
||||
app.use('/api/schedules', requireAuth, require('./routes/schedules'));
|
||||
app.use('/api/walls', requireAuth, require('./routes/video-walls'));
|
||||
app.use('/api/teams', requireAuth, require('./routes/teams'));
|
||||
app.use('/api/reports', requireAuth, require('./routes/reports'));
|
||||
app.use('/api/groups', requireAuth, require('./routes/device-groups'));
|
||||
app.use('/api/playlists', requireAuth, require('./routes/playlists'));
|
||||
app.use('/api/activity', requireAuth, require('./routes/activity'));
|
||||
app.use('/api/white-label', requireAuth, require('./routes/white-label'));
|
||||
app.use('/api/widgets', (req, res, next) => { if (req._skipAuth) return next(); requireAuth(req, res, next); }, resolveTenancy, require('./routes/widgets'));
|
||||
app.use('/api/schedules', requireAuth, resolveTenancy, require('./routes/schedules'));
|
||||
app.use('/api/walls', requireAuth, resolveTenancy, require('./routes/video-walls'));
|
||||
app.use('/api/teams', requireAuth, resolveTenancy, require('./routes/teams'));
|
||||
app.use('/api/reports', requireAuth, resolveTenancy, require('./routes/reports'));
|
||||
app.use('/api/groups', requireAuth, resolveTenancy, require('./routes/device-groups'));
|
||||
app.use('/api/playlists', requireAuth, resolveTenancy, require('./routes/playlists'));
|
||||
app.use('/api/activity', requireAuth, resolveTenancy, require('./routes/activity'));
|
||||
app.use('/api/white-label', requireAuth, resolveTenancy, require('./routes/white-label'));
|
||||
// Kiosk render is public (accessed by devices), CRUD is protected
|
||||
app.get('/api/kiosk/:id/render', (req, res, next) => {
|
||||
// Let it through to the kiosk route without auth
|
||||
|
|
@ -324,7 +329,7 @@ app.get('/api/kiosk/:id/render', (req, res, next) => {
|
|||
app.use('/api/kiosk', (req, res, next) => {
|
||||
if (req._skipAuth) return next();
|
||||
requireAuth(req, res, next);
|
||||
}, require('./routes/kiosk'));
|
||||
}, resolveTenancy, require('./routes/kiosk'));
|
||||
|
||||
// Frontend version hash (changes when files are modified, triggers soft reload)
|
||||
const crypto = require('crypto');
|
||||
|
|
|
|||
Loading…
Reference in a new issue