Skip to content

Commit

Permalink
Convert two more privileges to (CRUD) permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
beverloo committed Jul 22, 2024
1 parent e640853 commit 4b7ee1b
Show file tree
Hide file tree
Showing 11 changed files with 102 additions and 43 deletions.
11 changes: 8 additions & 3 deletions app/admin/components/SectionHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2024 Peter Beverloo & AnimeCon. All rights reserved.
// Use of this source code is governed by a MIT license that can be found in the LICENSE file.

import type { BooleanPermission, CRUDPermission } from '@lib/auth/Access';
import type { Privilege } from '@lib/auth/Privileges';
import Stack from '@mui/material/Stack';
import Typography, { type TypographyProps } from '@mui/material/Typography';
Expand All @@ -20,10 +21,9 @@ export interface SectionHeaderProps {
icon?: React.ReactNode;

/**
* Privilege behind which availability of this section is gated, to inform the volunteer that
* not everyone has access to this information.
* The permission behind which this feature has been gated.
*/
privilege?: Privilege;
permission?: BooleanPermission | CRUDPermission;

/**
* Title of this section. Required.
Expand All @@ -39,6 +39,11 @@ export interface SectionHeaderProps {
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: TypographyProps['sx'],

/**
* @todo Remove
*/
privilege?: Privilege,
}

/**
Expand Down
8 changes: 6 additions & 2 deletions app/admin/events/[event]/[team]/schedule/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2024 Peter Beverloo & AnimeCon. All rights reserved.
// Use of this source code is governed by a MIT license that can be found in the LICENSE file.

import { notFound } from 'next/navigation';
import { z } from 'zod';

import Alert from '@mui/material/Alert';
Expand All @@ -26,7 +27,9 @@ const kExpandSection = z.record(z.string(), z.boolean());
* relies on the timeline component as well as data from many different sources.
*/
export default async function EventTeamSchedulePage(props: NextPageParams<'event' | 'team'>) {
const { event, team, user } = await verifyAccessAndFetchPageInfo(props.params);
const { access, event, team, user } = await verifyAccessAndFetchPageInfo(props.params);
if (!access.can('event.schedules', 'read', { event: event.slug, team: team.slug }))
notFound();

const userSettings = await readUserSettings(user.userId, [
'user-admin-schedule-date',
Expand All @@ -40,7 +43,8 @@ export default async function EventTeamSchedulePage(props: NextPageParams<'event
inclusiveShifts: userSettings['user-admin-schedule-inclusive-shifts'] ?? false,
};

const readOnly = !can(user, Privilege.EventScheduleManagement);
const readOnly =
!access.can('event.schedules', 'update', { event: event.slug, team: team.slug });

let sections: z.infer<typeof kExpandSection> = {};
try {
Expand Down
10 changes: 5 additions & 5 deletions app/admin/events/[event]/[team]/shifts/ShiftTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,20 @@ function formatMinutes(minutes: number): string {
*/
export type ShiftTableProps = EventShiftContext['context'] & {
/**
* Whether the shift table should be shown in read only mode.
* Whether shifts displayed in this table can be deleted.
*/
readOnly?: boolean;
canDeleteShifts: boolean;
};

/**
* The <ShiftTable> component is a Data Table that allows rows to be shown and deleted, which
* displays the shifts that exist for a particular { event, team } pair.
*/
export function ShiftTable(props: ShiftTableProps) {
const { readOnly, ...context } = props;
const { canDeleteShifts, ...context } = props;

const deleteColumn: RemoteDataTableColumn<EventShiftRowModel>[] = [];
if (!readOnly) {
if (canDeleteShifts) {
deleteColumn.push({
field: 'id',
headerName: /* no header= */ '',
Expand Down Expand Up @@ -227,6 +227,6 @@ export function ShiftTable(props: ShiftTableProps) {
return (
<RemoteDataTable columns={columns} endpoint="/api/admin/event/shifts" context={context}
defaultSort={{ field: 'categoryOrder', sort: 'asc' }} subject="shift"
enableDelete={!readOnly} pageSize={100} disableFooter />
enableDelete={canDeleteShifts} pageSize={100} disableFooter />
);
}
8 changes: 5 additions & 3 deletions app/admin/events/[event]/[team]/shifts/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { NextPageParams } from '@lib/NextRouterParams';
import type { ShiftDemandTeamInfo } from './ShiftDemandTimeline';
import type { TimelineEvent } from '@beverloo/volunteer-manager-timeline';
import { CollapsableSection } from '@app/admin/components/CollapsableSection';
import { Privilege, can } from '@lib/auth/Privileges';
import { RegistrationStatus } from '@lib/database/Types';
import { ScheduledShiftsSection } from './ScheduledShiftsSection';
import { Section } from '@app/admin/components/Section';
Expand Down Expand Up @@ -67,9 +66,12 @@ function demandToTimelineEvents(
* actual volunteers cannot be adjusted; this is something that has to be done on the schedule page.
*/
export default async function EventTeamShiftPage(props: NextPageParams<'event' | 'team' | 'id'>) {
const { event, team, user } = await verifyAccessAndFetchPageInfo(props.params);
const { access, event, team, user } = await verifyAccessAndFetchPageInfo(props.params);

const readOnly = !can(user, Privilege.EventShiftManagement);
if (!access.can('event.shifts', 'read', { event: event.slug, team: team.slug }))
notFound();

const readOnly = !access.can('event.shifts', 'update', { event: event.slug, team: team.slug });
const warnings: any[] = [ ];

const step = await readSetting('schedule-time-step-minutes');
Expand Down
28 changes: 18 additions & 10 deletions app/admin/events/[event]/[team]/shifts/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// Copyright 2024 Peter Beverloo & AnimeCon. All rights reserved.
// Use of this source code is governed by a MIT license that can be found in the LICENSE file.

import { notFound } from 'next/navigation';

import Alert from '@mui/material/Alert';
import Paper from '@mui/material/Paper';

import type { NextPageParams } from '@lib/NextRouterParams';
import { CollapsableSection } from '@app/admin/components/CollapsableSection';
import { Privilege, can } from '@lib/auth/Privileges';
import { Section } from '@app/admin/components/Section';
import { SectionIntroduction } from '@app/admin/components/SectionIntroduction';
import { ShiftCreateSection } from './ShiftCreateSection';
Expand All @@ -21,35 +22,42 @@ import { verifyAccessAndFetchPageInfo } from '@app/admin/events/verifyAccessAndF
* end up out of sync.
*/
export default async function EventTeamShiftsPage(props: NextPageParams<'event' | 'team'>) {
const { event, team, user } = await verifyAccessAndFetchPageInfo(props.params);
const { access, event, team, user } = await verifyAccessAndFetchPageInfo(props.params);

// TODO: Box with warnings regarding the shifts (e.g. out-of-sync entries).
const accessScope = { event: event.slug, team: team.slug };

const canCreateShifts = access.can('event.shifts', 'create', accessScope);
const canReadShifts = access.can('event.shifts', 'read', accessScope);
const canUpdateShifts = access.can('event.shifts', 'update', accessScope);
const canDeleteShifts = access.can('event.shifts', 'delete', accessScope);

if (!canReadShifts)
notFound();

const { activities, categories, locations } = await getShiftMetadata(event.festivalId);

const readOnly = !can(user, Privilege.EventShiftManagement);
// TODO: Box with warnings regarding the shifts (e.g. out-of-sync entries).
const warnings: any[] = [ ];

return (
<>
{ !!readOnly &&
{ (!canCreateShifts && !canUpdateShifts && !canDeleteShifts) &&
<Paper component={Alert} severity="warning">
Please ask your Staff member to add you to the scheduling team if you would like
to be able to make any changes.
</Paper> }
<Section title="Shifts" subtitle={team.name}>
<ShiftTable event={event.slug} team={team.slug} readOnly={readOnly} />
<ShiftTable event={event.slug} team={team.slug} canDeleteShifts={canDeleteShifts} />
</Section>
<CollapsableSection in={!!warnings.length} title="Shift warnings">
<SectionIntroduction important>
The shifts tool has not been implemented yet.
</SectionIntroduction>
</CollapsableSection>
{ !readOnly &&
<Section title="Create a new shift" privilege={Privilege.EventShiftManagement}>
{ canCreateShifts &&
<Section title="Create a new shift" permission="event.shifts">
<ShiftCreateSection activities={activities} categories={categories}
locations={locations} event={event.slug}
team={team.slug} readOnly={readOnly} />
locations={locations} event={event.slug} team={team.slug} />
</Section> }
</>
);
Expand Down
7 changes: 6 additions & 1 deletion app/api/admin/event/schedule/createScheduleEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,13 @@ type Response = ApiResponse<typeof kCreateScheduleEntryDefinition>;
* API that allows leaders to create a new schedule entry.
*/
export async function createScheduleEntry(request: Request, props: ActionProps): Promise<Response> {
if (!props.user || !can(props.user, Privilege.EventScheduleManagement))
if (
!props.user ||
!props.access.can(
'event.schedules', 'update', { event: request.event, team: request.team }))
{
notFound();
}

const event = await getEventBySlug(request.event);
if (!event)
Expand Down
7 changes: 6 additions & 1 deletion app/api/admin/event/schedule/deleteScheduleEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,13 @@ type Response = ApiResponse<typeof kDeleteScheduleEntryDefinition>;
* API that allows leaders to delete a schedule entry.
*/
export async function deleteScheduleEntry(request: Request, props: ActionProps): Promise<Response> {
if (!props.user || !can(props.user, Privilege.EventScheduleManagement))
if (
!props.user ||
!props.access.can(
'event.schedules', 'update', { event: request.event, team: request.team }))
{
notFound();
}

if (request.id.length !== 1)
notFound(); // invalid request
Expand Down
7 changes: 6 additions & 1 deletion app/api/admin/event/schedule/updateScheduleEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,13 @@ type Response = ApiResponse<typeof kUpdateScheduleEntryDefinition>;
* API that allows leaders to update a schedule entry.
*/
export async function updateScheduleEntry(request: Request, props: ActionProps): Promise<Response> {
if (!props.user || !can(props.user, Privilege.EventScheduleManagement))
if (
!props.user ||
!props.access.can(
'event.schedules', 'update', { event: request.event, team: request.team }))
{
notFound();
}

if (request.id.length !== 1)
notFound(); // invalid request
Expand Down
19 changes: 14 additions & 5 deletions app/api/admin/event/shifts/[[...id]]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { z } from 'zod';
import { type DataTableEndpoints, createDataTableApi } from '../../../../createDataTableApi';
import { Log, LogSeverity, LogType } from '@lib/Log';
import { Privilege } from '@lib/auth/Privileges';
import { executeAccessCheck } from '@lib/auth/AuthenticationContext';
import { executeAccessCheck, type PermissionAccessCheck } from '@lib/auth/AuthenticationContext';
import { getShiftsForEvent } from '@app/admin/lib/getShiftsForEvent';
import { validateContext } from '../../validateContext';
import db, { tActivities, tSchedule, tShifts, tShiftsCategories } from '@lib/database';
Expand Down Expand Up @@ -145,16 +145,25 @@ export type EventShiftContext = z.infer<typeof kEventShiftContext>;
export const { DELETE, GET, POST, PUT } =
createDataTableApi(kEventShiftRowModel, kEventShiftContext, {
async accessCheck({ context }, action, props) {
let privilege: Privilege | undefined;
const permission: PermissionAccessCheck = {
permission: 'event.shifts',
operation: 'read', // to be updated
options: {
event: context.event,
team: context.team,
},
};

switch (action) {
case 'get':
case 'list':
break; // no additional privilege necessary
permission.operation = 'read';
break;

case 'create':
case 'delete':
case 'update':
privilege = Privilege.EventShiftManagement;
permission.operation = action;
break;

default:
Expand All @@ -164,7 +173,7 @@ createDataTableApi(kEventShiftRowModel, kEventShiftContext, {
executeAccessCheck(props.authenticationContext, {
check: 'admin-event',
event: context.event,
privilege,
permission,
});
},

Expand Down
30 changes: 28 additions & 2 deletions app/lib/auth/Access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,29 @@ export const kPermissions = {
type: 'boolean',
},

'event.schedules': {
name: 'Event schedule planning',
description:
'This permission decides whether they have the ability to read or update event ' +
'scheduling, which is specific to a given event and team. The schedules are the ' +
'rosters shared with volunteers, telling them when they have to be where, doing what.',
hide: [ 'create', 'delete'], // schedules are either in read-only more, or fully mutable
requireEvent: true,
requireTeam: true,
type: 'crud',
},

'event.shifts': {
name: 'Event shift planning',
description:
'This permission decides whether they have the ability to see or manage the shifts ' +
'that are planned for a particular event and team. The shifts decide what it is that ' +
'we expect volunteers to do.',
requireEvent: true,
requireTeam: true,
type: 'crud',
},

'event.retention': {
name: 'Retention management',
description:
Expand Down Expand Up @@ -338,18 +361,21 @@ export const kPermissionGroups: Record<string, string[]> = {
],

staff: [
'event.applications:read',
'event.applications:update',
'event.applications',
'event.help-requests',
'event.requests',
'event.retention',
'event.schedules',
'event.shifts',
'event.vendors',
'event.visible',
'volunteer.avatars',
],

senior: [
'event.applications:read',
'event.schedules:read',
'event.shifts:read',
'event.vendors:read',
'event.visible',
'volunteer.avatars',
Expand Down
10 changes: 0 additions & 10 deletions app/lib/auth/Privileges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ export enum Privilege {
EventApplicationOverride = 1 << 3,
EventContentOverride = 1 << 2,
EventHotelManagement = 1 << 12,
EventScheduleManagement = 1 << 29,
EventScheduleOverride = 1 << 4,
EventShiftManagement = 1 << 26,
EventTrainingManagement = 1 << 13,
EventVolunteerApplicationOverrides = 1 << 14,

Expand Down Expand Up @@ -65,9 +63,7 @@ const PrivilegeExpansion: { [key in Privilege]?: Privilege[] } = {
Privilege.EventApplicationOverride,
Privilege.EventContentOverride,
Privilege.EventHotelManagement,
Privilege.EventScheduleManagement,
Privilege.EventScheduleOverride,
Privilege.EventShiftManagement,
Privilege.EventTrainingManagement,
Privilege.EventVolunteerApplicationOverrides,
],
Expand Down Expand Up @@ -106,15 +102,12 @@ export function expand(privileges: Privileges): Privileges {
export const PrivilegeGroups: { [key in Privilege]: string } = {
[Privilege.Administrator]: 'Special access',
[Privilege.Refunds]: 'Special access',
//[Privilege.Statistics]: 'Special access',

[Privilege.EventAdministrator]: 'Special access',
[Privilege.EventApplicationOverride]: 'Event access',
[Privilege.EventContentOverride]: 'Event access',
[Privilege.EventHotelManagement]: 'Event access',
[Privilege.EventScheduleManagement]: 'Event access',
[Privilege.EventScheduleOverride]: 'Event access',
[Privilege.EventShiftManagement]: 'Event access',
[Privilege.EventTrainingManagement]: 'Event access',
[Privilege.EventVolunteerApplicationOverrides]: 'Event access',

Expand All @@ -130,15 +123,12 @@ export const PrivilegeGroups: { [key in Privilege]: string } = {
export const PrivilegeNames: { [key in Privilege]: string } = {
[Privilege.Administrator]: 'Administrator',
[Privilege.Refunds]: 'Refund requests',
//[Privilege.Statistics]: 'Statistics',

[Privilege.EventAdministrator]: 'Event administrator',
[Privilege.EventApplicationOverride]: 'Always accept their applications',
[Privilege.EventContentOverride]: 'Always allow access to event content',
[Privilege.EventHotelManagement]: 'Manage hotel rooms',
[Privilege.EventScheduleManagement]: 'Manage schedules',
[Privilege.EventScheduleOverride]: 'Always allow access to the volunteer portal',
[Privilege.EventShiftManagement]: 'Manage shifts',
[Privilege.EventTrainingManagement]: 'Manage trainings',
[Privilege.EventVolunteerApplicationOverrides]: 'Manage application overrides',

Expand Down

0 comments on commit 4b7ee1b

Please sign in to comment.