mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-21 07:36:40 +00:00
a3026f47a8
* feat(rbac): make Settings authorization permission-aware Align Settings visibility and mutations with the existing permission matrix so Node Admin can edit node-scoped operational settings while system and credential surfaces stay Admin-protected. * fix(rbac): tighten settings permission buckets and tests Collapse settings key permission maps into one source of truth, and cover mixed PATCH atomicity plus image-update enabled writes. * fix(rbac): tighten Settings scoped grants and CI assertions Empty settings PATCH fails closed, node:manage is scoped to the active node, system-only Settings stay hidden without system:settings, and Check updates / webhooks mutate gates follow the permission matrix. * fix(rbac): defer Settings section fallback until authz is ready Keep deep links to permission-gated sections (e.g. license) intact while can() is still fail-closed during permission metadata load. * docs(settings): clarify Notifications channels vs routing authz Channels use node:manage via /api/agents; routing and mute stay Admin-only.
70 lines
3.1 KiB
TypeScript
70 lines
3.1 KiB
TypeScript
import { Router, type Request, type Response } from 'express';
|
|
import { DatabaseService } from '../services/DatabaseService';
|
|
import { authMiddleware } from '../middleware/auth';
|
|
import { requirePermission } from '../middleware/permissions';
|
|
import { isDebugEnabled } from '../utils/debug';
|
|
import { sanitizeForLog } from '../utils/safeLog';
|
|
import {
|
|
NOTIFICATION_CHANNEL_TYPES,
|
|
normalizeAppriseStoredJson,
|
|
redactedChannelWriteError,
|
|
resolvePreservedAppriseConfig,
|
|
serializePublicAgent,
|
|
validateNotificationChannel,
|
|
} from '../helpers/notificationChannels';
|
|
|
|
export const agentsRouter = Router();
|
|
|
|
agentsRouter.get('/', authMiddleware, async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const nodeId = req.nodeId ?? 0;
|
|
const agents = DatabaseService.getInstance().getAgents(nodeId);
|
|
res.json(agents.map(serializePublicAgent));
|
|
} catch (error) {
|
|
console.error('Failed to fetch agents:', error);
|
|
res.status(500).json({ error: 'Failed to fetch agents' });
|
|
}
|
|
});
|
|
|
|
agentsRouter.post('/', authMiddleware, async (req: Request, res: Response): Promise<void> => {
|
|
const nodeId = req.nodeId ?? 0;
|
|
if (!requirePermission(req, res, 'node:manage', 'node', String(nodeId))) return;
|
|
try {
|
|
const { type, url, enabled, config } = req.body;
|
|
if (!type || !(NOTIFICATION_CHANNEL_TYPES as readonly string[]).includes(type)) {
|
|
res.status(400).json({ error: `type must be ${NOTIFICATION_CHANNEL_TYPES.join(', ')}` });
|
|
return;
|
|
}
|
|
if (typeof enabled !== 'boolean') {
|
|
res.status(400).json({ error: 'enabled must be a boolean' });
|
|
return;
|
|
}
|
|
const existing = DatabaseService.getInstance().getAgents(nodeId).find(agent => agent.type === type);
|
|
const effectiveUrl = url === undefined ? existing?.url : url;
|
|
|
|
let effectiveConfig: unknown = config ?? null;
|
|
if (type === 'apprise' && config === undefined && existing) {
|
|
const resolved = resolvePreservedAppriseConfig(typeof effectiveUrl === 'string' ? effectiveUrl : existing.url, existing.config);
|
|
if (!resolved.ok) { res.status(400).json({ error: resolved.error }); return; }
|
|
effectiveConfig = resolved.config;
|
|
}
|
|
|
|
const redactedErr = redactedChannelWriteError(type, effectiveUrl, effectiveConfig, config);
|
|
if (redactedErr) { res.status(400).json({ error: redactedErr }); return; }
|
|
const channelErr = validateNotificationChannel(type, effectiveUrl, effectiveConfig);
|
|
if (channelErr) { res.status(400).json({ error: `url ${channelErr}` }); return; }
|
|
DatabaseService.getInstance().upsertAgent(nodeId, {
|
|
type,
|
|
url: effectiveUrl.trim(),
|
|
enabled,
|
|
config: type === 'apprise' ? normalizeAppriseStoredJson(effectiveUrl.trim(), effectiveConfig) : null,
|
|
});
|
|
console.log('[Agents] Agent %s updated', sanitizeForLog(type));
|
|
if (isDebugEnabled()) console.log('[Agents:diag] Agent %s upsert: enabled=%s', sanitizeForLog(type), sanitizeForLog(enabled));
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Failed to update agent:', error);
|
|
res.status(500).json({ error: 'Failed to update agent' });
|
|
}
|
|
});
|