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/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/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/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/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/server.ts b/api.planx.uk/server.ts
index e6d5910e9f..6264fdf11b 100644
--- a/api.planx.uk/server.ts
+++ b/api.planx.uk/server.ts
@@ -40,7 +40,6 @@ import {
} from "./modules/auth/middleware";
import airbrake from "./airbrake";
-import { adminGraphQLClient as adminClient } from "./hasura";
import { sendEmailLimiter, apiLimiter } from "./rateLimit";
import {
privateDownloadController,
@@ -73,6 +72,7 @@ import webhookRoutes from "./modules/webhooks/routes";
import analyticsRoutes from "./modules/analytics/routes";
import { useSwaggerDocs } from "./docs";
import { Role } from "@opensystemslab/planx-core/types";
+import { $public } from "./client";
const router = express.Router();
@@ -279,13 +279,21 @@ app.get(
copyPortalAsFlow,
);
-// unauthenticated because accessing flow schema only, no user data
+interface FlowSchema {
+ node: string;
+ type: string;
+ text: string;
+ planx_variable: string;
+}
+
app.get("/flows/:flowId/download-schema", async (req, res, next) => {
try {
- const schema = await adminClient.request(
+ const { flowSchema } = await $public.client.request<{
+ flowSchema: FlowSchema[];
+ }>(
gql`
query ($flow_id: String!) {
- get_flow_schema(args: { published_flow_id: $flow_id }) {
+ flowSchema: get_flow_schema(args: { published_flow_id: $flow_id }) {
node
type
text
@@ -296,7 +304,7 @@ app.get("/flows/:flowId/download-schema", async (req, res, next) => {
{ flow_id: req.params.flowId },
);
- if (schema.get_flow_schema.length < 1) {
+ if (!flowSchema.length) {
next({
status: 404,
message:
@@ -304,7 +312,7 @@ app.get("/flows/:flowId/download-schema", async (req, res, next) => {
});
} else {
// build a CSV and stream it
- stringify(schema.get_flow_schema, { header: true }).pipe(res);
+ stringify(flowSchema, { header: true }).pipe(res);
res.header("Content-type", "text/csv");
res.attachment(`${req.params.flowId}.csv`);
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();