- Backend: migration 080_role_permissions table (hotel-scoped, upsert) - Backend: routes GET/PUT/DELETE /api/hotels/:slug/role-permissions - Frontend: RolePermissionsContext — loads saved perms from API, provides can() - Frontend: Sidebar uses context can() instead of hardcoded ROLE_PERMS - Frontend: fixed module ID→permKey mapping (room-service, olap-reports, website-builder) - Frontend: Documents page added to Управление nav (was missing) - Frontend: equipment/wifi/ttlock get own permission keys (not bundled under 'settings') - Frontend: floor_map gets own permission key (not bundled under 'rooms') - Frontend: new module group 'Технологии': wifi, equipment, ttlock, floor_map - Frontend: 'schedule' added to Администрирование module group - Updated INITIAL_ROLE_PERMISSIONS: receptionist+availability+reports+documents+website, accountant+documents, technician+floor_map+equipment+ttlock Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
159 lines
7.1 KiB
TypeScript
159 lines
7.1 KiB
TypeScript
import Fastify, { FastifyRequest, FastifyReply } from 'fastify'
|
|
import jwt from '@fastify/jwt'
|
|
import cookie from '@fastify/cookie'
|
|
import cors from '@fastify/cors'
|
|
import helmet from '@fastify/helmet'
|
|
import rateLimit from '@fastify/rate-limit'
|
|
import multipart from '@fastify/multipart'
|
|
import staticFiles from '@fastify/static'
|
|
import { join } from 'path'
|
|
import { config } from './config'
|
|
import './types' // side-effect: augments fastify types
|
|
|
|
import fastifyWebsocket from '@fastify/websocket'
|
|
import wsRoutes from './routes/ws'
|
|
import authRoutes from './routes/auth'
|
|
import hotelsRoutes from './routes/hotels'
|
|
import roomsRoutes from './routes/rooms'
|
|
import bookingsRoutes from './routes/bookings'
|
|
import housekeepingRoutes from './routes/housekeeping'
|
|
import channelsRoutes from './routes/channels'
|
|
import usersRoutes from './routes/users'
|
|
import netupRoutes from './routes/netup'
|
|
import guestsRoutes from './routes/guests'
|
|
import bookingGuestsRoutes from './routes/booking-guests'
|
|
import hotelSettingsRoutes from './routes/hotel-settings'
|
|
import rentalRoutes from './routes/rental'
|
|
import categoriesRoutes from './routes/categories'
|
|
import tariffsRoutes from './routes/tariffs'
|
|
import ratePeriodsRoutes from './routes/rate-periods'
|
|
import rateOverridesRoutes from './routes/rate-overrides'
|
|
import uploadRoutes from './routes/upload'
|
|
import housekeepingSettingsRoutes from './routes/housekeeping-settings'
|
|
import notificationsRoutes from './routes/notifications'
|
|
import scheduleRoutes from './routes/schedule'
|
|
import loyaltyRoutes from './routes/loyalty'
|
|
import chatRoutes from './routes/chat'
|
|
import pushRoutes from './routes/push'
|
|
import workstationRoutes from './routes/workstations'
|
|
import agentReleaseRoutes from './routes/agent-release'
|
|
import wifiSettingsRoutes, { wifiAuthVerify } from './routes/wifi-settings'
|
|
import ttlockRoutes from './routes/ttlock'
|
|
import checklistsRoutes from './routes/checklists'
|
|
import minibarRoutes from './routes/minibar'
|
|
import depositRoutes from './routes/deposit'
|
|
import paymentsRoutes from './routes/payments'
|
|
import paymentMethodsRoutes from './routes/paymentMethods'
|
|
import paymentGatewaysRoutes from './routes/paymentGateways'
|
|
import publicWidgetRoutes from './routes/publicWidget'
|
|
import yookassaWebhookRoutes from './routes/yookassaWebhook'
|
|
import rolePermissionsRoutes from './routes/role-permissions'
|
|
import { setupAgentWsRoute } from './agent-ws'
|
|
import { startJobs } from './jobs'
|
|
import { initWebPush } from './push'
|
|
|
|
export async function buildApp() {
|
|
const fastify = Fastify({
|
|
logger: {
|
|
level: config.nodeEnv === 'production' ? 'warn' : 'info',
|
|
...(config.nodeEnv !== 'production' && {
|
|
transport: { target: 'pino-pretty', options: { colorize: true } },
|
|
}),
|
|
},
|
|
bodyLimit: 20 * 1024 * 1024, // 20MB — for base64 photo uploads
|
|
trustProxy: true, // get real IP from X-Real-IP / X-Forwarded-For via nginx
|
|
})
|
|
|
|
// ── File uploads & static ─────────────────────────────────────────────────
|
|
await fastify.register(multipart)
|
|
const uploadsDir = process.env.UPLOADS_DIR ?? join(process.cwd(), 'uploads')
|
|
await fastify.register(staticFiles, { root: uploadsDir, prefix: '/uploads/' })
|
|
const agentUpdatesDir = process.env.AGENT_UPDATES_DIR ?? join(process.cwd(), '..', 'agent-updates')
|
|
await fastify.register(staticFiles, { root: agentUpdatesDir, prefix: '/agent-updates/', decorateReply: false })
|
|
|
|
// ── WebSocket ──────────────────────────────────────────────────────────────
|
|
await fastify.register(fastifyWebsocket)
|
|
|
|
// ── Security ───────────────────────────────────────────────────────────────
|
|
await fastify.register(helmet, { contentSecurityPolicy: false, crossOriginResourcePolicy: { policy: 'cross-origin' } })
|
|
|
|
await fastify.register(cors, {
|
|
origin: config.cors.origins,
|
|
credentials: true,
|
|
methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'],
|
|
})
|
|
|
|
await fastify.register(rateLimit, {
|
|
max: 200,
|
|
timeWindow: '1 minute',
|
|
})
|
|
|
|
// ── Cookies & JWT ──────────────────────────────────────────────────────────
|
|
await fastify.register(cookie)
|
|
|
|
await fastify.register(jwt, {
|
|
secret: config.jwt.secret,
|
|
})
|
|
|
|
// Authenticate decorator used as onRequest hook in routes
|
|
fastify.decorate(
|
|
'authenticate',
|
|
async (request: FastifyRequest, reply: FastifyReply) => {
|
|
try {
|
|
await request.jwtVerify()
|
|
} catch (err) {
|
|
reply.code(401).send({ error: 'Unauthorized' })
|
|
}
|
|
},
|
|
)
|
|
|
|
// ── Health check ───────────────────────────────────────────────────────────
|
|
fastify.get('/health', async () => ({ status: 'ok', ts: new Date().toISOString() }))
|
|
|
|
// ── Routes ─────────────────────────────────────────────────────────────────
|
|
await fastify.register(wsRoutes)
|
|
await fastify.register(authRoutes)
|
|
await fastify.register(hotelsRoutes)
|
|
await fastify.register(roomsRoutes)
|
|
await fastify.register(bookingsRoutes)
|
|
await fastify.register(housekeepingRoutes)
|
|
await fastify.register(channelsRoutes)
|
|
await fastify.register(usersRoutes)
|
|
await fastify.register(netupRoutes)
|
|
await fastify.register(guestsRoutes)
|
|
await fastify.register(bookingGuestsRoutes)
|
|
await fastify.register(hotelSettingsRoutes)
|
|
await fastify.register(rentalRoutes)
|
|
await fastify.register(categoriesRoutes)
|
|
await fastify.register(tariffsRoutes)
|
|
await fastify.register(ratePeriodsRoutes)
|
|
await fastify.register(rateOverridesRoutes)
|
|
await fastify.register(uploadRoutes)
|
|
await fastify.register(housekeepingSettingsRoutes)
|
|
await fastify.register(notificationsRoutes)
|
|
await fastify.register(scheduleRoutes)
|
|
await fastify.register(loyaltyRoutes)
|
|
await fastify.register(chatRoutes)
|
|
await fastify.register(pushRoutes)
|
|
await fastify.register(workstationRoutes)
|
|
await fastify.register(agentReleaseRoutes)
|
|
await fastify.register(wifiSettingsRoutes)
|
|
await fastify.register(wifiAuthVerify)
|
|
await fastify.register(ttlockRoutes)
|
|
await fastify.register(checklistsRoutes)
|
|
await fastify.register(minibarRoutes)
|
|
await fastify.register(depositRoutes)
|
|
await fastify.register(paymentsRoutes)
|
|
await fastify.register(paymentMethodsRoutes)
|
|
await fastify.register(paymentGatewaysRoutes)
|
|
await fastify.register(publicWidgetRoutes)
|
|
await fastify.register(yookassaWebhookRoutes)
|
|
await fastify.register(rolePermissionsRoutes)
|
|
await fastify.register(setupAgentWsRoute)
|
|
|
|
startJobs()
|
|
initWebPush()
|
|
|
|
return fastify
|
|
}
|