From 03eede2e9d8d4506157a71c5a02b94b3107de16f Mon Sep 17 00:00:00 2001 From: Zaki Date: Wed, 5 Nov 2025 15:13:30 +0000 Subject: [PATCH 1/4] feat: add abc queries to get degree year of student from their email --- .env.template | 4 ++++ lib/abcApi.ts | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 lib/abcApi.ts diff --git a/.env.template b/.env.template index f4a9922..8a998d1 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/lib/abcApi.ts b/lib/abcApi.ts new file mode 100644 index 0000000..3f6f7e6 --- /dev/null +++ b/lib/abcApi.ts @@ -0,0 +1,61 @@ +"use server" + +// October is designated as the cut off month for determining the current academic year +const ROLLOVER_MONTH = 9 + +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 abcWithYear = `${ABC_ROOT}/${currentShortYear()}` + +/** + * Returns the current academic year as a short string using today's date: e.g. 14 Feb 2025 -> '2425' + * The rollover month (i.e. using previous or future year) is a constant. + * + * @returns {string} The short year string. + */ +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}` +} + +async function fetchFromAbc(endpoint: string, xProxiedUser?: string) { + const credentials = btoa(`${ABC_USERNAME}:${ABC_PASSWORD}`) + + return fetch(endpoint, { + headers: new Headers({ + Authorization: `Basic ${credentials}`, + ...(xProxiedUser ? { "x-proxied-user": xProxiedUser } : {}), + }), + }) +} + +async function getLogin(email: string): Promise { + const endpoint = `${abcWithYear}/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 = `${abcWithYear}/students/${login}` + const student = await fetchFromAbc(endpoint, login) + if (!student.ok) return null + const json = await student.json() + return json.degree_year +} + +export async function isPlacementStudent(email: string): Promise { + 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") +} From 288afcfc1dcdc70fc672a5726658be3f91d9c4fb Mon Sep 17 00:00:00 2001 From: Zaki Date: Wed, 5 Nov 2025 18:07:25 +0000 Subject: [PATCH 2/4] feat: filter placement opportunities so they are only available to m3 students in current year --- README.md | 4 ++++ app/opportunities/page.tsx | 15 +++++++++++++-- lib/abcApi.ts | 24 ++++++++++++++++-------- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c4323ce..31654cc 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 ade07c4..5d1d785 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 index 3f6f7e6..9982c72 100644 --- a/lib/abcApi.ts +++ b/lib/abcApi.ts @@ -1,13 +1,13 @@ "use server" -// October is designated as the cut off month for determining the current academic year +// October is designated as the cut-off month for determining the current academic year const ROLLOVER_MONTH = 9 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 abcWithYear = `${ABC_ROOT}/${currentShortYear()}` +const abcCurrentYear = `${ABC_ROOT}/${currentShortYear()}` /** * Returns the current academic year as a short string using today's date: e.g. 14 Feb 2025 -> '2425' @@ -24,7 +24,7 @@ function currentShortYear(): string { } async function fetchFromAbc(endpoint: string, xProxiedUser?: string) { - const credentials = btoa(`${ABC_USERNAME}:${ABC_PASSWORD}`) + const credentials = Buffer.from(`${ABC_USERNAME}:${ABC_PASSWORD}`).toString("base64") return fetch(endpoint, { headers: new Headers({ @@ -35,7 +35,7 @@ async function fetchFromAbc(endpoint: string, xProxiedUser?: string) { } async function getLogin(email: string): Promise { - const endpoint = `${abcWithYear}/identity?email=${encodeURIComponent(email)}` + const endpoint = `${abcCurrentYear}/identity?email=${encodeURIComponent(email)}` const identity = await fetchFromAbc(endpoint) if (!identity.ok) return null const { login } = await identity.json() @@ -43,14 +43,22 @@ async function getLogin(email: string): Promise { } async function getDegreeYear(login: string): Promise { - const endpoint = `${abcWithYear}/students/${login}` + const endpoint = `${abcCurrentYear}/students/${login}` const student = await fetchFromAbc(endpoint, login) if (!student.ok) return null - const json = await student.json() - return json.degree_year + const { degree_year } = await student.json() + return degree_year } -export async function isPlacementStudent(email: string): Promise { +/** + * 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 From a696222f7f8f7ba10ca9a6ff340db9ee2ed4a47f Mon Sep 17 00:00:00 2001 From: Zaki Date: Thu, 6 Nov 2025 10:46:14 +0000 Subject: [PATCH 3/4] refactor: move year utils --- lib/abcApi.ts | 17 +---------------- lib/util/academicYear.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 lib/util/academicYear.ts diff --git a/lib/abcApi.ts b/lib/abcApi.ts index 9982c72..10f27b6 100644 --- a/lib/abcApi.ts +++ b/lib/abcApi.ts @@ -1,7 +1,6 @@ "use server" -// October is designated as the cut-off month for determining the current academic year -const ROLLOVER_MONTH = 9 +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" @@ -9,20 +8,6 @@ const ABC_PASSWORD = process.env.API_ROLE_PASSWORD || "bob" const abcCurrentYear = `${ABC_ROOT}/${currentShortYear()}` -/** - * Returns the current academic year as a short string using today's date: e.g. 14 Feb 2025 -> '2425' - * The rollover month (i.e. using previous or future year) is a constant. - * - * @returns {string} The short year string. - */ -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}` -} - async function fetchFromAbc(endpoint: string, xProxiedUser?: string) { const credentials = Buffer.from(`${ABC_USERNAME}:${ABC_PASSWORD}`).toString("base64") diff --git a/lib/util/academicYear.ts b/lib/util/academicYear.ts new file mode 100644 index 0000000..3842977 --- /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}` +} From 7403305a3747044f80a817cadd56a1486f07985c Mon Sep 17 00:00:00 2001 From: Zaki Date: Thu, 6 Nov 2025 10:54:03 +0000 Subject: [PATCH 4/4] fix: correct default env var for abc api --- lib/abcApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/abcApi.ts b/lib/abcApi.ts index 10f27b6..251953e 100644 --- a/lib/abcApi.ts +++ b/lib/abcApi.ts @@ -2,7 +2,7 @@ import { currentShortYear } from "@/lib/util/academicYear" -const ABC_ROOT = process.env.ABC_API || "https://abc-api.doc.ic.ac.uk/" +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"