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

chore(api): Typescript work and minor fixes #1382

Closed
wants to merge 4 commits into from
Closed
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
8 changes: 1 addition & 7 deletions packages/api/src/auth/decoders/auth0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import jwksClient from 'jwks-rsa'
* ^1: https://manage.auth0.com/#/rules/new
*
*/
export const verifyAuth0Token = (
export const auth0 = (
bearerToken: string
): Promise<null | Record<string, unknown>> => {
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -62,9 +62,3 @@ export const verifyAuth0Token = (
)
})
}

export const auth0 = async (
token: string
): Promise<null | Record<string, unknown>> => {
return verifyAuth0Token(token)
}
9 changes: 5 additions & 4 deletions packages/api/src/auth/decoders/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import type { GlobalContext } from 'src/globalContext'
import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda'
import type { SupportedAuthTypes } from '@redwoodjs/auth'
import type { GlobalContext } from '../../globalContext'

import { netlify } from './netlify'
import { auth0 } from './auth0'
import { netlify } from './netlify'

const noop = (token: string) => token

const typesToDecoders: Record<SupportedAuthTypes, Function> = {
const typesToDecoders = {
auth0: auth0,
netlify: netlify,
goTrue: netlify,
magicLink: noop,
firebase: noop,
supabase: noop,
custom: noop,
}
} as const

export const decodeToken = async (
type: SupportedAuthTypes,
Expand Down
11 changes: 5 additions & 6 deletions packages/api/src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import type { GlobalContext } from 'src/globalContext'
import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda'
import type { SupportedAuthTypes } from '@redwoodjs/auth'

import type { GlobalContext } from '../globalContext'

import { decodeToken } from './decoders'

// This is shared by `@redwoodjs/web`
const AUTH_PROVIDER_HEADER = 'auth-provider'

export const getAuthProviderHeader = (
event: APIGatewayProxyEvent
): SupportedAuthTypes => {
return event?.headers[AUTH_PROVIDER_HEADER] as SupportedAuthTypes
export const getAuthProviderHeader = (event: APIGatewayProxyEvent) => {
return event?.headers[AUTH_PROVIDER_HEADER] as SupportedAuthTypes | undefined
}

export interface AuthorizationHeader {
Expand All @@ -23,7 +22,7 @@ export interface AuthorizationHeader {
export const parseAuthorizationHeader = (
event: APIGatewayProxyEvent
): AuthorizationHeader => {
const [schema, token] = event.headers?.authorization?.split(' ')
const [schema, token] = (event.headers?.authorization ?? '').split(' ')
if (!schema.length || !token.length) {
throw new Error('The `Authorization` header is not valid.')
}
Expand Down
19 changes: 10 additions & 9 deletions packages/api/src/functions/authDecoder.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import mockedAPIGatewayProxyEvent from './fixtures/apiGatewayProxyEvent.fixture'
import * as auth0Decoder from './../auth/decoders/auth0'
import * as netlifyDecoder from './../auth/decoders/netlify'
Expand Down Expand Up @@ -25,7 +26,7 @@ describe('Uses correct Auth decoder', () => {
it('handles auth0', async () => {
const output = await decodeToken('auth0', MOCKED_JWT, {
event: mockedAPIGatewayProxyEvent,
context: {},
context: {} as any,
})

expect(auth0Decoder.auth0).toHaveBeenCalledWith(
Expand All @@ -41,7 +42,7 @@ describe('Uses correct Auth decoder', () => {
it('decodes goTrue with netlify decoder', async () => {
const output = await decodeToken('goTrue', MOCKED_JWT, {
event: mockedAPIGatewayProxyEvent,
context: {},
context: {} as any,
})

expect(netlifyDecoder.netlify).toHaveBeenCalledWith(
Expand All @@ -57,7 +58,7 @@ describe('Uses correct Auth decoder', () => {
it('decodes netlify with netlify decoder', async () => {
const output = await decodeToken('netlify', MOCKED_JWT, {
event: mockedAPIGatewayProxyEvent,
context: {},
context: {} as any,
})

expect(netlifyDecoder.netlify).toHaveBeenCalledWith(
Expand All @@ -73,7 +74,7 @@ describe('Uses correct Auth decoder', () => {
it('returns undecoded token for custom', async () => {
const output = await decodeToken('custom', MOCKED_JWT, {
event: mockedAPIGatewayProxyEvent,
context: {},
context: {} as any,
})

expect(output).toEqual(MOCKED_JWT)
Expand All @@ -82,7 +83,7 @@ describe('Uses correct Auth decoder', () => {
it('returns undecoded token for magicLink', async () => {
const output = await decodeToken('magicLink', MOCKED_JWT, {
event: mockedAPIGatewayProxyEvent,
context: {},
context: {} as any,
})

expect(output).toEqual(MOCKED_JWT)
Expand All @@ -91,7 +92,7 @@ describe('Uses correct Auth decoder', () => {
it('returns undecoded token for firebase', async () => {
const output = await decodeToken('firebase', MOCKED_JWT, {
event: mockedAPIGatewayProxyEvent,
context: {},
context: {} as any,
})

expect(output).toEqual(MOCKED_JWT)
Expand All @@ -100,16 +101,16 @@ describe('Uses correct Auth decoder', () => {
it('returns undecoded token for supabase', async () => {
const output = await decodeToken('supabase', MOCKED_JWT, {
event: mockedAPIGatewayProxyEvent,
context: {},
context: {} as any,
})

expect(output).toEqual(MOCKED_JWT)
})

it('returns undecoded token for unknown values', async () => {
const output = await decodeToken('SOMETHING_ELSE!', MOCKED_JWT, {
const output = await decodeToken('SOMETHING_ELSE!' as any, MOCKED_JWT, {
event: mockedAPIGatewayProxyEvent,
context: {},
context: {} as any,
})

expect(output).toEqual(MOCKED_JWT)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { APIGatewayProxyEvent } from 'aws-lambda'

export const mockedAPIGatewayProxyEvent: APIGatewayProxyEvent = {
export default {
body: 'MOCKED_BODY',
headers: {},
multiValueHeaders: {},
Expand Down Expand Up @@ -41,6 +41,4 @@ export const mockedAPIGatewayProxyEvent: APIGatewayProxyEvent = {
resourcePath: 'MOCKED_RESOURCE_PATH',
},
resource: 'MOCKED_RESOURCE',
}

export default mockedAPIGatewayProxyEvent
} as APIGatewayProxyEvent
46 changes: 24 additions & 22 deletions packages/api/src/functions/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda'
import type { Config, CreateHandlerOptions } from 'apollo-server-lambda'
import type { Context, ContextFunction } from 'apollo-server-core'
import type { GlobalContext } from 'src/globalContext'
import type { AuthContextPayload } from 'src/auth'
import type { GlobalContext } from '../globalContext'
import type { AuthContextPayload } from '../auth'
import type { APIGatewayProxyCallback } from 'aws-lambda'
import { ApolloServer } from 'apollo-server-lambda'
import { getAuthenticationContext } from 'src/auth'
import { setContext } from 'src/globalContext'
import { getAuthenticationContext } from '../auth'
import { setContext } from '../globalContext'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, why is "../" preferred over "src"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #1381 it'll be addressed there, if fixable.


export type GetCurrentUser = (
decoded: AuthContextPayload[0],
raw: AuthContextPayload[1]
) => Promise<null | Record<string, unknown> | string>

type ContextProps = {
event: APIGatewayProxyEvent
context: GlobalContext & LambdaContext
}

/**
* We use Apollo Server's `context` option as an entry point to construct our
* own global context.
Expand All @@ -22,17 +28,11 @@ export type GetCurrentUser = (
* dataloader instances, and anything else that should be taken into account when
* resolving the query.
*/
export const createContextHandler = (
userContext?: Context | ContextFunction,
export function createContextHandler<T>(
userContext?: Context<T> | ContextFunction<ContextProps, T>,
getCurrentUser?: GetCurrentUser
) => {
return async ({
event,
context,
}: {
event: APIGatewayProxyEvent
context: GlobalContext & LambdaContext
}) => {
) {
return async ({ event, context }: ContextProps) => {
// Prevent the Serverless function from waiting for all resources (db connections)
// to be released before returning a reponse.
context.callbackWaitsForEmptyEventLoop = false
Expand All @@ -46,11 +46,13 @@ export const createContextHandler = (
: authContext
}

let customUserContext = userContext
if (typeof userContext === 'function') {
// if userContext is a function, run that and return just the result
customUserContext = await userContext({ event, context })
}
const customUserContext =
typeof userContext === 'function'
? await (userContext as ContextFunction<ContextProps, T>)({
event,
context,
})
: userContext

// Sets the **global** context object, which can be imported with:
// import { context } from '@redwoodjs/api'
Expand Down Expand Up @@ -86,14 +88,14 @@ interface GraphQLHandlerOptions extends Config {
* export const handler = createGraphQLHandler({ schema, context, getCurrentUser })
* ```
*/
export const createGraphQLHandler = ({
export function createGraphQLHandler({
context,
getCurrentUser,
onException,
cors,
onHealthCheck,
...options
}: GraphQLHandlerOptions = {}) => {
}: GraphQLHandlerOptions = {}) {
const isDevEnv = process.env.NODE_ENV !== 'production'
const handler = new ApolloServer({
// Turn off playground, introspection and debug in production.
Expand Down Expand Up @@ -124,7 +126,7 @@ export const createGraphQLHandler = ({
return (
event: APIGatewayProxyEvent,
context: LambdaContext,
callback: any
callback: APIGatewayProxyCallback
): void => {
try {
handler(event, context, callback)
Expand Down
32 changes: 18 additions & 14 deletions packages/api/src/parseJWT.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
const appMetadata = (token: {
decoded: { [index: string]: Record<string, unknown> }
type MetaDataBase = { roles?: string[]; authorization?: { roles?: string[] } }

function appMetadata(token: {
decoded: { [index: string]: any }
namespace?: string
}): any => {
}): MetaDataBase {
const claim = token.namespace
? `${token.namespace}/app_metadata`
: 'app_metadata'
return token.decoded?.[claim] || {}
}

const roles = (token: {
decoded: { [index: string]: Record<string, unknown> }
namespace?: string
}): any => {
const metadata = appMetadata(token)
function roles(
token: {
decoded: { [index: string]: any }
},
metadata: MetaDataBase
): string[] {
return (
token.decoded?.roles ||
((token.decoded?.roles as unknown) as string[]) ||
metadata?.roles ||
metadata.authorization?.roles ||
[]
)
}

export const parseJWT = (token: {
decoded: { [index: string]: Record<string, unknown> }
export function parseJWT(token: {
decoded: { [index: string]: any }
namespace?: string
}): any => {
}) {
const appMetaData = appMetadata(token)
return {
appMetadata: appMetadata(token),
roles: roles(token),
appMetadata: appMetaData,
roles: roles(token, appMetaData),
}
}
19 changes: 12 additions & 7 deletions packages/cli/src/commands/generate/auth/__tests__/auth.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
global.__dirname = __dirname

import {
waitFor,
} from '@testing-library/react'
import { waitFor } from '@testing-library/react'

jest.mock('fs')
jest.mock('src/lib', () => ({
Expand All @@ -17,28 +15,35 @@ import chalk from 'chalk'

import * as auth from '../auth'

const EXISTING_AUTH_PROVIDER_ERROR = 'Existing auth provider found.\nUse --force to override existing provider.';
const EXISTING_AUTH_PROVIDER_ERROR =
'Existing auth provider found.\nUse --force to override existing provider.'

test(`no error thrown when auth provider not found`, async () => {
// Mock process.exit to make sure CLI quites
const cSpy = jest.spyOn(console, 'log').mockImplementation(() => {})

auth.handler({ provider: 'netlify' })
await waitFor(() => expect(console.log).toHaveBeenCalledTimes(1))
expect(console.log).not.toHaveBeenCalledWith(chalk.bold.red(EXISTING_AUTH_PROVIDER_ERROR))
expect(console.log).not.toHaveBeenCalledWith(
chalk.bold.red(EXISTING_AUTH_PROVIDER_ERROR)
)

// Restore mocks
cSpy.mockRestore()
})

test('throws an error if auth provider exists', async () => {
// Mock process.exit to make sure CLI quites
const fsSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => `import { AuthProvider } from '@redwoodjs/auth'`)
const fsSpy = jest
.spyOn(fs, 'readFileSync')
.mockImplementation(() => `import { AuthProvider } from '@redwoodjs/auth'`)
const cSpy = jest.spyOn(console, 'log').mockImplementation(() => {})

auth.handler({ provider: 'netlify' })
await waitFor(() => expect(console.log).toHaveBeenCalledTimes(1))
expect(console.log).toHaveBeenCalledWith(chalk.bold.red(EXISTING_AUTH_PROVIDER_ERROR))
expect(console.log).toHaveBeenCalledWith(
chalk.bold.red(EXISTING_AUTH_PROVIDER_ERROR)
)

// Restore mocks
fsSpy.mockRestore()
Expand Down