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 #2645

Merged
merged 10 commits into from
Jan 9, 2024
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
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,14 @@ UNIFORM_CLIENT_AYLESBURY_VALE=👻
UNIFORM_CLIENT_CHILTERN=👻
UNIFORM_CLIENT_WYCOMBE=👻

## Camden
BOPS_SUBMISSION_URL_CAMDEN=👻

## Gloucester
BOPS_SUBMISSION_URL_GLOUCESTER=👻

## Medway
GOV_UK_PAY_TOKEN_MEDWAY=👻

## End-to-end test team (borrows Lambeth's details)
GOV_UK_PAY_TOKEN_E2E=👻
21 changes: 16 additions & 5 deletions api.planx.uk/.env.test.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ HASURA_PLANX_API_KEY=👻

# Integrations
BOPS_API_TOKEN=👻

GOV_UK_PAY_TOKEN_BUCKINGHAMSHIRE=👻
GOV_UK_PAY_TOKEN_LAMBETH=👻
GOV_UK_PAY_TOKEN_SOUTHWARK=👻
BOPS_API_TOKEN=👻

GOVUK_NOTIFY_API_KEY=testlocaldev2-👻

Expand All @@ -38,12 +35,26 @@ UNIFORM_SUBMISSION_URL=👻

SLACK_WEBHOOK_URL=👻

ORDNANCE_SURVEY_API_KEY=👻

# Local authority specific integrations
## Lambeth
GOV_UK_PAY_TOKEN_LAMBETH=👻
BOPS_SUBMISSION_URL_LAMBETH=👻

## Southwark
GOV_UK_PAY_TOKEN_SOUTHWARK=👻
BOPS_SUBMISSION_URL_SOUTHWARK=👻

## Buckinghamshire
GOV_UK_PAY_TOKEN_BUCKINGHAMSHIRE=👻
GOV_UK_PAY_TOKEN_BUCKINGHAMSHIRE=👻
BOPS_SUBMISSION_URL_BUCKINGHAMSHIRE=👻

## Camden
BOPS_SUBMISSION_URL_CAMDEN=👻

## Gloucester
BOPS_SUBMISSION_URL_GLOUCESTER=👻

## Medway
GOV_UK_PAY_TOKEN_MEDWAY=👻
111 changes: 56 additions & 55 deletions api.planx.uk/modules/auth/controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { CookieOptions, RequestHandler, Response } from "express";
import { Request } from "express-jwt";

import { isLiveEnv } from "../../helpers";

export const failedLogin: RequestHandler = (_req, _res, next) =>
next({
status: 401,
Expand All @@ -17,61 +15,64 @@ export const logout: RequestHandler = (req, res) => {
};

export const handleSuccess = (req: Request, res: Response) => {
if (req.user) {
const { returnTo = process.env.EDITOR_URL_EXT } = req.session!;

const domain = (() => {
if (isLiveEnv()) {
if (returnTo?.includes("editor.planx.")) {
// user is logging in to staging from editor.planx.dev
// or production from editor.planx.uk
return `.${new URL(returnTo).host}`;
} else {
// user is logging in from either a netlify preview build,
// or from localhost, to staging (or production... temporarily)
return undefined;
}
} else {
// user is logging in from localhost, to development
return "localhost";
}
})();

if (domain) {
// As domain is set, we know that we're either redirecting back to
// editor.planx.dev/login, editor.planx.uk, or localhost:PORT
// (if this code is running in development). With the respective
// domain set in the cookie.
const cookie: CookieOptions = {
domain,
maxAge: new Date(
new Date().setFullYear(new Date().getFullYear() + 1),
).getTime(),
httpOnly: false,
};

if (isLiveEnv()) {
cookie.secure = true;
cookie.sameSite = "none";
}

res.cookie("jwt", req.user.jwt, cookie);

res.redirect(returnTo);
} else {
// Redirect back to localhost:PORT/login (if this API is in staging or
// production), or a netlify preview build url. As the login page is on a
// different domain to whatever this API is running on, we can't set a
// cookie. To solve this issue we inject the JWT into the return url as
// a parameter that can be extracted by the frontend code instead.
const url = new URL(returnTo);
url.searchParams.set("jwt", req.user.jwt);
res.redirect(url.href);
}
} else {
res.json({
if (!req.user) {
return res.json({
message: "no user",
success: true,
});
}

// Check referrer of original request
// This means requests from Pizzas to the staging API will not get flagged as `isStagingOrProd`
const { returnTo = process.env.EDITOR_URL_EXT } = req.session!;
if (!returnTo) throw Error("Can't generate returnTo URL from session");

const isStagingOrProd = returnTo.includes("editor.planx.");

isStagingOrProd
? setJWTCookie(returnTo, res, req)
: setJWTSearchParams(returnTo, res, req);
};

/**
* Handle auth for staging and production
*
* Use a httpOnly cookie to pass the JWT securely back to the client.
* The client will then use the JWT to make authenticated requests to the API.
*/
function setJWTCookie(returnTo: string, res: Response, req: Request) {
const defaultCookieOptions: CookieOptions = {
domain: `.${new URL(returnTo).host}`,
maxAge: new Date(
new Date().setFullYear(new Date().getFullYear() + 1),
).getTime(),
sameSite: "none",
secure: true,
};

const httpOnlyCookieOptions: CookieOptions = {
...defaultCookieOptions,
httpOnly: true,
};

// Set secure, httpOnly cookie with JWT
res.cookie("jwt", req.user!.jwt, httpOnlyCookieOptions);

// Set second cookie which can be read by browser to detect presence of the unreadable httpOnly cookie
const authCookie = btoa(JSON.stringify({ loggedIn: true }));
res.cookie("auth", authCookie, defaultCookieOptions);

res.redirect(returnTo);
}

/**
* Handle auth for local development and Pizzas
*
* We can't use cookies cross-domain.
* Inject the JWT into the return URL, which can then be set as a cookie by the frontend
*/
function setJWTSearchParams(returnTo: string, res: Response, req: Request) {
const url = new URL(returnTo);
url.searchParams.set("jwt", req.user!.jwt);
res.redirect(url.href);
}
2 changes: 2 additions & 0 deletions api.planx.uk/modules/gis/service/digitalLand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
}

const localAuthorityMetadata: Record<string, LocalAuthorityMetadata> = {
barnet: require("./local_authorities/metadata/barnet"),
birmingham: require("./local_authorities/metadata/birmingham"),
buckinghamshire: require("./local_authorities/metadata/buckinghamshire"),
camden: require("./local_authorities/metadata/camden"),
Expand Down Expand Up @@ -69,8 +70,8 @@
options,
)}${datasets}`;
const res = await fetch(url)
.then((response: { json: () => any }) => response.json())

Check warning on line 73 in api.planx.uk/modules/gis/service/digitalLand.ts

View workflow job for this annotation

GitHub Actions / Run API Tests

Unexpected any. Specify a different type
.catch((error: any) => console.log(error));

Check warning on line 74 in api.planx.uk/modules/gis/service/digitalLand.ts

View workflow job for this annotation

GitHub Actions / Run API Tests

Unexpected any. Specify a different type

// if analytics are "on", store an audit record of the raw response
if (extras?.analytics !== "false") {
Expand Down Expand Up @@ -104,7 +105,7 @@
// check for & add any 'positive' constraints to the formattedResult
let formattedResult: Record<string, Constraint> = {};
if (res && res.count > 0 && res.entities) {
res.entities.forEach((entity: { dataset: any }) => {

Check warning on line 108 in api.planx.uk/modules/gis/service/digitalLand.ts

View workflow job for this annotation

GitHub Actions / Run API Tests

Unexpected any. Specify a different type
// get the planx variable that corresponds to this entity's 'dataset', should never be null because our initial request is filtered on 'dataset'
const key = Object.keys(baseSchema).find(
(key) =>
Expand Down Expand Up @@ -153,7 +154,7 @@
formattedResult["designated.nationalPark"] &&
formattedResult["designated.nationalPark"].value
) {
formattedResult["designated.nationalPark"]?.data?.forEach((entity: any) => {

Check warning on line 157 in api.planx.uk/modules/gis/service/digitalLand.ts

View workflow job for this annotation

GitHub Actions / Run API Tests

Unexpected any. Specify a different type
if (
baseSchema[broads]["digital-land-entities"]?.includes(entity.entity)
) {
Expand Down Expand Up @@ -203,6 +204,7 @@
// these are various ways we link source data to granular planx values (see local_authorities/metadata for specifics)
entity.name.replace(/\r?\n|\r/g, " ") === a4s[key] ||
entity.reference === a4s[key] ||
entity?.["article-4-direction"] === a4s[key] ||
entity?.notes === a4s[key] ||
entity?.description?.startsWith(a4s[key]) ||
formattedResult[key]?.value // if this granular var is already true, make sure it remains true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
LAD20CD: E09000003
LAD20NM: Barnet
LAD20NMW:
FID:

https://www.planning.data.gov.uk/entity/?dataset=article-4-direction-area&geometry_curie=statistical-geography%3AE09000003&entry_date_day=&entry_date_month=&entry_date_year=
https://docs.google.com/spreadsheets/d/1ZjqYdC7upA8YS9rBoyRIQPT1sqCXJBaxQDrvUh1todU/edit#gid=0
*/

import { LocalAuthorityMetadata } from "../../digitalLand";

const planningConstraints: LocalAuthorityMetadata["planningConstraints"] = {
article4: {
// Planx granular values link to Digital Land article-4-direction and entity.reference
records: {
"article4.barnet.finchleyChurchEnd": "A4D1",
"article4.barnet.finchleyGardenVillage": "A4D2A1",
"article4.barnet.glenhillClose": "A4D3A1",
"article4.barnet.hendonBurroughs.1": "A4D5A1",
"article4.barnet.hendonBurroughs.2": "A4D5A2",
"article4.barnet.hampsteadGardenSuburb": "A4D4A1",
"article4.barnet.spaniardsEnd": " A4D4A2",
"article4.barnet.millHillA": "A4D6",
"article4.barnet.millHillB": "A4D7",
"article4.barnet.monkenHadleyA": "A4D8",
"article4.barnet.monkenHadleyB": "A4D9",
"article4.barnet.mossHall": "A4D10",
"article4.barnet.totteridgeA": "A4D11",
"article4.barnet.totteridgeB": "A4D12",
"article4.barnet.woodStreet": "A4D13",
"article4.barnet.hmo": "A4D14",
"article4.barnet.agriculturalLand": "A4D15",
},
},
};

export { planningConstraints };
19 changes: 12 additions & 7 deletions api.planx.uk/modules/send/bops/bops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,16 +261,16 @@ const sendToBOPSV2 = async (
.catch((error) => {
if (error.response) {
throw new Error(
`Sending to BOPS v2 failed (${localAuthority}):\n${JSON.stringify(
error.response.data,
null,
2,
)}`,
`Sending to BOPS v2 failed (${[localAuthority, payload?.sessionId]
.filter(Boolean)
.join(" - ")}):\n${JSON.stringify(error.response.data, null, 2)}`,
);
} else {
// re-throw other errors
throw new Error(
`Sending to BOPS v2 failed (${localAuthority}):\n${error}`,
`Sending to BOPS v2 failed (${[localAuthority, payload?.sessionId]
.filter(Boolean)
.join(" - ")}):\n${error}`,
);
}
});
Expand All @@ -279,7 +279,12 @@ const sendToBOPSV2 = async (
next(
new ServerError({
status: 500,
message: `Sending to BOPS v2 failed (${localAuthority})`,
message: `Sending to BOPS v2 failed (${[
localAuthority,
payload?.sessionId,
]
.filter(Boolean)
.join(" - ")})`,
cause: err,
}),
);
Expand Down
21 changes: 9 additions & 12 deletions api.planx.uk/modules/send/createSendEvents/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,25 @@ const createSendEvents: CreateSendEventsController = async (
if (bops) {
const bopsEvent = await createScheduledEvent({
webhook: `{{HASURA_PLANX_API_URL}}/bops/${bops.localAuthority}`,
schedule_at: new Date(now.getTime() + 30 * 1000),
schedule_at: new Date(now.getTime() + 25 * 1000),
payload: bops.body,
comment: `bops_submission_${sessionId}`,
});
combinedResponse["bops"] = bopsEvent;

const isProduction = process.env.APP_ENVIRONMENT === "production";
if (!isProduction) {
const bopsV2Event = await createScheduledEvent({
webhook: `{{HASURA_PLANX_API_URL}}/bops-v2/${bops.localAuthority}`,
schedule_at: new Date(now.getTime() + 45 * 1000),
payload: bops.body,
comment: `bops_v2_submission_${sessionId}`,
});
combinedResponse["bops_v2"] = bopsV2Event;
}
const bopsV2Event = await createScheduledEvent({
webhook: `{{HASURA_PLANX_API_URL}}/bops-v2/${bops.localAuthority}`,
schedule_at: new Date(now.getTime() + 50 * 1000),
payload: bops.body,
comment: `bops_v2_submission_${sessionId}`,
});
combinedResponse["bops_v2"] = bopsV2Event;
}

if (uniform) {
const uniformEvent = await createScheduledEvent({
webhook: `{{HASURA_PLANX_API_URL}}/uniform/${uniform.localAuthority}`,
schedule_at: new Date(now.getTime() + 60 * 1000),
schedule_at: new Date(now.getTime() + 75 * 1000),
payload: uniform.body,
comment: `uniform_submission_${sessionId}`,
});
Expand Down
9 changes: 7 additions & 2 deletions api.planx.uk/modules/user/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const deleteUser: DeleteUser = async (_req, res, next) => {

export const getLoggedInUserDetails: RequestHandler<
Record<string, never>,
User
User & { jwt: string | undefined }
> = async (_req, res, next) => {
try {
const $client = getClient();
Expand All @@ -88,7 +88,12 @@ export const getLoggedInUserDetails: RequestHandler<
status: 400,
});

res.json(user);
const jwt = userContext.getStore()?.user.jwt;

res.json({
...user,
jwt: jwt,
});
} catch (error) {
next(error);
}
Expand Down
3 changes: 3 additions & 0 deletions api.planx.uk/modules/user/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ paths:
isPlatformAdmin:
type: boolean
example: true
jwt:
type: string
example: xxxxx.yyyyy.zzzzz
teams:
type: array
items:
Expand Down
18 changes: 9 additions & 9 deletions api.planx.uk/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,18 @@ useSwaggerDocs(app);

app.set("trust proxy", 1);

app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", req.headers.origin);
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept",
);
next();
});

app.use(
cors({
credentials: true,
methods: "*",
origin: process.env.EDITOR_URL_EXT,
allowedHeaders: [
"Accept",
"Authorization",
"Content-Type",
"Origin",
"X-Requested-With",
],
}),
);

Expand Down Expand Up @@ -90,6 +89,7 @@ assert(process.env.BOPS_API_TOKEN);
assert(process.env.UNIFORM_TOKEN_URL);
assert(process.env.UNIFORM_SUBMISSION_URL);

// Medway has sandbox pay only, so skip assertion as this will fail in production
["BUCKINGHAMSHIRE", "LAMBETH", "SOUTHWARK"].forEach((authority) => {
assert(process.env[`GOV_UK_PAY_TOKEN_${authority}`]);
});
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ services:
UNIFORM_CLIENT_AYLESBURY_VALE: ${UNIFORM_CLIENT_AYLESBURY_VALE}
UNIFORM_CLIENT_CHILTERN: ${UNIFORM_CLIENT_CHILTERN}
UNIFORM_CLIENT_WYCOMBE: ${UNIFORM_CLIENT_WYCOMBE}
#Medway
GOV_UK_PAY_TOKEN_MEDWAY: ${GOV_UK_PAY_TOKEN_MEDWAY}

sharedb:
restart: unless-stopped
Expand Down
Loading
Loading