diff --git a/api.planx.uk/admin/session/html.test.ts b/api.planx.uk/admin/session/html.test.ts
index f6fdf385e2..3a35a62319 100644
--- a/api.planx.uk/admin/session/html.test.ts
+++ b/api.planx.uk/admin/session/html.test.ts
@@ -17,7 +17,7 @@ const mockGenerateHTMLData = jest.fn().mockResolvedValue({
});
jest.mock("../../client", () => {
return {
- $admin: {
+ $api: {
export: {
csvData: () => mockGenerateHTMLData(),
},
diff --git a/api.planx.uk/client/index.ts b/api.planx.uk/client/index.ts
index 4a8e99fdb8..6fb02ff19d 100644
--- a/api.planx.uk/client/index.ts
+++ b/api.planx.uk/client/index.ts
@@ -1,19 +1,24 @@
import { CoreDomainClient } from "@opensystemslab/planx-core";
import { userContext } from "../modules/auth/middleware";
import { ServerError } from "../errors";
+import { buildJWTForAPIRole } from "../modules/auth/service";
/**
- * @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().
+ * Connects to Hasura using the "api" role
*
- * Consider removing this and replacing with an "api" role using "backend-only" operations in Hasura
+ * Should be used when a request is not initiated by a user, but another PlanX service (e.g. Hasura events).
+ * Can also be used for "side effects" triggered by a user (e.g. writing audit logs)
*/
-export const $admin = new CoreDomainClient({
- auth: { adminSecret: process.env.HASURA_GRAPHQL_ADMIN_SECRET! },
+export const $api = new CoreDomainClient({
+ auth: {
+ jwt: buildJWTForAPIRole(),
+ },
targetURL: process.env.HASURA_GRAPHQL_URL!,
});
+/**
+ * Connects to Hasura using the "public" role
+ */
export const $public = new CoreDomainClient({
targetURL: process.env.HASURA_GRAPHQL_URL!,
});
@@ -31,12 +36,12 @@ export const getClient = () => {
message: "Missing user context",
});
- const client = new CoreDomainClient({
+ const $client = new CoreDomainClient({
targetURL: process.env.HASURA_GRAPHQL_URL!,
auth: {
jwt: store.user.jwt,
},
});
- return client;
+ return $client;
};
diff --git a/api.planx.uk/hasura/index.ts b/api.planx.uk/hasura/index.ts
index 2b6b085003..0837de56c4 100644
--- a/api.planx.uk/hasura/index.ts
+++ b/api.planx.uk/hasura/index.ts
@@ -9,9 +9,4 @@ const adminGraphQLClient = new GraphQLClient(process.env.HASURA_GRAPHQL_URL!, {
},
});
-/**
- * Connect to Hasura using the "Public" role
- */
-const publicGraphQLClient = new GraphQLClient(process.env.HASURA_GRAPHQL_URL!);
-
-export { adminGraphQLClient, publicGraphQLClient };
+export { adminGraphQLClient };
diff --git a/api.planx.uk/inviteToPay/createPaymentSendEvents.ts b/api.planx.uk/inviteToPay/createPaymentSendEvents.ts
index 7e2b7e05ef..8a6bee74b9 100644
--- a/api.planx.uk/inviteToPay/createPaymentSendEvents.ts
+++ b/api.planx.uk/inviteToPay/createPaymentSendEvents.ts
@@ -1,7 +1,7 @@
import { ComponentType } from "@opensystemslab/planx-core/types";
import { NextFunction, Request, Response } from "express";
import { gql } from "graphql-request";
-import { $admin } from "../client";
+import { $api } from "../client";
import { adminGraphQLClient as adminClient } from "../hasura";
import {
ScheduledEventResponse,
@@ -40,7 +40,7 @@ const createPaymentSendEvents = async (
const now = new Date();
const combinedResponse: CombinedResponse = {};
- const session = await $admin.getSessionById(payload.sessionId);
+ const session = await $api.getSessionById(payload.sessionId);
if (!session) {
return next({
status: 400,
diff --git a/api.planx.uk/inviteToPay/inviteToPay.ts b/api.planx.uk/inviteToPay/inviteToPay.ts
index 96d7fbd7f0..8d781ca6d5 100644
--- a/api.planx.uk/inviteToPay/inviteToPay.ts
+++ b/api.planx.uk/inviteToPay/inviteToPay.ts
@@ -2,7 +2,7 @@ import { NextFunction, Request, Response } from "express";
import type { PaymentRequest, KeyPath } from "@opensystemslab/planx-core/types";
import { ServerError } from "../errors";
-import { $admin } from "../client";
+import { $api } from "../client";
export async function inviteToPay(
req: Request,
@@ -39,7 +39,7 @@ export async function inviteToPay(
}
// lock session before creating a payment request
- const locked = await $admin.lockSession(sessionId);
+ const locked = await $api.lockSession(sessionId);
if (locked === null) {
return next(
new ServerError({
@@ -63,7 +63,7 @@ export async function inviteToPay(
let paymentRequest: PaymentRequest | undefined;
try {
- paymentRequest = await $admin.createPaymentRequest({
+ paymentRequest = await $api.createPaymentRequest({
sessionId,
applicantName,
payeeName,
@@ -72,7 +72,7 @@ export async function inviteToPay(
});
} catch (e: unknown) {
// revert the session lock on failure
- await $admin.unlockSession(sessionId);
+ await $api.unlockSession(sessionId);
return next(
new ServerError({
message:
diff --git a/api.planx.uk/inviteToPay/sendConfirmationEmail.ts b/api.planx.uk/inviteToPay/sendConfirmationEmail.ts
index 42f5d93f44..06a5cca1f3 100644
--- a/api.planx.uk/inviteToPay/sendConfirmationEmail.ts
+++ b/api.planx.uk/inviteToPay/sendConfirmationEmail.ts
@@ -1,4 +1,4 @@
-import { $public, $admin } from "../client";
+import { $public, $api } from "../client";
import { sendEmail } from "../notify";
import { gql } from "graphql-request";
import { convertSlugToName } from "../saveAndReturn/utils";
@@ -85,7 +85,7 @@ async function getDataForPayeeAndAgentEmails(
applicantName: string;
}[];
}[];
- } = await $admin.client.request(query, { sessionId });
+ } = await $api.client.request(query, { sessionId });
const data = response.lowcal_sessions[0];
const { emailReplyToId, helpEmail, helpOpeningHours, helpPhone } =
data.flow.team.notifyPersonalisation;
diff --git a/api.planx.uk/inviteToPay/sendPaymentEmail.test.ts b/api.planx.uk/inviteToPay/sendPaymentEmail.test.ts
index a4fff2b3ec..cc73b3cffc 100644
--- a/api.planx.uk/inviteToPay/sendPaymentEmail.test.ts
+++ b/api.planx.uk/inviteToPay/sendPaymentEmail.test.ts
@@ -6,16 +6,11 @@ import {
validatePaymentRequestNotFoundQueryMock,
validatePaymentRequestQueryMock,
} from "../tests/mocks/inviteToPayMocks";
+import { CoreDomainClient } from "@opensystemslab/planx-core";
-jest.mock("@opensystemslab/planx-core", () => {
- return {
- CoreDomainClient: jest.fn().mockImplementation(() => ({
- formatRawProjectTypes: jest
- .fn()
- .mockResolvedValue(["New office premises"]),
- })),
- };
-});
+jest
+ .spyOn(CoreDomainClient.prototype, "formatRawProjectTypes")
+ .mockResolvedValue("New office premises");
const TEST_PAYMENT_REQUEST_ID = "09655c28-3f34-4619-9385-cd57312acc44";
diff --git a/api.planx.uk/modules/auth/service.ts b/api.planx.uk/modules/auth/service.ts
index db8dd538f1..8488c22e0f 100644
--- a/api.planx.uk/modules/auth/service.ts
+++ b/api.planx.uk/modules/auth/service.ts
@@ -1,9 +1,9 @@
import { sign } from "jsonwebtoken";
-import { $admin } from "../../client";
+import { $api } from "../../client";
import { User, Role } from "@opensystemslab/planx-core/types";
export const buildJWT = async (email: string): Promise => {
- const user = await $admin.user.getByEmail(email);
+ const user = await $api.user.getByEmail(email);
if (!user) return;
const data = {
@@ -16,6 +16,17 @@ export const buildJWT = async (email: string): Promise => {
return jwt;
};
+export const buildJWTForAPIRole = () =>
+ sign(
+ {
+ "https://hasura.io/jwt/claims": {
+ "x-hasura-allowed-roles": ["api"],
+ "x-hasura-default-role": "api",
+ },
+ },
+ process.env.JWT_SECRET!,
+ );
+
const generateHasuraClaimsForUser = (user: User) => ({
"x-hasura-allowed-roles": getAllowedRolesForUser(user),
"x-hasura-default-role": getDefaultRoleForUser(user),
diff --git a/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts
index 9e178f682e..eba539c049 100644
--- a/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts
+++ b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts
@@ -31,7 +31,7 @@ const mockRunSQL = runSQL as jest.MockedFunction;
const mockFindSession = jest.fn();
jest.mock("../../../../client", () => {
return {
- $admin: {
+ $api: {
session: {
find: jest.fn().mockImplementation(() => mockFindSession()),
},
diff --git a/api.planx.uk/modules/webhooks/service/sendNotification/index.test.ts b/api.planx.uk/modules/webhooks/service/sendNotification/index.test.ts
index b7d274c741..4fea47f742 100644
--- a/api.planx.uk/modules/webhooks/service/sendNotification/index.test.ts
+++ b/api.planx.uk/modules/webhooks/service/sendNotification/index.test.ts
@@ -2,7 +2,7 @@ import supertest from "supertest";
import app from "../../../../server";
import SlackNotify from "slack-notify";
import { BOPSBody, EmailBody, UniformBody } from "./types";
-import { $admin } from "../../../../client";
+import { $api } from "../../../../client";
import { CoreDomainClient } from "@opensystemslab/planx-core";
const mockSessionWithFee = {
@@ -36,7 +36,7 @@ const mockSessionWithResubmissionExemption = {
};
jest.mock("../../../../client");
-const mockAdmin = jest.mocked($admin);
+const mockAdmin = jest.mocked($api);
const mockSend = jest.fn();
jest.mock("slack-notify", () =>
diff --git a/api.planx.uk/modules/webhooks/service/sendNotification/index.ts b/api.planx.uk/modules/webhooks/service/sendNotification/index.ts
index 57ffcc6862..8d70f6a84b 100644
--- a/api.planx.uk/modules/webhooks/service/sendNotification/index.ts
+++ b/api.planx.uk/modules/webhooks/service/sendNotification/index.ts
@@ -7,7 +7,7 @@ import {
EventType,
UniformEventData,
} from "./types";
-import { $admin } from "../../../../client";
+import { $api } from "../../../../client";
export const sendSlackNotification = async (
data: EventData,
@@ -51,7 +51,7 @@ const getSessionIdFromEvent = (data: EventData, type: EventType) =>
})[type];
const getExemptionStatusesForSession = async (sessionId: string) => {
- const session = await $admin.session.find(sessionId);
+ const session = await $api.session.find(sessionId);
if (!session) throw Error(`Unable to find session with ID ${sessionId}`);
const passport = new Passport(session.data.passport);
diff --git a/api.planx.uk/notify/notify.ts b/api.planx.uk/notify/notify.ts
index 5d8315178e..2824506575 100644
--- a/api.planx.uk/notify/notify.ts
+++ b/api.planx.uk/notify/notify.ts
@@ -1,11 +1,7 @@
-import { GraphQLClient } from "graphql-request";
import { NotifyClient } from "notifications-node-client";
-import {
- adminGraphQLClient as adminClient,
- publicGraphQLClient as publicClient,
-} from "../hasura";
import { softDeleteSession } from "../saveAndReturn/utils";
import { NotifyConfig } from "../types";
+import { $api, $public } from "../client";
const notifyClient = new NotifyClient(process.env.GOVUK_NOTIFY_API_KEY);
@@ -84,7 +80,7 @@ const sendEmail = async (
}
};
-const getClientForTemplate = (template: Template): GraphQLClient =>
- template in privateEmailTemplates ? adminClient : publicClient;
+const getClientForTemplate = (template: Template) =>
+ template in privateEmailTemplates ? $api.client : $public.client;
export { sendEmail, getClientForTemplate };
diff --git a/api.planx.uk/notify/routeSendEmailRequest.test.ts b/api.planx.uk/notify/routeSendEmailRequest.test.ts
index 748fd7e4e2..35bfbcbfb7 100644
--- a/api.planx.uk/notify/routeSendEmailRequest.test.ts
+++ b/api.planx.uk/notify/routeSendEmailRequest.test.ts
@@ -8,20 +8,15 @@ import {
mockSoftDeleteLowcalSession,
mockValidateSingleSessionRequest,
} from "../tests/mocks/saveAndReturnMocks";
+import { CoreDomainClient } from "@opensystemslab/planx-core";
// https://docs.notifications.service.gov.uk/node.html#email-addresses
const TEST_EMAIL = "simulate-delivered@notifications.service.gov.uk";
const SAVE_ENDPOINT = "/send-email/save";
-jest.mock("@opensystemslab/planx-core", () => {
- return {
- CoreDomainClient: jest.fn().mockImplementation(() => ({
- formatRawProjectTypes: jest
- .fn()
- .mockResolvedValue(["New office premises"]),
- })),
- };
-});
+jest
+ .spyOn(CoreDomainClient.prototype, "formatRawProjectTypes")
+ .mockResolvedValue("New office premises");
describe("Send Email endpoint", () => {
beforeEach(() => {
@@ -93,7 +88,7 @@ describe("Send Email endpoint", () => {
name: "ValidateSingleSessionRequest",
data: {
flows_by_pk: mockFlow,
- lowcal_sessions: [],
+ lowcalSessions: [],
},
});
@@ -258,7 +253,7 @@ describe("Setting up send email events", () => {
name: "ValidateSingleSessionRequest",
data: {
flows_by_pk: mockFlow,
- lowcal_sessions: [{ ...mockLowcalSession, has_user_saved: true }],
+ lowcalSessions: [{ ...mockLowcalSession, has_user_saved: true }],
},
matchOnVariables: false,
});
diff --git a/api.planx.uk/pay/pay.ts b/api.planx.uk/pay/pay.ts
index db7a529e71..ee7f818969 100644
--- a/api.planx.uk/pay/pay.ts
+++ b/api.planx.uk/pay/pay.ts
@@ -5,7 +5,7 @@ import SlackNotify from "slack-notify";
import { logPaymentStatus } from "../send/helpers";
import { usePayProxy } from "./proxy";
import { addGovPayPaymentIdToPaymentRequest } from "../inviteToPay";
-import { $admin } from "../client";
+import { $api } from "../client";
import { ServerError } from "../errors";
import { GovUKPayment } from "@opensystemslab/planx-core/types";
@@ -44,7 +44,7 @@ export async function makePaymentViaProxy(
);
}
- const session = await $admin.session.findDetails(sessionId);
+ const session = await $api.session.findDetails(sessionId);
if (session?.lockedAt) {
return next(
diff --git a/api.planx.uk/saveAndReturn/utils.ts b/api.planx.uk/saveAndReturn/utils.ts
index 2de72f806f..b9409058e0 100644
--- a/api.planx.uk/saveAndReturn/utils.ts
+++ b/api.planx.uk/saveAndReturn/utils.ts
@@ -92,7 +92,10 @@ const validateSingleSessionRequest = async (
try {
const query = gql`
query ValidateSingleSessionRequest($sessionId: uuid!) {
- lowcal_sessions(where: { id: { _eq: $sessionId } }, limit: 1) {
+ lowcalSessions: lowcal_sessions(
+ where: { id: { _eq: $sessionId } }
+ limit: 1
+ ) {
id
data
created_at
@@ -112,8 +115,12 @@ const validateSingleSessionRequest = async (
const client = getClientForTemplate(template);
const headers = getSaveAndReturnPublicHeaders(sessionId, email);
const {
- lowcal_sessions: [session],
- } = await client.request(query, { sessionId }, headers);
+ lowcalSessions: [session],
+ } = await client.request<{ lowcalSessions: LowCalSession[] }>(
+ query,
+ { sessionId },
+ headers,
+ );
if (!session) throw Error(`Unable to find session: ${sessionId}`);
diff --git a/api.planx.uk/send/bops.ts b/api.planx.uk/send/bops.ts
index 5ce11c974a..b0e07058f4 100644
--- a/api.planx.uk/send/bops.ts
+++ b/api.planx.uk/send/bops.ts
@@ -3,7 +3,7 @@ import { adminGraphQLClient as adminClient } from "../hasura";
import { markSessionAsSubmitted } from "../saveAndReturn/utils";
import { NextFunction, Request, Response } from "express";
import { gql } from "graphql-request";
-import { $admin } from "../client";
+import { $api } from "../client";
import { ServerError } from "../errors";
interface SendToBOPSRequest {
@@ -70,7 +70,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => {
);
}
const target = `${bopsSubmissionURL}/api/v1/planning_applications`;
- const { exportData } = await $admin.export.bopsPayload(payload?.sessionId);
+ const { exportData } = await $api.export.bopsPayload(payload?.sessionId);
try {
const bopsResponse = await axios({
diff --git a/api.planx.uk/send/exportZip.test.ts b/api.planx.uk/send/exportZip.test.ts
index cb31258db3..39ce06e6b2 100644
--- a/api.planx.uk/send/exportZip.test.ts
+++ b/api.planx.uk/send/exportZip.test.ts
@@ -57,7 +57,7 @@ const mockGenerateOneAppXML = jest
jest.mock("../client", () => {
return {
- $admin: {
+ $api: {
generateOneAppXML: () => mockGenerateOneAppXML(),
getDocumentTemplateNamesForSession: jest
.fn()
diff --git a/api.planx.uk/send/exportZip.ts b/api.planx.uk/send/exportZip.ts
index 000c70c546..9c2e6ffbe6 100644
--- a/api.planx.uk/send/exportZip.ts
+++ b/api.planx.uk/send/exportZip.ts
@@ -1,6 +1,6 @@
import os from "os";
import path from "path";
-import { $admin } from "../client";
+import { $api } from "../client";
import { stringify } from "csv-stringify";
import fs from "fs";
import str from "string-to-stream";
@@ -28,7 +28,7 @@ export async function buildSubmissionExportZip({
const zip = new ExportZip(sessionId);
// fetch session data
- const sessionData = await $admin.session.find(sessionId);
+ const sessionData = await $api.session.find(sessionId);
if (!sessionData) {
throw new Error(
`session ${sessionId} not found so could not create Uniform submission zip`,
@@ -39,7 +39,7 @@ export async function buildSubmissionExportZip({
// add OneApp XML to the zip
if (includeOneAppXML) {
try {
- const xml = await $admin.generateOneAppXML(sessionId);
+ const xml = await $api.generateOneAppXML(sessionId);
const xmlStream = str(xml.trim());
await zip.addStream({
name: "proposal.xml", // must be named "proposal.xml" to be processed by Uniform
@@ -65,8 +65,7 @@ export async function buildSubmissionExportZip({
}
// generate csv data
- const { responses, redactedResponses } =
- await $admin.export.csvData(sessionId);
+ const { responses, redactedResponses } = await $api.export.csvData(sessionId);
// write csv to the zip
try {
@@ -84,7 +83,7 @@ export async function buildSubmissionExportZip({
// add template files to zip
const templateNames =
- await $admin.getDocumentTemplateNamesForSession(sessionId);
+ await $api.getDocumentTemplateNamesForSession(sessionId);
for (const templateName of templateNames || []) {
try {
const isTemplateSupported = hasRequiredDataForTemplate({
diff --git a/api.planx.uk/send/uniform.ts b/api.planx.uk/send/uniform.ts
index df4a74eca9..4140a14146 100644
--- a/api.planx.uk/send/uniform.ts
+++ b/api.planx.uk/send/uniform.ts
@@ -6,7 +6,7 @@ import fs from "fs";
import { adminGraphQLClient as adminClient } from "../hasura";
import { markSessionAsSubmitted } from "../saveAndReturn/utils";
import { gql } from "graphql-request";
-import { $admin } from "../client";
+import { $api } from "../client";
import { buildSubmissionExportZip } from "./exportZip";
interface UniformClient {
@@ -373,7 +373,7 @@ const createUniformApplicationAuditRecord = async ({
localAuthority: string;
submissionDetails: UniformSubmissionResponse;
}): Promise => {
- const xml = await $admin.generateOneAppXML(payload?.sessionId);
+ const xml = await $api.generateOneAppXML(payload?.sessionId);
const application: Record<
"insert_uniform_applications_one",
diff --git a/api.planx.uk/session/files.test.ts b/api.planx.uk/session/files.test.ts
index 12a6adcceb..824df21f6e 100644
--- a/api.planx.uk/session/files.test.ts
+++ b/api.planx.uk/session/files.test.ts
@@ -4,7 +4,7 @@ const mockFindSession = jest.fn();
jest.mock("../client", () => {
return {
- $admin: {
+ $api: {
session: {
find: jest.fn().mockImplementation(() => mockFindSession()),
},
diff --git a/api.planx.uk/session/files.ts b/api.planx.uk/session/files.ts
index 2cb5d162c3..7f5550afc5 100644
--- a/api.planx.uk/session/files.ts
+++ b/api.planx.uk/session/files.ts
@@ -1,10 +1,10 @@
import { Passport } from "@opensystemslab/planx-core";
-import { $admin } from "../client";
+import { $api } from "../client";
export const getFilesForSession = async (
sessionId: string,
): Promise => {
- const session = await $admin.session.find(sessionId);
+ const session = await $api.session.find(sessionId);
if (!session?.data.passport?.data) return [];
const files = new Passport(session.data.passport).files();
diff --git a/api.planx.uk/tests/mocks/saveAndReturnMocks.ts b/api.planx.uk/tests/mocks/saveAndReturnMocks.ts
index 3a92bc01fc..02393437d2 100644
--- a/api.planx.uk/tests/mocks/saveAndReturnMocks.ts
+++ b/api.planx.uk/tests/mocks/saveAndReturnMocks.ts
@@ -144,7 +144,7 @@ export const mockValidateSingleSessionRequest = {
name: "ValidateSingleSessionRequest",
data: {
flows_by_pk: mockFlow,
- lowcal_sessions: [mockLowcalSession],
+ lowcalSessions: [mockLowcalSession],
},
variables: {
sessionId: mockLowcalSession.id,
diff --git a/api.planx.uk/types.ts b/api.planx.uk/types.ts
index ee8d0c46c9..b7e17693f8 100644
--- a/api.planx.uk/types.ts
+++ b/api.planx.uk/types.ts
@@ -75,7 +75,7 @@ export interface LowCalSession {
has_user_saved: boolean;
flow: {
slug: string;
- team?: Team;
+ team: Team;
};
lockedAt?: string;
paymentRequests?: Pick[];