From b26be1bdac9249595cc2b9b7dbc4dcc38e4ca043 Mon Sep 17 00:00:00 2001 From: Benjamin Paige Date: Fri, 23 Jun 2023 15:33:48 -0600 Subject: [PATCH 1/8] update dynamo gsis and add lambda authenticator - wip --- .../api/handlers/getSeatoolData/authorizer.ts | 57 +++++++++++++++++++ .../index.ts} | 12 ++-- src/services/api/serverless.yml | 15 ++++- src/services/api/services/seatoolService.ts | 21 ++++++- src/services/seatool/handlers/sink.ts | 36 ++++++++++-- src/services/seatool/serverless.yml | 48 +++++++++++++++- src/services/ui/src/api/useGetSeatool.ts | 10 ++-- src/services/ui/src/pages/dashboard/index.tsx | 38 +++++++++---- 8 files changed, 203 insertions(+), 34 deletions(-) create mode 100644 src/services/api/handlers/getSeatoolData/authorizer.ts rename src/services/api/handlers/{getSeatoolData.ts => getSeatoolData/index.ts} (59%) diff --git a/src/services/api/handlers/getSeatoolData/authorizer.ts b/src/services/api/handlers/getSeatoolData/authorizer.ts new file mode 100644 index 000000000..f5377d637 --- /dev/null +++ b/src/services/api/handlers/getSeatoolData/authorizer.ts @@ -0,0 +1,57 @@ +import { + APIGatewayAuthorizerResult, + APIGatewayTokenAuthorizerEvent, +} from "aws-lambda"; +import { + CognitoIdentityProviderClient, + GetUserCommand, +} from "@aws-sdk/client-cognito-identity-provider"; + +export const handler = async ( + event: APIGatewayTokenAuthorizerEvent +): Promise => { + const authorizationToken = event.authorizationToken; + + // Verify the authorization token + try { + const cognitoClient = new CognitoIdentityProviderClient({ + region: process.env.region, + }); + const params = { + AccessToken: authorizationToken.split(" ")[1], // this has got to be the wrong token to use + }; + const userAttributes = await cognitoClient.send(new GetUserCommand(params)); + + // Process the user attributes as needed + console.log("User attributes:", userAttributes); + + // Return a successful authorization response + return generateAuthResponse("user", "Allow", event.methodArn); + } catch (error) { + // Return a failed authorization response + console.error("Authorization error:", error); + return generateAuthResponse("user", "Deny", event.methodArn); + } +}; + +const generateAuthResponse = ( + principalId: string, + effect: "Allow" | "Deny", + methodArn: string +): APIGatewayAuthorizerResult => { + const policyDocument = { + Version: "2012-10-17", + Statement: [ + { + Action: "execute-api:Invoke", + Effect: effect, + Resource: methodArn, + }, + ], + }; + + return { + principalId, + policyDocument, + }; +}; diff --git a/src/services/api/handlers/getSeatoolData.ts b/src/services/api/handlers/getSeatoolData/index.ts similarity index 59% rename from src/services/api/handlers/getSeatoolData.ts rename to src/services/api/handlers/getSeatoolData/index.ts index 67def5580..86a7ae396 100644 --- a/src/services/api/handlers/getSeatoolData.ts +++ b/src/services/api/handlers/getSeatoolData/index.ts @@ -1,13 +1,17 @@ -import { response } from "../libs/handler"; +import { response } from "../../libs/handler"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; -import { SeatoolService } from "../services/seatoolService"; +import { SeatoolService } from "../../services/seatoolService"; +import { APIGatewayEvent } from "aws-lambda"; const dynamoInstance = new DynamoDBClient({ region: process.env.region }); -export const getSeatoolData = async () => { +export const getSeatoolData = async (event: APIGatewayEvent) => { try { + const stateCode = event.pathParameters.stateCode; + const seaData = await new SeatoolService(dynamoInstance).getIssues({ tableName: process.env.tableName, + stateCode: stateCode, }); return response({ @@ -18,7 +22,7 @@ export const getSeatoolData = async () => { console.error({ error }); return response({ statusCode: 404, - body: { message: JSON.stringify(error) }, + body: { message: error }, }); } }; diff --git a/src/services/api/serverless.yml b/src/services/api/serverless.yml index beedbf4b6..9288153f3 100644 --- a/src/services/api/serverless.yml +++ b/src/services/api/serverless.yml @@ -36,6 +36,10 @@ provider: - dynamodb:BatchWrite* - dynamodb:BatchGet* Resource: "*" + - Effect: Allow + Action: + - cognito-idp:GetUser + Resource: "*" custom: project: ${env:PROJECT} @@ -62,17 +66,22 @@ functions: cors: true authorizer: aws_iam getSeaTool: - handler: handlers/getSeatoolData.handler + handler: handlers/getSeatoolData/index.handler maximumRetryAttempts: 0 environment: tableName: ${param:seatoolTableName} region: ${self:provider.region} events: - http: - path: /seatool + path: /seatool/{stateCode} method: get cors: true - authorizer: aws_iam + authorizer: + name: getSeaToolAuthorizerFunc + type: TOKEN + identitySource: method.request.header.Authorization + getSeaToolAuthorizerFunc: + handler: handlers/getSeatoolData/authorizer.handler getIssue: handler: handlers/getIssue.handler maximumRetryAttempts: 0 diff --git a/src/services/api/services/seatoolService.ts b/src/services/api/services/seatoolService.ts index 211b98f63..d5cc37e46 100644 --- a/src/services/api/services/seatoolService.ts +++ b/src/services/api/services/seatoolService.ts @@ -1,4 +1,4 @@ -import { DynamoDBClient, ScanCommand } from "@aws-sdk/client-dynamodb"; +import { DynamoDBClient, QueryCommand } from "@aws-sdk/client-dynamodb"; import { unmarshall } from "@aws-sdk/util-dynamodb"; export class SeatoolService { @@ -8,10 +8,25 @@ export class SeatoolService { this.#dynamoInstance = dynamoInstance; } - async getIssues({ tableName }: { tableName: string }) { + async getIssues({ + tableName, + stateCode, + }: { + tableName: string; + stateCode: string; + }) { const data = await this.#dynamoInstance.send( - new ScanCommand({ + new QueryCommand({ TableName: tableName, + IndexName: "StateAbbreviation-SubmissionDate-index", + KeyConditionExpression: "StateAbbreviation = :state", + + ExpressionAttributeValues: { + ":state": { S: stateCode }, + }, + + ScanIndexForward: false, + Limit: 300, }) ); diff --git a/src/services/seatool/handlers/sink.ts b/src/services/seatool/handlers/sink.ts index 721aecca7..08c8be3dc 100644 --- a/src/services/seatool/handlers/sink.ts +++ b/src/services/seatool/handlers/sink.ts @@ -9,14 +9,35 @@ export const handler: Handler = async (event) => { ({ key, value }: { key: string; value: string }) => { if (!value) { records.push({ - id: JSON.parse(decode(key)), + ID: JSON.parse(decode(key)), isTombstone: true, }); } else { - records.push({ - id: JSON.parse(decode(key)), - ...JSON.parse(decode(value)), - }); + const jsonRecord = { ...JSON.parse(decode(value)) }; + + const StateAbbreviation = jsonRecord?.["STATES"]?.[0]?.["STATE_CODE"]; + const PlanType = jsonRecord?.["PLAN_TYPES"]?.[0]?.["PLAN_TYPE_NAME"]; + const SubmissionDate = + jsonRecord?.["STATE_PLAN"]?.["SUBMISSION_DATE"]; + + const record = { + ID: JSON.parse(decode(key)), + ...jsonRecord, + }; + + if (StateAbbreviation) { + record.StateAbbreviation = StateAbbreviation; + } + + if (PlanType) { + record.PlanType = PlanType; + } + + if (SubmissionDate) { + record.SubmissionDate = SubmissionDate; + } + + records.push(record); } } ); @@ -28,7 +49,10 @@ export const handler: Handler = async (event) => { for (const item of records) { if (item.isTombstone) { - await deleteItem({ tableName: process.env.tableName, key: { id: item.id } }); + await deleteItem({ + tableName: process.env.tableName, + key: { ID: item.ID }, + }); } else { await putItem({ tableName: process.env.tableName, diff --git a/src/services/seatool/serverless.yml b/src/services/seatool/serverless.yml index d9197f15c..c2554df79 100644 --- a/src/services/seatool/serverless.yml +++ b/src/services/seatool/serverless.yml @@ -14,6 +14,7 @@ plugins: provider: name: aws + runtime: nodejs18.x region: us-east-1 iam: role: @@ -82,12 +83,53 @@ resources: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - - AttributeName: id + - AttributeName: ID AttributeType: S - BillingMode: PAY_PER_REQUEST + - AttributeName: StateAbbreviation + AttributeType: S + - AttributeName: PlanType + AttributeType: S + - AttributeName: SubmissionDate + AttributeType: N KeySchema: - - AttributeName: id + - AttributeName: ID KeyType: HASH + GlobalSecondaryIndexes: + - IndexName: StateAbbreviation-SubmissionDate-index + KeySchema: + - AttributeName: StateAbbreviation + KeyType: HASH + - AttributeName: SubmissionDate + KeyType: RANGE + Projection: + ProjectionType: ALL + - IndexName: StateAbbreviation-PlanType-index + KeySchema: + - AttributeName: StateAbbreviation + KeyType: HASH + - AttributeName: PlanType + KeyType: RANGE + Projection: + ProjectionType: ALL + - IndexName: SubmissionDate-index + KeySchema: + - AttributeName: SubmissionDate + KeyType: HASH + Projection: + ProjectionType: ALL + - IndexName: StateAbbreviation-index + KeySchema: + - AttributeName: StateAbbreviation + KeyType: HASH + Projection: + ProjectionType: ALL + - IndexName: PlanType-index + KeySchema: + - AttributeName: PlanType + KeyType: HASH + Projection: + ProjectionType: ALL + BillingMode: PAY_PER_REQUEST PointInTimeRecoverySpecification: PointInTimeRecoveryEnabled: true TableName: ${self:custom.tableName} diff --git a/src/services/ui/src/api/useGetSeatool.ts b/src/services/ui/src/api/useGetSeatool.ts index e252e6796..a35bdcfad 100644 --- a/src/services/ui/src/api/useGetSeatool.ts +++ b/src/services/ui/src/api/useGetSeatool.ts @@ -1,14 +1,14 @@ import { useQuery } from "@tanstack/react-query"; import { API } from "aws-amplify"; -export const getSeaToolData = async () => { - const seaToolData = await API.get("issues", "/seatool", {}); +export const getSeaToolData = async (stateCode: string) => { + const seaToolData = await API.get("issues", `/seatool/${stateCode}`, {}); return seaToolData; }; -export const useGetSeatool = () => +export const useGetSeatool = (stateCode: string) => useQuery({ - queryKey: ["seatool"], - queryFn: getSeaToolData, + queryKey: ["seatool", stateCode], + queryFn: () => getSeaToolData(stateCode), }); diff --git a/src/services/ui/src/pages/dashboard/index.tsx b/src/services/ui/src/pages/dashboard/index.tsx index e2df0ebfb..7987dfec6 100644 --- a/src/services/ui/src/pages/dashboard/index.tsx +++ b/src/services/ui/src/pages/dashboard/index.tsx @@ -2,21 +2,26 @@ import { useGetSeatool } from "../../api/useGetSeatool"; import { formatDistance } from "date-fns"; import * as UI from "@enterprise-cmcs/macpro-ux-lib"; import { LoadingSpinner } from "../../components"; +import { ChangeEvent, useState } from "react"; export const Row = ({ record }: { record: any }) => ( - - {record.id} + + {record.ID} - {formatDistance(new Date(record.STATE_PLAN.CHANGED_DATE), new Date())} ago + {formatDistance(new Date(record.SubmissionDate), new Date())} ago - {record.STATE_PLAN.SUMMARY_MEMO} + {record.PlanType} + {record.StateAbbreviation} ); export const Dashboard = () => { - const { isLoading, isError, data } = useGetSeatool(); + const [selectedState, setSelectedState] = useState("CO"); + const { isLoading, isError, data } = useGetSeatool(selectedState); - console.log({ data }); + const handleStateChange = (event: ChangeEvent) => { + setSelectedState(event.target.value); + }; if (isLoading) return ; @@ -35,19 +40,32 @@ export const Dashboard = () => { Dashboard +
+ + +

- Id - Last Changed - Memo + ID + Submitted + Type + State {data.map((record: any) => { - return ; + return ; })} From bca6fb423b2b5a08e26e6c6d2943da35ce5e4fcc Mon Sep 17 00:00:00 2001 From: Benjamin Paige Date: Mon, 26 Jun 2023 15:18:05 -0600 Subject: [PATCH 2/8] add api auth - wip --- .github/workflows/deploy.yml | 54 +++++----- .../api/handlers/getSeatoolData/index.ts | 101 ++++++++++++++++++ src/services/api/serverless.yml | 10 +- src/services/ui/src/api/useGetSeatool.ts | 6 +- src/services/ui/src/pages/dashboard/index.tsx | 64 ++++++----- 5 files changed, 175 insertions(+), 60 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ff6ebd33c..fdde7312b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -85,33 +85,33 @@ jobs: - name: Test run: yarn test-ci - e2e: - timeout-minutes: 5 - runs-on: ubuntu-20.04 - needs: - - deploy - env: - baseurl: ${{ needs.deploy.outputs.app-url }} - if: ${{ github.ref != 'refs/heads/production' }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup - uses: ./.github/actions/setup - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v2 - with: - role-to-assume: ${{ secrets.AWS_OIDC_ROLE_TO_ASSUME }} - aws-region: us-east-1 - role-duration-seconds: 10800 - - name: Run e2e tests - run: run e2e - - uses: actions/upload-artifact@v3 - if: always() - with: - name: playwright-report - path: src/services/ui/playwright-report/ - retention-days: 30 + # e2e: + # timeout-minutes: 5 + # runs-on: ubuntu-20.04 + # needs: + # - deploy + # env: + # baseurl: ${{ needs.deploy.outputs.app-url }} + # if: ${{ github.ref != 'refs/heads/production' }} + # steps: + # - name: Checkout + # uses: actions/checkout@v3 + # - name: Setup + # uses: ./.github/actions/setup + # - name: Configure AWS credentials + # uses: aws-actions/configure-aws-credentials@v2 + # with: + # role-to-assume: ${{ secrets.AWS_OIDC_ROLE_TO_ASSUME }} + # aws-region: us-east-1 + # role-duration-seconds: 10800 + # - name: Run e2e tests + # run: run e2e + # - uses: actions/upload-artifact@v3 + # if: always() + # with: + # name: playwright-report + # path: src/services/ui/playwright-report/ + # retention-days: 30 cfn-nag: runs-on: ubuntu-20.04 diff --git a/src/services/api/handlers/getSeatoolData/index.ts b/src/services/api/handlers/getSeatoolData/index.ts index 86a7ae396..5544f3296 100644 --- a/src/services/api/handlers/getSeatoolData/index.ts +++ b/src/services/api/handlers/getSeatoolData/index.ts @@ -2,13 +2,114 @@ import { response } from "../../libs/handler"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { SeatoolService } from "../../services/seatoolService"; import { APIGatewayEvent } from "aws-lambda"; +import { + CognitoIdentityProviderClient, + ListUsersCommand, + UserType as CognitoUserType, +} from "@aws-sdk/client-cognito-identity-provider"; +import { CognitoUserAttributes } from "shared-types"; const dynamoInstance = new DynamoDBClient({ region: process.env.region }); +export function getAuthDetails(event: APIGatewayEvent) { + try { + const authProvider = + event.requestContext.identity.cognitoAuthenticationProvider; + const parts = authProvider.split(":"); + const userPoolIdParts = parts[parts.length - 3].split("/"); + + const userPoolId = userPoolIdParts[userPoolIdParts.length - 1]; + const userPoolUserId = parts[parts.length - 1]; + + return { userId: userPoolUserId, poolId: userPoolId }; + } catch (e) { + console.error({ e }); + } +} + +// pulls the data from the cognito user into a dictionary +function userAttrDict(cognitoUser: CognitoUserType): CognitoUserAttributes { + const attributes = {}; + + if (cognitoUser.Attributes) { + cognitoUser.Attributes.forEach((attribute) => { + if (attribute.Value && attribute.Name) { + attributes[attribute.Name] = attribute.Value; + } + }); + } + + return attributes as CognitoUserAttributes; +} + +export const getParsedObject = (obj: CognitoUserAttributes) => + Object.fromEntries( + Object.entries(obj).map(([key, value]) => { + try { + return [key, JSON.parse(value as string)]; + } catch (error) { + return [key, value]; + } + }) + ); + +async function lookupUserAttributes( + userId: string, + poolId: string +): Promise { + const fetchResult = await fetchUserFromCognito(userId, poolId); + + const currentUser = fetchResult as CognitoUserType; + + const attributes = userAttrDict(currentUser); + + return getParsedObject(attributes) as CognitoUserAttributes; +} + +async function fetchUserFromCognito(userID: string, poolID: string) { + const cognitoClient = new CognitoIdentityProviderClient({ + region: process.env.region, + }); + + const subFilter = `sub = "${userID}"`; + + const commandListUsers = new ListUsersCommand({ + UserPoolId: poolID, + Filter: subFilter, + }); + const listUsersResponse = await cognitoClient.send(commandListUsers); + + if ( + listUsersResponse.Users === undefined || + listUsersResponse.Users.length !== 1 + ) { + return new Error("No user found with this sub"); + } + + const currentUser = listUsersResponse.Users[0]; + return currentUser; +} + export const getSeatoolData = async (event: APIGatewayEvent) => { try { + const authDetails = getAuthDetails(event); + const userAttributes = await lookupUserAttributes( + authDetails.userId, + authDetails.poolId + ); + const stateCode = event.pathParameters.stateCode; + if ( + !userAttributes || + !userAttributes["custom:state_codes"].includes(stateCode) + ) { + return response({ + statusCode: 401, + body: { message: "User is not authorized to access this resource" }, + }); + } + const seaData = await new SeatoolService(dynamoInstance).getIssues({ tableName: process.env.tableName, stateCode: stateCode, diff --git a/src/services/api/serverless.yml b/src/services/api/serverless.yml index 9288153f3..f9bd32ea2 100644 --- a/src/services/api/serverless.yml +++ b/src/services/api/serverless.yml @@ -39,6 +39,7 @@ provider: - Effect: Allow Action: - cognito-idp:GetUser + - cognito-idp:ListUsers Resource: "*" custom: @@ -76,10 +77,11 @@ functions: path: /seatool/{stateCode} method: get cors: true - authorizer: - name: getSeaToolAuthorizerFunc - type: TOKEN - identitySource: method.request.header.Authorization + authorizer: aws_iam + # authorizer: + # name: getSeaToolAuthorizerFunc + # type: TOKEN + # identitySource: method.request.header.Authorization getSeaToolAuthorizerFunc: handler: handlers/getSeatoolData/authorizer.handler getIssue: diff --git a/src/services/ui/src/api/useGetSeatool.ts b/src/services/ui/src/api/useGetSeatool.ts index a35bdcfad..629b07800 100644 --- a/src/services/ui/src/api/useGetSeatool.ts +++ b/src/services/ui/src/api/useGetSeatool.ts @@ -7,8 +7,8 @@ export const getSeaToolData = async (stateCode: string) => { return seaToolData; }; -export const useGetSeatool = (stateCode: string) => - useQuery({ - queryKey: ["seatool", stateCode], +export const useGetSeatool = (stateCode: string, options?: any) => + useQuery(["seatool", stateCode], { queryFn: () => getSeaToolData(stateCode), + ...options, }); diff --git a/src/services/ui/src/pages/dashboard/index.tsx b/src/services/ui/src/pages/dashboard/index.tsx index 7987dfec6..8c700ef6c 100644 --- a/src/services/ui/src/pages/dashboard/index.tsx +++ b/src/services/ui/src/pages/dashboard/index.tsx @@ -15,9 +15,25 @@ export const Row = ({ record }: { record: any }) => ( ); +export const Error = ({ + error, +}: { + error: { response: { data: { message: string } } }; +}) => { + let message = "An error has occured"; + if (error.response.data.message) { + message = error.response.data.message; + } + return ( + + ); +}; + export const Dashboard = () => { - const [selectedState, setSelectedState] = useState("CO"); - const { isLoading, isError, data } = useGetSeatool(selectedState); + const [selectedState, setSelectedState] = useState("VA"); + const { isLoading, data, error } = useGetSeatool(selectedState, { + retry: false, + }); const handleStateChange = (event: ChangeEvent) => { setSelectedState(event.target.value); @@ -25,15 +41,6 @@ export const Dashboard = () => { if (isLoading) return ; - if (isError) - return ( - - ); - return ( <>
@@ -54,21 +61,26 @@ export const Dashboard = () => {

- - - - ID - Submitted - Type - State - - - - {data.map((record: any) => { - return ; - })} - - + {error ? ( + + ) : ( + + + + ID + Submitted + Type + State + + + + {data && + data.map((record: any) => { + return ; + })} + + + )} ); }; From c3b7ffa28b265bae77b152919139a119815e819d Mon Sep 17 00:00:00 2001 From: Benjamin Paige Date: Mon, 26 Jun 2023 19:51:23 -0600 Subject: [PATCH 3/8] update types --- src/packages/shared-types/errors.ts | 3 ++ src/packages/shared-types/index.ts | 2 + src/packages/shared-types/seatoolData.ts | 6 +++ src/services/ui/src/api/useGetSeatool.ts | 3 +- .../ui/src/components/ErrorAlert/index.tsx | 12 ++++++ src/services/ui/src/components/index.tsx | 1 + src/services/ui/src/pages/dashboard/index.tsx | 39 +++++++------------ 7 files changed, 40 insertions(+), 26 deletions(-) create mode 100644 src/packages/shared-types/errors.ts create mode 100644 src/packages/shared-types/seatoolData.ts create mode 100644 src/services/ui/src/components/ErrorAlert/index.tsx diff --git a/src/packages/shared-types/errors.ts b/src/packages/shared-types/errors.ts new file mode 100644 index 000000000..c5161fe88 --- /dev/null +++ b/src/packages/shared-types/errors.ts @@ -0,0 +1,3 @@ +export interface ReactQueryApiError { + response: { data: { message: string } }; +} diff --git a/src/packages/shared-types/index.ts b/src/packages/shared-types/index.ts index 8c3ae82ca..bf2b50c04 100644 --- a/src/packages/shared-types/index.ts +++ b/src/packages/shared-types/index.ts @@ -1,2 +1,4 @@ export * from "./issue"; export * from "./user"; +export * from "./seatoolData"; +export * from "./errors"; diff --git a/src/packages/shared-types/seatoolData.ts b/src/packages/shared-types/seatoolData.ts new file mode 100644 index 000000000..02d6cd3a5 --- /dev/null +++ b/src/packages/shared-types/seatoolData.ts @@ -0,0 +1,6 @@ +export interface SeatoolData { + ID: string; + SubmissionDate: number; + PlanType: string; // should be enum + StateAbbreviation: string; // should be enum +} diff --git a/src/services/ui/src/api/useGetSeatool.ts b/src/services/ui/src/api/useGetSeatool.ts index 629b07800..d1b66067c 100644 --- a/src/services/ui/src/api/useGetSeatool.ts +++ b/src/services/ui/src/api/useGetSeatool.ts @@ -1,5 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { API } from "aws-amplify"; +import { SeatoolData, ReactQueryApiError } from "shared-types"; export const getSeaToolData = async (stateCode: string) => { const seaToolData = await API.get("issues", `/seatool/${stateCode}`, {}); @@ -8,7 +9,7 @@ export const getSeaToolData = async (stateCode: string) => { }; export const useGetSeatool = (stateCode: string, options?: any) => - useQuery(["seatool", stateCode], { + useQuery(["seatool", stateCode], { queryFn: () => getSeaToolData(stateCode), ...options, }); diff --git a/src/services/ui/src/components/ErrorAlert/index.tsx b/src/services/ui/src/components/ErrorAlert/index.tsx new file mode 100644 index 000000000..80208b00d --- /dev/null +++ b/src/services/ui/src/components/ErrorAlert/index.tsx @@ -0,0 +1,12 @@ +import * as UI from "@enterprise-cmcs/macpro-ux-lib"; +import { ReactQueryApiError } from "shared-types"; + +export const ErrorAlert = ({ error }: { error: ReactQueryApiError }) => { + let message = "An error has occured"; + if (error?.response?.data?.message) { + message = error.response.data.message; + } + return ( + + ); +}; diff --git a/src/services/ui/src/components/index.tsx b/src/services/ui/src/components/index.tsx index ec46ff5de..2106534a6 100644 --- a/src/services/ui/src/components/index.tsx +++ b/src/services/ui/src/components/index.tsx @@ -1,3 +1,4 @@ export * from "./MainWrapper"; export * from "./AddIssueForm"; export * from "./LoadingSpinner"; +export * from "./ErrorAlert"; diff --git a/src/services/ui/src/pages/dashboard/index.tsx b/src/services/ui/src/pages/dashboard/index.tsx index 8c700ef6c..72d42a6bc 100644 --- a/src/services/ui/src/pages/dashboard/index.tsx +++ b/src/services/ui/src/pages/dashboard/index.tsx @@ -1,33 +1,22 @@ import { useGetSeatool } from "../../api/useGetSeatool"; import { formatDistance } from "date-fns"; import * as UI from "@enterprise-cmcs/macpro-ux-lib"; -import { LoadingSpinner } from "../../components"; +import { LoadingSpinner, ErrorAlert } from "../../components"; import { ChangeEvent, useState } from "react"; +import { SeatoolData } from "shared-types"; -export const Row = ({ record }: { record: any }) => ( - - {record.ID} - - {formatDistance(new Date(record.SubmissionDate), new Date())} ago - - {record.PlanType} - {record.StateAbbreviation} - -); - -export const Error = ({ - error, -}: { - error: { response: { data: { message: string } } }; -}) => { - let message = "An error has occured"; - if (error.response.data.message) { - message = error.response.data.message; - } +export function Row({ record }: { record: SeatoolData }) { return ( - + + {record.ID} + + {formatDistance(new Date(record.SubmissionDate), new Date())} ago + + {record.PlanType} + {record.StateAbbreviation} + ); -}; +} export const Dashboard = () => { const [selectedState, setSelectedState] = useState("VA"); @@ -62,7 +51,7 @@ export const Dashboard = () => {
{error ? ( - + ) : ( @@ -75,7 +64,7 @@ export const Dashboard = () => { {data && - data.map((record: any) => { + data.map((record) => { return ; })} From 5c2a0045c54721a3594b5bad5f2309bc6ea0e413 Mon Sep 17 00:00:00 2001 From: Benjamin Paige Date: Mon, 26 Jun 2023 20:12:32 -0600 Subject: [PATCH 4/8] remove authorizer --- .../api/handlers/getSeatoolData/authorizer.ts | 57 ------------------- src/services/api/serverless.yml | 6 -- 2 files changed, 63 deletions(-) delete mode 100644 src/services/api/handlers/getSeatoolData/authorizer.ts diff --git a/src/services/api/handlers/getSeatoolData/authorizer.ts b/src/services/api/handlers/getSeatoolData/authorizer.ts deleted file mode 100644 index f5377d637..000000000 --- a/src/services/api/handlers/getSeatoolData/authorizer.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { - APIGatewayAuthorizerResult, - APIGatewayTokenAuthorizerEvent, -} from "aws-lambda"; -import { - CognitoIdentityProviderClient, - GetUserCommand, -} from "@aws-sdk/client-cognito-identity-provider"; - -export const handler = async ( - event: APIGatewayTokenAuthorizerEvent -): Promise => { - const authorizationToken = event.authorizationToken; - - // Verify the authorization token - try { - const cognitoClient = new CognitoIdentityProviderClient({ - region: process.env.region, - }); - const params = { - AccessToken: authorizationToken.split(" ")[1], // this has got to be the wrong token to use - }; - const userAttributes = await cognitoClient.send(new GetUserCommand(params)); - - // Process the user attributes as needed - console.log("User attributes:", userAttributes); - - // Return a successful authorization response - return generateAuthResponse("user", "Allow", event.methodArn); - } catch (error) { - // Return a failed authorization response - console.error("Authorization error:", error); - return generateAuthResponse("user", "Deny", event.methodArn); - } -}; - -const generateAuthResponse = ( - principalId: string, - effect: "Allow" | "Deny", - methodArn: string -): APIGatewayAuthorizerResult => { - const policyDocument = { - Version: "2012-10-17", - Statement: [ - { - Action: "execute-api:Invoke", - Effect: effect, - Resource: methodArn, - }, - ], - }; - - return { - principalId, - policyDocument, - }; -}; diff --git a/src/services/api/serverless.yml b/src/services/api/serverless.yml index f9bd32ea2..0cb3fbd50 100644 --- a/src/services/api/serverless.yml +++ b/src/services/api/serverless.yml @@ -78,12 +78,6 @@ functions: method: get cors: true authorizer: aws_iam - # authorizer: - # name: getSeaToolAuthorizerFunc - # type: TOKEN - # identitySource: method.request.header.Authorization - getSeaToolAuthorizerFunc: - handler: handlers/getSeatoolData/authorizer.handler getIssue: handler: handlers/getIssue.handler maximumRetryAttempts: 0 From f0017d2c7ac239d1d9d534b889fdf5f33a5ad892 Mon Sep 17 00:00:00 2001 From: Benjamin Paige Date: Tue, 27 Jun 2023 14:47:10 -0600 Subject: [PATCH 5/8] refactor --- src/services/api/handlers/getSeatoolData.ts | 47 ++++++++++++++++++ .../index.ts => libs/auth/user.ts} | 49 +------------------ src/services/api/serverless.yml | 2 +- 3 files changed, 50 insertions(+), 48 deletions(-) create mode 100644 src/services/api/handlers/getSeatoolData.ts rename src/services/api/{handlers/getSeatoolData/index.ts => libs/auth/user.ts} (65%) diff --git a/src/services/api/handlers/getSeatoolData.ts b/src/services/api/handlers/getSeatoolData.ts new file mode 100644 index 000000000..4341d0deb --- /dev/null +++ b/src/services/api/handlers/getSeatoolData.ts @@ -0,0 +1,47 @@ +import { response } from "../libs/handler"; +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { SeatoolService } from "../services/seatoolService"; +import { APIGatewayEvent } from "aws-lambda"; +import { getAuthDetails, lookupUserAttributes } from "../libs/auth/user"; + +const dynamoInstance = new DynamoDBClient({ region: process.env.region }); + +export const getSeatoolData = async (event: APIGatewayEvent) => { + try { + const authDetails = getAuthDetails(event); + const userAttributes = await lookupUserAttributes( + authDetails.userId, + authDetails.poolId + ); + + const stateCode = event.pathParameters.stateCode; + + if ( + !userAttributes || + !userAttributes["custom:state_codes"].includes(stateCode) + ) { + return response({ + statusCode: 401, + body: { message: "User is not authorized to access this resource" }, + }); + } + + const seaData = await new SeatoolService(dynamoInstance).getIssues({ + tableName: process.env.tableName, + stateCode: stateCode, + }); + + return response({ + statusCode: 200, + body: seaData, + }); + } catch (error) { + console.error({ error }); + return response({ + statusCode: 404, + body: { message: error }, + }); + } +}; + +export const handler = getSeatoolData; diff --git a/src/services/api/handlers/getSeatoolData/index.ts b/src/services/api/libs/auth/user.ts similarity index 65% rename from src/services/api/handlers/getSeatoolData/index.ts rename to src/services/api/libs/auth/user.ts index 5544f3296..1a7a44694 100644 --- a/src/services/api/handlers/getSeatoolData/index.ts +++ b/src/services/api/libs/auth/user.ts @@ -1,15 +1,10 @@ -import { response } from "../../libs/handler"; -import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; -import { SeatoolService } from "../../services/seatoolService"; -import { APIGatewayEvent } from "aws-lambda"; import { CognitoIdentityProviderClient, ListUsersCommand, UserType as CognitoUserType, } from "@aws-sdk/client-cognito-identity-provider"; import { CognitoUserAttributes } from "shared-types"; - -const dynamoInstance = new DynamoDBClient({ region: process.env.region }); +import { APIGatewayEvent } from "aws-lambda"; export function getAuthDetails(event: APIGatewayEvent) { try { @@ -53,7 +48,7 @@ export const getParsedObject = (obj: CognitoUserAttributes) => }) ); -async function lookupUserAttributes( +export async function lookupUserAttributes( userId: string, poolId: string ): Promise { @@ -89,43 +84,3 @@ async function fetchUserFromCognito(userID: string, poolID: string) { const currentUser = listUsersResponse.Users[0]; return currentUser; } - -export const getSeatoolData = async (event: APIGatewayEvent) => { - try { - const authDetails = getAuthDetails(event); - const userAttributes = await lookupUserAttributes( - authDetails.userId, - authDetails.poolId - ); - - const stateCode = event.pathParameters.stateCode; - - if ( - !userAttributes || - !userAttributes["custom:state_codes"].includes(stateCode) - ) { - return response({ - statusCode: 401, - body: { message: "User is not authorized to access this resource" }, - }); - } - - const seaData = await new SeatoolService(dynamoInstance).getIssues({ - tableName: process.env.tableName, - stateCode: stateCode, - }); - - return response({ - statusCode: 200, - body: seaData, - }); - } catch (error) { - console.error({ error }); - return response({ - statusCode: 404, - body: { message: error }, - }); - } -}; - -export const handler = getSeatoolData; diff --git a/src/services/api/serverless.yml b/src/services/api/serverless.yml index 0cb3fbd50..8091be735 100644 --- a/src/services/api/serverless.yml +++ b/src/services/api/serverless.yml @@ -67,7 +67,7 @@ functions: cors: true authorizer: aws_iam getSeaTool: - handler: handlers/getSeatoolData/index.handler + handler: handlers/getSeatoolData.handler maximumRetryAttempts: 0 environment: tableName: ${param:seatoolTableName} From e9e4a416b0cd5ee0a36cdaa55cdc99cedfd36271 Mon Sep 17 00:00:00 2001 From: Benjamin Paige Date: Tue, 27 Jun 2023 15:07:13 -0600 Subject: [PATCH 6/8] refactor more --- src/services/api/handlers/getSeatoolData.ts | 33 +++++++++-- src/services/api/libs/auth/user.ts | 62 ++++++++++++--------- 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/src/services/api/handlers/getSeatoolData.ts b/src/services/api/handlers/getSeatoolData.ts index 4341d0deb..a12a98e08 100644 --- a/src/services/api/handlers/getSeatoolData.ts +++ b/src/services/api/handlers/getSeatoolData.ts @@ -4,33 +4,56 @@ import { SeatoolService } from "../services/seatoolService"; import { APIGatewayEvent } from "aws-lambda"; import { getAuthDetails, lookupUserAttributes } from "../libs/auth/user"; +// Create an instance of DynamoDB client const dynamoInstance = new DynamoDBClient({ region: process.env.region }); +// Handler function to get Seatool data export const getSeatoolData = async (event: APIGatewayEvent) => { try { + // Retrieve authentication details of the user const authDetails = getAuthDetails(event); + + // Look up user attributes from Cognito const userAttributes = await lookupUserAttributes( authDetails.userId, authDetails.poolId ); - const stateCode = event.pathParameters.stateCode; + // Retrieve the state code from the path parameters + const stateCode = event.pathParameters?.stateCode; + + // Check if stateCode is provided + if (!stateCode) { + return response({ + statusCode: 400, + body: { message: "State code is missing" }, + }); + } + // Check if user is authorized to access the resource based on their attributes if ( !userAttributes || - !userAttributes["custom:state_codes"].includes(stateCode) + !userAttributes["custom:state_codes"]?.includes(stateCode) ) { return response({ - statusCode: 401, + statusCode: 403, body: { message: "User is not authorized to access this resource" }, }); } + // Retrieve Seatool data using the SeatoolService const seaData = await new SeatoolService(dynamoInstance).getIssues({ tableName: process.env.tableName, stateCode: stateCode, }); + if (!seaData) { + return response({ + statusCode: 404, + body: { message: "No Seatool data found for the provided state code" }, + }); + } + return response({ statusCode: 200, body: seaData, @@ -38,8 +61,8 @@ export const getSeatoolData = async (event: APIGatewayEvent) => { } catch (error) { console.error({ error }); return response({ - statusCode: 404, - body: { message: error }, + statusCode: 500, + body: { message: "Internal server error" }, }); } }; diff --git a/src/services/api/libs/auth/user.ts b/src/services/api/libs/auth/user.ts index 1a7a44694..fb8fdd624 100644 --- a/src/services/api/libs/auth/user.ts +++ b/src/services/api/libs/auth/user.ts @@ -6,23 +6,19 @@ import { import { CognitoUserAttributes } from "shared-types"; import { APIGatewayEvent } from "aws-lambda"; +// Retrieve user authentication details from the APIGatewayEvent export function getAuthDetails(event: APIGatewayEvent) { - try { - const authProvider = - event.requestContext.identity.cognitoAuthenticationProvider; - const parts = authProvider.split(":"); - const userPoolIdParts = parts[parts.length - 3].split("/"); - - const userPoolId = userPoolIdParts[userPoolIdParts.length - 1]; - const userPoolUserId = parts[parts.length - 1]; - - return { userId: userPoolUserId, poolId: userPoolId }; - } catch (e) { - console.error({ e }); - } + const authProvider = + event.requestContext.identity.cognitoAuthenticationProvider; + const parts = authProvider.split(":"); + const userPoolIdParts = parts[parts.length - 3].split("/"); + const userPoolId = userPoolIdParts[userPoolIdParts.length - 1]; + const userPoolUserId = parts[parts.length - 1]; + + return { userId: userPoolUserId, poolId: userPoolId }; } -// pulls the data from the cognito user into a dictionary +// Convert Cognito user attributes to a dictionary format function userAttrDict(cognitoUser: CognitoUserType): CognitoUserAttributes { const attributes = {}; @@ -37,31 +33,40 @@ function userAttrDict(cognitoUser: CognitoUserType): CognitoUserAttributes { return attributes as CognitoUserAttributes; } +// Parse object values as JSON if possible export const getParsedObject = (obj: CognitoUserAttributes) => Object.fromEntries( Object.entries(obj).map(([key, value]) => { try { - return [key, JSON.parse(value as string)]; + return [key, JSON.parse(value)]; } catch (error) { return [key, value]; } }) ); +// Retrieve and parse user attributes from Cognito using the provided userId and poolId export async function lookupUserAttributes( userId: string, poolId: string ): Promise { const fetchResult = await fetchUserFromCognito(userId, poolId); - const currentUser = fetchResult as CognitoUserType; + if (fetchResult instanceof Error) { + throw fetchResult; + } + const currentUser = fetchResult as CognitoUserType; const attributes = userAttrDict(currentUser); return getParsedObject(attributes) as CognitoUserAttributes; } -async function fetchUserFromCognito(userID: string, poolID: string) { +// Fetch user data from Cognito based on the provided userId and poolId +async function fetchUserFromCognito( + userID: string, + poolID: string +): Promise { const cognitoClient = new CognitoIdentityProviderClient({ region: process.env.region, }); @@ -72,15 +77,20 @@ async function fetchUserFromCognito(userID: string, poolID: string) { UserPoolId: poolID, Filter: subFilter, }); - const listUsersResponse = await cognitoClient.send(commandListUsers); - if ( - listUsersResponse.Users === undefined || - listUsersResponse.Users.length !== 1 - ) { - return new Error("No user found with this sub"); + try { + const listUsersResponse = await cognitoClient.send(commandListUsers); + + if ( + listUsersResponse.Users === undefined || + listUsersResponse.Users.length !== 1 + ) { + throw new Error("No user found with this sub"); + } + + const currentUser = listUsersResponse.Users[0]; + return currentUser; + } catch (error) { + throw new Error("Error fetching user from Cognito"); } - - const currentUser = listUsersResponse.Users[0]; - return currentUser; } From f22289b76c7b192025af08d3e9fd7004b722799c Mon Sep 17 00:00:00 2001 From: Benjamin Paige Date: Tue, 27 Jun 2023 20:40:08 -0600 Subject: [PATCH 7/8] refactor more more --- src/packages/shared-types/errors.ts | 4 +-- src/packages/shared-types/seatoolData.ts | 6 ++-- src/packages/shared-types/user.ts | 4 +-- src/services/api/libs/auth/user.ts | 2 +- src/services/api/services/seatoolService.ts | 4 +-- src/services/seatool/handlers/sink.ts | 18 +++++------ src/services/seatool/serverless.yml | 30 +++++++++---------- src/services/ui/src/pages/dashboard/index.tsx | 6 ++-- 8 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/packages/shared-types/errors.ts b/src/packages/shared-types/errors.ts index c5161fe88..1ed569539 100644 --- a/src/packages/shared-types/errors.ts +++ b/src/packages/shared-types/errors.ts @@ -1,3 +1,3 @@ -export interface ReactQueryApiError { +export type ReactQueryApiError = { response: { data: { message: string } }; -} +}; diff --git a/src/packages/shared-types/seatoolData.ts b/src/packages/shared-types/seatoolData.ts index 02d6cd3a5..4f389f8f6 100644 --- a/src/packages/shared-types/seatoolData.ts +++ b/src/packages/shared-types/seatoolData.ts @@ -1,6 +1,6 @@ export interface SeatoolData { ID: string; - SubmissionDate: number; - PlanType: string; // should be enum - StateAbbreviation: string; // should be enum + SUBMISSION_DATE: number; + PLAN_TYPE: string; // should be enum + STATE_CODE: string; // should be enum } diff --git a/src/packages/shared-types/user.ts b/src/packages/shared-types/user.ts index 16b5dcd5a..9c7bb00e5 100644 --- a/src/packages/shared-types/user.ts +++ b/src/packages/shared-types/user.ts @@ -1,4 +1,4 @@ -export interface CognitoUserAttributes { +export type CognitoUserAttributes = { sub: string; "custom:roles": string[]; email_verified: boolean; @@ -6,4 +6,4 @@ export interface CognitoUserAttributes { given_name: string; family_name: string; email: string; -} +}; diff --git a/src/services/api/libs/auth/user.ts b/src/services/api/libs/auth/user.ts index fb8fdd624..df7549230 100644 --- a/src/services/api/libs/auth/user.ts +++ b/src/services/api/libs/auth/user.ts @@ -38,7 +38,7 @@ export const getParsedObject = (obj: CognitoUserAttributes) => Object.fromEntries( Object.entries(obj).map(([key, value]) => { try { - return [key, JSON.parse(value)]; + return [key, JSON.parse(value as string)]; } catch (error) { return [key, value]; } diff --git a/src/services/api/services/seatoolService.ts b/src/services/api/services/seatoolService.ts index d5cc37e46..4b804aef5 100644 --- a/src/services/api/services/seatoolService.ts +++ b/src/services/api/services/seatoolService.ts @@ -18,8 +18,8 @@ export class SeatoolService { const data = await this.#dynamoInstance.send( new QueryCommand({ TableName: tableName, - IndexName: "StateAbbreviation-SubmissionDate-index", - KeyConditionExpression: "StateAbbreviation = :state", + IndexName: "STATE_CODE-SUBMISSION_DATE-index", + KeyConditionExpression: "STATE_CODE = :state", ExpressionAttributeValues: { ":state": { S: stateCode }, diff --git a/src/services/seatool/handlers/sink.ts b/src/services/seatool/handlers/sink.ts index 08c8be3dc..189650a78 100644 --- a/src/services/seatool/handlers/sink.ts +++ b/src/services/seatool/handlers/sink.ts @@ -15,9 +15,9 @@ export const handler: Handler = async (event) => { } else { const jsonRecord = { ...JSON.parse(decode(value)) }; - const StateAbbreviation = jsonRecord?.["STATES"]?.[0]?.["STATE_CODE"]; - const PlanType = jsonRecord?.["PLAN_TYPES"]?.[0]?.["PLAN_TYPE_NAME"]; - const SubmissionDate = + const STATE_CODE = jsonRecord?.["STATES"]?.[0]?.["STATE_CODE"]; + const PLAN_TYPE = jsonRecord?.["PLAN_TYPES"]?.[0]?.["PLAN_TYPE_NAME"]; + const SUBMISSION_DATE = jsonRecord?.["STATE_PLAN"]?.["SUBMISSION_DATE"]; const record = { @@ -25,16 +25,16 @@ export const handler: Handler = async (event) => { ...jsonRecord, }; - if (StateAbbreviation) { - record.StateAbbreviation = StateAbbreviation; + if (STATE_CODE) { + record.STATE_CODE = STATE_CODE; } - if (PlanType) { - record.PlanType = PlanType; + if (PLAN_TYPE) { + record.PLAN_TYPE = PLAN_TYPE; } - if (SubmissionDate) { - record.SubmissionDate = SubmissionDate; + if (SUBMISSION_DATE) { + record.SUBMISSION_DATE = SUBMISSION_DATE; } records.push(record); diff --git a/src/services/seatool/serverless.yml b/src/services/seatool/serverless.yml index c2554df79..d48d3319f 100644 --- a/src/services/seatool/serverless.yml +++ b/src/services/seatool/serverless.yml @@ -85,47 +85,47 @@ resources: AttributeDefinitions: - AttributeName: ID AttributeType: S - - AttributeName: StateAbbreviation + - AttributeName: STATE_CODE AttributeType: S - - AttributeName: PlanType + - AttributeName: PLAN_TYPE AttributeType: S - - AttributeName: SubmissionDate + - AttributeName: SUBMISSION_DATE AttributeType: N KeySchema: - AttributeName: ID KeyType: HASH GlobalSecondaryIndexes: - - IndexName: StateAbbreviation-SubmissionDate-index + - IndexName: STATE_CODE-SUBMISSION_DATE-index KeySchema: - - AttributeName: StateAbbreviation + - AttributeName: STATE_CODE KeyType: HASH - - AttributeName: SubmissionDate + - AttributeName: SUBMISSION_DATE KeyType: RANGE Projection: ProjectionType: ALL - - IndexName: StateAbbreviation-PlanType-index + - IndexName: STATE_CODE-PLAN_TYPE-index KeySchema: - - AttributeName: StateAbbreviation + - AttributeName: STATE_CODE KeyType: HASH - - AttributeName: PlanType + - AttributeName: PLAN_TYPE KeyType: RANGE Projection: ProjectionType: ALL - - IndexName: SubmissionDate-index + - IndexName: SUBMISSION_DATE-index KeySchema: - - AttributeName: SubmissionDate + - AttributeName: SUBMISSION_DATE KeyType: HASH Projection: ProjectionType: ALL - - IndexName: StateAbbreviation-index + - IndexName: STATE_CODE-index KeySchema: - - AttributeName: StateAbbreviation + - AttributeName: STATE_CODE KeyType: HASH Projection: ProjectionType: ALL - - IndexName: PlanType-index + - IndexName: PLAN_TYPE-index KeySchema: - - AttributeName: PlanType + - AttributeName: PLAN_TYPE KeyType: HASH Projection: ProjectionType: ALL diff --git a/src/services/ui/src/pages/dashboard/index.tsx b/src/services/ui/src/pages/dashboard/index.tsx index 72d42a6bc..f9235bbef 100644 --- a/src/services/ui/src/pages/dashboard/index.tsx +++ b/src/services/ui/src/pages/dashboard/index.tsx @@ -10,10 +10,10 @@ export function Row({ record }: { record: SeatoolData }) { {record.ID} - {formatDistance(new Date(record.SubmissionDate), new Date())} ago + {formatDistance(new Date(record.SUBMISSION_DATE), new Date())} ago - {record.PlanType} - {record.StateAbbreviation} + {record.PLAN_TYPE} + {record.STATE_CODE} ); } From 097d04a2ec457d0af28780498b79b3ecacc190ed Mon Sep 17 00:00:00 2001 From: Benjamin Paige Date: Tue, 27 Jun 2023 20:57:02 -0600 Subject: [PATCH 8/8] update types --- src/packages/shared-types/seatoolData.ts | 4 ++-- src/services/ui/src/api/useGetSeatool.ts | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/packages/shared-types/seatoolData.ts b/src/packages/shared-types/seatoolData.ts index 4f389f8f6..145784e59 100644 --- a/src/packages/shared-types/seatoolData.ts +++ b/src/packages/shared-types/seatoolData.ts @@ -1,6 +1,6 @@ -export interface SeatoolData { +export type SeatoolData = { ID: string; SUBMISSION_DATE: number; PLAN_TYPE: string; // should be enum STATE_CODE: string; // should be enum -} +}; diff --git a/src/services/ui/src/api/useGetSeatool.ts b/src/services/ui/src/api/useGetSeatool.ts index d1b66067c..85e48b274 100644 --- a/src/services/ui/src/api/useGetSeatool.ts +++ b/src/services/ui/src/api/useGetSeatool.ts @@ -1,14 +1,19 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { API } from "aws-amplify"; import { SeatoolData, ReactQueryApiError } from "shared-types"; -export const getSeaToolData = async (stateCode: string) => { +export const getSeaToolData = async ( + stateCode: string +): Promise => { const seaToolData = await API.get("issues", `/seatool/${stateCode}`, {}); return seaToolData; }; -export const useGetSeatool = (stateCode: string, options?: any) => +export const useGetSeatool = ( + stateCode: string, + options?: UseQueryOptions +) => useQuery(["seatool", stateCode], { queryFn: () => getSeaToolData(stateCode), ...options,