- 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>
111 lines
4.3 KiB
TypeScript
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
|