Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -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=
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
15 changes: 13 additions & 2 deletions app/opportunities/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <Text>Not authenticated</Text>

if (session.user.role === Role.STUDENT) {
const isPlacement = await isPlacementStudent(session.user.email)
if (!isPlacement) opportunities = opportunities.filter(o => o.type !== OpportunityType.Placement)
}

return (
<RestrictedArea allowedRoles={["STUDENT"]}>
<Flex direction="column" gap="5" align="center" width="100%">
Expand Down
54 changes: 54 additions & 0 deletions lib/abcApi.ts
Original file line number Diff line number Diff line change
@@ -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<string | null> {
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<string | null> {
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<boolean>} Resolves to `true` when the student is a third-year Master's student; otherwise `false`.
*/
export async function isPlacementStudent(email: string | null | undefined): Promise<boolean> {
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")
}
17 changes: 17 additions & 0 deletions lib/util/academicYear.ts
Original file line number Diff line number Diff line change
@@ -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}`
}