feat: add profile check mutation and enhance navigation access control

This commit is contained in:
estifanos
2026-08-01 08:23:37 +00:00
parent cdb5812124
commit 6fce0b4fa7
2 changed files with 48 additions and 2 deletions

View File

@@ -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) {

View File

@@ -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 = [