Skip to content

Commit 768d990

Browse files
committed
Added permission checks
1 parent 18a5fa1 commit 768d990

21 files changed

+164
-115
lines changed

src/calendars/CalendarPage.tsx

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import {
1515
TableHead
1616
} from "@mui/material";
1717
import { Delete as DeleteIcon, CalendarMonth as CalendarIcon, Groups as GroupsIcon } from "@mui/icons-material";
18-
import { ApiHelper, UserHelper, Loading, PageHeader, Locale } from "@churchapps/apphelper";
18+
import { ApiHelper, UserHelper, Loading, PageHeader, Locale, Permissions } from "@churchapps/apphelper";
1919
import { type CuratedCalendarInterface, type GroupInterface, type CuratedEventInterface } from "@churchapps/helpers";
20+
import { PermissionDenied } from "../components";
2021
import { CuratedCalendar } from "./components/CuratedCalendar";
2122

2223
export const CalendarPage = () => {
@@ -65,22 +66,24 @@ export const CalendarPage = () => {
6566
{g.name}
6667
</Typography>
6768
</TableCell>
68-
<TableCell align="right">
69-
<Tooltip title={Locale.label("calendars.calendarPage.removeGroup")} arrow>
70-
<IconButton
71-
size="small"
72-
onClick={() => handleGroupDelete(g.id)}
73-
data-testid={`remove-group-${g.id}-button`}
74-
aria-label={Locale.label("calendars.calendarPage.removeGroupAria", g.name)}
75-
sx={{
76-
color: "error.main",
77-
"&:hover": { backgroundColor: "error.light" }
78-
}}
79-
>
80-
<DeleteIcon fontSize="small" />
81-
</IconButton>
82-
</Tooltip>
83-
</TableCell>
69+
{UserHelper.checkAccess(Permissions.contentApi.content.edit) && (
70+
<TableCell align="right">
71+
<Tooltip title={Locale.label("calendars.calendarPage.removeGroup")} arrow>
72+
<IconButton
73+
size="small"
74+
onClick={() => handleGroupDelete(g.id)}
75+
data-testid={`remove-group-${g.id}-button`}
76+
aria-label={Locale.label("calendars.calendarPage.removeGroupAria", g.name)}
77+
sx={{
78+
color: "error.main",
79+
"&:hover": { backgroundColor: "error.light" }
80+
}}
81+
>
82+
<DeleteIcon fontSize="small" />
83+
</IconButton>
84+
</Tooltip>
85+
</TableCell>
86+
)}
8487
</TableRow>
8588
));
8689

@@ -89,6 +92,7 @@ export const CalendarPage = () => {
8992
}, [curatedCalendarId]);
9093

9194
if (!curatedCalendarId) return null;
95+
if (!UserHelper.checkAccess(Permissions.contentApi.content.edit)) return <PermissionDenied permissions={[Permissions.contentApi.content.edit]} />;
9296

9397
return (
9498
<>

src/calendars/CalendarsPage.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
Description as DescriptionIcon
3030
} from "@mui/icons-material";
3131
import { CalendarEdit } from "./components";
32+
import { PermissionDenied } from "../components";
3233

3334
export const CalendarsPage = () => {
3435
const [calendars, setCalendars] = useState<CuratedCalendarInterface[]>([]);
@@ -152,6 +153,8 @@ export const CalendarsPage = () => {
152153
loadData();
153154
}, []);
154155

156+
if (!UserHelper.checkAccess(Permissions.contentApi.content.edit)) return <PermissionDenied permissions={[Permissions.contentApi.content.edit]} />;
157+
155158
return (
156159
<>
157160
<PageHeader
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from "react";
2+
import { Alert, Box } from "@mui/material";
3+
import { UserHelper } from "@churchapps/apphelper";
4+
import type { IApiPermission } from "@churchapps/helpers";
5+
6+
interface Props {
7+
permissions: IApiPermission[];
8+
message?: string;
9+
}
10+
11+
export const hasPermission = (...perms: IApiPermission[]): boolean => perms.every(p => UserHelper.checkAccess(p));
12+
13+
export const PermissionDenied: React.FC<Props> = ({ permissions, message }) => (
14+
<Box sx={{ p: 3 }}>
15+
<Alert severity="warning">
16+
{message || "You do not have the required permission(s) to access this feature:"}
17+
<ul style={{ margin: "8px 0 0 0", paddingLeft: 20 }}>
18+
{permissions.map((p, i) => (
19+
<li key={i}>{p.api} - {p.contentType} - {p.action}</li>
20+
))}
21+
</ul>
22+
Please contact your church administrator to request access.
23+
</Alert>
24+
</Box>
25+
);

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export { Question } from "./Question";
1313
export { Search } from "./Search";
1414
export { StateOptions } from "./StateOptions";
1515
export { Wrapper } from "./Wrapper";
16+
export { PermissionDenied, hasPermission } from "./PermissionDenied";
1617
export { DocChatWidget } from "./docChat";
1718

1819
// Person Management Components (moved from AppHelper)

src/dashboard/components/QuickSetupModal.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from "react";
22
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Stack, Typography, Alert } from "@mui/material";
3-
import { ApiHelper } from "@churchapps/apphelper";
3+
import { ApiHelper, UserHelper, Permissions } from "@churchapps/apphelper";
44
import { type GroupInterface, type GroupMemberInterface } from "@churchapps/helpers";
55
import UserContext from "../../UserContext";
66

@@ -78,6 +78,10 @@ export const QuickSetupModal: React.FC<Props> = ({ wizardType, open, onClose, on
7878
};
7979

8080
const handleFreeshowSetup = async () => {
81+
if (!UserHelper.checkAccess(Permissions.membershipApi.groups.edit)) {
82+
setError("You don't have permission to perform this action. Required: MembershipApi - Groups - Edit");
83+
return;
84+
}
8185
if (!ministryName.trim() || !teamName.trim()) {
8286
setError("Please enter both a ministry name and a team name.");
8387
return;
@@ -108,6 +112,10 @@ export const QuickSetupModal: React.FC<Props> = ({ wizardType, open, onClose, on
108112
};
109113

110114
const handleFreeplaySetup = async () => {
115+
if (!UserHelper.checkAccess(Permissions.membershipApi.groups.edit) || !UserHelper.checkAccess(Permissions.membershipApi.plans.edit)) {
116+
setError("You don't have permission to perform this action. Required: MembershipApi - Groups - Edit, MembershipApi - Plans - Edit");
117+
return;
118+
}
111119
if (!ministryName.trim() || !classroomName.trim()) {
112120
setError("Please enter both a ministry name and a classroom name.");
113121
return;
@@ -132,6 +140,10 @@ export const QuickSetupModal: React.FC<Props> = ({ wizardType, open, onClose, on
132140
};
133141

134142
const handleWebpageSetup = async () => {
143+
if (!UserHelper.checkAccess(Permissions.contentApi.content.edit)) {
144+
setError("You don't have permission to perform this action. Required: ContentApi - Content - Edit");
145+
return;
146+
}
135147
if (!pageTitle.trim()) {
136148
setError("Please enter a page title.");
137149
return;
@@ -145,6 +157,10 @@ export const QuickSetupModal: React.FC<Props> = ({ wizardType, open, onClose, on
145157
};
146158

147159
const handleGroupSetup = async () => {
160+
if (!UserHelper.checkAccess(Permissions.membershipApi.groups.edit)) {
161+
setError("You don't have permission to perform this action. Required: MembershipApi - Groups - Edit");
162+
return;
163+
}
148164
if (!groupName.trim()) {
149165
setError("Please enter a group name.");
150166
return;

src/forms/FormsPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Icon, Table, TableBody, TableCell, TableRow, TableHead, Box, Typography
77
import { Description as DescriptionIcon, Add as AddIcon, Archive as ArchiveIcon } from "@mui/icons-material";
88
import { SmallButton } from "@churchapps/apphelper";
99
import { PageHeader } from "@churchapps/apphelper";
10+
import { PermissionDenied } from "../components";
1011
import { useQuery } from "@tanstack/react-query";
1112
import { SmartTabs } from "../components/ui";
1213

@@ -139,6 +140,7 @@ export const FormsPage = () => {
139140
if (selectedTab === "forms") return <FormEdit formId={selectedFormId} updatedFunction={handleUpdate}></FormEdit>;
140141
};
141142

143+
if (!formPermission) return <PermissionDenied permissions={[Permissions.membershipApi.forms.admin, Permissions.membershipApi.forms.edit]} />;
142144
if (forms.isLoading || archivedForms.isLoading) return <Loading />;
143145

144146
const renderTable = (rows: JSX.Element[], isArchived: boolean) => (

src/groups/components/GroupMembers.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@ export const GroupMembers: React.FC<Props> = memo((props) => {
3636
const [count, setCount] = useState<number>(0);
3737
const [showInviteDialog, setShowInviteDialog] = useState<boolean>(false);
3838

39+
const canView = useMemo(() => UserHelper.checkAccess(Permissions.membershipApi.groupMembers.view), []);
40+
3941
const groupMembers = useQuery<GroupMemberInterface[]>({
4042
queryKey: [`/groupmembers?groupId=${props.group?.id}`, "MembershipApi"],
4143
placeholderData: [],
42-
enabled: !!props.group?.id
44+
enabled: !!props.group?.id && canView
4345
});
4446

4547
const handleRemove = useCallback(

src/helpers/SecondaryMenuHelper.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,12 @@ export class SecondaryMenuHelper {
5555
static getMobileMenu = (path: string) => {
5656
const menuItems: MenuItem[] = [];
5757
let label: string = Locale.label("common.mobile");
58-
menuItems.push({ url: "/mobile/navigation", label: Locale.label("common.navigation"), icon: "menu" });
59-
menuItems.push({ url: "/mobile/theme", label: Locale.label("common.appTheme"), icon: "palette" });
60-
menuItems.push({ url: "/mobile/b1-mobile", label: Locale.label("common.b1Mobile"), icon: "phone_android" });
61-
menuItems.push({ url: "/mobile/checkin", label: Locale.label("common.b1CheckIn"), icon: "qr_code" });
58+
if (UserHelper.checkAccess(Permissions.membershipApi.settings.edit)) {
59+
menuItems.push({ url: "/mobile/navigation", label: Locale.label("common.navigation"), icon: "menu" });
60+
menuItems.push({ url: "/mobile/theme", label: Locale.label("common.appTheme"), icon: "palette" });
61+
menuItems.push({ url: "/mobile/b1-mobile", label: Locale.label("common.b1Mobile"), icon: "phone_android" });
62+
menuItems.push({ url: "/mobile/checkin", label: Locale.label("common.b1CheckIn"), icon: "qr_code" });
63+
}
6264

6365
if (path.startsWith("/mobile/theme")) label = Locale.label("common.appTheme");
6466
else if (path.startsWith("/mobile/b1-mobile")) label = Locale.label("common.b1Mobile");
@@ -127,12 +129,14 @@ export class SecondaryMenuHelper {
127129
const menuItems: MenuItem[] = [];
128130
let label: string = "Website";
129131

130-
menuItems.push({ url: "/site/pages", label: "Pages", icon: "article" });
131-
menuItems.push({ url: "/site/blocks", label: "Blocks", icon: "widgets" });
132-
menuItems.push({ url: "/site/appearance", label: "Appearance", icon: "palette" });
133-
menuItems.push({ url: "/site/files", label: "Files", icon: "folder_open" });
134-
menuItems.push({ url: "/calendars", label: "Calendars", icon: "calendar_month" });
135-
menuItems.push({ url: "/registrations", label: "Registrations", icon: "how_to_reg" });
132+
if (UserHelper.checkAccess(Permissions.contentApi.content.edit)) {
133+
menuItems.push({ url: "/site/pages", label: "Pages", icon: "article" });
134+
menuItems.push({ url: "/site/blocks", label: "Blocks", icon: "widgets" });
135+
menuItems.push({ url: "/site/appearance", label: "Appearance", icon: "palette" });
136+
menuItems.push({ url: "/site/files", label: "Files", icon: "folder_open" });
137+
menuItems.push({ url: "/calendars", label: "Calendars", icon: "calendar_month" });
138+
menuItems.push({ url: "/registrations", label: "Registrations", icon: "how_to_reg" });
139+
}
136140

137141
if (path.startsWith("/registrations")) label = "Registrations";
138142
else if (path.startsWith("/site/pages")) label = "Pages";
@@ -148,10 +152,12 @@ export class SecondaryMenuHelper {
148152
static getSermonsMenu = (path: string) => {
149153
const menuItems: MenuItem[] = [];
150154
let label: string = "";
151-
menuItems.push({ url: "/sermons", label: "Sermons", icon: "live_tv" });
152-
menuItems.push({ url: "/sermons/playlists", label: "Playlists", icon: "video_library" });
153-
menuItems.push({ url: "/sermons/times", label: "Live Stream Times", icon: "schedule" });
154-
menuItems.push({ url: "/sermons/bulk", label: "Bulk Import", icon: "cloud_upload" });
155+
if (UserHelper.checkAccess(Permissions.contentApi.streamingServices.edit)) {
156+
menuItems.push({ url: "/sermons", label: "Sermons", icon: "live_tv" });
157+
menuItems.push({ url: "/sermons/playlists", label: "Playlists", icon: "video_library" });
158+
menuItems.push({ url: "/sermons/times", label: "Live Stream Times", icon: "schedule" });
159+
menuItems.push({ url: "/sermons/bulk", label: "Bulk Import", icon: "cloud_upload" });
160+
}
155161

156162
if (path.startsWith("/sermons/bulk")) label = "Bulk Import";
157163
else if (path.startsWith("/sermons/times")) label = "Live Stream Times";

src/mobile/AppThemePage.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import React from "react";
22
import { Box } from "@mui/material";
3-
import { Locale, PageHeader } from "@churchapps/apphelper";
3+
import { Locale, PageHeader, UserHelper, Permissions } from "@churchapps/apphelper";
44
import { AppThemeEdit } from "../settings/components/AppThemeEdit";
5+
import { PermissionDenied } from "../components";
56

6-
export const AppThemePage: React.FC = () => (
7-
<>
8-
<PageHeader title={Locale.label("mobile.appThemePage.title")} subtitle={Locale.label("mobile.appThemePage.subtitle")} />
9-
<Box sx={{ p: 3 }}>
10-
<AppThemeEdit />
11-
</Box>
12-
</>
13-
);
7+
export const AppThemePage: React.FC = () => {
8+
if (!UserHelper.checkAccess(Permissions.membershipApi.settings.edit)) return <PermissionDenied permissions={[Permissions.membershipApi.settings.edit]} />;
9+
10+
return (
11+
<>
12+
<PageHeader title={Locale.label("mobile.appThemePage.title")} subtitle={Locale.label("mobile.appThemePage.subtitle")} />
13+
<Box sx={{ p: 3 }}>
14+
<AppThemeEdit />
15+
</Box>
16+
</>
17+
);
18+
};

src/mobile/B1MobilePage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React from "react";
22
import { Box, Button, FormControl, Grid, Icon, InputLabel, MenuItem, Select, Stack, Tooltip, Typography } from "@mui/material";
3-
import { ApiHelper, Locale, PageHeader, UniqueIdHelper, UserHelper } from "@churchapps/apphelper";
3+
import { ApiHelper, Locale, PageHeader, UniqueIdHelper, UserHelper, Permissions } from "@churchapps/apphelper";
44
import type { GenericSettingInterface, GroupInterface, VisibilityPreferenceInterface } from "@churchapps/helpers";
5+
import { PermissionDenied } from "../components";
56

67
export const B1MobilePage: React.FC = () => {
78
const [groups, setGroups] = React.useState<GroupInterface[]>(null);
@@ -49,6 +50,8 @@ export const B1MobilePage: React.FC = () => {
4950

5051
React.useEffect(() => { loadData(); }, [loadData]);
5152

53+
if (!UserHelper.checkAccess(Permissions.membershipApi.settings.edit)) return <PermissionDenied permissions={[Permissions.membershipApi.settings.edit]} />;
54+
5255
const handlePrefChange = (name: string, value: string) => {
5356
setPref(prev => ({ ...prev, [name]: value }));
5457
};

0 commit comments

Comments
 (0)