diff --git a/api.planx.uk/admin/feedback/downloadFeedbackCSV.ts b/api.planx.uk/admin/feedback/downloadFeedbackCSV.ts index 316af72c61..6836fb37a8 100644 --- a/api.planx.uk/admin/feedback/downloadFeedbackCSV.ts +++ b/api.planx.uk/admin/feedback/downloadFeedbackCSV.ts @@ -62,7 +62,7 @@ type ParsedFeedback = Feedback & { * tags: * - admin * security: - * - userJWT: [] + * - bearerAuth: [] */ export const downloadFeedbackCSV = async ( req: Request, diff --git a/api.planx.uk/admin/session/bops.ts b/api.planx.uk/admin/session/bops.ts index 1bf26828be..74aa8feb0a 100644 --- a/api.planx.uk/admin/session/bops.ts +++ b/api.planx.uk/admin/session/bops.ts @@ -1,5 +1,5 @@ import { Request, Response, NextFunction } from "express"; -import { $admin } from "../../client"; +import { getClient } from "../../client"; /** * @swagger @@ -12,7 +12,7 @@ import { $admin } from "../../client"; * parameters: * - $ref: '#/components/parameters/sessionId' * security: - * - userJWT: [] + * - bearerAuth: [] */ export const getBOPSPayload = async ( req: Request, @@ -20,7 +20,8 @@ export const getBOPSPayload = async ( next: NextFunction, ) => { try { - const { exportData } = await $admin.export.bopsPayload( + const $client = getClient(); + const { exportData } = await $client.export.bopsPayload( req.params.sessionId, ); res.set("content-type", "application/json"); diff --git a/api.planx.uk/admin/session/csv.ts b/api.planx.uk/admin/session/csv.ts index e99659678f..06d0bb00be 100644 --- a/api.planx.uk/admin/session/csv.ts +++ b/api.planx.uk/admin/session/csv.ts @@ -1,6 +1,6 @@ import { stringify } from "csv-stringify"; import { NextFunction, Request, Response } from "express"; -import { $admin } from "../../client"; +import { getClient } from "../../client"; /** * @swagger @@ -18,7 +18,7 @@ import { $admin } from "../../client"; * required: false * description: If a CSV file should be downloaded, or its raw data returned * security: - * - userJWT: [] + * - bearerAuth: [] */ export async function getCSVData( req: Request, @@ -26,7 +26,8 @@ export async function getCSVData( next: NextFunction, ) { try { - const { responses } = await $admin.export.csvData(req.params.sessionId); + const $client = getClient(); + const { responses } = await $client.export.csvData(req.params.sessionId); if (req.query?.download) { stringify(responses, { @@ -61,7 +62,7 @@ export async function getCSVData( * required: false * description: If a CSV file should be downloaded, or its raw data returned * security: - * - userJWT: [] + * - bearerAuth: [] */ export async function getRedactedCSVData( req: Request, @@ -69,7 +70,8 @@ export async function getRedactedCSVData( next: NextFunction, ) { try { - const { redactedResponses } = await $admin.export.csvData( + const $client = getClient(); + const { redactedResponses } = await $client.export.csvData( req.params.sessionId, ); diff --git a/api.planx.uk/admin/session/html.ts b/api.planx.uk/admin/session/html.ts index 01a0f89b7a..52560d79da 100644 --- a/api.planx.uk/admin/session/html.ts +++ b/api.planx.uk/admin/session/html.ts @@ -1,5 +1,5 @@ import { generateApplicationHTML } from "@opensystemslab/planx-core"; -import { $admin } from "../../client"; +import { getClient } from "../../client"; import type { RequestHandler } from "express"; import type { PlanXExportData } from "@opensystemslab/planx-core/types"; @@ -16,14 +16,15 @@ type HTMLExportHandler = RequestHandler<{ sessionId: string }, string>; * parameters: * - $ref: '#/components/parameters/sessionId' * security: - * - userJWT: [] + * - bearerAuth: [] */ export const getHTMLExport: HTMLExportHandler = async (req, res, next) => { try { - const session = await $admin.session.find(req.params.sessionId); + const $client = getClient(); + const session = await $client.session.find(req.params.sessionId); if (!session) throw Error(`Unable to find session ${req.params.sessionId}`); - const { responses } = await $admin.export.csvData(req.params.sessionId); + const { responses } = await $client.export.csvData(req.params.sessionId); const boundingBox = session.data.passport.data["property.boundary.site.buffered"]; @@ -52,7 +53,7 @@ export const getHTMLExport: HTMLExportHandler = async (req, res, next) => { * parameters: * - $ref: '#/components/parameters/sessionId' * security: - * - userJWT: [] + * - bearerAuth: [] */ export const getRedactedHTMLExport: HTMLExportHandler = async ( req, @@ -60,10 +61,11 @@ export const getRedactedHTMLExport: HTMLExportHandler = async ( next, ) => { try { - const session = await $admin.session.find(req.params.sessionId); + const $client = getClient(); + const session = await $client.session.find(req.params.sessionId); if (!session) throw Error(`Unable to find session ${req.params.sessionId}`); - const { redactedResponses } = await $admin.export.csvData( + const { redactedResponses } = await $client.export.csvData( req.params.sessionId, ); const boundingBox = diff --git a/api.planx.uk/admin/session/oneAppXML.ts b/api.planx.uk/admin/session/oneAppXML.ts index d91e1897e7..2f95b144cc 100644 --- a/api.planx.uk/admin/session/oneAppXML.ts +++ b/api.planx.uk/admin/session/oneAppXML.ts @@ -13,7 +13,7 @@ import { adminGraphQLClient as client } from "../../hasura"; * parameters: * - $ref: '#/components/parameters/sessionId' * security: - * - userJWT: [] + * - bearerAuth: [] */ export const getOneAppXML = async ( req: Request, diff --git a/api.planx.uk/admin/session/summary.ts b/api.planx.uk/admin/session/summary.ts index 5e7949cd6e..d0975c9e0a 100644 --- a/api.planx.uk/admin/session/summary.ts +++ b/api.planx.uk/admin/session/summary.ts @@ -20,7 +20,7 @@ import { Breadcrumb, Flow, LowCalSession, Passport, Team } from "../../types"; * parameters: * - $ref: '#/components/parameters/sessionId' * security: - * - userJWT: [] + * - bearerAuth: [] */ export async function getSessionSummary( req: Request, diff --git a/api.planx.uk/admin/session/zip.ts b/api.planx.uk/admin/session/zip.ts index 25b9c9c1ed..e57179807f 100644 --- a/api.planx.uk/admin/session/zip.ts +++ b/api.planx.uk/admin/session/zip.ts @@ -17,7 +17,7 @@ import { buildSubmissionExportZip } from "../../send/exportZip"; * required: false * description: If the OneApp XML file should be included in the zip * security: - * - userJWT: [] + * - bearerAuth: [] */ export async function generateZip( req: Request, diff --git a/api.planx.uk/client/index.ts b/api.planx.uk/client/index.ts index e14bd8ac95..4a8e99fdb8 100644 --- a/api.planx.uk/client/index.ts +++ b/api.planx.uk/client/index.ts @@ -1,7 +1,13 @@ import { CoreDomainClient } from "@opensystemslab/planx-core"; +import { userContext } from "../modules/auth/middleware"; +import { ServerError } from "../errors"; + /** - * core doesn't expose a graphql interface like the graphql/hasura clients do - * instead, they encapsulates query and business logic to only expose declarative interfaces + * @deprecated This client's permissions set are higher than required. + * Should only be used by trusted service-to-service calls (e.g Hasura -> API). + * Calls made by users should be directed through $public or the role-scoped getClient(). + * + * Consider removing this and replacing with an "api" role using "backend-only" operations in Hasura */ export const $admin = new CoreDomainClient({ auth: { adminSecret: process.env.HASURA_GRAPHQL_ADMIN_SECRET! }, @@ -11,3 +17,26 @@ export const $admin = new CoreDomainClient({ export const $public = new CoreDomainClient({ targetURL: process.env.HASURA_GRAPHQL_URL!, }); + +/** + * Get a planx-core client with permissions scoped to the current user. + * This client instance ensures that all operations are restricted + * to the permissions of the user who initiated the request. + */ +export const getClient = () => { + const store = userContext.getStore(); + if (!store) + throw new ServerError({ + status: 500, + message: "Missing user context", + }); + + const client = new CoreDomainClient({ + targetURL: process.env.HASURA_GRAPHQL_URL!, + auth: { + jwt: store.user.jwt, + }, + }); + + return client; +}; diff --git a/api.planx.uk/editor/copyFlow.ts b/api.planx.uk/editor/copyFlow.ts index 2549625143..8f8e703e0c 100644 --- a/api.planx.uk/editor/copyFlow.ts +++ b/api.planx.uk/editor/copyFlow.ts @@ -1,6 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { makeUniqueFlow, getFlowData, insertFlow } from "../helpers"; import { Flow } from "../types"; +import { userContext } from "../modules/auth/middleware"; const copyFlow = async ( req: Request, @@ -25,13 +26,15 @@ const copyFlow = async ( const shouldInsert = (req.body?.insert as boolean) || false; if (shouldInsert) { const newSlug = flow.slug + "-copy"; - const creatorId = parseInt(req.user!.sub!, 10); + const creatorId = userContext.getStore()?.user?.sub; + if (!creatorId) throw Error("User details missing from request"); + // Insert the flow and an associated operation await insertFlow( flow.team_id, newSlug, uniqueFlowData, - creatorId, + parseInt(creatorId), req.params.flowId, ); } diff --git a/api.planx.uk/editor/publish.ts b/api.planx.uk/editor/publish.ts index ed19f40ba1..1c81377389 100644 --- a/api.planx.uk/editor/publish.ts +++ b/api.planx.uk/editor/publish.ts @@ -5,6 +5,7 @@ import { dataMerged, getMostRecentPublishedFlow } from "../helpers"; import { gql } from "graphql-request"; import intersection from "lodash/intersection"; import { ComponentType } from "@opensystemslab/planx-core/types"; +import { userContext } from "../modules/auth/middleware"; const validateAndDiffFlow = async ( req: Request, @@ -73,6 +74,9 @@ const publishFlow = async ( const mostRecent = await getMostRecentPublishedFlow(req.params.flowId); const delta = jsondiffpatch.diff(mostRecent, flattenedFlow); + const userId = userContext.getStore()?.user?.sub; + if (!userId) throw Error("User details missing from request"); + if (delta) { const response = await adminClient.request( gql` @@ -101,7 +105,7 @@ const publishFlow = async ( { data: flattenedFlow, flow_id: req.params.flowId, - publisher_id: parseInt(req.user!.sub!, 10), + publisher_id: parseInt(userId), summary: req.query?.summary || null, }, ); diff --git a/api.planx.uk/inviteToPay/sendConfirmationEmail.ts b/api.planx.uk/inviteToPay/sendConfirmationEmail.ts index ea6e50c80c..42f5d93f44 100644 --- a/api.planx.uk/inviteToPay/sendConfirmationEmail.ts +++ b/api.planx.uk/inviteToPay/sendConfirmationEmail.ts @@ -1,4 +1,4 @@ -import { $admin } from "../client"; +import { $public, $admin } from "../client"; import { sendEmail } from "../notify"; import { gql } from "graphql-request"; import { convertSlugToName } from "../saveAndReturn/utils"; @@ -8,7 +8,7 @@ export async function sendAgentAndPayeeConfirmationEmail(sessionId: string) { const { personalisation, applicantEmail, payeeEmail, projectTypes } = await getDataForPayeeAndAgentEmails(sessionId); const projectType = projectTypes.length - ? await $admin.formatRawProjectTypes(projectTypes) + ? await $public.formatRawProjectTypes(projectTypes) : "Project type not submitted"; const config: AgentAndPayeeSubmissionNotifyConfig = { personalisation: { diff --git a/api.planx.uk/inviteToPay/sendPaymentEmail.ts b/api.planx.uk/inviteToPay/sendPaymentEmail.ts index d9b884f57b..96c6e31e99 100644 --- a/api.planx.uk/inviteToPay/sendPaymentEmail.ts +++ b/api.planx.uk/inviteToPay/sendPaymentEmail.ts @@ -8,7 +8,7 @@ import { Template, getClientForTemplate, sendEmail } from "../notify"; import { InviteToPayNotifyConfig } from "../types"; import { Team } from "../types"; import type { PaymentRequest } from "@opensystemslab/planx-core/types"; -import { $admin } from "../client"; +import { $public } from "../client"; interface SessionDetails { email: string; @@ -118,7 +118,7 @@ const getInviteToPayNotifyConfig = async ( ).title, fee: getFee(paymentRequest), projectType: - (await $admin.formatRawProjectTypes( + (await $public.formatRawProjectTypes( paymentRequest.sessionPreviewData?.["proposal.projectType"] as string[], )) || "Project type not submitted", serviceName: convertSlugToName(session.flow.slug), diff --git a/api.planx.uk/modules/auth/middleware.ts b/api.planx.uk/modules/auth/middleware.ts index 0548197f63..a751ea0a23 100644 --- a/api.planx.uk/modules/auth/middleware.ts +++ b/api.planx.uk/modules/auth/middleware.ts @@ -8,6 +8,9 @@ import passport from "passport"; import { RequestHandler } from "http-proxy-middleware"; import { Role } from "@opensystemslab/planx-core/types"; +import { AsyncLocalStorage } from "async_hooks"; + +export const userContext = new AsyncLocalStorage<{ user: Express.User }>(); /** * Validate that a provided string (e.g. API key) matches the expected value @@ -153,11 +156,22 @@ export const useRoleAuth: UseRoleAuth = }); } - next(); + // Establish a context for the current request/response call stack using AsyncLocalStorage + // The validated user will be accessible to all subsequent functions + // Store the raw JWT to pass on to plan-core client + userContext.run( + { + user: { + ...req.user, + jwt: req.cookies.jwt, + }, + }, + () => next(), + ); }); }; -// Convenience methods +// Convenience methods for role-based access export const useTeamViewerAuth = useRoleAuth([ "teamViewer", "teamEditor", @@ -165,3 +179,26 @@ export const useTeamViewerAuth = useRoleAuth([ ]); export const useTeamEditorAuth = useRoleAuth(["teamEditor", "platformAdmin"]); export const usePlatformAdminAuth = useRoleAuth(["platformAdmin"]); + +/** + * Allow any logged in user to access route, without checking roles + */ +export const useLoginAuth: RequestHandler = (req, res, next) => + useJWT(req, res, () => { + if (req?.user?.sub) { + userContext.run( + { + user: { + ...req.user, + jwt: req.cookies.jwt, + }, + }, + () => next(), + ); + } else { + return next({ + status: 401, + message: "No authorization token was found", + }); + } + }); diff --git a/api.planx.uk/modules/auth/service.ts b/api.planx.uk/modules/auth/service.ts index e6201b1170..db8dd538f1 100644 --- a/api.planx.uk/modules/auth/service.ts +++ b/api.planx.uk/modules/auth/service.ts @@ -8,6 +8,7 @@ export const buildJWT = async (email: string): Promise => { const data = { sub: user.id.toString(), + email, "https://hasura.io/jwt/claims": generateHasuraClaimsForUser(user), }; diff --git a/api.planx.uk/modules/misc/controller.ts b/api.planx.uk/modules/misc/controller.ts new file mode 100644 index 0000000000..7f283599ab --- /dev/null +++ b/api.planx.uk/modules/misc/controller.ts @@ -0,0 +1,35 @@ +import { RequestHandler } from "express"; +import { getClient } from "../../client"; +import { userContext } from "../auth/middleware"; +import { ServerError } from "../../errors"; + +export const getLoggedInUserDetails: RequestHandler = async ( + _req, + res, + next, +) => { + try { + const $client = getClient(); + + const email = userContext.getStore()?.user.email; + if (!email) + throw new ServerError({ + message: "User email missing from request", + status: 400, + }); + + const user = await $client.user.getByEmail(email); + if (!user) + throw new ServerError({ + message: `Unable to locate user with email ${email}`, + status: 400, + }); + + res.json(user); + } catch (error) { + next(error); + } +}; + +export const healthCheck: RequestHandler = (_req, res) => + res.json({ hello: "world" }); diff --git a/api.planx.uk/modules/misc/docs.yaml b/api.planx.uk/modules/misc/docs.yaml new file mode 100644 index 0000000000..3d28bd523d --- /dev/null +++ b/api.planx.uk/modules/misc/docs.yaml @@ -0,0 +1,81 @@ +openapi: 3.1.0 +info: + title: Planâś• API + version: 0.1.0 +tags: + - name: misc + description: Miscellaneous +paths: + /: + get: + summary: Health check + description: Confirms the API is healthy + tags: + - misc + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + hello: + type: string + example: + hello: world + /me: + get: + summary: Get information about currently logged in user + tags: + - misc + security: + - bearerAuth: [] + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + id: + type: integer + format: int32 + example: 123 + firstName: + type: string + example: Albert + lastName: + type: string + example: Einstein + email: + type: string + example: albert@princeton.edu + isPlatformAdmin: + type: boolean + example: true + teams: + type: array + items: + type: object + properties: + team: + type: object + properties: + id: + type: integer + format: int32 + example: 123 + slug: + type: string + example: opensystemslab + name: + type: string + example: Open Systems Lab + role: + type: string + enum: ["teamEditor", "teamViewer"] + example: "teamEditor" + "401": + $ref: "#/components/responses/Unauthorised" diff --git a/api.planx.uk/modules/misc/routes.test.ts b/api.planx.uk/modules/misc/routes.test.ts new file mode 100644 index 0000000000..e80e97c015 --- /dev/null +++ b/api.planx.uk/modules/misc/routes.test.ts @@ -0,0 +1,111 @@ +import supertest from "supertest"; +import app from "../../server"; +import { authHeader, getJWT } from "../../tests/mockJWT"; +import { userContext } from "../auth/middleware"; + +const getStoreMock = jest.spyOn(userContext, "getStore"); + +const mockGetByEmail = jest.fn().mockResolvedValue({ + id: 36, + firstName: "Albert", + lastName: "Einstein", + email: "albert@princeton.edu", + isPlatformAdmin: true, + teams: [ + { + teamId: 1, + role: "teamEditor", + }, + { + teamId: 24, + role: "teamEditor", + }, + ], +}); + +jest.mock("@opensystemslab/planx-core", () => { + return { + CoreDomainClient: jest.fn().mockImplementation(() => ({ + user: { + getByEmail: () => mockGetByEmail(), + }, + })), + }; +}); + +describe("/me endpoint", () => { + beforeEach(() => { + getStoreMock.mockReturnValue({ + user: { + sub: "123", + email: "test@opensystemslab.io", + jwt: getJWT({ role: "teamEditor" }), + }, + }); + }); + + it("returns an error if authorization headers are not set", async () => { + await supertest(app) + .get("/me") + .expect(401) + .then((res) => { + expect(res.body).toEqual({ + error: "No authorization token was found", + }); + }); + }); + + it("returns an error for invalid user context", async () => { + getStoreMock.mockReturnValue({ + user: { + sub: "123", + email: undefined, + jwt: getJWT({ role: "teamEditor" }), + }, + }); + + await supertest(app) + .get("/me") + .set(authHeader({ role: "teamEditor" })) + .expect(400) + .then((res) => { + expect(res.body).toEqual({ + error: "User email missing from request", + }); + }); + }); + + it("returns an error for an invalid email address", async () => { + mockGetByEmail.mockResolvedValueOnce(null); + + await supertest(app) + .get("/me") + .set(authHeader({ role: "teamEditor" })) + .expect(400) + .then((res) => { + expect(res.body).toEqual({ + error: "Unable to locate user with email test@opensystemslab.io", + }); + }); + }); + + it("returns user details for a logged in user", async () => { + await supertest(app) + .get("/me") + .set(authHeader({ role: "teamEditor" })) + .expect(200) + .then((res) => { + expect(res.body).toHaveProperty("email", "albert@princeton.edu"); + expect(res.body.teams).toHaveLength(2); + }); + }); +}); + +describe("healthcheck endpoint", () => { + it("always returns a 200", async () => { + await supertest(app) + .get("/") + .expect(200) + .then((res) => expect(res.body).toHaveProperty("hello", "world")); + }); +}); diff --git a/api.planx.uk/modules/misc/routes.ts b/api.planx.uk/modules/misc/routes.ts new file mode 100644 index 0000000000..75d202e32e --- /dev/null +++ b/api.planx.uk/modules/misc/routes.ts @@ -0,0 +1,10 @@ +import { Router } from "express"; +import { useLoginAuth } from "../auth/middleware"; +import { getLoggedInUserDetails, healthCheck } from "./controller"; + +const router = Router(); + +router.get("/", healthCheck); +router.get("/me", useLoginAuth, getLoggedInUserDetails); + +export default router; diff --git a/api.planx.uk/modules/team/controller.ts b/api.planx.uk/modules/team/controller.ts index e6eabd9e8c..03d12aa738 100644 --- a/api.planx.uk/modules/team/controller.ts +++ b/api.planx.uk/modules/team/controller.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { $admin } from "../../client"; +import { getClient } from "../../client"; import { ValidatedRequestHandler } from "../../shared/middleware/validate"; interface TeamMemberResponse { @@ -39,7 +39,8 @@ export const addMember: UpsertMember = async (req, res) => { const { teamId } = req.params; const { userId, role } = req.body; - const isSuccess = await $admin.team.addMember({ teamId, userId, role }); + const $client = getClient(); + const isSuccess = await $client.team.addMember({ teamId, userId, role }); if (!isSuccess) return res.status(500).json({ message: "Failed to add member to team" }); @@ -51,7 +52,8 @@ export const changeMemberRole: UpsertMember = async (req, res) => { const { teamId } = req.params; const { userId, role } = req.body; - const isSuccess = await $admin.team.changeMemberRole({ + const $client = getClient(); + const isSuccess = await $client.team.changeMemberRole({ teamId, userId, role, @@ -67,7 +69,8 @@ export const removeMember: RemoveMember = async (req, res) => { const { teamId } = req.params; const { userId } = req.body; - const isSuccess = await $admin.team.removeMember({ teamId, userId }); + const $client = getClient(); + const isSuccess = await $client.team.removeMember({ teamId, userId }); if (!isSuccess) return res diff --git a/api.planx.uk/saveAndReturn/resumeApplication.ts b/api.planx.uk/saveAndReturn/resumeApplication.ts index 1222e0efb2..292636fb79 100644 --- a/api.planx.uk/saveAndReturn/resumeApplication.ts +++ b/api.planx.uk/saveAndReturn/resumeApplication.ts @@ -5,7 +5,7 @@ import { LowCalSession, Team } from "../types"; import { convertSlugToName, getResumeLink, calculateExpiryDate } from "./utils"; import { sendEmail } from "../notify"; import type { SiteAddress } from "@opensystemslab/planx-core/types"; -import { $admin } from "../client"; +import { $public } from "../client"; /** * Send a "Resume" email to an applicant which list all open applications for a given council (team) @@ -121,7 +121,7 @@ const buildContentFromSessions = async ( const address: SiteAddress | undefined = session.data?.passport?.data?._address; const addressLine = address?.single_line_address || address?.title; - const projectType = await $admin.formatRawProjectTypes( + const projectType = await $public.formatRawProjectTypes( session.data?.passport?.data?.["proposal.projectType"], ); const resumeLink = getResumeLink(session, team, session.flow.slug); diff --git a/api.planx.uk/saveAndReturn/utils.ts b/api.planx.uk/saveAndReturn/utils.ts index b64e75af55..2de72f806f 100644 --- a/api.planx.uk/saveAndReturn/utils.ts +++ b/api.planx.uk/saveAndReturn/utils.ts @@ -4,7 +4,7 @@ import { gql } from "graphql-request"; import { adminGraphQLClient as adminClient } from "../hasura"; import { LowCalSession, Team } from "../types"; import { Template, getClientForTemplate, sendEmail } from "../notify"; -import { $admin } from "../client"; +import { $public } from "../client"; const DAYS_UNTIL_EXPIRY = 28; const REMINDER_DAYS_FROM_EXPIRY = [7, 1]; @@ -145,7 +145,7 @@ const getSessionDetails = async ( session.data.passport?.data?.["proposal.projectType"]; const projectTypes = passportProtectTypes && - (await $admin.formatRawProjectTypes(passportProtectTypes)); + (await $public.formatRawProjectTypes(passportProtectTypes)); const address: SiteAddress | undefined = session.data?.passport?.data?._address; const addressLine = address?.single_line_address || address?.title; diff --git a/api.planx.uk/server.test.js b/api.planx.uk/server.test.js index 125375c05e..7428f7baef 100644 --- a/api.planx.uk/server.test.js +++ b/api.planx.uk/server.test.js @@ -3,15 +3,6 @@ import supertest from "supertest"; import { queryMock } from "./tests/graphqlQueryMock"; import app from "./server"; -it("works", async () => { - await supertest(app) - .get("/") - .expect(200) - .then((response) => { - expect(response.body).toEqual({ hello: "world" }); - }); -}); - it("mocks hasura", async () => { queryMock.mockQuery({ name: "GetTeams", diff --git a/api.planx.uk/server.ts b/api.planx.uk/server.ts index b5891dec7e..575022c70f 100644 --- a/api.planx.uk/server.ts +++ b/api.planx.uk/server.ts @@ -79,6 +79,7 @@ import { getSessionSummary } from "./admin/session/summary"; import { googleStrategy } from "./modules/auth/strategy/google"; import authRoutes from "./modules/auth/routes"; import teamRoutes from "./modules/team/routes"; +import miscRoutes from "./modules/misc/routes"; import { useSwaggerDocs } from "./docs"; import { getDigitalPlanningDataPayload } from "./admin/session/digitalPlanningData"; import { Role } from "@opensystemslab/planx-core/types"; @@ -193,6 +194,7 @@ app.use(passport.session()); app.use(urlencoded({ extended: true })); app.use(authRoutes); +app.use(miscRoutes); app.use("/team", teamRoutes); app.use("/gis", router); @@ -212,72 +214,6 @@ app.get("/hasura", async function (_req, res, next) { } }); -/** - * @swagger - * /me: - * get: - * summary: Get information about currently logged in user - * tags: - * - misc - * security: - * - userJWT: [] - * responses: - * '401': - * $ref: '#/components/responses/Unauthorised' - * '200': - * description: OK - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * type: integer - * format: int32 - * example: 123 - * first_name: - * type: string - * example: Albert - * last_name: - * type: string - * example: Einstein - * email: - * type: string - * example: albert@princeton.edu - * created_at: - * type: string - * example: 2020-08-11T11:28:38.237493+00:00 - * updated_at: - * type: string - * example: 2023-08-11T11:28:38.237493+00:00 - */ -app.get("/me", usePlatformAdminAuth, async function (req, res, next) { - try { - const user = await adminClient.request( - gql` - query ($id: Int!) { - users_by_pk(id: $id) { - id - first_name - last_name - email - created_at - updated_at - } - } - `, - { id: req.user?.sub }, - ); - - if (!user.users_by_pk) - next({ status: 404, message: `User (${req.user?.sub}) not found` }); - - res.json(user.users_by_pk); - } catch (err) { - next(err); - } -}); - app.get("/gis", (_req, _res, next) => { next({ status: 400, @@ -289,31 +225,6 @@ app.get("/gis/:localAuthority", locationSearch); app.get("/roads", classifiedRoadsSearch); -/** - * @swagger - * /: - * get: - * summary: Health check - * description: Confirms the API is healthy - * tags: - * - misc - * responses: - * '200': - * description: OK - * content: - * application/json: - * schema: - * type: object - * properties: - * hello: - * type: string - * example: - * hello: world - */ -app.get("/", (_req, res) => { - res.json({ hello: "world" }); -}); - app.use("/admin", usePlatformAdminAuth); app.get("/admin/feedback", downloadFeedbackCSV); app.get("/admin/session/:sessionId/xml", getOneAppXML); @@ -621,6 +532,7 @@ declare global { interface User { jwt: string; sub?: string; + email?: string; "https://hasura.io/jwt/claims"?: { "x-hasura-allowed-roles": Role[]; }; diff --git a/api.planx.uk/tests/mockJWT.ts b/api.planx.uk/tests/mockJWT.ts index 706c834b12..5e90671e6f 100644 --- a/api.planx.uk/tests/mockJWT.ts +++ b/api.planx.uk/tests/mockJWT.ts @@ -4,6 +4,7 @@ import { sign } from "jsonwebtoken"; function getJWT({ role }: { role: Role }) { const data = { sub: "123", + email: "test@opensystemslab.io", "https://hasura.io/jwt/claims": { "x-hasura-allowed-roles": [role], "x-hasura-default-role": role,