Skip to content

Commit

Permalink
Merge pull request #90 from OxfordRSE/enrolment-key
Browse files Browse the repository at this point in the history
Add enrolment via a secret key
  • Loading branch information
alasdairwilson authored Oct 30, 2023
2 parents c4707f5 + 31f6c14 commit fc8d638
Show file tree
Hide file tree
Showing 13 changed files with 190 additions and 30 deletions.
109 changes: 106 additions & 3 deletions components/EnrolDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { EventStatus, UserOnEvent } from '@prisma/client'
import { Button, Modal, Toast } from 'flowbite-react'
import { Button, Modal, Toast, TextInput } from 'flowbite-react'
import { Event } from 'lib/types'
import { useSession } from 'next-auth/react'
import React from 'react'
import Content from './content/Content'
import { HiMail, HiX } from 'react-icons/hi'
import { HiCheckCircle, HiMail, HiX } from 'react-icons/hi'
import postUserOnEvent from 'lib/actions/postUserOnEvent'
import putUserOnEvent from 'lib/actions/putUserOnEvent'
import useEvent from 'lib/hooks/useEvent'
import { useForm } from 'react-hook-form'
import Stack from './ui/Stack'
import TextField from './forms/Textfield'
import { delay } from 'cypress/types/bluebird'


type Props = {
Expand All @@ -14,10 +20,18 @@ type Props = {
onEnrol: (userOnEvent: UserOnEvent | undefined) => void
}

interface Enrol {
enrolKey: string
}

const EnrolDialog: React.FC<Props> = ({ event, show, onEnrol}) => {
const { control, handleSubmit, reset } = useForm<Enrol>();
const [error, setError] = React.useState<string | undefined>(undefined)
const [enrolError, setEnrolError] = React.useState<string | undefined>(undefined)
const [keySuccess, setKeySuccess] = React.useState<string | undefined>(undefined)
const [success, setSuccess] = React.useState<string | null>(null)
const session = useSession()
const { event: eventData, error: eventError, isLoading: eventIsLoading, mutate: mutateEvent } = useEvent(event.id)

const onClose = () => {
onEnrol(undefined)
Expand All @@ -28,6 +42,16 @@ const EnrolDialog: React.FC<Props> = ({ event, show, onEnrol}) => {
return null;
}

const checkKey = (enrolKey: string) => {
if (enrolKey === event.enrolKey) {
return 'STUDENT'
}
else if (enrolKey === event.instructorKey) {
return 'INSTRUCTOR'
}
return null
}

const onClick = () => {
postUserOnEvent(event)
.then((data) => {
Expand All @@ -45,6 +69,42 @@ const EnrolDialog: React.FC<Props> = ({ event, show, onEnrol}) => {
})
};

const enrolWithKey = async(data: Enrol) => {
const status = checkKey(data.enrolKey)
if (status === null) {
setEnrolError("error")
} else {
postUserOnEvent(event)
.then((data) => {
if ('userOnEvent' in data) {
let newUser = data.userOnEvent
if (newUser) {
if (status === 'STUDENT') {
newUser.status = EventStatus.STUDENT
} else if (status === 'INSTRUCTOR') {
newUser.status = EventStatus.INSTRUCTOR
}
if (eventData) {

putUserOnEvent(event.id, newUser).then(() => {
setKeySuccess("success")
setTimeout(() => {
onEnrol(newUser)
setSuccess("success")
setEnrolError(undefined)
}, 2000);

})
}
}
} else if ('error' in data) {
setError(data.error)
setSuccess(null)
}
})
}
}

return (
<>
<Modal
Expand All @@ -56,9 +116,52 @@ const EnrolDialog: React.FC<Props> = ({ event, show, onEnrol}) => {
{event.name}
</Modal.Header>
<Modal.Body>
<Content markdown={event.enrol} />
<Stack>
<Content markdown={event.enrol} />
<p>
You should have received an enrolment key from the course organiser.
</p>
<form onSubmit={handleSubmit(enrolWithKey)}>
<Stack direction='row' spacing={2} className="justify-center flex-row">
<TextField
textfieldProps={{autoComplete: "off",
placeholder: "Enrolment Key",
autoFocus: true,
}}
name={"enrolKey"}
control={control}
/>
<Button type="submit" className="m-0 h-10 mt-1">
Enrol
</Button>
</Stack>
</form>
{ enrolError && (
<Toast className=''>
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-red-100 text-red-500 dark:bg-red-800 dark:text-red-200">
<HiX className="h-5 w-5" />
</div>
<div className="ml-3 text-sm font-normal">
Invalid Enrolment Key
</div>
</Toast>
)}
{ keySuccess && (
<Toast className=''>
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-green-100 text-green-500 dark:bg-green-800 dark:text-green-200">
<HiCheckCircle className="h-5 w-5" />
</div>
<div className="ml-3 text-sm font-normal">
Enrolment Successful!
</div>
</Toast>
)}
</Stack>
</Modal.Body>
<Modal.Footer>
<p>
If you have not received an enrolment key, you can request enrolment:
</p>
<Button onClick={onClick}>
Request Enrollment
</Button>
Expand Down
4 changes: 3 additions & 1 deletion components/forms/Textfield.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ type Props<T extends FieldValues> = {
name: FieldPath<T>;
control: Control<T>;
rules?: Object;
textfieldProps?: Object;
};

function Textfield<T extends FieldValues>({ label, name, control, rules }: Props<T>): React.ReactElement {
function Textfield<T extends FieldValues>({ label, name, control, rules, textfieldProps }: Props<T>): React.ReactElement {
return (
<div data-cy={`textfield-${name}`}>
<Controller
Expand All @@ -31,6 +32,7 @@ function Textfield<T extends FieldValues>({ label, name, control, rules }: Props
value={value === undefined || value === null ? '' : value}
onChange={onChange}
onBlur={onBlur}
{...textfieldProps}
/>
</>
);
Expand Down
6 changes: 6 additions & 0 deletions cypress/component/CommentThread.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ context ('with non-owner student', () => {
start: new Date(),
id: 1,
enrol: '',
enrolKey: 'test',
instructorKey: 'instructortest',
name: 'test',
EventGroup: [],
hidden: false,
Expand Down Expand Up @@ -217,6 +219,8 @@ context('with owner student', () => {
start: new Date(),
id: 1,
enrol: '',
enrolKey: 'test',
instructorKey: 'instructortest',
name: 'test',
EventGroup: [],
hidden: false,
Expand Down Expand Up @@ -317,6 +321,8 @@ context('with non-owner instructor', () => {
start: new Date(),
id: 1,
enrol: '',
enrolKey: 'test',
instructorKey: 'instructortest',
name: 'test',
EventGroup: [],
hidden: false,
Expand Down
2 changes: 2 additions & 0 deletions cypress/component/EventCommentThreads.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ describe('EventCommentThreads component', () => {
end: new Date(),
start: new Date(),
enrol: '',
enrolKey: 'test',
instructorKey: 'instructortest',
name: 'test',
EventGroup: [],
hidden: false,
Expand Down
10 changes: 5 additions & 5 deletions lib/actions/putEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { Data } from 'pages/api/event/[eventId]'

// function that returns a promise that does a PUT request for this endpoint
export const putEvent = async (event: Event): Promise<Data> => {
const apiPath = `${basePath}/api/event/${event.id}`
const requestOptions = {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event })
const apiPath = `${basePath}/api/event/${event.id}`
const requestOptions = {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event })
};
return fetch(apiPath, requestOptions)
.then(response => response.json())
Expand Down
20 changes: 20 additions & 0 deletions lib/actions/putUserOnEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { basePath } from "lib/basePath"
import { Data } from 'pages/api/userOnEvent/[eventId]'
import { UserOnEvent } from '@prisma/client'
import { Event } from 'lib/types'
import { data } from "cypress/types/jquery"

export const putUserOnEvent = async (eventId: number, userOnEvent: UserOnEvent): Promise<Event> => {
const apiPath = `${basePath}/api/userOnEvent/${eventId}`
const requestOptions = {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userOnEvent })
};
return fetch(apiPath, requestOptions).then(response => response.json()).then(data => {
if ('error' in data) throw data.error
if ('userOnEvent' in data) return data.userOnEvent
})
}

export default putUserOnEvent;
1 change: 0 additions & 1 deletion lib/prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { PrismaClient } from "@prisma/client";
// https://pris.ly/d/help/next-js-best-practices

let prisma: PrismaClient

if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient()
} else {
Expand Down
4 changes: 3 additions & 1 deletion pages/api/event/[eventId].ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const eventHandler = async (

res.status(200).json({ event });
} else if (req.method === 'PUT') {
const { name, enrol, content, start, end, summary, hidden } = req.body.event;
const { name, enrol, content, enrolKey, instructorKey, start, end, summary, hidden } = req.body.event;
const eventGroupData: EventGroup[] = req.body.event.EventGroup;
const userOnEventData: UserOnEvent[] = req.body.event.UserOnEvent;

Expand All @@ -91,6 +91,8 @@ const eventHandler = async (
name,
summary,
enrol,
enrolKey,
instructorKey,
content,
start,
end,
Expand Down
1 change: 0 additions & 1 deletion pages/api/eventGroup/[eventGroupId].ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ const eventHandler = async (
eventItem.order = parseInt(eventItem.order);
}
});

const updatedEventGroup = await prisma.eventGroup.update({
where: { id: parseInt(eventGroupId) },
data: {
Expand Down
52 changes: 35 additions & 17 deletions pages/api/userOnEvent/[eventId].ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,11 @@ export type Data = {
error?: string,
}

// function that returns a promise that does a PUT request for this endpoint
export const putUserOnEvent = async (eventId: number, userOnEvent: UserOnEvent): Promise<Event> => {
const apiPath = `${basePath}/api/userOnEvent/${eventId}`
const requestOptions = {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userOnEvent })
};
return fetch(apiPath, requestOptions)
.then(response => response.json())
}

const UserOnEvent= async (req: NextApiRequest, res: NextApiResponse<Data>) => {
const UserOnEvent = async (req: NextApiRequest, res: NextApiResponse<Data>) => {
const { method } = req;
const eventIdStr = req.query.eventId;
const session = await getServerSession(req, res, authOptions)
const userEmail = session?.user?.email

if (!eventIdStr) {
return res.status(400).send({ error: "No event id" });
}
Expand All @@ -43,17 +30,25 @@ const UserOnEvent= async (req: NextApiRequest, res: NextApiResponse<Data>) => {

const eventId = parseInt(eventIdStr as string)

let userOnEvent = null;
const currentUser = await prisma.user.findUnique({
where: { email: userEmail },
});

const isAdmin = currentUser?.admin === true;

let updatedUserOnEvent = null;
let userOnEvent = null;
switch (method) {
case 'GET':
userOnEvent = await prisma.userOnEvent.findUnique({
where: { userEmail_eventId: { userEmail: userEmail as string, eventId: eventId }},
});
if (userOnEvent) {
res.status(200).json({ userOnEvent : userOnEvent })
return
} else {
res.status(404).json({ error: "userOnEvent not found for this user" });
return
}
break;
case 'POST':
Expand All @@ -64,13 +59,36 @@ const UserOnEvent= async (req: NextApiRequest, res: NextApiResponse<Data>) => {
})
if (userOnEvent) {
res.status(200).json({ userOnEvent: userOnEvent})
return
} else {
res.status(404).json({ error: "failed to create userOnEvent" });
return
}
break;
case 'PUT':
if (!isAdmin && userEmail !== req.body.userOnEvent.userEmail) {
res.status(401).send({ error: "Not authorised" });
return
}
userOnEvent = req.body.userOnEvent
if (userOnEvent) {
updatedUserOnEvent = await prisma.userOnEvent.update({
where: { userEmail_eventId: {userEmail: userOnEvent.userEmail, eventId: userOnEvent.eventId} },
data: req.body.userOnEvent
});
}
if (updatedUserOnEvent) {
res.status(200).json({ userOnEvent: updatedUserOnEvent})
return
}
else {
res.status(404).json({ error: "failed to update userOnEvent" });
return
}
break;
default:
res.setHeader('Allow', ['GET', 'PUT']);
res.status(405).end(`Method ${method} Not Allowed`);
res.setHeader('Allow', ['GET', 'POST', 'PUT']);
res.status(404).end(`Method ${method} Not Allowed`);
break;
}
}
Expand Down
6 changes: 5 additions & 1 deletion pages/event/[eventId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ const Event: NextPage<EventProps> = ({ material, event, pageInfo}) => {
const isAdmin = userProfile?.admin;

const onSubmit = (data: EventWithUsers) => {
putEvent(data).then((data) => data.event && mutateEvent(data.event));
putEvent(data).then((data) => {
data.event && mutateEvent(data.event)
});
}

useEffect(() => {
Expand Down Expand Up @@ -114,6 +116,8 @@ const Event: NextPage<EventProps> = ({ material, event, pageInfo}) => {
<Stack>
<Textfield label="Title" name="name" control={control} />
<Textarea label="Enrol" name="enrol" control={control} />
<Textarea label="Enrolment Key" name="enrolKey" control={control} />
<Textarea label="Instructor Key" name="instructorKey" control={control} />
<Textarea label="Summary" name="summary" control={control} />
<Textarea label="Content" name="content" control={control} />
<DateTimeField label="Start" name="start" control={control} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Event" ADD COLUMN "enrolKey" TEXT NOT NULL,
ADD COLUMN "instructorKey" TEXT NOT NULL;
Loading

0 comments on commit fc8d638

Please sign in to comment.