Files
hotelsync/backend/src/routes/role-permissions.ts
HotelSync 512e3c6659 feat: full role permissions system — new modules, API persistence, sidebar context
- 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>
2026-04-17 12:16:15 +03:00

111 lines
4.3 KiB
TypeScript

import { FastifyPluginAsync } from 'fastify'
import { db } from '../db'
type SlugParam = { Params: { slug: string } }
type SlugRoleParam = { Params: { slug: string; roleKey: string } }
const rolePermissionsRoute: FastifyPluginAsync = async (fastify) => {
const getHotelId = async (slug: string): Promise<string | null> => {
const { rows } = await db.query('SELECT id FROM hotels WHERE slug = $1', [slug])
return rows[0]?.id ?? null
}
const canAccess = (userSlug: string | null, role: string, slug: string) =>
role === 'super_admin' || userSlug === slug
const canManage = (role: string) =>
['manager', 'hotel_admin', 'super_admin'].includes(role)
// ── GET /api/hotels/:slug/role-permissions ─────────────────────────────────
fastify.get<SlugParam>(
'/api/hotels/:slug/role-permissions',
{ onRequest: [fastify.authenticate] },
async (request, reply) => {
const { slug } = request.params
if (!canAccess(request.user.hotelSlug, request.user.role, slug)) {
return reply.code(403).send({ error: 'Forbidden' })
}
const hotelId = await getHotelId(slug)
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
const { rows } = await db.query(
`SELECT id, hotel_id, role_key, name, color, is_system, permissions, created_at, updated_at
FROM role_permissions
WHERE hotel_id = $1
ORDER BY is_system DESC, name`,
[hotelId],
)
return rows
},
)
// ── PUT /api/hotels/:slug/role-permissions/:roleKey ────────────────────────
fastify.put<SlugRoleParam & {
Body: { name: string; color: string; isSystem?: boolean; permissions: Record<string, boolean> }
}>(
'/api/hotels/:slug/role-permissions/:roleKey',
{ onRequest: [fastify.authenticate] },
async (request, reply) => {
if (!canManage(request.user.role)) {
return reply.code(403).send({ error: 'Forbidden' })
}
const { slug, roleKey } = request.params
if (!canAccess(request.user.hotelSlug, request.user.role, slug)) {
return reply.code(403).send({ error: 'Forbidden' })
}
const hotelId = await getHotelId(slug)
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
const { name, color, isSystem = false, permissions } = request.body
const { rows } = await db.query(
`INSERT INTO role_permissions (hotel_id, role_key, name, color, is_system, permissions)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (hotel_id, role_key) DO UPDATE
SET name = EXCLUDED.name,
color = EXCLUDED.color,
permissions = EXCLUDED.permissions,
updated_at = NOW()
RETURNING *`,
[hotelId, roleKey, name, color, isSystem, JSON.stringify(permissions)],
)
return rows[0]
},
)
// ── DELETE /api/hotels/:slug/role-permissions/:roleKey ─────────────────────
fastify.delete<SlugRoleParam>(
'/api/hotels/:slug/role-permissions/:roleKey',
{ onRequest: [fastify.authenticate] },
async (request, reply) => {
if (!canManage(request.user.role)) {
return reply.code(403).send({ error: 'Forbidden' })
}
const { slug, roleKey } = request.params
if (!canAccess(request.user.hotelSlug, request.user.role, slug)) {
return reply.code(403).send({ error: 'Forbidden' })
}
const hotelId = await getHotelId(slug)
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
// System roles cannot be deleted
const { rows: existing } = await db.query(
'SELECT is_system FROM role_permissions WHERE hotel_id = $1 AND role_key = $2',
[hotelId, roleKey],
)
if (existing[0]?.is_system) {
return reply.code(403).send({ error: 'Нельзя удалить системную роль' })
}
const { rowCount } = await db.query(
'DELETE FROM role_permissions WHERE hotel_id = $1 AND role_key = $2',
[hotelId, roleKey],
)
if (!rowCount) return reply.code(404).send({ error: 'Role not found' })
return reply.code(204).send()
},
)
}
export default rolePermissionsRoute