diff --git a/src/export/oneApp/index.test.ts b/src/export/oneApp/index.test.ts index 6662e03b..4c40a583 100644 --- a/src/export/oneApp/index.test.ts +++ b/src/export/oneApp/index.test.ts @@ -1,7 +1,7 @@ import { X2jOptionsOptional, XMLParser, XMLValidator } from "fast-xml-parser"; import get from "lodash.get"; -import { graphQLClient } from "../../requests/graphql"; +import { getGraphQLClient } from "../../requests/graphql"; import { generateOneAppXML } from "./index"; import { mockSession } from "./mocks/session"; import { OneAppPayload } from "./model"; @@ -33,7 +33,7 @@ const parseOptions: X2jOptionsOptional = { const parser = new XMLParser(parseOptions); -const client = graphQLClient({ url: process.env.HASURA_GRAPHQL_URL! }); +const client = getGraphQLClient({ url: process.env.HASURA_GRAPHQL_URL! }); describe("generateOneAppXML", () => { test("includes template doc files when the flow has document templates", async () => { diff --git a/src/requests/graphql.ts b/src/requests/graphql.ts index 58410f99..55c59839 100644 --- a/src/requests/graphql.ts +++ b/src/requests/graphql.ts @@ -1,25 +1,66 @@ import { GraphQLClient } from "graphql-request"; -export function graphQLClient({ - url, - secret, - authorizedSession, -}: { - url: string; - secret?: string; - authorizedSession?: { +interface JWTAuth { + jwt: string; +} + +interface SessionAuth { + session: { email: string; sessionId: string; }; +} + +interface AdminAuth { + adminSecret: string; +} + +export type Auth = JWTAuth | SessionAuth | AdminAuth; + +/** + * Client which queries using user's JWT for role-bases access to Hasura + * Should be used for all requests made my authorised users + */ +const getRoleBasedClient = (url: string, auth: JWTAuth) => + new GraphQLClient(url, { headers: { authorization: `Bearer ${auth.jwt}` } }); + +/** + * Session based client which allows non-authorised users access to their session + * Used for Save & Return + */ +const getSessionClient = (url: string, auth: SessionAuth) => + new GraphQLClient(url, { + headers: { + "x-hasura-lowcal-session-id": auth.session.sessionId, + "x-hasura-lowcal-email": auth.session.email, + }, + }); + +/** + * Admin client with full access to Hasura + * Should be used with caution - only for testing / scripting + * @deprecated Consider moving to an "api" role in Hasura + */ +const getAdminClient = (url: string, auth: AdminAuth) => + new GraphQLClient(url, { + headers: { "x-hasura-admin-secret": auth.adminSecret }, + }); + +export function getGraphQLClient({ + url, + auth, +}: { + url: string; + auth?: Auth; }): GraphQLClient { - let headers = {}; - if (secret) { - headers = { "x-hasura-admin-secret": secret }; - } else if (authorizedSession) { - headers = { - "x-hasura-lowcal-session-id": authorizedSession.sessionId, - "x-hasura-lowcal-email": authorizedSession.email, - }; - } - return new GraphQLClient(url, { headers }); + // Return public client by default + if (!auth) return new GraphQLClient(url); + + if ("jwt" in auth) return getRoleBasedClient(url, auth); + if ("session" in auth) return getSessionClient(url, auth); + if ("adminSecret" in auth) return getAdminClient(url, auth); + + throw Error( + "Unable to instantiate GraphQL client - insufficient auth details provided", + ); } diff --git a/src/requests/index.test.ts b/src/requests/index.test.ts index da09ab5c..4736df70 100644 --- a/src/requests/index.test.ts +++ b/src/requests/index.test.ts @@ -11,35 +11,35 @@ describe("CoreDomainClient", () => { expect(core).toBeInstanceOf(CoreDomainClient); }); - test("instantiating a client with a secret", () => { - const core = new CoreDomainClient({ hasuraSecret: "shhh..." }); + test("instantiating a client with an admin secret", () => { + const auth = { adminSecret: "shhh..." }; + const core = new CoreDomainClient({ auth }); expect(core).toBeInstanceOf(CoreDomainClient); }); - test("instantiating a client with a URL and a secret", () => { + test("instantiating a client with a URL and an admin secret", () => { + const auth = { adminSecret: "shhh..." }; const core = new CoreDomainClient({ - hasuraSecret: "shhh...", targetURL: "https://example.com", + auth, }); expect(core).toBeInstanceOf(CoreDomainClient); }); - test("a public client can authorize a session", () => { - const core = new CoreDomainClient(); - core.authorizeSession({ email: "blah@email.com", sessionId: "1234" }); + test("instantiating a client with session details", () => { + const auth = { session: { email: "blah@email.com", sessionId: "1234" } }; + const core = new CoreDomainClient({ auth }); expect(core.client.requestConfig.headers).toEqual({ "x-hasura-lowcal-session-id": "1234", "x-hasura-lowcal-email": "blah@email.com", }); }); - test("an admin client cannot authorize a session", () => { - const core = new CoreDomainClient({ - hasuraSecret: "shhh...", - targetURL: "https://example.com", + test("instantiating a client with a JSON web token", () => { + const auth = { jwt: "ABC123" }; + const core = new CoreDomainClient({ auth }); + expect(core.client.requestConfig.headers).toEqual({ + authorization: "Bearer ABC123", }); - expect(() => - core.authorizeSession({ email: "blah@email.com", sessionId: "1234" }), - ).toThrow(); }); }); diff --git a/src/requests/index.ts b/src/requests/index.ts index d777a985..f23faf72 100644 --- a/src/requests/index.ts +++ b/src/requests/index.ts @@ -10,7 +10,7 @@ import { } from "./document-templates"; import { ExportClient } from "./export"; import { createFlow, FlowClient, publishFlow } from "./flow"; -import { graphQLClient } from "./graphql"; +import { Auth, getGraphQLClient } from "./graphql"; import { createPaymentRequest, PaymentRequestClient } from "./payment-request"; import { formatRawProjectTypes } from "./project-types"; import { @@ -22,11 +22,10 @@ import { import { createTeam, TeamClient } from "./team"; import { createUser, UserClient } from "./user"; -const defaultURL = process.env.HASURA_GRAPHQL_URL; +const defaultURL = process.env.HASURA_GRAPHQL_URL!; export class CoreDomainClient { client!: GraphQLClient; - protected type: "admin" | "public"; protected url: string; // client namespaces @@ -38,14 +37,10 @@ export class CoreDomainClient { paymentRequest!: PaymentRequestClient; export!: ExportClient; - constructor(args?: { - hasuraSecret?: string | undefined; - targetURL?: string | undefined; - }) { - const url: string = args?.targetURL ? args?.targetURL : defaultURL!; + constructor(args?: { targetURL?: string | undefined; auth?: Auth }) { + const url = args?.targetURL || defaultURL; this.url = url; - this.type = args?.hasuraSecret ? "admin" : "public"; - const client = graphQLClient({ url, secret: args?.hasuraSecret }); + const client = getGraphQLClient({ url, auth: args?.auth }); this.setClient(client); } @@ -61,19 +56,6 @@ export class CoreDomainClient { this.export = new ExportClient(this.client); } - authorizeSession(sessionDetails: { email: string; sessionId: string }) { - if (this.type === "admin") { - throw new Error( - "authorizing a session with an admin client is not allowed", - ); - } - const client = graphQLClient({ - url: this.url, - authorizedSession: sessionDetails, - }); - this.setClient(client); - } - // TODO: refactor below into client namespaces (e.g. SessionClient) // namspacing prevents this class from growing too unwieldy while still allowing for // a simple interface for callers.