mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
feat: add profile check mutation and enhance navigation access control
This commit is contained in:
@@ -27,8 +27,10 @@ import { notify, useErrorHandler } from "@ema-platform/ui";
|
||||
import {
|
||||
authStorage,
|
||||
setUser,
|
||||
setCurrentProfile,
|
||||
logout,
|
||||
type AuthUser,
|
||||
type CurrentProfile,
|
||||
} from "@ema-platform/auth";
|
||||
import { useAppDispatch, useAppSelector } from "../../../store/hooks";
|
||||
import {
|
||||
@@ -144,6 +146,10 @@ export function ProfileSetupPage() {
|
||||
const [profileTrigger] = useApiMutation<{ id: string }>();
|
||||
const [addressTrigger] = useApiMutation<{ id: string }>();
|
||||
const [meTrigger] = useApiMutation<AuthUser>();
|
||||
const [profileCheckTrigger] = useApiMutation<{
|
||||
total: number;
|
||||
items: CurrentProfile[];
|
||||
}>();
|
||||
const [fetchProfessions] = useApiMutation<{
|
||||
count: number;
|
||||
items: Array<{ id: string; name: { en: string } }>;
|
||||
@@ -300,6 +306,21 @@ export function ProfileSetupPage() {
|
||||
const me = await meTrigger({ url: "/auth/me", method: "GET" }).unwrap();
|
||||
dispatch(setUser(me));
|
||||
|
||||
// POST /profiles doesn't return the profession relation the portal's nav/route
|
||||
// guard reads (PortalLayout.tsx) — re-fetch with it, same as LoginPage does on login.
|
||||
try {
|
||||
const q = `w=user_id:=:${me.id}&i=user,address,profession`;
|
||||
const profileCheck = await profileCheckTrigger({
|
||||
url: `/profiles?q=${encodeURIComponent(q)}`,
|
||||
method: "GET",
|
||||
}).unwrap();
|
||||
if (profileCheck.total > 0 && profileCheck.items.length > 0) {
|
||||
dispatch(setCurrentProfile(profileCheck.items[0]));
|
||||
}
|
||||
} catch {
|
||||
// non-fatal — sidebar/route guard falls back to FALLBACK_ACCESS
|
||||
}
|
||||
|
||||
notify.success("Profile setup complete!");
|
||||
navigate("/dashboard");
|
||||
} catch (e) {
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
IconUsers,
|
||||
IconAnchor,
|
||||
} from "@tabler/icons-react";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { notify, AppHeader, AppSidebar } from "@ema-platform/ui";
|
||||
@@ -27,6 +27,7 @@ import type { NavItem } from "@ema-platform/ui";
|
||||
import { logout } from "@ema-platform/auth";
|
||||
import { SUPPORTED_LANGUAGES } from "../i18n/config";
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
import { PAYMENTS_ROUTE } from "../features/payment/constants";
|
||||
|
||||
type SectionId = "seafarer" | "vessel" | "logistics" | "common";
|
||||
|
||||
@@ -180,11 +181,16 @@ const NAV_ITEMS: (NavItem & { i18nKey: string; section?: SectionId })[] = [
|
||||
|
||||
const ALWAYS_VISIBLE = ["/notifications", "/profile", "/support", "/documents"];
|
||||
|
||||
// Routes reachable by URL but absent from NAV_ITEMS — listing them here has no sidebar
|
||||
// effect, it only keeps the route guard below from blocking them for everyone.
|
||||
const ALWAYS_ALLOWED = [PAYMENTS_ROUTE];
|
||||
|
||||
const NAV_ACCESS: Record<string, string[] | "*"> = {
|
||||
null: "*",
|
||||
// cadet: "*",
|
||||
cadet: [
|
||||
"/dashboard",
|
||||
"/seafarer-registration",
|
||||
"/seafarer-registry",
|
||||
"/seaman-book",
|
||||
"/certificates",
|
||||
@@ -195,11 +201,13 @@ const NAV_ACCESS: Record<string, string[] | "*"> = {
|
||||
"/vessel-registration-dashboard",
|
||||
"/vessel-registration",
|
||||
"/vessel-registration/transfer",
|
||||
"/vessel-registrations",
|
||||
],
|
||||
transfer: [
|
||||
"/vessel-registration-dashboard",
|
||||
"/vessel-registration",
|
||||
"/vessel-registration/transfer",
|
||||
"/vessel-registrations",
|
||||
],
|
||||
logistics: [
|
||||
"/logistics-dashboard",
|
||||
@@ -213,7 +221,13 @@ const NAV_ACCESS: Record<string, string[] | "*"> = {
|
||||
};
|
||||
|
||||
// Used when profession is missing or matches no key above. Set to '*' to show everything.
|
||||
const FALLBACK_ACCESS: string[] | "*" = ["/dashboard"];
|
||||
const FALLBACK_ACCESS: string[] | "*" = ["/dashboard", "/seafarer-registration"];
|
||||
|
||||
// Matches `pathname` against `base` itself or any of its sub-paths, so child routes
|
||||
// (e.g. "/seaman-book/apply") inherit their parent's grant without loose prefix
|
||||
// matches (e.g. "/vessel-registration" must not match "/vessel-registration-dashboard").
|
||||
const matchesPath = (pathname: string, base: string) =>
|
||||
pathname === base || pathname.startsWith(base + "/");
|
||||
|
||||
const PAGE_META: Record<string, { i18nKey: string }> = {
|
||||
"/dashboard": { i18nKey: "nav.dashboard" },
|
||||
@@ -254,6 +268,12 @@ export function PortalLayout() {
|
||||
);
|
||||
const allowed =
|
||||
(matchedKey ? NAV_ACCESS[matchedKey] : undefined) ?? FALLBACK_ACCESS;
|
||||
const home = allowed === "*" ? "/dashboard" : (allowed[0] ?? "/dashboard");
|
||||
const isAllowed =
|
||||
allowed === "*" ||
|
||||
[...ALWAYS_VISIBLE, ...ALWAYS_ALLOWED, ...allowed].some((p) =>
|
||||
matchesPath(location.pathname, p),
|
||||
);
|
||||
const visibleNavItems =
|
||||
allowed === "*"
|
||||
? NAV_ITEMS
|
||||
@@ -265,6 +285,11 @@ export function PortalLayout() {
|
||||
const [navOpened, { toggle: toggleNav, close: closeNav }] = useDisclosure();
|
||||
const [sidebarCollapsed, { toggle: toggleSidebar }] = useDisclosure(false);
|
||||
|
||||
if (!isAllowed) {
|
||||
notify.error(t("msg.permissionError"));
|
||||
return <Navigate to={home} replace />;
|
||||
}
|
||||
|
||||
// Breadcrumb trail
|
||||
const segments = location.pathname.split("/").filter(Boolean);
|
||||
const crumbs = [
|
||||
|
||||
Reference in New Issue
Block a user