Skip to content

Commit

Permalink
feat: Improve /me endpoint
Browse files Browse the repository at this point in the history
- Add tests
- Move to module, use YAML docs
- Return role details
  • Loading branch information
DafyddLlyr committed Sep 19, 2023
1 parent ea011d1 commit 7a49fb4
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 67 deletions.
14 changes: 13 additions & 1 deletion api.planx.uk/modules/auth/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,23 @@ export const useRoleAuth: UseRoleAuth =
});
};

// Convenience methods
// Convenience methods for role-based access
export const useTeamViewerAuth = useRoleAuth([
"teamViewer",
"teamEditor",
"platformAdmin",
]);
export const useTeamEditorAuth = useRoleAuth(["teamEditor", "platformAdmin"]);
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, () => (
req?.user?.sub
? next()
: next({
status: 401,
message: "No authorization token was found",
})
));
1 change: 1 addition & 0 deletions api.planx.uk/modules/auth/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const buildJWT = async (email: string): Promise<string | undefined> => {

const data = {
sub: user.id.toString(),
email,
"https://hasura.io/jwt/claims": generateHasuraClaimsForUser(user),
};

Expand Down
20 changes: 20 additions & 0 deletions api.planx.uk/modules/misc/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { RequestHandler } from "express";
import { getClient } from "../../client";
import { userContext } from "../auth/middleware";
import { ServerError } from "../../errors";

export const getLoggedInUserDetails: RequestHandler = async (_req, res, next) => {
try {
const $client = getClient();

const email = userContext.getStore()?.user.email
if (!email) throw new ServerError({ message: "User email missing from request", status: 400 })

const user = await $client.user.getByEmail(email);
if (!user) throw new ServerError({ message: `Unable to locate user with email ${email}`, status: 400 })

res.json(user);
} catch (error) {
next(error)
}
};
54 changes: 54 additions & 0 deletions api.planx.uk/modules/misc/docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
openapi: 3.1.0
info:
title: Plan✕ API
version: 0.1.0
tags:
- name: misc
description: Miscellaneous
paths:
/me:
get:
summary: Get information about currently logged in user
tags:
- misc
security:
- userJWT: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
id:
type: integer
format: int32
example: 123
firstName:
type: string
example: Albert
lastName:
type: string
example: Einstein
email:
type: string
example: [email protected]
isPlatformAdmin:
type: boolean
example: true
teams:
type: array
items:
type: object
properties:
teamId:
type: integer
format: int32
example: 123
role:
type: string
enum: ["teamEditor", "teamViewer"]
example: "teamEditor"
'401':
$ref: '#/components/responses/Unauthorised'
103 changes: 103 additions & 0 deletions api.planx.uk/modules/misc/routes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import supertest from "supertest";
import app from "../../server";
import { authHeader, getJWT } from "../../tests/mockJWT";
import { userContext } from "../auth/middleware";

const getStoreMock = jest.spyOn(userContext, "getStore");

const mockGetByEmail = jest.fn().mockResolvedValue({
id: 36,
firstName: "Albert",
lastName: "Einstein",
email: "[email protected]",
isPlatformAdmin: true,
teams: [
{
teamId: 1,
role: "teamEditor"
},
{
teamId: 24,
role: "teamEditor"
}
]
});

jest.mock("@opensystemslab/planx-core", () => {
return {
CoreDomainClient: jest.fn().mockImplementation(() => ({
user: {
getByEmail: () => mockGetByEmail(),
}
}))
}
});

describe("/me endpoint", () => {
beforeEach(() => {
getStoreMock.mockReturnValue({
user: {
sub: "123",
email: "[email protected]",
jwt: getJWT({ role: "teamEditor" })
}
});
});

it("returns an error if authorization headers are not set", async () => {
await supertest(app)
.get("/me")
.expect(401)
.then((res) => {
expect(res.body).toEqual({
error: "No authorization token was found",
});
});
});

it("returns an error for invalid user context", async () => {
getStoreMock.mockReturnValue({
user: {
sub: "123",
email: undefined,
jwt: getJWT({ role: "teamEditor" })
}
});

await supertest(app)
.get("/me")
.set(authHeader({ role: "teamEditor" }))
.expect(400)
.then((res) => {
expect(res.body).toEqual({
error: "User email missing from request",
});
});
});

it("returns an error for an invalid email address", async () => {
mockGetByEmail.mockResolvedValueOnce(null)

await supertest(app)
.get("/me")
.set(authHeader({ role: "teamEditor" }))
.expect(400)
.then((res) => {
expect(res.body).toEqual({
error: "Unable to locate user with email [email protected]",
});
});
});


it("returns user details for a logged in user", async () => {
await supertest(app)
.get("/me")
.set(authHeader({ role: "teamEditor" }))
.expect(200)
.then((res) => {
expect(res.body).toHaveProperty("email", "[email protected]");
expect(res.body.teams).toHaveLength(2);
});
});
});
9 changes: 9 additions & 0 deletions api.planx.uk/modules/misc/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Router } from "express";
import { useLoginAuth } from "../auth/middleware";
import { getLoggedInUserDetails } from "./controller";

const router = Router();

router.get("/me", useLoginAuth, getLoggedInUserDetails);

export default router;
69 changes: 3 additions & 66 deletions api.planx.uk/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import { getSessionSummary } from "./admin/session/summary";
import { googleStrategy } from "./modules/auth/strategy/google";
import authRoutes from "./modules/auth/routes";
import teamRoutes from "./modules/team/routes";
import miscRoutes from "./modules/misc/routes";
import { useSwaggerDocs } from "./docs";
import { Role } from "@opensystemslab/planx-core/types";

Expand Down Expand Up @@ -192,6 +193,7 @@ app.use(passport.session());
app.use(urlencoded({ extended: true }));

app.use(authRoutes);
app.use(miscRoutes);
app.use("/team", teamRoutes);

app.use("/gis", router);
Expand All @@ -211,72 +213,6 @@ app.get("/hasura", async function (_req, res, next) {
}
});

/**
* @swagger
* /me:
* get:
* summary: Get information about currently logged in user
* tags:
* - misc
* security:
* - userJWT: []
* responses:
* '401':
* $ref: '#/components/responses/Unauthorised'
* '200':
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* format: int32
* example: 123
* first_name:
* type: string
* example: Albert
* last_name:
* type: string
* example: Einstein
* email:
* type: string
* example: [email protected]
* created_at:
* type: string
* example: 2020-08-11T11:28:38.237493+00:00
* updated_at:
* type: string
* example: 2023-08-11T11:28:38.237493+00:00
*/
app.get("/me", usePlatformAdminAuth, async function (req, res, next) {
try {
const user = await adminClient.request(
gql`
query ($id: Int!) {
users_by_pk(id: $id) {
id
first_name
last_name
email
created_at
updated_at
}
}
`,
{ id: req.user?.sub },
);

if (!user.users_by_pk)
next({ status: 404, message: `User (${req.user?.sub}) not found` });

res.json(user.users_by_pk);
} catch (err) {
next(err);
}
});

app.get("/gis", (_req, _res, next) => {
next({
status: 400,
Expand Down Expand Up @@ -616,6 +552,7 @@ declare global {
interface User {
jwt: string;
sub?: string;
email?: string;
"https://hasura.io/jwt/claims"?: {
"x-hasura-allowed-roles": Role[];
};
Expand Down
1 change: 1 addition & 0 deletions api.planx.uk/tests/mockJWT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { sign } from "jsonwebtoken";
function getJWT({ role }: { role: Role }) {
const data = {
sub: "123",
email: "[email protected]",
"https://hasura.io/jwt/claims": {
"x-hasura-allowed-roles": [role],
"x-hasura-default-role": role,
Expand Down

0 comments on commit 7a49fb4

Please sign in to comment.