Skip to content

Commit

Permalink
Merge branch 'main' into jh/validate-add-team-editor
Browse files Browse the repository at this point in the history
  • Loading branch information
jamdelion authored Aug 26, 2024
2 parents 55c558b + 469aa2b commit 1c5651f
Show file tree
Hide file tree
Showing 188 changed files with 6,144 additions and 3,286 deletions.
32 changes: 16 additions & 16 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -206,29 +206,29 @@ jobs:
- run: pnpm build
if: steps.cache-react-build-assets.outputs.cache-hit != 'true'
env:
REACT_APP_AIRBRAKE_PROJECT_ID: ${{ secrets.AIRBRAKE_PROJECT_ID }}
REACT_APP_AIRBRAKE_PROJECT_KEY: ${{ secrets.AIRBRAKE_PROJECT_KEY }}
REACT_APP_API_URL: https://api.${{ env.FULL_DOMAIN }}
REACT_APP_HASURA_URL: https://hasura.${{ env.FULL_DOMAIN }}/v1/graphql
REACT_APP_HASURA_WEBSOCKET: wss://hasura.${{ env.FULL_DOMAIN }}/v1/graphql
REACT_APP_SHAREDB_URL: wss://sharedb.${{ env.FULL_DOMAIN }}
VITE_APP_AIRBRAKE_PROJECT_ID: ${{ secrets.AIRBRAKE_PROJECT_ID }}
VITE_APP_AIRBRAKE_PROJECT_KEY: ${{ secrets.AIRBRAKE_PROJECT_KEY }}
VITE_APP_API_URL: https://api.${{ env.FULL_DOMAIN }}
VITE_APP_HASURA_URL: https://hasura.${{ env.FULL_DOMAIN }}/v1/graphql
VITE_APP_HASURA_WEBSOCKET: wss://hasura.${{ env.FULL_DOMAIN }}/v1/graphql
VITE_APP_SHAREDB_URL: wss://sharedb.${{ env.FULL_DOMAIN }}
# needed because there's no API to change google's allowed OAuth URLs
REACT_APP_GOOGLE_OAUTH_OVERRIDE: https://api.editor.planx.dev
REACT_APP_ENV: pizza
VITE_APP_GOOGLE_OAUTH_OVERRIDE: https://api.editor.planx.dev
VITE_APP_ENV: pizza
working-directory: ${{ env.EDITOR_DIRECTORY }}
- run: pnpm build-storybook
if: steps.cache-react-build-assets.outputs.cache-hit != 'true'
working-directory: ${{ env.EDITOR_DIRECTORY }}
env:
# same env as above, if it's job.env it can't access existing env.[variable]
REACT_APP_AIRBRAKE_PROJECT_ID: ${{ secrets.AIRBRAKE_PROJECT_ID }}
REACT_APP_AIRBRAKE_PROJECT_KEY: ${{ secrets.AIRBRAKE_PROJECT_KEY }}
REACT_APP_API_URL: https://api.${{ env.FULL_DOMAIN }}
REACT_APP_HASURA_URL: https://hasura.${{ env.FULL_DOMAIN }}/v1/graphql
REACT_APP_HASURA_WEBSOCKET: wss://hasura.${{ env.FULL_DOMAIN }}/v1/graphql
REACT_APP_SHAREDB_URL: wss://sharedb.${{ env.FULL_DOMAIN }}
REACT_APP_GOOGLE_OAUTH_OVERRIDE: https://api.editor.planx.dev
REACT_APP_ENV: pizza
VITE_APP_AIRBRAKE_PROJECT_ID: ${{ secrets.AIRBRAKE_PROJECT_ID }}
VITE_APP_AIRBRAKE_PROJECT_KEY: ${{ secrets.AIRBRAKE_PROJECT_KEY }}
VITE_APP_API_URL: https://api.${{ env.FULL_DOMAIN }}
VITE_APP_HASURA_URL: https://hasura.${{ env.FULL_DOMAIN }}/v1/graphql
VITE_APP_HASURA_WEBSOCKET: wss://hasura.${{ env.FULL_DOMAIN }}/v1/graphql
VITE_APP_SHAREDB_URL: wss://sharedb.${{ env.FULL_DOMAIN }}
VITE_APP_GOOGLE_OAUTH_OVERRIDE: https://api.editor.planx.dev
VITE_APP_ENV: pizza

pulumi_preview:
name: Run Pulumi Preview
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/push-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ jobs:
- run: pnpm build
working-directory: editor.planx.uk
env:
REACT_APP_API_URL: https://api.editor.planx.dev
REACT_APP_HASURA_URL: https://hasura.editor.planx.dev/v1/graphql
REACT_APP_HASURA_WEBSOCKET: wss://hasura.editor.planx.dev/v1/graphql
REACT_APP_SHAREDB_URL: wss://sharedb.editor.planx.dev
REACT_APP_AIRBRAKE_PROJECT_ID: ${{ secrets.AIRBRAKE_PROJECT_ID }}
REACT_APP_AIRBRAKE_PROJECT_KEY: ${{ secrets.AIRBRAKE_PROJECT_KEY }}
REACT_APP_ENV: staging
VITE_APP_API_URL: https://api.editor.planx.dev
VITE_APP_HASURA_URL: https://hasura.editor.planx.dev/v1/graphql
VITE_APP_HASURA_WEBSOCKET: wss://hasura.editor.planx.dev/v1/graphql
VITE_APP_SHAREDB_URL: wss://sharedb.editor.planx.dev
VITE_APP_AIRBRAKE_PROJECT_ID: ${{ secrets.AIRBRAKE_PROJECT_ID }}
VITE_APP_AIRBRAKE_PROJECT_KEY: ${{ secrets.AIRBRAKE_PROJECT_KEY }}
VITE_APP_ENV: staging
- name: Upload Build Artifact
uses: actions/upload-artifact@v4
with:
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/push-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ jobs:
- run: pnpm build
working-directory: editor.planx.uk
env:
REACT_APP_API_URL: https://api.editor.planx.uk
REACT_APP_HASURA_URL: https://hasura.editor.planx.uk/v1/graphql
REACT_APP_HASURA_WEBSOCKET: wss://hasura.editor.planx.uk/v1/graphql
REACT_APP_SHAREDB_URL: wss://sharedb.editor.planx.uk
REACT_APP_AIRBRAKE_PROJECT_ID: ${{ secrets.AIRBRAKE_PROJECT_ID }}
REACT_APP_AIRBRAKE_PROJECT_KEY: ${{ secrets.AIRBRAKE_PROJECT_KEY }}
REACT_APP_ENV: production
VITE_APP_API_URL: https://api.editor.planx.uk
VITE_APP_HASURA_URL: https://hasura.editor.planx.uk/v1/graphql
VITE_APP_HASURA_WEBSOCKET: wss://hasura.editor.planx.uk/v1/graphql
VITE_APP_SHAREDB_URL: wss://sharedb.editor.planx.uk
VITE_APP_AIRBRAKE_PROJECT_ID: ${{ secrets.AIRBRAKE_PROJECT_ID }}
VITE_APP_AIRBRAKE_PROJECT_KEY: ${{ secrets.AIRBRAKE_PROJECT_KEY }}
VITE_APP_ENV: production
- name: Upload Build Artifact
uses: actions/upload-artifact@v4
with:
Expand Down
11 changes: 9 additions & 2 deletions api.planx.uk/modules/send/idox/nexus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface RawIdoxNexusAuthResponse {

interface IdoxNexusAuthResponse {
token: string;
organisations: Record<string, string>;
organisations: Record<number, string>;
authorities: string[];
}

Expand Down Expand Up @@ -90,7 +90,14 @@ export async function sendToIdoxNexus(
// Switch to `team_integrations`-based approach later
const orgIds = Object.keys(organisations);
const randomOrgId = orgIds[Math.floor(Math.random() * orgIds.length)];
const randomOrg = organisations[randomOrgId];
// Dev auth endpoint returns "Multi-organisation" as every org name, but Idox expects these (OSL typo is "correct")
const orgNamesMap: Record<string, string> = {
"210051": "Open System Labs 001",
"210052": "Open System Labs 002",
"210053": "Open System Labs 003",
"210054": "Open System Labs 004",
};
const randomOrg = orgNamesMap[randomOrgId];

// Create a zip containing only the ODP Schema JSON
// Do this BEFORE creating a submission in order to throw any validation errors early
Expand Down
46 changes: 46 additions & 0 deletions api.planx.uk/modules/slack/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import SlackNotify from "slack-notify";
import { z } from "zod";
import { ServerError } from "../../errors/index.js";
import { ValidatedRequestHandler } from "../../shared/middleware/validate.js";

interface SendSlackNotificationResponse {
message: string;
}

export const slackNotificationSchema = z.object({
body: z.object({
message: z.string(),
}),
});

export type SendSlackNotificationController = ValidatedRequestHandler<
typeof slackNotificationSchema,
SendSlackNotificationResponse
>;

export const sendSlackNotificationController: SendSlackNotificationController =
async (_req, res, next) => {
const { message } = res.locals.parsedReq.body;

const isProduction = process.env.APP_ENVIRONMENT === "production";
if (!isProduction) {
return res.status(200).send({
message: `Staging environment, skipping Slack notification. Message "${message}"`,
});
}

try {
const slack = SlackNotify(process.env.SLACK_WEBHOOK_URL!);
await slack.send(message);

return res.status(200).send({
message: `Sent Slack notification. Message "${message}"`,
});
} catch (error) {
return next(
new ServerError({
message: `Failed to send Slack notification. Error ${error}`,
}),
);
}
};
37 changes: 37 additions & 0 deletions api.planx.uk/modules/slack/docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
openapi: 3.1.0
info:
title: Plan✕ API
version: 0.1.0
components:
schemas:
SendSlackNotificationDirect:
type: object
properties:
message: string
responses:
SendSlackNotificationDirect:
content:
application/json:
schema:
type: object
properties:
message: string
paths:
/send-slack-notification:
post:
summary: Send a Slack notification
description: Allows authenticated users to trigger a Slack notification when they update settings in the Planx Editor
tags: ["misc"]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SendSlackNotificationDirect"
responses:
"200":
$ref: "#/components/responses/SendSlackNotificationDirect"
"500":
$ref: "#/components/responses/ErrorMessage"
97 changes: 97 additions & 0 deletions api.planx.uk/modules/slack/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import SlackNotify from "slack-notify";
import supertest from "supertest";

import app from "../../server.js";
import { authHeader } from "../../tests/mockJWT.js";

const validAuth = authHeader({ role: "teamEditor" });
const validBody = {
message: ":emoji: *council-a/find-out-if* is now online",
};
const invalidBody = {
wrong: "message",
};

const mockSend = jest.fn();
jest.mock<typeof SlackNotify>("slack-notify", () =>
jest.fn().mockImplementation(() => {
return { send: mockSend };
}),
);

afterEach(jest.clearAllMocks);

it("returns an error if authorization headers are not set", async () => {
await supertest(app)
.post("/send-slack-notifcation")
.send(validBody)
.expect(404);
});

it("returns an error if the user does not have the correct role", async () => {
await supertest(app)
.post("/send-slack-notification")
.send(validBody)
.set(authHeader({ role: "teamViewer" }))
.expect(403);
});

it("returns an error if a message isn't provided in the request body", async () => {
await supertest(app)
.post("/send-slack-notification")
.send(invalidBody)
.set(validAuth)
.expect(400)
.then((res) => {
expect(res.body).toHaveProperty("issues");
expect(res.body).toHaveProperty("name", "ZodError");
});
});

it("skips the staging environment", async () => {
process.env.APP_ENVIRONMENT = "staging";
await supertest(app)
.post("/send-slack-notification")
.send(validBody)
.set(validAuth)
.expect(200)
.then((res) => {
expect(res.body).toHaveProperty("message");
expect(res.body.message).toMatch(
/Staging environment, skipping Slack notification. Message/,
);
});
});

it("successfully sends a Slack message", async () => {
process.env.APP_ENVIRONMENT = "production";
await supertest(app)
.post("/send-slack-notification")
.send(validBody)
.set(validAuth)
.expect(200)
.then((res) => {
expect(SlackNotify).toHaveBeenCalledWith(process.env.SLACK_WEBHOOK_URL);
expect(mockSend).toHaveBeenCalledTimes(1);
expect(res.body.message).toEqual(
'Sent Slack notification. Message ":emoji: *council-a/find-out-if* is now online"',
);
});
});

it("returns an error when Slack fails", async () => {
process.env.APP_ENVIRONMENT = "production";
mockSend.mockRejectedValue("Fail!");

await supertest(app)
.post("/send-slack-notification")
.send(validBody)
.set(validAuth)
.expect(500)
.then((res) => {
expect(mockSend).toHaveBeenCalledTimes(1);
expect(res.body.error).toMatch(
/Failed to send Slack notification. Error/,
);
});
});
18 changes: 18 additions & 0 deletions api.planx.uk/modules/slack/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Router } from "express";
import { validate } from "../../shared/middleware/validate.js";
import { useTeamEditorAuth } from "../auth/middleware.js";
import {
sendSlackNotificationController,
slackNotificationSchema,
} from "./controller.js";

const router = Router();

router.post(
"/send-slack-notification",
useTeamEditorAuth,
validate(slackNotificationSchema),
sendSlackNotificationController,
);

export default router;
46 changes: 24 additions & 22 deletions api.planx.uk/server.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,40 @@
import "isomorphic-fetch";
import "express-async-errors";
import bodyParser from "body-parser";
import { Role } from "@opensystemslab/planx-core/types";
import assert from "assert";
import bodyParser from "body-parser";
import cookieParser from "cookie-parser";
import cookieSession from "cookie-session";
import cors, { CorsOptions } from "cors";
import express, { ErrorRequestHandler } from "express";
import noir from "pino-noir";
import "express-async-errors";
import pinoLogger from "express-pino-logger";
import helmet from "helmet";
import { Server } from "http";
import "isomorphic-fetch";
import passport from "passport";
import helmet from "helmet";
import { ServerError } from "./errors/index.js";
import noir from "pino-noir";
import airbrake from "./airbrake.js";
import { apiLimiter } from "./rateLimit.js";
import { registerSessionStubs } from "./session.js";
import { googleStrategy } from "./modules/auth/strategy/google.js";
import authRoutes from "./modules/auth/routes.js";
import teamRoutes from "./modules/team/routes.js";
import miscRoutes from "./modules/misc/routes.js";
import userRoutes from "./modules/user/routes.js";
import webhookRoutes from "./modules/webhooks/routes.js";
import analyticsRoutes from "./modules/analytics/routes.js";
import { useSwaggerDocs } from "./docs/index.js";
import { ServerError } from "./errors/index.js";
import adminRoutes from "./modules/admin/routes.js";
import flowRoutes from "./modules/flows/routes.js";
import ordnanceSurveyRoutes from "./modules/ordnanceSurvey/routes.js";
import saveAndReturnRoutes from "./modules/saveAndReturn/routes.js";
import sendEmailRoutes from "./modules/sendEmail/routes.js";
import analyticsRoutes from "./modules/analytics/routes.js";
import authRoutes from "./modules/auth/routes.js";
import { googleStrategy } from "./modules/auth/strategy/google.js";
import fileRoutes from "./modules/file/routes.js";
import flowRoutes from "./modules/flows/routes.js";
import gisRoutes from "./modules/gis/routes.js";
import miscRoutes from "./modules/misc/routes.js";
import ordnanceSurveyRoutes from "./modules/ordnanceSurvey/routes.js";
import payRoutes from "./modules/pay/routes.js";
import saveAndReturnRoutes from "./modules/saveAndReturn/routes.js";
import sendRoutes from "./modules/send/routes.js";
import sendEmailRoutes from "./modules/sendEmail/routes.js";
import slackRoutes from "./modules/slack/routes.js";
import teamRoutes from "./modules/team/routes.js";
import testRoutes from "./modules/test/routes.js";
import { useSwaggerDocs } from "./docs/index.js";
import { Role } from "@opensystemslab/planx-core/types";
import userRoutes from "./modules/user/routes.js";
import webhookRoutes from "./modules/webhooks/routes.js";
import { apiLimiter } from "./rateLimit.js";
import { registerSessionStubs } from "./session.js";

const app = express();

Expand Down Expand Up @@ -147,10 +148,11 @@ app.use(payRoutes);
app.use(saveAndReturnRoutes);
app.use(sendEmailRoutes);
app.use(sendRoutes);
app.use(slackRoutes);
app.use(teamRoutes);
app.use(testRoutes);
app.use(userRoutes);
app.use(webhookRoutes);
app.use(testRoutes);

const errorHandler: ErrorRequestHandler = (errorObject, _req, res, _next) => {
const { status = 500, message = "Something went wrong" } = (() => {
Expand Down
2 changes: 1 addition & 1 deletion doc/how-to/how-to-add-a-list-component-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The ideal maintainers of these schemas are still the services team though, rathe

2. **GitHub** - In the `.ts` file, ensure the schema has this basic structure:
```ts
import { Schema } from "@planx/components/List/model";
import { Schema } from "@planx/component/shared/Schema/model";

export const YourSchemasName: Schema = {
type: "Title (singular if no max, plural if max = 1)",
Expand Down
Loading

0 comments on commit 1c5651f

Please sign in to comment.