refactor(auth): unify authorization patterns with MANAGE_OWN_PROFILE permission

Issue #2: The profile route used a custom role-based check instead
of the permission-based pattern used everywhere else.

Changes:
- Add MANAGE_OWN_PROFILE permission to backend Permission enum
- Add permission to ROLE_REGULAR role definition
- Update profile routes to use require_permission(MANAGE_OWN_PROFILE)
- Remove custom require_regular_user dependency
- Update frontend Permission constant and profile page
- Update invites page to use permission instead of role check
- Update profile tests with proper permission mocking

This ensures consistent authorization patterns across all routes.
This commit is contained in:
counterweight 2025-12-21 23:50:06 +01:00
parent 81cd34b0e7
commit 21698203fe
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
7 changed files with 40 additions and 23 deletions

View file

@ -13,6 +13,7 @@ export const Permission = {
INCREMENT_COUNTER: "increment_counter",
USE_SUM: "use_sum",
VIEW_AUDIT: "view_audit",
MANAGE_OWN_PROFILE: "manage_own_profile",
MANAGE_INVITES: "manage_invites",
VIEW_OWN_INVITES: "view_own_invites",
// Booking permissions (regular users)

View file

@ -6,6 +6,7 @@ import { Header } from "../components/Header";
import { useRequireAuth } from "../hooks/useRequireAuth";
import { components } from "../generated/api";
import constants from "../../../shared/constants.json";
import { Permission } from "../auth-context";
import {
layoutStyles,
cardStyles,
@ -19,7 +20,7 @@ type Invite = components["schemas"]["UserInviteResponse"];
export default function InvitesPage() {
const { user, isLoading, isAuthorized } = useRequireAuth({
requiredRole: constants.roles.REGULAR,
requiredPermission: Permission.VIEW_OWN_INVITES,
fallbackRedirect: "/audit",
});
const [invites, setInvites] = useState<Invite[]>([]);

View file

@ -15,11 +15,14 @@ let mockUser: { id: number; email: string; roles: string[]; permissions: string[
id: 1,
email: "test@example.com",
roles: ["regular"],
permissions: ["view_counter", "increment_counter", "use_sum"],
permissions: ["view_counter", "increment_counter", "use_sum", "manage_own_profile"],
};
let mockIsLoading = false;
const mockLogout = vi.fn();
const mockHasRole = vi.fn((role: string) => mockUser?.roles.includes(role) ?? false);
const mockHasPermission = vi.fn(
(permission: string) => mockUser?.permissions.includes(permission) ?? false
);
vi.mock("../auth-context", () => ({
useAuth: () => ({
@ -27,7 +30,23 @@ vi.mock("../auth-context", () => ({
isLoading: mockIsLoading,
logout: mockLogout,
hasRole: mockHasRole,
hasPermission: mockHasPermission,
}),
Permission: {
VIEW_COUNTER: "view_counter",
INCREMENT_COUNTER: "increment_counter",
USE_SUM: "use_sum",
VIEW_AUDIT: "view_audit",
MANAGE_OWN_PROFILE: "manage_own_profile",
MANAGE_INVITES: "manage_invites",
VIEW_OWN_INVITES: "view_own_invites",
BOOK_APPOINTMENT: "book_appointment",
VIEW_OWN_APPOINTMENTS: "view_own_appointments",
CANCEL_OWN_APPOINTMENT: "cancel_own_appointment",
MANAGE_AVAILABILITY: "manage_availability",
VIEW_ALL_APPOINTMENTS: "view_all_appointments",
CANCEL_ANY_APPOINTMENT: "cancel_any_appointment",
},
}));
// Mock profile data
@ -45,10 +64,13 @@ beforeEach(() => {
id: 1,
email: "test@example.com",
roles: ["regular"],
permissions: ["view_counter", "increment_counter", "use_sum"],
permissions: ["view_counter", "increment_counter", "use_sum", "manage_own_profile"],
};
mockIsLoading = false;
mockHasRole.mockImplementation((role: string) => mockUser?.roles.includes(role) ?? false);
mockHasPermission.mockImplementation(
(permission: string) => mockUser?.permissions.includes(permission) ?? false
);
});
afterEach(() => {

View file

@ -7,6 +7,7 @@ import { Header } from "../components/Header";
import { useRequireAuth } from "../hooks/useRequireAuth";
import { components } from "../generated/api";
import constants from "../../../shared/constants.json";
import { Permission } from "../auth-context";
import {
layoutStyles,
cardStyles,
@ -121,7 +122,7 @@ function toFormData(data: ProfileData): FormData {
export default function ProfilePage() {
const { user, isLoading, isAuthorized } = useRequireAuth({
requiredRole: constants.roles.REGULAR,
requiredPermission: Permission.MANAGE_OWN_PROFILE,
fallbackRedirect: "/audit",
});
const [originalData, setOriginalData] = useState<FormData | null>(null);