feat: better navigation in backoffice

This commit is contained in:
Nathnael
2026-08-07 12:24:30 +00:00
parent d5d7c91e24
commit 1e9149ce00
12 changed files with 861 additions and 676 deletions

View File

@@ -0,0 +1,57 @@
import { describe, expect, it } from "vitest";
import type { AuthUser } from "@/auth/types";
import { FREIGHT_PERMS } from "./permissions";
import { NO_ACCESS_PATH, resolveLandingPath } from "./landing";
const withPermissions = (...keys: string[]): AuthUser => ({
permissionKeys: keys,
});
const withRole = (roleKey: string): AuthUser => ({ roles: [{ key: roleKey }] });
describe("resolveLandingPath", () => {
it("keeps normal staff on the overview", () => {
expect(resolveLandingPath(withPermissions(FREIGHT_PERMS.overview.view))).toBe(
"/dashboard/overview",
);
});
it("sends super admins to the overview (they pass every check)", () => {
expect(resolveLandingPath(withRole("super_admin"))).toBe(
"/dashboard/overview",
);
});
it("sends IAM-only admins to the user-management dashboard", () => {
// A pure unit_admin holds `can:*` IAM keys and no edr_freight_app:* key, so
// the freight sidebar filters down to nothing. Landing them on the overview
// is what produced the blank page.
for (const role of ["unit_admin", "admin", "organization_admin"]) {
expect(resolveLandingPath(withRole(role))).toBe(
"/user-management/user_management-dashboard",
);
}
});
it("lands staff without overview:view on a page they can see", () => {
const target = resolveLandingPath(
withPermissions(FREIGHT_PERMS.warehouses.view),
);
expect(target).not.toBe("/dashboard/overview");
expect(target.startsWith("/dashboard/")).toBe(true);
});
it("never returns /user-management for a freight-only staff key", () => {
// The Staff sidebar item points at /user-management, which redirects by
// ROLE — returning it here would ping-pong for a user with no IAM role.
expect(resolveLandingPath(withPermissions(FREIGHT_PERMS.staff.roles.view))).toBe(
NO_ACCESS_PATH,
);
});
it("falls back to the no-access page when nothing is granted", () => {
expect(resolveLandingPath({})).toBe(NO_ACCESS_PATH);
expect(resolveLandingPath(null)).toBe(NO_ACCESS_PATH);
});
});

View File

@@ -0,0 +1,51 @@
import {
buildSidebarSections,
filterSidebarByPermission,
flattenSidebarItems,
} from "@/components/layout/sidebar-sections";
import type { AuthUser } from "@/auth/types";
import { isSuperAdmin } from "./permissions";
/**
* IAM roles that grant the user-management subtree but no freight permissions.
* Mirrors the `allowedRoles` on the org-admin route group in
* `user-management/route.tsx` — a role outside this list is bounced by its
* PrivateRoute, so landing anyone else there would just bounce again.
*/
const IAM_ADMIN_ROLES = ["admin", "organization_admin", "unit_admin"];
/** Terminal page for accounts with nothing granted. Never permission-gated. */
export const NO_ACCESS_PATH = "/no-access";
/**
* The first route a user can actually reach.
*
* Every redirect in the app funnels through here instead of hardcoding
* `/dashboard/overview`: that page is itself gated on `overview:view`, so
* sending a user who lacks the key there redirects them to the page that just
* rejected them — React Router renders nothing and the user sees a blank frame.
* A pure `unit_admin` holds only `can:*` IAM keys and no `edr_freight_app:*`
* key at all, so this hit them on every login.
*/
export function resolveLandingPath(user: AuthUser | null | undefined): string {
const visible = filterSidebarByPermission(buildSidebarSections([]), user);
// Only /dashboard/* items are safe landings. The "Staff" item points at
// /user-management, which re-redirects by ROLE — a freight user holding
// staff:* keys but no IAM role would ping-pong between the two.
const firstFreightPage = flattenSidebarItems(visible).find((item) =>
item.href.startsWith("/dashboard/"),
)?.href;
if (firstFreightPage) return firstFreightPage;
if (isSuperAdmin(user)) return "/user-management/dashboard";
const roleKeys = (user?.roles ?? []).map((role) => role.key ?? "");
if (roleKeys.some((key) => IAM_ADMIN_ROLES.includes(key))) {
return "/user-management/user_management-dashboard";
}
return NO_ACCESS_PATH;
}