diff --git a/.env.template b/.env.template index f4a99222..8a998d10 100644 --- a/.env.template +++ b/.env.template @@ -30,3 +30,7 @@ EMAIL_SERVER_PORT=587 EMAIL_SERVER_USER="" EMAIL_SERVER_PASSWORD="" EMAIL_FROM="" + +ABC_API="https://abc-api.doc.ic.ac.uk" +API_ROLE_USERNAME= +API_ROLE_PASSWORD= diff --git a/README.md b/README.md index c4323ced..31654ccd 100644 --- a/README.md +++ b/README.md @@ -382,6 +382,10 @@ The following variables are required for the app to function properly: 10. **MS_ENTRA_CLIENT_SECRET** (Same as in dev) 11. **MS_ENTRA_TENANT_ID** (Same as in dev) 12. **NEXTAUTH_URL** +13. **ABC_API** +14. **API_ROLE_USERNAME** (super user credentials for ABC API) +15. **API_ROLE_PASSWORD** (super user credentials for ABC API) + To get the DATABASE_URL, run the following command: diff --git a/app/opportunities/page.tsx b/app/opportunities/page.tsx index ade07c4d..5d1d7855 100644 --- a/app/opportunities/page.tsx +++ b/app/opportunities/page.tsx @@ -1,18 +1,29 @@ import OpportunityTable from "@/app/opportunities/OpportunityTable" +import { auth } from "@/auth" import RestrictedArea from "@/components/rbac/RestrictedArea" +import { isPlacementStudent } from "@/lib/abcApi" import prisma from "@/lib/db" -import { Flex, Heading } from "@radix-ui/themes" +import { OpportunityType, Role } from "@prisma/client" +import { Flex, Heading, Text } from "@radix-ui/themes" import React from "react" const OpportunitiesPage = async () => { - const opportunities = await prisma.opportunity.findMany({ + let opportunities = await prisma.opportunity.findMany({ orderBy: { createdAt: "desc" }, include: { company: true, }, }) + const session = await auth() + if (!session) return Not authenticated + + if (session.user.role === Role.STUDENT) { + const isPlacement = await isPlacementStudent(session.user.email) + if (!isPlacement) opportunities = opportunities.filter(o => o.type !== OpportunityType.Placement) + } + return ( diff --git a/lib/abcApi.ts b/lib/abcApi.ts new file mode 100644 index 00000000..251953e0 --- /dev/null +++ b/lib/abcApi.ts @@ -0,0 +1,54 @@ +"use server" + +import { currentShortYear } from "@/lib/util/academicYear" + +const ABC_ROOT = process.env.ABC_API || "https://abc-api.doc.ic.ac.uk" +const ABC_USERNAME = process.env.API_ROLE_USERNAME || "adumble" +const ABC_PASSWORD = process.env.API_ROLE_PASSWORD || "bob" + +const abcCurrentYear = `${ABC_ROOT}/${currentShortYear()}` + +async function fetchFromAbc(endpoint: string, xProxiedUser?: string) { + const credentials = Buffer.from(`${ABC_USERNAME}:${ABC_PASSWORD}`).toString("base64") + + return fetch(endpoint, { + headers: new Headers({ + Authorization: `Basic ${credentials}`, + ...(xProxiedUser ? { "x-proxied-user": xProxiedUser } : {}), + }), + }) +} + +async function getLogin(email: string): Promise { + const endpoint = `${abcCurrentYear}/identity?email=${encodeURIComponent(email)}` + const identity = await fetchFromAbc(endpoint) + if (!identity.ok) return null + const { login } = await identity.json() + return login +} + +async function getDegreeYear(login: string): Promise { + const endpoint = `${abcCurrentYear}/students/${login}` + const student = await fetchFromAbc(endpoint, login) + if (!student.ok) return null + const { degree_year } = await student.json() + return degree_year +} + +/** + * Returns `true` only if the student is a third-year Master's student. This should be used to restrict the visibility of opportunities marked as placements. + * + * @param email - The student's email address. + * @returns {Promise} Resolves to `true` when the student is a third-year Master's student; otherwise `false`. + */ +export async function isPlacementStudent(email: string | null | undefined): Promise { + if (!email) return false + + const login = await getLogin(email) + if (!login) return false + + const degreeYear = await getDegreeYear(login) + if (!degreeYear) return false + + return degreeYear.startsWith("m") && degreeYear.endsWith("3") +} diff --git a/lib/util/academicYear.ts b/lib/util/academicYear.ts new file mode 100644 index 00000000..38429775 --- /dev/null +++ b/lib/util/academicYear.ts @@ -0,0 +1,17 @@ +// October is designated as the cut-off month +const ROLLOVER_MONTH = 9 + +/** + * Returns the current academic year as a short string using today's date + * The rollover month (i.e. using previous or future year) is a constant, set to October. + * e.g. 12 Sep 2023 -> '2324', 6 Oct 2025 -> 2526 + * + * @returns {string} The short year string. + */ +export function currentShortYear(): string { + const date = new Date() + const year = date.getFullYear() + const month = date.getMonth() + const [curr, prev, next] = [year, year - 1, year + 1].map(y => `${y}`.slice(2)) + return month < ROLLOVER_MONTH ? `${prev}${curr}` : `${curr}${next}` +}