feat: implement permission-based access control across various pages and components

- Added RequirePermission component to manage access based on user permissions.
- Integrated permission checks in MySeaRecordsPage, SeafarerRegistrationPage, VesselRegistrationPage, WaiverPage, and PortalLayout.
- Updated router to enforce permissions for specific routes.
- Introduced PORTAL_PERMISSIONS and LICENSE_PERMISSIONS constants for consistent permission management.
- Enhanced usePermissions hook to fetch permissions from the server and determine access rights.
- Refactored UI components to conditionally render based on user permissions, improving security and user experience.
This commit is contained in:
Nati
2026-08-13 12:58:11 +00:00
parent 49fbd8fc53
commit 6852fc86c6
46 changed files with 1093 additions and 581 deletions

View File

@@ -17,6 +17,7 @@ import {
import { IconInfoCircle, IconCheck, IconX } from '@tabler/icons-react';
import { AdvancedTable, ModalFooter, notify, useServerTable } from '@ema-platform/ui';
import { extractErrorMessage } from '@ema-platform/api';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
import { recordResultColumns } from './columns';
import { useCreateResultMutation } from '../../api/result-api';
import { useGetExamRegistrationsQuery } from '../../../exam/api/exam-api';
@@ -207,9 +208,11 @@ export function RecordResultModal({
<ModalFooter>
<Button variant="default" onClick={onClose} size="sm">{t('result.cancel')}</Button>
<Button onClick={handleSave} size="sm" loading={isSaving}>
{t('result.saveResult')}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_RESULT]} hideOnly>
<Button onClick={handleSave} size="sm" loading={isSaving}>
{t('result.saveResult')}
</Button>
</RequirePermission>
</ModalFooter>
</>
)}

View File

@@ -2,6 +2,7 @@ import { Badge, Button, Text } from '@mantine/core';
import { IconGavel } from '@tabler/icons-react';
import type { TFunction } from 'i18next';
import type { AdvancedColumn } from '@ema-platform/ui';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
import type { ExamAppeal } from '../../types/result';
export function examAppealsColumns(
@@ -59,15 +60,17 @@ export function examAppealsColumns(
label: t('result.appeals.decide'),
align: 'right',
cell: ({ row }) => (
<Button
size="compact-xs"
variant="light"
color="grape"
leftSection={<IconGavel size={12} />}
onClick={() => handlers.onDecide(row.original)}
>
{t('result.appeals.decide')}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.DECIDE_EXAM_APPEAL]} hideOnly>
<Button
size="compact-xs"
variant="light"
color="grape"
leftSection={<IconGavel size={12} />}
onClick={() => handlers.onDecide(row.original)}
>
{t('result.appeals.decide')}
</Button>
</RequirePermission>
),
},
];

View File

@@ -2,6 +2,7 @@ import { Button, Group } from '@mantine/core';
import { IconEye, IconTrash } from '@tabler/icons-react';
import type { TFunction } from 'i18next';
import type { AdvancedColumn } from '@ema-platform/ui';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
import type { Result } from '../../types/result';
export type QcAction = 'moderate' | 'approve' | 'return';
@@ -23,31 +24,45 @@ export function resultActionsColumn(
<Group gap="xs">
{(r.reviewStatus === 'MARKED' || r.reviewStatus === 'MODERATED') && (
<>
<Button size="compact-xs" variant="light" color="yellow" onClick={() => handlers.onQc(r, 'moderate')}>
{t('result.review.moderate')}
</Button>
<Button size="compact-xs" variant="light" color="blue" onClick={() => handlers.onQc(r, 'approve')}>
{t('result.review.approve')}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MODERATE_EXAM_RESULT]} hideOnly>
<Button size="compact-xs" variant="light" color="yellow" onClick={() => handlers.onQc(r, 'moderate')}>
{t('result.review.moderate')}
</Button>
</RequirePermission>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_EXAM_RESULT]} hideOnly>
<Button size="compact-xs" variant="light" color="blue" onClick={() => handlers.onQc(r, 'approve')}>
{t('result.review.approve')}
</Button>
</RequirePermission>
</>
)}
{(r.reviewStatus === 'APPROVED' || r.reviewStatus === 'MODERATED') && (
<Button size="compact-xs" variant="subtle" color="orange" onClick={() => handlers.onQc(r, 'return')}>
{t('result.review.return')}
</Button>
<RequirePermission
anyOf={[
LICENSE_PERMISSIONS.MODERATE_EXAM_RESULT,
LICENSE_PERMISSIONS.APPROVE_EXAM_RESULT,
]}
hideOnly
>
<Button size="compact-xs" variant="subtle" color="orange" onClick={() => handlers.onQc(r, 'return')}>
{t('result.review.return')}
</Button>
</RequirePermission>
)}
<Button size="xs" variant="subtle" leftSection={<IconEye size={13} />} onClick={() => handlers.onViewDetail(r)}>
{t('result.action.viewEdit')}
</Button>
<Button
size="xs"
variant="subtle"
color="red"
leftSection={<IconTrash size={13} />}
onClick={() => handlers.onDelete(r)}
>
{t('result.action.delete')}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_EXAM_RESULT]} hideOnly>
<Button
size="xs"
variant="subtle"
color="red"
leftSection={<IconTrash size={13} />}
onClick={() => handlers.onDelete(r)}
>
{t('result.action.delete')}
</Button>
</RequirePermission>
</Group>
);
},

View File

@@ -38,6 +38,7 @@ import { notify, BilingualInput, useErrorHandler, AdvancedTable, useServerTable,
import { useDateDisplayer } from '@ema-platform/shared';
import type { BilingualValue } from '@ema-platform/ui';
import { extractErrorMessage, useLocalized } from '@ema-platform/api';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
import {
useGetResultsQuery,
useLazyGetResultQuery,
@@ -300,17 +301,19 @@ export function ResultPage() {
<Text fz="sm" c="dimmed">{t('result.subtitle')}</Text>
</div>
<Group gap="sm">
<Button
variant="light"
color="teal"
size="sm"
loading={isPublishing}
disabled={!examFilter}
leftSection={<IconSend size={15} />}
onClick={handlePublish}
>
{t('result.review.publish')}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.PUBLISH_EXAM_RESULT]} hideOnly>
<Button
variant="light"
color="teal"
size="sm"
loading={isPublishing}
disabled={!examFilter}
leftSection={<IconSend size={15} />}
onClick={handlePublish}
>
{t('result.review.publish')}
</Button>
</RequirePermission>
<Button leftSection={<IconPlus size={15} />} onClick={openPicker} size="sm">
{t('result.record')}
</Button>