From d142fe3c0856f54bb59a760771a19352ae0075ca Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Fri, 9 Feb 2024 17:15:50 -0500 Subject: [PATCH 01/19] chore: add boilerplate localauth function --- .../localAuth/localAuth.scenarios.ts | 8 +++++ api/src/functions/localAuth/localAuth.test.ts | 29 ++++++++++++++++ api/src/functions/localAuth/localAuth.ts | 33 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 api/src/functions/localAuth/localAuth.scenarios.ts create mode 100644 api/src/functions/localAuth/localAuth.test.ts create mode 100644 api/src/functions/localAuth/localAuth.ts diff --git a/api/src/functions/localAuth/localAuth.scenarios.ts b/api/src/functions/localAuth/localAuth.scenarios.ts new file mode 100644 index 00000000..d24ff747 --- /dev/null +++ b/api/src/functions/localAuth/localAuth.scenarios.ts @@ -0,0 +1,8 @@ +import type { ScenarioData } from '@redwoodjs/testing/api' + +export const standard = defineScenario({ + // Define the "fixture" to write into your test database here + // See guide: https://redwoodjs.com/docs/testing#scenarios +}) + +export type StandardScenario = ScenarioData diff --git a/api/src/functions/localAuth/localAuth.test.ts b/api/src/functions/localAuth/localAuth.test.ts new file mode 100644 index 00000000..43f8538e --- /dev/null +++ b/api/src/functions/localAuth/localAuth.test.ts @@ -0,0 +1,29 @@ +import { mockHttpEvent } from '@redwoodjs/testing/api' + +import { handler } from './localAuth' + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-functions + +describe('localAuth function', () => { + it('Should respond with 200', async () => { + const httpEvent = mockHttpEvent({ + queryStringParameters: { + id: '42', // Add parameters here + }, + }) + + const response = await handler(httpEvent, null) + const { data } = JSON.parse(response.body) + + expect(response.statusCode).toBe(200) + expect(data).toBe('localAuth function') + }) + + // You can also use scenarios to test your api functions + // See guide here: https://redwoodjs.com/docs/testing#scenarios + // + // scenario('Scenario test', async () => { + // + // }) +}) diff --git a/api/src/functions/localAuth/localAuth.ts b/api/src/functions/localAuth/localAuth.ts new file mode 100644 index 00000000..4b6c626b --- /dev/null +++ b/api/src/functions/localAuth/localAuth.ts @@ -0,0 +1,33 @@ +import type { APIGatewayEvent, Context } from 'aws-lambda' + +import { logger } from 'src/lib/logger' + +/** + * The handler function is your code that processes http request events. + * You can use return and throw to send a response or error, respectively. + * + * Important: When deployed, a custom serverless function is an open API endpoint and + * is your responsibility to secure appropriately. + * + * @see {@link https://redwoodjs.com/docs/serverless-functions#security-considerations|Serverless Function Considerations} + * in the RedwoodJS documentation for more information. + * + * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent + * @typedef { import('aws-lambda').Context } Context + * @param { APIGatewayEvent } event - an object which contains information from the invoker. + * @param { Context } context - contains information about the invocation, + * function, and execution environment. + */ +export const handler = async (event: APIGatewayEvent, _context: Context) => { + logger.info(`${event.httpMethod} ${event.path}: localAuth function`) + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: 'localAuth function', + }), + } +} From 2b524debd7f8cda63d83b7e0cb1a25a8253b9bd3 Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Tue, 13 Feb 2024 09:46:15 -0500 Subject: [PATCH 02/19] chore: add an env var for auth providers --- .env.defaults | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.env.defaults b/.env.defaults index 307bfe55..5300f0cf 100644 --- a/.env.defaults +++ b/.env.defaults @@ -39,3 +39,6 @@ DD_RUM_SESSION_REPLAY_SAMPLE_RATE=20 DD_RUM_TRACK_USER_INTERACTIONS=true DD_RUM_TRACK_RESOURCES=true DD_RUM_TRACK_LONG_TASKS=true + +# Auth provider environment variables +AUTH_PROVIDER=local From 5c8b72782a4a016b57fbe38c6a1d7829e00bd46b Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Tue, 13 Feb 2024 10:23:12 -0500 Subject: [PATCH 03/19] chore: add library needed for testing --- web/package.json | 1 + yarn.lock | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/web/package.json b/web/package.json index d39ea1cc..e66d29ac 100644 --- a/web/package.json +++ b/web/package.json @@ -28,6 +28,7 @@ }, "devDependencies": { "@redwoodjs/vite": "6.4.2", + "@testing-library/react": "^14.2.1", "@types/node": "^20.10.4", "@types/react": "18.2.39", "@types/react-dom": "18.2.17", diff --git a/yarn.lock b/yarn.lock index d1b5f6d6..268bd645 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7470,6 +7470,20 @@ __metadata: languageName: node linkType: hard +"@testing-library/react@npm:^14.2.1": + version: 14.2.1 + resolution: "@testing-library/react@npm:14.2.1" + dependencies: + "@babel/runtime": ^7.12.5 + "@testing-library/dom": ^9.0.0 + "@types/react-dom": ^18.0.0 + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 7054ae69a0e06c0777da8105fa08fac7e8dac570476a065285d7b993947acda5c948598764a203ebaac759c161c562d6712f19f5bd08be3f09a07e23baee5426 + languageName: node + linkType: hard + "@testing-library/user-event@npm:14.4.3": version: 14.4.3 resolution: "@testing-library/user-event@npm:14.4.3" @@ -24518,6 +24532,7 @@ __metadata: "@redwoodjs/vite": 6.4.2 "@redwoodjs/web": 6.4.2 "@tanstack/react-table": ^8.10.7 + "@testing-library/react": ^14.2.1 "@types/node": ^20.10.4 "@types/react": 18.2.39 "@types/react-dom": 18.2.17 From 02338f69124d7ee1c6bb520bdbaa92e125e6c24b Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Tue, 13 Feb 2024 10:36:48 -0500 Subject: [PATCH 04/19] feat: add login page that is compatible with local auth --- web/src/pages/LoginPage/LoginPage.test.tsx | 14 +++++- web/src/pages/LoginPage/LoginPage.tsx | 55 ++++++++++++++++++---- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/web/src/pages/LoginPage/LoginPage.test.tsx b/web/src/pages/LoginPage/LoginPage.test.tsx index 7ac9b837..5c56e3d7 100644 --- a/web/src/pages/LoginPage/LoginPage.test.tsx +++ b/web/src/pages/LoginPage/LoginPage.test.tsx @@ -1,3 +1,5 @@ +import { screen } from '@testing-library/react' + import { render } from '@redwoodjs/testing/web' import LoginPage from './LoginPage' @@ -6,9 +8,19 @@ import LoginPage from './LoginPage' // https://redwoodjs.com/docs/testing#testing-pages-layouts describe('LoginPage', () => { - it('renders successfully', () => { + it('renders successfully for local auth', () => { expect(() => { render() }).not.toThrow() }) + it('renders a login button for local auth', () => { + render() + expect(screen.getByRole('button', { name: 'Login' })).toBeInTheDocument() + }) + it('renders an empty page when valid auth provider is missing', () => { + process.env.AUTH_PROVIDER = 'no-auth' + render() + const submitButton = screen.queryByText('submit') + expect(submitButton).not.toBeInTheDocument() + }) }) diff --git a/web/src/pages/LoginPage/LoginPage.tsx b/web/src/pages/LoginPage/LoginPage.tsx index 2287b15b..a58e7cbd 100644 --- a/web/src/pages/LoginPage/LoginPage.tsx +++ b/web/src/pages/LoginPage/LoginPage.tsx @@ -1,19 +1,54 @@ -import { Link, routes } from '@redwoodjs/router' +import { + Form, + EmailField, + Submit, + FieldError, + Label, + SubmitHandler, +} from '@redwoodjs/forms' import { MetaTags } from '@redwoodjs/web' +import { useAuth } from 'src/auth' + +interface FormValues { + email: string +} + const LoginPage = () => { + const { logIn } = useAuth() + const onSuccess = (event: SubmitHandler) => { + logIn(event) + } + const localAuth = ( + <> +
+
+
+ +
+ +
+ + Login + +
+
+ + ) + return ( <> - -

LoginPage

-

- Find me in ./web/src/pages/LoginPage/LoginPage.tsx -

-

- My default route is named login, link to me with ` - Login` -

+
{process.env.AUTH_PROVIDER === 'local' ? localAuth : ''}
) } From 5b5ad048726f3ef9d4258cb98d482d53bb3a50d4 Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Wed, 14 Feb 2024 00:00:03 -0500 Subject: [PATCH 05/19] feat: adds implementation of /localAuth endpoint --- .../localAuth/localAuth.scenarios.ts | 25 +++++ api/src/functions/localAuth/localAuth.test.ts | 64 ++++++++++- api/src/functions/localAuth/localAuth.ts | 100 ++++++++++++++++-- 3 files changed, 176 insertions(+), 13 deletions(-) diff --git a/api/src/functions/localAuth/localAuth.scenarios.ts b/api/src/functions/localAuth/localAuth.scenarios.ts index d24ff747..e66f9115 100644 --- a/api/src/functions/localAuth/localAuth.scenarios.ts +++ b/api/src/functions/localAuth/localAuth.scenarios.ts @@ -3,6 +3,31 @@ import type { ScenarioData } from '@redwoodjs/testing/api' export const standard = defineScenario({ // Define the "fixture" to write into your test database here // See guide: https://redwoodjs.com/docs/testing#scenarios + user: { + one: { + data: { + email: 'grants-admin@usdr.dev', + name: 'Grants Admin', + role: 'USDR_ADMIN', + agency: { + create: { + name: 'Main Agency', + abbreviation: 'MAUSDR', + code: 'MAUSDR', + }, + }, + organization: { + create: { + name: 'USDR', + }, + }, + }, + include: { + agency: true, + organization: true, + }, + }, + }, }) export type StandardScenario = ScenarioData diff --git a/api/src/functions/localAuth/localAuth.test.ts b/api/src/functions/localAuth/localAuth.test.ts index 43f8538e..a6da92b9 100644 --- a/api/src/functions/localAuth/localAuth.test.ts +++ b/api/src/functions/localAuth/localAuth.test.ts @@ -1,25 +1,79 @@ import { mockHttpEvent } from '@redwoodjs/testing/api' -import { handler } from './localAuth' +import { handler, LocalAuthResponse } from './localAuth' // Improve this test with help from the Redwood Testing Doc: // https://redwoodjs.com/docs/testing#testing-functions describe('localAuth function', () => { - it('Should respond with 200', async () => { + it('GET requests should respond with 405', async () => { const httpEvent = mockHttpEvent({ + httpMethod: 'GET', queryStringParameters: { id: '42', // Add parameters here }, + headers: { + 'Content-Type': 'application/json', + }, }) const response = await handler(httpEvent, null) - const { data } = JSON.parse(response.body) + expect(response.statusCode).toBe(405) + }) - expect(response.statusCode).toBe(200) - expect(data).toBe('localAuth function') + it('POST request for invalid user should respond with 404', async () => { + const httpEvent = mockHttpEvent({ + httpMethod: 'POST', + payload: JSON.stringify({ + authMethod: 'getUserMetadata', + email: 'foo@example.com', + }), + headers: { + 'Content-Type': 'application/json', + }, + }) + + const response = await handler(httpEvent, null) + expect(response.statusCode).toBe(404) }) + scenario( + 'POST request for valid user should respond with 200 and user', + async (scenario) => { + console.log(scenario) + const httpEvent = mockHttpEvent({ + httpMethod: 'POST', + payload: JSON.stringify({ + authMethod: 'getUserMetadata', + email: 'grants-admin@usdr.dev', + }), + headers: { + 'Content-Type': 'application/json', + }, + }) + + const response: LocalAuthResponse = await handler(httpEvent, null) + console.log(response) + expect(response.statusCode).toBe(200) + expect(response.body).toEqual( + JSON.stringify({ + data: { + user: { + id: scenario.user.one.id, + email: scenario.user.one.email, + name: scenario.user.one.name, + agencyId: scenario.user.one.agency.id, + organizationId: scenario.user.one.organization.id, + createdAt: scenario.user.one.createdAt, + updatedAt: scenario.user.one.updatedAt, + role: scenario.user.one.role, + }, + }, + }) + ) + } + ) + // You can also use scenarios to test your api functions // See guide here: https://redwoodjs.com/docs/testing#scenarios // diff --git a/api/src/functions/localAuth/localAuth.ts b/api/src/functions/localAuth/localAuth.ts index 4b6c626b..19dd4cc8 100644 --- a/api/src/functions/localAuth/localAuth.ts +++ b/api/src/functions/localAuth/localAuth.ts @@ -1,5 +1,6 @@ import type { APIGatewayEvent, Context } from 'aws-lambda' +import { db } from 'src/lib/db' import { logger } from 'src/lib/logger' /** @@ -18,16 +19,99 @@ import { logger } from 'src/lib/logger' * @param { Context } context - contains information about the invocation, * function, and execution environment. */ -export const handler = async (event: APIGatewayEvent, _context: Context) => { - logger.info(`${event.httpMethod} ${event.path}: localAuth function`) +export interface LocalAuthResponse { + statusCode: number + headers: { [key: string]: string } + body?: string +} + +const defaultHeaders = { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': 'http://localhost:8910', + 'Access-Control-Allow-Headers': '*', +} + +const handleOptions = () => { return { statusCode: 200, - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - data: 'localAuth function', - }), + headers: defaultHeaders, + } +} + +const handleGetUserMetadata = async (event: APIGatewayEvent) => { + console.log('getting user metadata') + let body + if (event.body) { + body = JSON.parse(event.body) + } + if (!(body && body.email)) { + return { + statusCode: 400, + headers: defaultHeaders, + } + } + const user = await db.user.findFirst({ + where: { email: body.email }, + }) + + if (user) { + return { + statusCode: 200, + headers: defaultHeaders, + body: JSON.stringify({ + data: { user }, + }), + } + } else { + return { + statusCode: 404, + headers: defaultHeaders, + } + } +} + +const handlePost = (event: APIGatewayEvent) => { + let body + if (event.body) { + body = JSON.parse(event.body) + } + + if (!(body && body.authMethod)) { + return { + statusCode: 400, + headers: defaultHeaders, + } + } + + if (body.authMethod === 'getUserMetadata') { + return handleGetUserMetadata(event) + } else if (body.authMethod === 'login') { + // since we're not maintaining a session, we can verify the existence of the user + // through the use of getting the user metadata + return handleGetUserMetadata(event) + } else { + return { + statusCode: 400, + headers: defaultHeaders, + } + } +} + +export const handler = async ( + event: APIGatewayEvent, + _context: Context +): Promise => { + logger.info(`${event.httpMethod} ${event.path}: localAuth function`) + + if (event.httpMethod === 'OPTIONS') { + return handleOptions() + } else if (event.httpMethod === 'POST') { + return handlePost(event) + } else { + return { + statusCode: 405, + headers: defaultHeaders, + } } } From bd80d19d49e6144ac05bdb91defe95d5441881c4 Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Wed, 14 Feb 2024 00:05:41 -0500 Subject: [PATCH 06/19] feat: adds localAuth implementation for web --- web/src/auth/localAuth.ts | 118 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 web/src/auth/localAuth.ts diff --git a/web/src/auth/localAuth.ts b/web/src/auth/localAuth.ts new file mode 100644 index 00000000..f3d830b5 --- /dev/null +++ b/web/src/auth/localAuth.ts @@ -0,0 +1,118 @@ +import { SubmitHandler } from '@redwoodjs/forms' + +// If you're integrating with an auth service provider you should delete this interface. +// Instead you should import the type from their auth client sdk. +export interface AuthClient { + login: () => User + logout: () => void + signup: () => User + getToken: () => string + getUserMetadata: () => User | null +} + +export interface LoginEventInterface { + email: string +} + +export interface LocalAuthClient { + login: (event: SubmitHandler) => void + logout: () => Promise + signup: () => null + getToken: () => Promise + getUserMetadata: () => Promise +} + +interface User { + id: string + name: string + email: string + role: string + agencyId: number + organizationId: number + createdAt: string + updatedAt: string +} + +export const localAuthClient = { + logout: async () => { + localStorage.removeItem('local_auth_token') + window.location.href = '/login' + }, + getToken: async () => { + return localStorage.getItem('local_auth_token') + }, + getUserMetadata: async () => { + if (!localStorage.getItem('local_auth_token')) { + return null + } + + console.log('local: getUserMetadata') + let user: User | null = null + // Explicitly setting the url to ensure these requests are not made outside of the local environment + await fetch(`http://localhost:8911/localAuth`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + authMethod: 'getUserMetadata', + email: localStorage.getItem('local_auth_token'), + }), + }) + .then((response: Response) => { + if (response.ok) { + return response.json() + } + return Promise.reject(response) + }) + .then((jsonData: User) => { + user = jsonData + }) + .catch((error) => { + console.log(error.status) + throw new Error('Error getting user metadata') + }) + return user + }, + login: async (event) => { + await fetch(`http://localhost:8911/localAuth`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + authMethod: 'login', + email: event.email, + }), + }) + .then((response: Response) => { + if (response.ok) { + return response.json() + } + return Promise.reject(response) + }) + .catch((error) => { + console.log(error.status) + throw new Error('Error getting user metadata') + }) + + localStorage.setItem('local_auth_token', event.email) + window.location.href = '/' + }, + signup: () => { + console.log('implemented via GraphQL directly from the user creation page') + return null + }, +} + +export function createLocalAuthImplementation(client: LocalAuthClient) { + return { + type: 'local', + client, + login: async (e: SubmitHandler) => client.login(e), + logout: async () => client.logout(), + signup: async () => client.signup(), + getToken: async () => client.getToken(), + getUserMetadata: async () => client.getUserMetadata(), + } +} From 1fd212b357cb7c99ac64670d91c4812f4e9f8d52 Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Wed, 14 Feb 2024 00:06:29 -0500 Subject: [PATCH 07/19] chore: ensure instantiation of the correct auth provider based on environment --- web/src/auth.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/web/src/auth.ts b/web/src/auth.ts index eeb6664b..4fbfbb05 100644 --- a/web/src/auth.ts +++ b/web/src/auth.ts @@ -1,5 +1,10 @@ import { createAuthentication } from '@redwoodjs/auth' +import { + createLocalAuthImplementation, + localAuthClient, +} from 'src/auth/localAuth' + // If you're integrating with an auth service provider you should delete this interface. // Instead you should import the type from their auth client sdk. export interface AuthClient { @@ -53,11 +58,15 @@ const client = { } function createAuth() { - const authImplementation = createAuthImplementation(client) - - // You can pass custom provider hooks here if you need to as a second - // argument. See the Redwood framework source code for how that's used - return createAuthentication(authImplementation) + if (process.env.AUTH_PROVIDER == 'local') { + const authImplementation = createLocalAuthImplementation(localAuthClient) + return createAuthentication(authImplementation) + } else { + const authImplementation = createAuthImplementation(client) + // You can pass custom provider hooks here if you need to as a second + // argument. See the Redwood framework source code for how that's used + return createAuthentication(authImplementation) + } } // This is where most of the integration work will take place. You should keep From e3c2f51cee37247199049503cb3c2b49084f8a0d Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Wed, 14 Feb 2024 00:07:11 -0500 Subject: [PATCH 08/19] fix: login page uses the correct typing --- web/src/pages/LoginPage/LoginPage.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/web/src/pages/LoginPage/LoginPage.tsx b/web/src/pages/LoginPage/LoginPage.tsx index a58e7cbd..20be760c 100644 --- a/web/src/pages/LoginPage/LoginPage.tsx +++ b/web/src/pages/LoginPage/LoginPage.tsx @@ -9,15 +9,16 @@ import { import { MetaTags } from '@redwoodjs/web' import { useAuth } from 'src/auth' - -interface FormValues { - email: string -} +import { LoginEventInterface } from 'src/auth/localAuth' const LoginPage = () => { const { logIn } = useAuth() - const onSuccess = (event: SubmitHandler) => { - logIn(event) + const onSuccess = (event: SubmitHandler) => { + if (process.env.AUTH_PROVIDER === 'local') { + logIn(event) + } else { + logIn() + } } const localAuth = ( <> From 4eda4768461311bc46156755f7c3df3b49e547f2 Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Wed, 14 Feb 2024 00:09:14 -0500 Subject: [PATCH 09/19] feat: add localAuth implementation on the API --- api/src/lib/auth.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/api/src/lib/auth.ts b/api/src/lib/auth.ts index a2386ac5..dba7fe2b 100644 --- a/api/src/lib/auth.ts +++ b/api/src/lib/auth.ts @@ -2,10 +2,13 @@ import { SecretsManagerClient, GetSecretValueCommand, } from '@aws-sdk/client-secrets-manager' +import type { APIGatewayEvent, Context } from 'aws-lambda' import { Decoded } from '@redwoodjs/api' import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server' +import { db } from 'src/lib/db' + /** * Represents the user attributes returned by the decoding the * Authentication provider's JWT together with an optional list of roles. @@ -35,9 +38,22 @@ type RedwoodUser = Record & { roles?: string[] } * @returns RedwoodUser */ export const getCurrentUser = async ( - decoded: Decoded + decoded: Decoded, + { token, type }: { token: string; type: string }, + { event }: { event: APIGatewayEvent } ): Promise => { - console.log(decoded) + // Verify that the request is coming from the local development environment + // and is only being processed within the local environment + if ( + process.env.AUTH_PROVIDER === 'local' && + type === 'local' && + event.headers.origin === 'http://localhost:8910' + ) { + const user = await db.user.findFirst({ + where: { email: token }, + }) + return user + } return { id: 1, organizationId: 1, @@ -73,8 +89,7 @@ export const hasRole = (roles: AllowedRoles): boolean => { if (!isAuthenticated()) { return false } - - const currentUserRoles = context.currentUser?.roles + const currentUserRoles = context.currentUser?.role if (typeof roles === 'string') { if (typeof currentUserRoles === 'string') { From dec6fc6ba671e7222ab87928248aa5fe45e5e9a9 Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Wed, 14 Feb 2024 00:10:03 -0500 Subject: [PATCH 10/19] chore: adds useful seed data to be able to test localAuth functionality --- scripts/seed.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/scripts/seed.ts b/scripts/seed.ts index 23ab6bdb..0d8dcaf7 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -8,6 +8,33 @@ export default async () => { // Seeds automatically with `yarn rw prisma migrate dev` and `yarn rw prisma migrate reset` // + const organization: Prisma.OrganizationCreateArgs['data'] = { + name: 'US Digital Response', + } + const organizationRecord = await db.organization.create({ + data: organization, + }) + + const mainAgency: Prisma.AgencyCreateArgs['data'] = { + name: 'Main Agency', + abbreviation: 'MAUSDR', + code: 'MAUSDR', + organizationId: organizationRecord.id, + } + const mainAgencyRecord = await db.agency.create({ data: mainAgency }) + + const users: Prisma.UserCreateArgs['data'][] = [ + { + email: 'grants-admin@usdigitalresponse.org', + name: 'Grants Admin', + agencyId: mainAgencyRecord.id, + organizationId: organizationRecord.id, + role: 'USDR_ADMIN', + }, + ] + const userRecord = await db.user.create({ data: users[0] }) + console.log(userRecord) + const inputTemplates: Prisma.InputTemplateCreateArgs['data'][] = [ { name: 'Input Template 1', @@ -26,6 +53,13 @@ export default async () => { // Note: if using PostgreSQL, using `createMany` to insert multiple records is much faster // @see: https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#createmany + await Promise.all( + users.map(async (data: Prisma.UserCreateArgs['data']) => { + const record = await db.user.create({ data }) + console.log(record) + }) + ) + await Promise.all( // // Change to match your data model and seeding needs From 157fe01fb9c51fda35243f0ef186eeb43bd326ca Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Wed, 14 Feb 2024 00:31:32 -0500 Subject: [PATCH 11/19] chore: adds auth-provider environment variable for staging --- terraform/staging.tfvars | 3 +++ terraform/variables.tf | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/terraform/staging.tfvars b/terraform/staging.tfvars index 0522b4c8..31c68ce3 100644 --- a/terraform/staging.tfvars +++ b/terraform/staging.tfvars @@ -6,6 +6,9 @@ ssm_deployment_parameters_path_prefix = "/cpfreporter/deploy-config" log_bucket_versioning = false log_retention_in_days = 14 +// Auth Provider +auth_provider = "custom-auth" + // Datadog datadog_enabled = true datadog_draft = true diff --git a/terraform/variables.tf b/terraform/variables.tf index f025274f..5b989099 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -82,6 +82,13 @@ variable "log_retention_in_days" { default = 30 } +// Auth Provider Information +variable "auth_provider" { + description = "The authentication provider to use for the application." + type = string + default = "custom-auth" +} + // Datadog variable "datadog_enabled" { description = "Whether to enable datadog instrumentation in the current environment." From aeac354fdad9ca0f385c5ecca5d17a65eb2f4170 Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Wed, 14 Feb 2024 00:32:53 -0500 Subject: [PATCH 12/19] fix: linting error unnecessary import --- api/src/lib/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/lib/auth.ts b/api/src/lib/auth.ts index dba7fe2b..96db9f7f 100644 --- a/api/src/lib/auth.ts +++ b/api/src/lib/auth.ts @@ -2,7 +2,7 @@ import { SecretsManagerClient, GetSecretValueCommand, } from '@aws-sdk/client-secrets-manager' -import type { APIGatewayEvent, Context } from 'aws-lambda' +import type { APIGatewayEvent } from 'aws-lambda' import { Decoded } from '@redwoodjs/api' import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server' From c1404b834c4bd38aadbd5858770e9c5b39b51121 Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Wed, 14 Feb 2024 11:34:39 -0500 Subject: [PATCH 13/19] fix: terraform format --- terraform/staging.tfvars | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/staging.tfvars b/terraform/staging.tfvars index 31c68ce3..bbcaf2c0 100644 --- a/terraform/staging.tfvars +++ b/terraform/staging.tfvars @@ -7,7 +7,7 @@ log_bucket_versioning = false log_retention_in_days = 14 // Auth Provider -auth_provider = "custom-auth" +auth_provider = "custom-auth" // Datadog datadog_enabled = true From 3e090a03621c89252056b74cae2d269d56d41c0b Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Thu, 15 Feb 2024 10:45:58 -0500 Subject: [PATCH 14/19] fix: addresses comments --- terraform/staging.tfvars | 3 --- terraform/variables.tf | 7 ------- web/src/auth/localAuth.ts | 5 ++--- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/terraform/staging.tfvars b/terraform/staging.tfvars index bbcaf2c0..0522b4c8 100644 --- a/terraform/staging.tfvars +++ b/terraform/staging.tfvars @@ -6,9 +6,6 @@ ssm_deployment_parameters_path_prefix = "/cpfreporter/deploy-config" log_bucket_versioning = false log_retention_in_days = 14 -// Auth Provider -auth_provider = "custom-auth" - // Datadog datadog_enabled = true datadog_draft = true diff --git a/terraform/variables.tf b/terraform/variables.tf index 5b989099..f025274f 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -82,13 +82,6 @@ variable "log_retention_in_days" { default = 30 } -// Auth Provider Information -variable "auth_provider" { - description = "The authentication provider to use for the application." - type = string - default = "custom-auth" -} - // Datadog variable "datadog_enabled" { description = "Whether to enable datadog instrumentation in the current environment." diff --git a/web/src/auth/localAuth.ts b/web/src/auth/localAuth.ts index f3d830b5..873ed52b 100644 --- a/web/src/auth/localAuth.ts +++ b/web/src/auth/localAuth.ts @@ -48,8 +48,7 @@ export const localAuthClient = { console.log('local: getUserMetadata') let user: User | null = null - // Explicitly setting the url to ensure these requests are not made outside of the local environment - await fetch(`http://localhost:8911/localAuth`, { + await fetch(`http://${process.env.API_DOMAIN_NAME}/localAuth`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -75,7 +74,7 @@ export const localAuthClient = { return user }, login: async (event) => { - await fetch(`http://localhost:8911/localAuth`, { + await fetch(`http://${process.env.API_DOMAIN_NAME}/localAuth`, { method: 'POST', headers: { 'Content-Type': 'application/json', From cc265d947d56d44f1976989707ab88dece7cbe70 Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Thu, 15 Feb 2024 10:49:07 -0500 Subject: [PATCH 15/19] fix: addresses comments re redundant checks of auth provider and environment --- api/src/lib/auth.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/api/src/lib/auth.ts b/api/src/lib/auth.ts index 96db9f7f..9b50052f 100644 --- a/api/src/lib/auth.ts +++ b/api/src/lib/auth.ts @@ -44,11 +44,7 @@ export const getCurrentUser = async ( ): Promise => { // Verify that the request is coming from the local development environment // and is only being processed within the local environment - if ( - process.env.AUTH_PROVIDER === 'local' && - type === 'local' && - event.headers.origin === 'http://localhost:8910' - ) { + if (process.env.AUTH_PROVIDER === 'local') { const user = await db.user.findFirst({ where: { email: token }, }) From c85e1ff640a8e78c52760e40048038c388defbb9 Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Thu, 22 Feb 2024 10:07:45 -0500 Subject: [PATCH 16/19] fix: removes localAuth function --- .../localAuth/localAuth.scenarios.ts | 33 ----- api/src/functions/localAuth/localAuth.test.ts | 83 ------------- api/src/functions/localAuth/localAuth.ts | 117 ------------------ scripts/seed.ts | 18 ++- web/src/auth/localAuth.ts | 61 ++------- web/src/lib/seeds.ts | 47 +++++++ web/src/pages/LoginPage/LoginPage.tsx | 37 ++++-- 7 files changed, 104 insertions(+), 292 deletions(-) delete mode 100644 api/src/functions/localAuth/localAuth.scenarios.ts delete mode 100644 api/src/functions/localAuth/localAuth.test.ts delete mode 100644 api/src/functions/localAuth/localAuth.ts create mode 100644 web/src/lib/seeds.ts diff --git a/api/src/functions/localAuth/localAuth.scenarios.ts b/api/src/functions/localAuth/localAuth.scenarios.ts deleted file mode 100644 index e66f9115..00000000 --- a/api/src/functions/localAuth/localAuth.scenarios.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { ScenarioData } from '@redwoodjs/testing/api' - -export const standard = defineScenario({ - // Define the "fixture" to write into your test database here - // See guide: https://redwoodjs.com/docs/testing#scenarios - user: { - one: { - data: { - email: 'grants-admin@usdr.dev', - name: 'Grants Admin', - role: 'USDR_ADMIN', - agency: { - create: { - name: 'Main Agency', - abbreviation: 'MAUSDR', - code: 'MAUSDR', - }, - }, - organization: { - create: { - name: 'USDR', - }, - }, - }, - include: { - agency: true, - organization: true, - }, - }, - }, -}) - -export type StandardScenario = ScenarioData diff --git a/api/src/functions/localAuth/localAuth.test.ts b/api/src/functions/localAuth/localAuth.test.ts deleted file mode 100644 index a6da92b9..00000000 --- a/api/src/functions/localAuth/localAuth.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { mockHttpEvent } from '@redwoodjs/testing/api' - -import { handler, LocalAuthResponse } from './localAuth' - -// Improve this test with help from the Redwood Testing Doc: -// https://redwoodjs.com/docs/testing#testing-functions - -describe('localAuth function', () => { - it('GET requests should respond with 405', async () => { - const httpEvent = mockHttpEvent({ - httpMethod: 'GET', - queryStringParameters: { - id: '42', // Add parameters here - }, - headers: { - 'Content-Type': 'application/json', - }, - }) - - const response = await handler(httpEvent, null) - expect(response.statusCode).toBe(405) - }) - - it('POST request for invalid user should respond with 404', async () => { - const httpEvent = mockHttpEvent({ - httpMethod: 'POST', - payload: JSON.stringify({ - authMethod: 'getUserMetadata', - email: 'foo@example.com', - }), - headers: { - 'Content-Type': 'application/json', - }, - }) - - const response = await handler(httpEvent, null) - expect(response.statusCode).toBe(404) - }) - - scenario( - 'POST request for valid user should respond with 200 and user', - async (scenario) => { - console.log(scenario) - const httpEvent = mockHttpEvent({ - httpMethod: 'POST', - payload: JSON.stringify({ - authMethod: 'getUserMetadata', - email: 'grants-admin@usdr.dev', - }), - headers: { - 'Content-Type': 'application/json', - }, - }) - - const response: LocalAuthResponse = await handler(httpEvent, null) - console.log(response) - expect(response.statusCode).toBe(200) - expect(response.body).toEqual( - JSON.stringify({ - data: { - user: { - id: scenario.user.one.id, - email: scenario.user.one.email, - name: scenario.user.one.name, - agencyId: scenario.user.one.agency.id, - organizationId: scenario.user.one.organization.id, - createdAt: scenario.user.one.createdAt, - updatedAt: scenario.user.one.updatedAt, - role: scenario.user.one.role, - }, - }, - }) - ) - } - ) - - // You can also use scenarios to test your api functions - // See guide here: https://redwoodjs.com/docs/testing#scenarios - // - // scenario('Scenario test', async () => { - // - // }) -}) diff --git a/api/src/functions/localAuth/localAuth.ts b/api/src/functions/localAuth/localAuth.ts deleted file mode 100644 index 19dd4cc8..00000000 --- a/api/src/functions/localAuth/localAuth.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { APIGatewayEvent, Context } from 'aws-lambda' - -import { db } from 'src/lib/db' -import { logger } from 'src/lib/logger' - -/** - * The handler function is your code that processes http request events. - * You can use return and throw to send a response or error, respectively. - * - * Important: When deployed, a custom serverless function is an open API endpoint and - * is your responsibility to secure appropriately. - * - * @see {@link https://redwoodjs.com/docs/serverless-functions#security-considerations|Serverless Function Considerations} - * in the RedwoodJS documentation for more information. - * - * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent - * @typedef { import('aws-lambda').Context } Context - * @param { APIGatewayEvent } event - an object which contains information from the invoker. - * @param { Context } context - contains information about the invocation, - * function, and execution environment. - */ - -export interface LocalAuthResponse { - statusCode: number - headers: { [key: string]: string } - body?: string -} - -const defaultHeaders = { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': 'http://localhost:8910', - 'Access-Control-Allow-Headers': '*', -} - -const handleOptions = () => { - return { - statusCode: 200, - headers: defaultHeaders, - } -} - -const handleGetUserMetadata = async (event: APIGatewayEvent) => { - console.log('getting user metadata') - let body - if (event.body) { - body = JSON.parse(event.body) - } - if (!(body && body.email)) { - return { - statusCode: 400, - headers: defaultHeaders, - } - } - const user = await db.user.findFirst({ - where: { email: body.email }, - }) - - if (user) { - return { - statusCode: 200, - headers: defaultHeaders, - body: JSON.stringify({ - data: { user }, - }), - } - } else { - return { - statusCode: 404, - headers: defaultHeaders, - } - } -} - -const handlePost = (event: APIGatewayEvent) => { - let body - if (event.body) { - body = JSON.parse(event.body) - } - - if (!(body && body.authMethod)) { - return { - statusCode: 400, - headers: defaultHeaders, - } - } - - if (body.authMethod === 'getUserMetadata') { - return handleGetUserMetadata(event) - } else if (body.authMethod === 'login') { - // since we're not maintaining a session, we can verify the existence of the user - // through the use of getting the user metadata - return handleGetUserMetadata(event) - } else { - return { - statusCode: 400, - headers: defaultHeaders, - } - } -} - -export const handler = async ( - event: APIGatewayEvent, - _context: Context -): Promise => { - logger.info(`${event.httpMethod} ${event.path}: localAuth function`) - - if (event.httpMethod === 'OPTIONS') { - return handleOptions() - } else if (event.httpMethod === 'POST') { - return handlePost(event) - } else { - return { - statusCode: 405, - headers: defaultHeaders, - } - } -} diff --git a/scripts/seed.ts b/scripts/seed.ts index 0d8dcaf7..fa0db828 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -25,12 +25,26 @@ export default async () => { const users: Prisma.UserCreateArgs['data'][] = [ { - email: 'grants-admin@usdigitalresponse.org', - name: 'Grants Admin', + email: 'usdr-admin@usdr.dev', + name: 'USDR Admin', agencyId: mainAgencyRecord.id, organizationId: organizationRecord.id, role: 'USDR_ADMIN', }, + { + email: 'organization-admin@usdr.dev', + name: 'Organization Admin', + agencyId: mainAgencyRecord.id, + organizationId: organizationRecord.id, + role: 'ORGANIZATION_ADMIN', + }, + { + email: 'organization-staff@usdr.dev', + name: 'Organization Staff', + agencyId: mainAgencyRecord.id, + organizationId: organizationRecord.id, + role: 'ORGANIZATION_STAFF', + }, ] const userRecord = await db.user.create({ data: users[0] }) console.log(userRecord) diff --git a/web/src/auth/localAuth.ts b/web/src/auth/localAuth.ts index 873ed52b..1052963a 100644 --- a/web/src/auth/localAuth.ts +++ b/web/src/auth/localAuth.ts @@ -1,5 +1,7 @@ import { SubmitHandler } from '@redwoodjs/forms' +import { users } from 'src/lib/seeds' + // If you're integrating with an auth service provider you should delete this interface. // Instead you should import the type from their auth client sdk. export interface AuthClient { @@ -23,7 +25,6 @@ export interface LocalAuthClient { } interface User { - id: string name: string email: string role: string @@ -46,56 +47,20 @@ export const localAuthClient = { return null } - console.log('local: getUserMetadata') - let user: User | null = null - await fetch(`http://${process.env.API_DOMAIN_NAME}/localAuth`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - authMethod: 'getUserMetadata', - email: localStorage.getItem('local_auth_token'), - }), - }) - .then((response: Response) => { - if (response.ok) { - return response.json() - } - return Promise.reject(response) - }) - .then((jsonData: User) => { - user = jsonData - }) - .catch((error) => { - console.log(error.status) - throw new Error('Error getting user metadata') - }) + const user: User | null = users.find( + (u) => u.email === localStorage.getItem('local_auth_token') + ) + return user }, login: async (event) => { - await fetch(`http://${process.env.API_DOMAIN_NAME}/localAuth`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - authMethod: 'login', - email: event.email, - }), - }) - .then((response: Response) => { - if (response.ok) { - return response.json() - } - return Promise.reject(response) - }) - .catch((error) => { - console.log(error.status) - throw new Error('Error getting user metadata') - }) - - localStorage.setItem('local_auth_token', event.email) + let token + if (event.user === 'manual') { + token = event.email + } else { + token = event.user + } + localStorage.setItem('local_auth_token', token) window.location.href = '/' }, signup: () => { diff --git a/web/src/lib/seeds.ts b/web/src/lib/seeds.ts new file mode 100644 index 00000000..42803122 --- /dev/null +++ b/web/src/lib/seeds.ts @@ -0,0 +1,47 @@ +export const users = [ + { + email: 'usdr-admin@usdr.dev', + name: 'USDR Admin', + role: 'USDR_ADMIN', + agency: { + name: 'Main Agency', + abbreviation: 'MAUSDR', + code: 'MAUSDR', + organizationId: 1, + }, + agencyId: 1, // TO_DEPRECATE + organizationId: 1, // TO_DEPRECATE + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + email: 'organization-admin@usdr.dev', + name: 'Organization Admin', + role: 'ORGANIZATION_ADMIN', + agency: { + name: 'Main Agency', + abbreviation: 'MAUSDR', + code: 'MAUSDR', + organizationId: 1, + }, + agencyId: 1, // TO_DEPRECATE + organizationId: 1, // TO_DEPRECATE + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + email: 'organization-staff@usdr.dev', + name: 'Organization Staff', + role: 'ORGANIZATION_STAFF', + agency: { + name: 'Main Agency', + abbreviation: 'MAUSDR', + code: 'MAUSDR', + organizationId: 1, + }, + agencyId: 1, // TO_DEPRECATE + organizationId: 1, // TO_DEPRECATE + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, +] diff --git a/web/src/pages/LoginPage/LoginPage.tsx b/web/src/pages/LoginPage/LoginPage.tsx index 20be760c..22579407 100644 --- a/web/src/pages/LoginPage/LoginPage.tsx +++ b/web/src/pages/LoginPage/LoginPage.tsx @@ -4,12 +4,14 @@ import { Submit, FieldError, Label, + SelectField, SubmitHandler, } from '@redwoodjs/forms' import { MetaTags } from '@redwoodjs/web' import { useAuth } from 'src/auth' import { LoginEventInterface } from 'src/auth/localAuth' +import { users } from 'src/lib/seeds' const LoginPage = () => { const { logIn } = useAuth() @@ -25,20 +27,37 @@ const LoginPage = () => {
+ + + + + {users.map((user) => ( + + ))} + +

+

+

Either select a user above or provide an email below.

+

+ Note: If you supply a user email then ensure that it exists in the + database. +

+

- +
- +

+

Login
From 1233b88b245b6bcc16fadb1e96eb7339a959f26e Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Thu, 22 Feb 2024 13:56:38 -0500 Subject: [PATCH 17/19] fix: linting issue --- api/src/lib/auth.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/src/lib/auth.ts b/api/src/lib/auth.ts index 9b50052f..363697d0 100644 --- a/api/src/lib/auth.ts +++ b/api/src/lib/auth.ts @@ -39,8 +39,7 @@ type RedwoodUser = Record & { roles?: string[] } */ export const getCurrentUser = async ( decoded: Decoded, - { token, type }: { token: string; type: string }, - { event }: { event: APIGatewayEvent } + { token }: { token: string } ): Promise => { // Verify that the request is coming from the local development environment // and is only being processed within the local environment From f42561dfcb18cb6145f578c18f2dc13a1d99d98a Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Thu, 22 Feb 2024 13:59:52 -0500 Subject: [PATCH 18/19] fix: unused import --- api/src/lib/auth.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/lib/auth.ts b/api/src/lib/auth.ts index 363697d0..aeb9d2d8 100644 --- a/api/src/lib/auth.ts +++ b/api/src/lib/auth.ts @@ -2,7 +2,6 @@ import { SecretsManagerClient, GetSecretValueCommand, } from '@aws-sdk/client-secrets-manager' -import type { APIGatewayEvent } from 'aws-lambda' import { Decoded } from '@redwoodjs/api' import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server' From 0f35d9fa287e14c70bfcb4c59b98fb312b7a4b39 Mon Sep 17 00:00:00 2001 From: aditya Date: Fri, 1 Mar 2024 16:54:33 -0500 Subject: [PATCH 19/19] Apply suggestions from code review Co-authored-by: Tyler Hendrickson --- scripts/seed.ts | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/scripts/seed.ts b/scripts/seed.ts index 00330d96..863dbf9a 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -28,25 +28,29 @@ export default async () => { email: 'usdr-admin@usdr.dev', name: 'USDR Admin', agencyId: mainAgencyRecord.id, - organizationId: organizationRecord.id, role: 'USDR_ADMIN', }, { email: 'organization-admin@usdr.dev', name: 'Organization Admin', agencyId: mainAgencyRecord.id, - organizationId: organizationRecord.id, role: 'ORGANIZATION_ADMIN', }, { email: 'organization-staff@usdr.dev', name: 'Organization Staff', agencyId: mainAgencyRecord.id, - organizationId: organizationRecord.id, role: 'ORGANIZATION_STAFF', }, ] - const userRecord = await db.user.create({ data: users[0] }) + // Note: if using PostgreSQL, using `createMany` to insert multiple records is much faster + // @see: https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#createmany + await Promise.all( + users.map(async (data: Prisma.UserCreateArgs['data']) => { + const record = await db.user.create({ data }) + console.log(record) + }) + ) const inputTemplates: Prisma.InputTemplateCreateArgs['data'][] = [ { @@ -64,15 +68,6 @@ export default async () => { }, ] - // Note: if using PostgreSQL, using `createMany` to insert multiple records is much faster - // @see: https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#createmany - await Promise.all( - users.map(async (data: Prisma.UserCreateArgs['data']) => { - const record = await db.user.create({ data }) - console.log(record) - }) - ) - await Promise.all( // // Change to match your data model and seeding needs