Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use $api client for requests from Hasura → API #2297

Merged
merged 4 commits into from
Oct 13, 2023
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
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 @@
});
jest.mock("../../client", () => {
return {
$admin: {
$api: {
export: {
csvData: () => mockGenerateHTMLData(),
},
Expand Down Expand Up @@ -46,7 +46,7 @@
.expect(403);
});

it.skip("returns a HTML-formatted payload", () => {

Check warning on line 49 in api.planx.uk/admin/session/html.test.ts

View workflow job for this annotation

GitHub Actions / Run API Tests

Disabled test
return supertest(app)
.get(endpoint`123`)
.set(authHeader({ role: "platformAdmin" }))
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({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not totally thrilled with the name of this at it feels a little unspecific, but it follows the existing pattern of $<ROLE> being the client for that role.

We could rename getClient() -> getUserClient() which returns a $user, but similarly this feels a little unclear as you could end up with $user.user.addUser(newUser) 🫠

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
Loading