fix: harden permissions system after code review
- Gate permissions in /app-config behind optionalAuth so unauthenticated requests don't receive admin configuration - Fix trip_delete isMember parameter (was hardcoded false) - Return skipped keys from savePermissions for admin visibility - Add disabled prop to CustomSelect, use in BudgetPanel currency picker - Fix CollabChat reaction handler returning false instead of void - Pass canUploadFiles as prop to NoteFormModal instead of internal store read - Make edit-only NoteFormModal props optional (onDeleteFile, note, tripId) - Add missing trailing newlines to .gitignore and it.ts
This commit is contained in:
@@ -177,7 +177,7 @@ router.put('/permissions', (req: Request, res: Response) => {
|
||||
if (!permissions || typeof permissions !== 'object') {
|
||||
return res.status(400).json({ error: 'permissions object required' });
|
||||
}
|
||||
savePermissions(permissions);
|
||||
const { skipped } = savePermissions(permissions);
|
||||
writeAudit({
|
||||
userId: authReq.user.id,
|
||||
action: 'admin.permissions_update',
|
||||
@@ -185,7 +185,7 @@ router.put('/permissions', (req: Request, res: Response) => {
|
||||
ip: getClientIp(req),
|
||||
details: permissions,
|
||||
});
|
||||
res.json({ success: true, permissions: getAllPermissions() });
|
||||
res.json({ success: true, permissions: getAllPermissions(), ...(skipped.length ? { skipped } : {}) });
|
||||
});
|
||||
|
||||
router.get('/audit-log', (req: Request, res: Response) => {
|
||||
|
||||
@@ -10,13 +10,13 @@ import fetch from 'node-fetch';
|
||||
import { authenticator } from 'otplib';
|
||||
import QRCode from 'qrcode';
|
||||
import { db } from '../db/database';
|
||||
import { authenticate, demoUploadBlock } from '../middleware/auth';
|
||||
import { authenticate, optionalAuth, demoUploadBlock } from '../middleware/auth';
|
||||
import { JWT_SECRET } from '../config';
|
||||
import { encryptMfaSecret, decryptMfaSecret } from '../services/mfaCrypto';
|
||||
import { getAllPermissions } from '../services/permissions';
|
||||
import { randomBytes, createHash } from 'crypto';
|
||||
import { revokeUserSessions } from '../mcp';
|
||||
import { AuthRequest, User } from '../types';
|
||||
import { AuthRequest, OptionalAuthRequest, User } from '../types';
|
||||
import { writeAudit, getClientIp } from '../services/auditLog';
|
||||
import { decrypt_api_key, maybe_encrypt_api_key } from '../services/apiKeyCrypto';
|
||||
import { startTripReminders } from '../scheduler';
|
||||
@@ -171,7 +171,7 @@ function generateToken(user: { id: number | bigint }) {
|
||||
);
|
||||
}
|
||||
|
||||
router.get('/app-config', (_req: Request, res: Response) => {
|
||||
router.get('/app-config', optionalAuth, (req: Request, res: Response) => {
|
||||
const userCount = (db.prepare('SELECT COUNT(*) as count FROM users').get() as { count: number }).count;
|
||||
const setting = db.prepare("SELECT value FROM app_settings WHERE key = 'allow_registration'").get() as { value: string } | undefined;
|
||||
const allowRegistration = userCount === 0 || (setting?.value ?? 'true') === 'true';
|
||||
@@ -210,7 +210,7 @@ router.get('/app-config', (_req: Request, res: Response) => {
|
||||
timezone: process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
|
||||
notification_channel: notifChannel,
|
||||
trip_reminders_enabled: tripRemindersEnabled,
|
||||
permissions: getAllPermissions(),
|
||||
permissions: (req as OptionalAuthRequest).user ? getAllPermissions() : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -294,7 +294,8 @@ router.delete('/:id', authenticate, (req: Request, res: Response) => {
|
||||
const trip = db.prepare('SELECT user_id FROM trips WHERE id = ?').get(req.params.id) as { user_id: number } | undefined;
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
const tripOwnerId = trip.user_id;
|
||||
if (!checkPermission('trip_delete', authReq.user.role, tripOwnerId, authReq.user.id, false))
|
||||
const isMemberDel = tripOwnerId !== authReq.user.id;
|
||||
if (!checkPermission('trip_delete', authReq.user.role, tripOwnerId, authReq.user.id, isMemberDel))
|
||||
return res.status(403).json({ error: 'No permission to delete this trip' });
|
||||
const deletedTripId = Number(req.params.id);
|
||||
const delTrip = db.prepare('SELECT title, user_id FROM trips WHERE id = ?').get(req.params.id) as { title: string; user_id: number } | undefined;
|
||||
|
||||
@@ -95,18 +95,22 @@ export function getAllPermissions(): Record<string, PermissionLevel> {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function savePermissions(settings: Record<string, string>): void {
|
||||
export function savePermissions(settings: Record<string, string>): { skipped: string[] } {
|
||||
const skipped: string[] = [];
|
||||
const upsert = db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)");
|
||||
const txn = db.transaction(() => {
|
||||
for (const [actionKey, level] of Object.entries(settings)) {
|
||||
const action = ACTIONS_MAP.get(actionKey);
|
||||
if (!action) continue;
|
||||
if (!action.allowedLevels.includes(level as PermissionLevel)) continue;
|
||||
if (!action || !action.allowedLevels.includes(level as PermissionLevel)) {
|
||||
skipped.push(actionKey);
|
||||
continue;
|
||||
}
|
||||
upsert.run(`perm_${actionKey}`, level);
|
||||
}
|
||||
});
|
||||
txn();
|
||||
invalidatePermissionsCache();
|
||||
return { skipped };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user