Skip to content

Commit

Permalink
feat: Use $api client for requests from Hasura → API (#2297)
Browse files Browse the repository at this point in the history
* feat: Create client for 'api' role

* chore: Replace  with

* fix: Tidy up

* feat: Drop `publicGraphQLClient` in favour of `$public` client (#2300)
  • Loading branch information
DafyddLlyr authored Oct 13, 2023
1 parent 419a1bc commit 4c65722
Show file tree
Hide file tree
Showing 23 changed files with 81 additions and 78 deletions.
2 changes: 1 addition & 1 deletion api.planx.uk/admin/session/html.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const mockGenerateHTMLData = jest.fn().mockResolvedValue({
});
jest.mock("../../client", () => {
return {
$admin: {
$api: {
export: {
csvData: () => mockGenerateHTMLData(),
},
Expand Down
21 changes: 13 additions & 8 deletions api.planx.uk/client/index.ts
Original file line number Diff line number Diff line change
@@ -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!,
});
Expand All @@ -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;
};
7 changes: 1 addition & 6 deletions api.planx.uk/hasura/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
4 changes: 2 additions & 2 deletions api.planx.uk/inviteToPay/createPaymentSendEvents.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions api.planx.uk/inviteToPay/inviteToPay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand All @@ -63,7 +63,7 @@ export async function inviteToPay(

let paymentRequest: PaymentRequest | undefined;
try {
paymentRequest = await $admin.createPaymentRequest({
paymentRequest = await $api.createPaymentRequest({
sessionId,
applicantName,
payeeName,
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions api.planx.uk/inviteToPay/sendConfirmationEmail.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;
Expand Down
13 changes: 4 additions & 9 deletions api.planx.uk/inviteToPay/sendPaymentEmail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
15 changes: 13 additions & 2 deletions api.planx.uk/modules/auth/service.ts
Original file line number Diff line number Diff line change
@@ -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<string | undefined> => {
const user = await $admin.user.getByEmail(email);
const user = await $api.user.getByEmail(email);
if (!user) return;

const data = {
Expand All @@ -16,6 +16,17 @@ export const buildJWT = async (email: string): Promise<string | undefined> => {
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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const mockRunSQL = runSQL as jest.MockedFunction<typeof runSQL>;
const mockFindSession = jest.fn();
jest.mock("../../../../client", () => {
return {
$admin: {
$api: {
session: {
find: jest.fn().mockImplementation(() => mockFindSession()),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -36,7 +36,7 @@ const mockSessionWithResubmissionExemption = {
};

jest.mock<CoreDomainClient>("../../../../client");
const mockAdmin = jest.mocked($admin);
const mockAdmin = jest.mocked($api);

const mockSend = jest.fn();
jest.mock<typeof SlackNotify>("slack-notify", () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
EventType,
UniformEventData,
} from "./types";
import { $admin } from "../../../../client";
import { $api } from "../../../../client";

export const sendSlackNotification = async (
data: EventData,
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 3 additions & 7 deletions api.planx.uk/notify/notify.ts
Original file line number Diff line number Diff line change
@@ -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);

Expand Down Expand Up @@ -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 };
17 changes: 6 additions & 11 deletions api.planx.uk/notify/routeSendEmailRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]";
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(() => {
Expand Down Expand Up @@ -93,7 +88,7 @@ describe("Send Email endpoint", () => {
name: "ValidateSingleSessionRequest",
data: {
flows_by_pk: mockFlow,
lowcal_sessions: [],
lowcalSessions: [],
},
});

Expand Down Expand Up @@ -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,
});
Expand Down
4 changes: 2 additions & 2 deletions api.planx.uk/pay/pay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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(
Expand Down
13 changes: 10 additions & 3 deletions api.planx.uk/saveAndReturn/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}`);

Expand Down
4 changes: 2 additions & 2 deletions api.planx.uk/send/bops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion api.planx.uk/send/exportZip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const mockGenerateOneAppXML = jest

jest.mock("../client", () => {
return {
$admin: {
$api: {
generateOneAppXML: () => mockGenerateOneAppXML(),
getDocumentTemplateNamesForSession: jest
.fn()
Expand Down
Loading

0 comments on commit 4c65722

Please sign in to comment.