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}`
+}