mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-08 05:25:43 +00:00
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:
@@ -2,6 +2,7 @@ import { Badge, Button, Text } from '@mantine/core';
|
||||
import { IconUserCheck } 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 { AttendanceStatus, ExamRegistration } from '../../types/exam';
|
||||
|
||||
const ATTENDANCE_COLOR: Record<AttendanceStatus, string> = {
|
||||
@@ -78,14 +79,19 @@ export function examCandidateColumns(
|
||||
label: t('exam.candidates.record'),
|
||||
align: 'right',
|
||||
cell: ({ row }) => (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
leftSection={<IconUserCheck size={12} />}
|
||||
onClick={() => handlers.onRecord(row.original)}
|
||||
<RequirePermission
|
||||
anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_ATTENDANCE]}
|
||||
hideOnly
|
||||
>
|
||||
{t('exam.candidates.record')}
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
leftSection={<IconUserCheck size={12} />}
|
||||
onClick={() => handlers.onRecord(row.original)}
|
||||
>
|
||||
{t('exam.candidates.record')}
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Badge, Button, Text } from '@mantine/core';
|
||||
import { IconAlertTriangle } 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 { ExamIncident, ExamIncidentStatus } from '../../types/exam';
|
||||
|
||||
const STATUS_COLOR: Record<ExamIncidentStatus, string> = {
|
||||
@@ -70,14 +71,19 @@ export function examIncidentColumns(
|
||||
align: 'right',
|
||||
cell: ({ row }) =>
|
||||
row.original.status === 'OPEN' || row.original.status === 'UNDER_REVIEW' ? (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
leftSection={<IconAlertTriangle size={12} />}
|
||||
onClick={() => handlers.onResolve(row.original)}
|
||||
<RequirePermission
|
||||
anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAM_INCIDENTS]}
|
||||
hideOnly
|
||||
>
|
||||
{t('exam.incidents.resolve')}
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
leftSection={<IconAlertTriangle size={12} />}
|
||||
onClick={() => handlers.onResolve(row.original)}
|
||||
>
|
||||
{t('exam.incidents.resolve')}
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -16,6 +16,7 @@ import { IconInfoCircle, IconPlus } from '@tabler/icons-react';
|
||||
import { AdvancedTable, notify, useServerTable } from '@ema-platform/ui';
|
||||
import { useDateDisplayer } from '@ema-platform/shared';
|
||||
import { extractErrorMessage } from '@ema-platform/api';
|
||||
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
|
||||
import {
|
||||
useGetExamIncidentsQuery,
|
||||
useGetExamRegistrationsQuery,
|
||||
@@ -122,15 +123,20 @@ export function ExamIncidentsPanel({ examId }: { examId: string }) {
|
||||
<Paper withBorder radius="lg" p="lg">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={5}>{t('exam.incidents.section')}</Title>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="orange"
|
||||
leftSection={<IconPlus size={14} />}
|
||||
onClick={() => setAddOpen(true)}
|
||||
<RequirePermission
|
||||
anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAM_INCIDENTS]}
|
||||
hideOnly
|
||||
>
|
||||
{t('exam.incidents.add')}
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="orange"
|
||||
leftSection={<IconPlus size={14} />}
|
||||
onClick={() => setAddOpen(true)}
|
||||
>
|
||||
{t('exam.incidents.add')}
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</Group>
|
||||
|
||||
{(incidents ?? []).length === 0 ? (
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
} from '@tabler/icons-react';
|
||||
import { ModalFooter, notify, useErrorHandler } from '@ema-platform/ui';
|
||||
import { extractErrorMessage } from '@ema-platform/api';
|
||||
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
|
||||
import {
|
||||
useGetExamQuery,
|
||||
useUpdateExamMutation,
|
||||
@@ -393,14 +394,16 @@ export function ExamDetailPage() {
|
||||
<Title order={5}>
|
||||
{t("exam.detail.questionsSection", { pts: totalPoints })}
|
||||
</Title>
|
||||
<Button
|
||||
variant="light"
|
||||
size="xs"
|
||||
leftSection={<IconPlus size={14} />}
|
||||
onClick={openAssignModal}
|
||||
>
|
||||
{t("exam.manageQuestions")}
|
||||
</Button>
|
||||
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
|
||||
<Button
|
||||
variant="light"
|
||||
size="xs"
|
||||
leftSection={<IconPlus size={14} />}
|
||||
onClick={openAssignModal}
|
||||
>
|
||||
{t("exam.manageQuestions")}
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</Group>
|
||||
{(exam.questions ?? []).length === 0 ? (
|
||||
<Alert color="gray" icon={<IconInfoCircle size={16} />}>
|
||||
@@ -461,9 +464,11 @@ export function ExamDetailPage() {
|
||||
<Button variant="default" onClick={closeAssign} size="sm">
|
||||
{t("exam.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleAssign} size="sm" loading={isAssigning}>
|
||||
{t("exam.saveAssignments")}
|
||||
</Button>
|
||||
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
|
||||
<Button onClick={handleAssign} size="sm" loading={isAssigning}>
|
||||
{t("exam.saveAssignments")}
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</ModalFooter>
|
||||
</>
|
||||
) : (
|
||||
@@ -478,9 +483,11 @@ export function ExamDetailPage() {
|
||||
size="xs"
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
<Button size="xs" variant="light" loading={isDrawing} onClick={handleRandomSelect}>
|
||||
{t('exam.randomSelect')}
|
||||
</Button>
|
||||
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
|
||||
<Button size="xs" variant="light" loading={isDrawing} onClick={handleRandomSelect}>
|
||||
{t('exam.randomSelect')}
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</Group>
|
||||
<QuestionAssigner
|
||||
available={eligibleQuestions}
|
||||
@@ -492,9 +499,11 @@ export function ExamDetailPage() {
|
||||
<Button variant="default" onClick={closeAssign} size="sm">
|
||||
{t("exam.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleAssign} size="sm" loading={isAssigning}>
|
||||
{t("exam.saveAssignments")}
|
||||
</Button>
|
||||
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
|
||||
<Button onClick={handleAssign} size="sm" loading={isAssigning}>
|
||||
{t("exam.saveAssignments")}
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ActionIcon, Group } from "@mantine/core";
|
||||
import { IconEdit, IconTrash, IconDetails } 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 { Exam } from "../../types/exam";
|
||||
|
||||
export function examActionsColumn(
|
||||
@@ -17,22 +18,24 @@ export function examActionsColumn(
|
||||
align: "right",
|
||||
cell: ({ row }) => (
|
||||
<Group gap="xs">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
size="sm"
|
||||
onClick={() => handlers.onEdit(row.original)}
|
||||
>
|
||||
<IconEdit size={14} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={() => handlers.onDelete(row.original)}
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</ActionIcon>
|
||||
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
size="sm"
|
||||
onClick={() => handlers.onEdit(row.original)}
|
||||
>
|
||||
<IconEdit size={14} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={() => handlers.onDelete(row.original)}
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</ActionIcon>
|
||||
</RequirePermission>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
IconClipboardList,
|
||||
} from "@tabler/icons-react";
|
||||
import { notify, useErrorHandler, AdvancedTable, useServerTable, ModalFooter, AmharicDatePicker } from "@ema-platform/ui";
|
||||
import { LICENSE_PERMISSIONS, RequirePermission } from "@ema-platform/auth";
|
||||
import { useGetCertificationsQuery } from "../../../certification/api/certification-api";
|
||||
import {
|
||||
useGetExamsQuery,
|
||||
@@ -450,14 +451,16 @@ export function ExamPage() {
|
||||
</Text>
|
||||
</div>
|
||||
{!showForm && (
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={16} />}
|
||||
onClick={() => setShowForm(true)}
|
||||
size="sm"
|
||||
>
|
||||
{t("exam.add")}
|
||||
</Button>
|
||||
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={16} />}
|
||||
onClick={() => setShowForm(true)}
|
||||
size="sm"
|
||||
>
|
||||
{t("exam.add")}
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user