Skip to content

Commit b432d7e

Browse files
Merge pull request #1745 from InformaticsMatters/dev
refactor: improve API for checking project user permissions
2 parents 673b294 + 1a941eb commit b432d7e

1 file changed

Lines changed: 176 additions & 19 deletions

File tree

src/hooks/projectHooks.ts

Lines changed: 176 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { type ProjectDetail } from "@squonk/data-manager-client";
12
import { useGetProjects } from "@squonk/data-manager-client/project";
23

34
import { useRouter } from "next/router";
@@ -6,11 +7,114 @@ import { PROJECT_LOCAL_STORAGE_KEY, writeToLocalStorage } from "../utils/next/lo
67
import { useDMAuthorizationStatus } from "./useIsAuthorized";
78
import { useKeycloakUser } from "./useKeycloakUser";
89

10+
// --- Composable role-checking API ---
11+
export type ProjectRole = "administrator" | "creator" | "editor" | "observer";
12+
13+
export const hasProjectRole = (
14+
project: ProjectDetail | null,
15+
username: string | undefined,
16+
roles: ProjectRole | ProjectRole[],
17+
): boolean => {
18+
if (!project || !username) {
19+
return false;
20+
}
21+
const roleList = Array.isArray(roles) ? roles : [roles];
22+
return roleList.some((role) => {
23+
switch (role) {
24+
case "creator":
25+
return project.creator === username;
26+
case "editor":
27+
return Array.isArray(project.editors) && project.editors.includes(username);
28+
case "administrator":
29+
return Array.isArray(project.administrators) && project.administrators.includes(username);
30+
case "observer":
31+
return Array.isArray(project.observers) && project.observers.includes(username);
32+
default:
33+
return false;
34+
}
35+
});
36+
};
37+
38+
/**
39+
* Hook to check if a user has any of the specified roles on a project by ID.
40+
* @param projectId Project ID
41+
* @param roles Single role or array of roles
42+
* @param username Optional username (defaults to current user)
43+
*/
44+
export const useHasProjectRole = (
45+
projectId: string | undefined,
46+
roles: ProjectRole | ProjectRole[],
47+
username?: string,
48+
): boolean => {
49+
const { user } = useKeycloakUser();
50+
const project = useProjectFromId(projectId ?? "");
51+
const checkUser = username ?? user.username;
52+
return hasProjectRole(project, checkUser, roles);
53+
};
54+
55+
/**
56+
* Hook to check if a user has any of the specified roles on the current project.
57+
* @param roles Single role or array of roles
58+
* @param username Optional username (defaults to current user)
59+
*/
60+
export const useHasProjectRoleOnCurrentProject = (
61+
roles: ProjectRole | ProjectRole[],
62+
username?: string,
63+
): boolean => {
64+
const { user } = useKeycloakUser();
65+
const project = useCurrentProject();
66+
const checkUser = username ?? user.username;
67+
return hasProjectRole(project, checkUser, roles);
68+
};
69+
export const isProjectCreator = (
70+
project: ProjectDetail | null,
71+
username: string | undefined,
72+
): boolean => {
73+
return !!project && !!username && project.creator === username;
74+
};
75+
976
export type ProjectId = string | undefined;
1077
export type ProjectLocalStoragePayload = { projectId: ProjectId; version: number };
1178

1279
export const projectPayload = (projectId: ProjectId) => ({ version: 1, projectId });
1380

81+
// --- Pure role-checking utilities ---
82+
83+
export const isProjectEditor = (
84+
project: ProjectDetail | null,
85+
username: string | undefined,
86+
): boolean => {
87+
return (
88+
!!project && !!username && Array.isArray(project.editors) && project.editors.includes(username)
89+
);
90+
};
91+
92+
export const isProjectAdministrator = (
93+
project: ProjectDetail | null,
94+
username: string | undefined,
95+
): boolean => {
96+
return (
97+
!!project &&
98+
!!username &&
99+
Array.isArray(project.administrators) &&
100+
project.administrators.includes(username)
101+
);
102+
};
103+
104+
export const isProjectObserver = (
105+
project: ProjectDetail | null,
106+
username: string | undefined,
107+
): boolean => {
108+
return (
109+
!!project &&
110+
!!username &&
111+
Array.isArray(project.observers) &&
112+
project.observers.includes(username)
113+
);
114+
};
115+
116+
// --- Hooks for getting current project and project ID ---
117+
14118
/**
15119
* @returns The selected projectId from the project key of the query parameters
16120
*/
@@ -55,43 +159,96 @@ export const useCurrentProjectId = () => {
55159
/**
56160
* @returns The project associated with the project-id in the current url query parameters
57161
*/
58-
export const useCurrentProject = () => {
162+
export const useCurrentProject = (): ProjectDetail | null => {
59163
const isDMAuthorized = useDMAuthorizationStatus();
60164
const { projectId } = useCurrentProjectId();
61165
const { data } = useGetProjects(undefined, { query: { enabled: !!isDMAuthorized } });
62166
const projects = data?.projects;
63-
64167
return projects?.find((project) => project.project_id === projectId) ?? null;
65168
};
66169

67170
/**
68171
* @param projectId Id of the project
69172
* @returns The project object matching the ID if it exists
70173
*/
71-
export const useProjectFromId = (projectId: string) => {
174+
export const useProjectFromId = (projectId: string): ProjectDetail | null => {
72175
const { data } = useGetProjects();
73-
74176
const projects = data?.projects;
75-
76-
return projects?.find((project) => project.project_id === projectId);
177+
return projects?.find((project) => project.project_id === projectId) ?? null;
77178
};
78179

79-
export const useIsUserAdminOrEditorOfCurrentProject = () => {
180+
// --- Internal shared hook for role checks ---
181+
const useProjectRoleCheck = (
182+
getProject: () => ProjectDetail | null,
183+
roleCheck: (project: ProjectDetail | null, username: string | undefined) => boolean,
184+
username?: string,
185+
): boolean => {
80186
const { user } = useKeycloakUser();
81-
const project = useCurrentProject();
187+
const project = getProject();
188+
const checkUser = username ?? user.username;
189+
return roleCheck(project, checkUser);
190+
};
82191

83-
return (
84-
!!user.username &&
85-
(!!project?.editors.includes(user.username) ||
86-
!!project?.administrators.includes(user.username))
192+
// --- Hooks for role checks by project ID ---
193+
194+
/**
195+
* Check if a user (or current user) is admin or editor of the project with the given ID
196+
*/
197+
export const useIsAdminOrEditorOfProject = (projectId: string | undefined, username?: string) =>
198+
useProjectRoleCheck(
199+
() => useProjectFromId(projectId ?? ""),
200+
(project, user) => isProjectEditor(project, user) || isProjectAdministrator(project, user),
201+
username,
87202
);
88-
};
89203

90-
export const useIsEditorOfCurrentProject = () => {
91-
const currentProject = useCurrentProject();
204+
/**
205+
* Check if a user (or current user) is admin or editor of the current project
206+
*/
207+
export const useIsUserAdminOrEditorOfCurrentProject = (username?: string) =>
208+
useProjectRoleCheck(
209+
useCurrentProject,
210+
(project, user) => isProjectEditor(project, user) || isProjectAdministrator(project, user),
211+
username,
212+
);
92213

93-
const { user } = useKeycloakUser();
94-
const isEditor = !!user.username && currentProject?.editors.includes(user.username);
214+
/**
215+
* Check if a user (or current user) is editor of the current project
216+
*/
217+
export const useIsEditorOfCurrentProject = (username?: string) =>
218+
useProjectRoleCheck(useCurrentProject, isProjectEditor, username);
95219

96-
return isEditor;
97-
};
220+
/**
221+
* Check if a user (or current user) is creator of the project with the given ID
222+
*/
223+
export const useIsCreatorOfProject = (projectId: string | undefined, username?: string) =>
224+
useProjectRoleCheck(() => useProjectFromId(projectId ?? ""), isProjectCreator, username);
225+
226+
/**
227+
* Check if a user (or current user) is observer of the project with the given ID
228+
*/
229+
export const useIsObserverOfProject = (projectId: string | undefined, username?: string) =>
230+
useProjectRoleCheck(() => useProjectFromId(projectId ?? ""), isProjectObserver, username);
231+
232+
/**
233+
* Check if a user (or current user) is administrator of the project with the given ID
234+
*/
235+
export const useIsAdministratorOfProject = (projectId: string | undefined, username?: string) =>
236+
useProjectRoleCheck(() => useProjectFromId(projectId ?? ""), isProjectAdministrator, username);
237+
238+
/**
239+
* Check if a user (or current user) is creator of the current project
240+
*/
241+
export const useIsCreatorOfCurrentProject = (username?: string) =>
242+
useProjectRoleCheck(useCurrentProject, isProjectCreator, username);
243+
244+
/**
245+
* Check if a user (or current user) is observer of the current project
246+
*/
247+
export const useIsObserverOfCurrentProject = (username?: string) =>
248+
useProjectRoleCheck(useCurrentProject, isProjectObserver, username);
249+
250+
/**
251+
* Check if a user (or current user) is administrator of the current project
252+
*/
253+
export const useIsAdministratorOfCurrentProject = (username?: string) =>
254+
useProjectRoleCheck(useCurrentProject, isProjectAdministrator, username);

0 commit comments

Comments
 (0)