diff --git a/apps/api/src/plugins/space.ts b/apps/api/src/plugins/space.ts index ee50796..956ab82 100644 --- a/apps/api/src/plugins/space.ts +++ b/apps/api/src/plugins/space.ts @@ -186,6 +186,20 @@ export function requireSpaceRole( return true } +/** + * Whole-DB operations (backup download/restore, legacy import) cross every space and carry API credentials, + * so per-space settings admin is not enough. + */ +export function requireGlobalAdmin(request: FastifyRequest, reply: FastifyReply): boolean { + const user = request.authUser + if (!user) return true + if (user.isAdmin || hasPermission(user.permissions, 'vps:spaces:admin')) return true + void reply.code(403).send({ + error: { code: 'FORBIDDEN', message: 'Операция доступна только глобальному администратору' }, + }) + return false +} + export function canWriteInSpace(request: FastifyRequest): boolean { const role = request.spaceRole ?? 'viewer' return roleAtLeast(role, 'member') diff --git a/apps/api/src/routes/backup.ts b/apps/api/src/routes/backup.ts index 1a72188..b1e2647 100644 --- a/apps/api/src/routes/backup.ts +++ b/apps/api/src/routes/backup.ts @@ -15,6 +15,7 @@ import { type FullBackupPayload, } from '../services/backup-full.js' import { restartScheduler } from '../services/scheduler.js' +import { requireGlobalAdmin } from '../plugins/space.js' /** Лимит тела для импорта бэкапа (Fastify default = 1 MiB → 413). */ function backupBodyLimitBytes(): number { @@ -29,6 +30,10 @@ function backupBodyLimitBytes(): number { export const backupRoutes: FastifyPluginAsync = async (app) => { const bodyLimit = backupBodyLimitBytes() + app.addHook('preHandler', async (req, reply) => { + if (!requireGlobalAdmin(req, reply)) return reply + }) + app.addContentTypeParser( 'application/octet-stream', { parseAs: 'buffer', bodyLimit }, diff --git a/apps/api/src/routes/migrate.ts b/apps/api/src/routes/migrate.ts index 78ef827..16cdafc 100644 --- a/apps/api/src/routes/migrate.ts +++ b/apps/api/src/routes/migrate.ts @@ -1,8 +1,10 @@ import type { FastifyPluginAsync } from 'fastify' import { consolidateAllProviderApiSources, getSqlite } from '@cfdm/db' +import { requireGlobalAdmin } from '../plugins/space.js' export const migrateRoutes: FastifyPluginAsync = async (app) => { app.post('/api/migrate', async (req, reply) => { + if (!requireGlobalAdmin(req, reply)) return reply const data = req.body if (!data || typeof data !== 'object') { return reply.code(400).send({ error: { code: 'VALIDATION', message: 'Invalid payload' } })