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

Production deploy #2796

Merged
merged 8 commits into from
Feb 15, 2024
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ Our public-facing live services were last audited by the [Digital Accessibility

### Security

Our whole stack was last assessed by [Jumpsec](https://www.jumpsec.com/) between the 21st and 30th November 2023. JUMPSEC then performed a retest of the issues identified in the initial test on the 8th of February 2024. This included verifying that fixes had been successfully applied and that no further risks were introduced as a result of the remediation work carried out. Their penetration test concluded that - "the security posture of PlanX was strong, and following industry best practices. JUMPSEC commend the PlanX team on their dedication to security and ability to both maintain and mitigate issues in a responsible and timely manner".
Our whole stack was last assessed by [Jumpsec](https://www.jumpsec.com/) between the 21st and 30th November 2023. JUMPSEC then performed a retest of the issues identified in the initial test on the 8th of February 2024. This included verifying that fixes had been successfully applied and that no further risks were introduced as a result of the remediation work carried out. Their penetration test concluded that - "the security posture of PlanX was strong, and following industry best practices. JUMPSEC commend the PlanX team on their dedication to security and ability to both maintain and mitigate issues in a responsible and timely manner". You can [review our report here](https://file.notion.so/f/f/d2306134-7ae0-417c-8db3-cdd87c524efa/ab940248-ca60-49a3-bfae-0b6faa916b1e/2024-02-13_JUMPSEC_Lambeth_PlanX_Web_Application_Assessment_Report_v2.0.pdf?id=aa4ed144-4b48-4a88-9693-6a3644bfd6cf&table=block&spaceId=d2306134-7ae0-417c-8db3-cdd87c524efa&expirationTimestamp=1707998400000&signature=76AfnXjSTzw8O5TEW9Ao0mOmWTG4WzE8rm-Rfa54wGU&downloadName=Penetration+test+%28Jumpsec%29+13%2F02%2F24.pdf).


## Related packages
Expand Down
10 changes: 10 additions & 0 deletions api.planx.uk/modules/auth/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,13 @@ export const useLoginAuth: RequestHandler = (req, res, next) =>
});
}
});

export const useNoCache: RequestHandler = (_req, res, next) => {
res.setHeader("Surrogate-Control", "no-store");
res.setHeader(
"Cache-Control",
"no-store, no-cache, must-revalidate, proxy-revalidate",
);
res.setHeader("Expires", "0");
next();
};
7 changes: 6 additions & 1 deletion api.planx.uk/modules/file/routes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Router } from "express";

import multer from "multer";
import { useFilePermission, useTeamEditorAuth } from "../auth/middleware";
import {
useNoCache,
useFilePermission,
useTeamEditorAuth,
} from "../auth/middleware";
import {
downloadFileSchema,
privateDownloadController,
Expand Down Expand Up @@ -37,6 +41,7 @@ router.get(

router.get(
"/file/private/:fileKey/:fileName",
useNoCache,
useFilePermission,
validate(downloadFileSchema),
privateDownloadController,
Expand Down
14 changes: 6 additions & 8 deletions api.planx.uk/modules/send/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ export async function logPaymentStatus({
}): Promise<void> {
if (!flowId || !sessionId) {
reportError({
message:
"Could not log the payment status due to missing context value(s)",
error: "Could not log the payment status due to missing context value(s)",
context: { sessionId, flowId, teamSlug },
});
} else {
Expand All @@ -38,21 +37,20 @@ export async function logPaymentStatus({
});
} catch (e) {
reportError({
message: "Failed to insert a payment status",
error: e,
govUkResponse,
error: `Failed to insert a payment status: ${e}`,
context: { govUkResponse },
});
}
}
}

// tmp explicit error handling
export function reportError(obj: object) {
export function reportError(report: { error: any; context: object }) {
if (airbrake) {
airbrake.notify(obj);
airbrake.notify(report);
return;
}
log(obj);
log(report);
}

// tmp logger
Expand Down
8 changes: 5 additions & 3 deletions api.planx.uk/modules/webhooks/service/validateInput/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ export const isCleanHTML = (input: unknown): boolean => {
*/
const logUncleanHTMLError = (input: string, cleanHTML: string) => {
reportError({
message: `Warning: Unclean HTML submitted!`,
input,
cleanHTML,
error: `Warning: Unclean HTML submitted!`,
context: {
input,
cleanHTML,
},
});
};
40 changes: 39 additions & 1 deletion e2e/tests/ui-driven/src/create-flow/create-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,45 @@ test.describe("Navigation", () => {
await expect(nodes.getByText(noBranchNoticeText)).toBeVisible();
});

test("Preview a created flow", async ({ browser }: { browser: Browser }) => {
test("Cannot preview an unpublished flow", async ({
browser,
}: {
browser: Browser;
}) => {
const page = await createAuthenticatedSession({
browser,
userId: context.user!.id!,
});

await page.goto(
`/${context.team.slug}/${serviceProps.slug}/preview?analytics=false`,
);

await expect(page.getByText("Not Found")).toBeVisible();
});

test("Publish a flow", async ({ browser }) => {
const page = await createAuthenticatedSession({
browser,
userId: context.user!.id!,
});

await page.goto(`/${context.team.slug}/${serviceProps.slug}`);

page.getByRole("button", { name: "CHECK FOR CHANGES TO PUBLISH" }).click();
page.getByRole("button", { name: "PUBLISH", exact: true }).click();

const previewLink = page.getByRole("link", {
name: "Open published service",
});
await expect(previewLink).toBeVisible();
});

test("Can preview a published flow", async ({
browser,
}: {
browser: Browser;
}) => {
const page = await createAuthenticatedSession({
browser,
userId: context.user!.id!,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function PropertyInformationComponent(props: Props) {
<form onSubmit={formik.handleSubmit} id="modal">
<ModalSection>
<ModalSectionContent
title="Propery information"
title="Property information"
Icon={ICONS[TYPES.PropertyInformation]}
>
<InputRow>
Expand Down
11 changes: 8 additions & 3 deletions editor.planx.uk/src/lib/dataMergedHotfix.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ComponentType as TYPES } from "@opensystemslab/planx-core/types";
import gql from "graphql-tag";
import { Store } from "pages/FlowEditor/lib/store";

import { publicClient } from "../lib/graphql";

Expand All @@ -23,10 +24,14 @@ const getFlowData = async (id: string) => {
// Flatten a flow's data to include main content & portals in a single JSON representation
// XXX: getFlowData & dataMerged are currently repeated in api.planx.uk/helpers.ts
// in order to load frontend /preview routes for flows that are not published
export const dataMerged = async (id: string, ob: Record<string, any> = {}) => {
export const dataMerged = async (
id: string,
ob: Store.flow = {},
): Promise<Store.flow> => {
// get the primary flow data
const { slug, data }: { slug: string; data: Record<string, any> } =
await getFlowData(id);
const { slug, data }: { slug: string; data: Store.flow } = await getFlowData(
id,
);

// recursively get and flatten internal portals & external portals
for (const [nodeId, node] of Object.entries(data)) {
Expand Down
45 changes: 29 additions & 16 deletions editor.planx.uk/src/pages/FlowEditor/components/PreviewBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const PreviewBrowser: React.FC<{
lastPublished,
lastPublisher,
validateAndDiffFlow,
isFlowPublished,
] = useStore((state) => [
state.id,
state.flowAnalyticsLink,
Expand All @@ -111,6 +112,7 @@ const PreviewBrowser: React.FC<{
state.lastPublished,
state.lastPublisher,
state.validateAndDiffFlow,
state.isFlowPublished,
]);
const [key, setKey] = useState<boolean>(false);
const [lastPublishedTitle, setLastPublishedTitle] = useState<string>(
Expand Down Expand Up @@ -189,17 +191,26 @@ const PreviewBrowser: React.FC<{
<OpenInNewIcon />
</Link>
</Tooltip>

<Tooltip arrow title="Open published service">
<Link
href={props.url + "?analytics=false"}
target="_blank"
rel="noopener noreferrer"
color="inherit"
>
<LanguageIcon />
</Link>
</Tooltip>
{isFlowPublished ? (
<Tooltip arrow title="Open published service">
<Link
href={props.url + "?analytics=false"}
target="_blank"
rel="noopener noreferrer"
color="inherit"
>
<LanguageIcon />
</Link>
</Tooltip>
) : (
<Tooltip arrow title="Flow not yet published">
<Box>
<Link component={"button"} disabled aria-disabled={true}>
<LanguageIcon />
</Link>
</Box>
</Tooltip>
)}
</Box>
<Box width="100%" mt={2}>
<Box display="flex" flexDirection="column" alignItems="flex-end">
Expand Down Expand Up @@ -283,12 +294,14 @@ const PreviewBrowser: React.FC<{
try {
setDialogOpen(false);
setLastPublishedTitle("Publishing changes...");
const publishedFlow = await publishFlow(flowId, summary);
const { alteredNodes, message } = await publishFlow(
flowId,
summary,
);
setLastPublishedTitle(
publishedFlow?.data.alteredNodes
? `Successfully published changes to ${publishedFlow.data.alteredNodes.length} node(s)`
: `${publishedFlow?.data?.message}` ||
"No new changes to publish",
alteredNodes
? `Successfully published changes to ${alteredNodes.length} node(s)`
: `${message}` || "No new changes to publish",
);
} catch (error) {
setLastPublishedTitle("Error trying to publish");
Expand Down
21 changes: 18 additions & 3 deletions editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export const editorUIStore: StateCreator<
},
});

interface PublishFlowResponse {
alteredNodes: Store.node[];
message: string;
}

export interface EditorStore extends Store.Store {
addNode: (node: any, relationships?: any) => void;
connect: (src: Store.nodeId, tgt: Store.nodeId, object?: any) => void;
Expand All @@ -67,6 +72,7 @@ export interface EditorStore extends Store.Store {
isClone: (id: Store.nodeId) => boolean;
lastPublished: (flowId: string) => Promise<string>;
lastPublisher: (flowId: string) => Promise<string>;
isFlowPublished: boolean;
makeUnique: (id: Store.nodeId, parent?: Store.nodeId) => void;
moveFlow: (flowId: string, teamSlug: string) => Promise<any>;
moveNode: (
Expand All @@ -76,7 +82,10 @@ export interface EditorStore extends Store.Store {
toParent?: Store.nodeId,
) => void;
pasteNode: (toParent: Store.nodeId, toBefore: Store.nodeId) => void;
publishFlow: (flowId: string, summary?: string) => Promise<any>;
publishFlow: (
flowId: string,
summary?: string,
) => Promise<PublishFlowResponse>;
removeNode: (id: Store.nodeId, parent: Store.nodeId) => void;
updateNode: (node: any, relationships?: any) => void;
}
Expand Down Expand Up @@ -325,6 +334,8 @@ export const editorStore: StateCreator<
return first_name.concat(" ", last_name);
},

isFlowPublished: false,

makeUnique: (id, parent) => {
const [, ops] = makeUnique(id, parent)(get().flow);
send(ops);
Expand Down Expand Up @@ -388,15 +399,15 @@ export const editorStore: StateCreator<
}
},

publishFlow(flowId: string, summary?: string) {
async publishFlow(flowId: string, summary?: string) {
const token = get().jwt;

const urlWithParams = (url: string, params: any) =>
[url, new URLSearchParams(omitBy(params, isEmpty))]
.filter(Boolean)
.join("?");

return axios.post(
const { data } = await axios.post<PublishFlowResponse>(
urlWithParams(
`${process.env.REACT_APP_API_URL}/flows/${flowId}/publish`,
{ summary },
Expand All @@ -408,6 +419,10 @@ export const editorStore: StateCreator<
},
},
);

set({ isFlowPublished: true });

return data;
},

removeNode: (id, parent) => {
Expand Down
Loading
Loading