Skip to content

Commit

Permalink
Merge branch 'main' into jh/experimental-readme-page
Browse files Browse the repository at this point in the history
  • Loading branch information
jamdelion committed Jan 28, 2025
2 parents 78c6dd3 + 71d8127 commit 4de0d58
Show file tree
Hide file tree
Showing 90 changed files with 2,124 additions and 304 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ FILE_API_KEY_EPSOM_EWELL=👻
FILE_API_KEY_MEDWAY=👻
FILE_API_KEY_GATESHEAD=👻
FILE_API_KEY_DONCASTER=👻
FILE_API_KEY_GLOUCESTER=👻
FILE_API_KEY_TEWKESBURY=👻


# Editor
EDITOR_URL_EXT=http://localhost:3000
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ hasura.planx.uk/.env.test
/playwright-report/
/playwright/.cache/
api.planx.uk/tmp/

# Python
.python-version
__pycache__
.venv/
.ruff_cache/

# Ignore certificate files
**/*.chain
Expand Down
4 changes: 3 additions & 1 deletion api.planx.uk/.env.test.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ FILE_API_KEY_EPSOM_EWELL=👻
FILE_API_KEY_MEDWAY=👻
FILE_API_KEY_GATESHEAD=👻
FILE_API_KEY_DONCASTER=👻
FILE_API_KEY_GLOUCESTER=👻
FILE_API_KEY_TEWKESBURY=👻

# Editor
EDITOR_URL_EXT=example.com
EDITOR_URL_EXT=https://www.example.com

# Hasura
HASURA_GRAPHQL_URL=http://hasura:8080/v1/graphql
Expand Down
4 changes: 2 additions & 2 deletions api.planx.uk/client/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CoreDomainClient } from "@opensystemslab/planx-core";
import { getClient } from "./index.js";
import { userContext } from "../modules/auth/middleware.js";
import { getJWT } from "../tests/mockJWT.js";
import { getTestJWT } from "../tests/mockJWT.js";

test("getClient() throws an error if a store is not set", () => {
expect(() => getClient()).toThrow();
Expand All @@ -12,7 +12,7 @@ test("getClient() returns a client if store is set", () => {
getStoreMock.mockReturnValue({
user: {
sub: "123",
jwt: getJWT({ role: "teamEditor" }),
jwt: getTestJWT({ role: "teamEditor" }),
},
});

Expand Down
53 changes: 53 additions & 0 deletions api.planx.uk/errors/requestHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { ErrorRequestHandler } from "express";
import { ServerError } from "./serverError.js";
import airbrake from "../airbrake.js";

/**
* Check for expired JWTs, redirect to /logout if found
*/
export const expiredJWTHandler: ErrorRequestHandler = (
errorObject,
_res,
res,
next,
) => {
const isJWTExpiryError =
errorObject?.name === "UnauthorizedError" &&
errorObject?.message === "jwt expired";

if (!isJWTExpiryError) return next(errorObject);

const logoutPage = new URL("/logout", process.env.EDITOR_URL_EXT!).toString();
return res.redirect(logoutPage);
};

/**
* Fallback error handler
* Must be final Express middleware
*/
export const errorHandler: ErrorRequestHandler = (
errorObject,
_req,
res,
_next,
) => {
const { status = 500, message = "Something went wrong" } = (() => {
if (
airbrake &&
(errorObject instanceof Error || errorObject instanceof ServerError)
) {
airbrake.notify(errorObject);
return {
...errorObject,
message: errorObject.message.concat(", this error has been logged"),
};
} else {
console.log(errorObject);
return errorObject;
}
})();

res.status(status).send({
error: message,
});
};
16 changes: 14 additions & 2 deletions api.planx.uk/modules/auth/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ export const useFilePermission: RequestHandler = (req, _res, next): void => {
req.headers["api-key"] as string,
process.env.FILE_API_KEY_EPSOM_EWELL!,
) ||
isEqual(
req.headers["api-key"] as string,
process.env.FILE_API_KEY_GLOUCESTER!,
) ||
isEqual(
req.headers["api-key"] as string,
process.env.FILE_API_KEY_TEWKESBURY!,
) ||
isEqual(
req.headers["api-key"] as string,
process.env.FILE_API_KEY_DONCASTER!,
Expand Down Expand Up @@ -209,7 +217,9 @@ type UseRoleAuth = (authRoles: Role[]) => RequestHandler;
*/
export const useRoleAuth: UseRoleAuth =
(authRoles) => async (req, res, next) => {
useJWT(req, res, () => {
useJWT(req, res, (err) => {
if (err) return next(err);

if (!req?.user)
return next({
status: 401,
Expand Down Expand Up @@ -273,7 +283,9 @@ export const usePlatformAdminAuth = useRoleAuth(["platformAdmin"]);
* Allow any logged in user to access route, without checking roles
*/
export const useLoginAuth: RequestHandler = (req, res, next) =>
useJWT(req, res, () => {
useJWT(req, res, (err) => {
if (err) return next(err);

if (req?.user?.sub) {
userContext.run(
{
Expand Down
7 changes: 4 additions & 3 deletions api.planx.uk/modules/auth/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { $api } from "../../client/index.js";
import type { User, Role } from "@opensystemslab/planx-core/types";
import type { HasuraClaims, JWTData } from "./types.js";

export const buildJWT = async (email: string): Promise<string | undefined> => {
export const buildUserJWT = async (
email: string,
): Promise<string | undefined> => {
const user = await $api.user.getByEmail(email);
if (!user) return;

Expand All @@ -16,7 +18,7 @@ export const buildJWT = async (email: string): Promise<string | undefined> => {
"https://hasura.io/jwt/claims": generateHasuraClaimsForUser(user),
};

return jwt.sign(data, process.env.JWT_SECRET!);
return jwt.sign(data, process.env.JWT_SECRET!, { expiresIn: "24h" });
};

export const buildJWTForAPIRole = () =>
Expand All @@ -28,7 +30,6 @@ export const buildJWTForAPIRole = () =>
},
},
process.env.JWT_SECRET!,
{ expiresIn: "24h" },
);

const generateHasuraClaimsForUser = (user: User): HasuraClaims => ({
Expand Down
4 changes: 2 additions & 2 deletions api.planx.uk/modules/auth/strategy/google.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { buildJWT } from "../service.js";
import { buildUserJWT } from "../service.js";

export const googleStrategy = new GoogleStrategy(
{
Expand All @@ -11,7 +11,7 @@ export const googleStrategy = new GoogleStrategy(
const { email } = profile._json;
if (!email) throw Error("Unable to authenticate without email");

const jwt = await buildJWT(email);
const jwt = await buildUserJWT(email);

if (!jwt) {
return done({
Expand Down
4 changes: 2 additions & 2 deletions api.planx.uk/modules/auth/strategy/microsoft-oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
StrategyVerifyCallbackReq,
} from "openid-client";
import { Strategy } from "openid-client";
import { buildJWT } from "../service.js";
import { buildUserJWT } from "../service.js";

export const MICROSOFT_OPENID_CONFIG_URL =
"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";
Expand Down Expand Up @@ -70,7 +70,7 @@ const verifyCallback: StrategyVerifyCallbackReq<Express.User> = async (
return done(new Error("Unable to authenticate without email"));
}

const jwt = await buildJWT(email);
const jwt = await buildUserJWT(email);
if (!jwt) {
return done({
status: 404,
Expand Down
7 changes: 6 additions & 1 deletion api.planx.uk/modules/file/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getFileFromS3 } from "./service/getFile.js";
import { z } from "zod";
import type { ValidatedRequestHandler } from "../../shared/middleware/validate.js";
import { ServerError } from "../../errors/index.js";
import { validateExtension } from "./middleware/useFileUpload.js";

assert(process.env.AWS_S3_BUCKET);
assert(process.env.AWS_S3_REGION);
Expand All @@ -18,7 +19,11 @@ interface UploadFileResponse {

export const uploadFileSchema = z.object({
body: z.object({
filename: z.string().trim().min(1),
filename: z
.string()
.trim()
.min(1)
.refine(validateExtension, { message: "Unsupported file type" }),
}),
});

Expand Down
Loading

0 comments on commit 4de0d58

Please sign in to comment.