diff --git a/cdk/lambdas/dbapihandler/dbapihandler.js b/cdk/lambdas/dbapihandler/dbapihandler.js index 24bbece..95b55cd 100644 --- a/cdk/lambdas/dbapihandler/dbapihandler.js +++ b/cdk/lambdas/dbapihandler/dbapihandler.js @@ -10,24 +10,38 @@ const headers = { exports.handler = async (event) => { const { httpMethod, path, body } = event; const resource = event.requestContext.resourcePath; - let tableName = (resource === '/users') ? 'harm-reduction-users' : 'harm-reduction-samples'; - + const tableName = event.queryStringParameters['tableName'] + if (httpMethod === 'POST') { return await createItem(tableName, JSON.parse(body)); } else if (httpMethod === 'PUT') { return await updateItem(tableName, JSON.parse(body)); } else if (httpMethod === 'GET') { - if (tableName === 'harm-reduction-users') { - return await getUser(tableName, event.queryStringParameters['sample-id']); - } else if (tableName === 'harm-reduction-samples') { - const sampleId = event.queryStringParameters['sample-id']; - if (sampleId) { - return await getSample(tableName, sampleId); - } else { - return await getAllSamples(tableName); + if (resource === '/users') { + return await getUser(tableName, event.queryStringParameters['sample-id']); + } + else if (resource === '/samples') { + const sampleId = event.queryStringParameters['sample-id']; + const query = event.queryStringParameters['query']; + + if (query === 'getCensoredUser'){ + return await getCensoredUser(tableName, sampleId) + } + + else if (query === 'getSample') { + return await getSample(tableName, sampleId); + } + else if (query === 'getContentOptions'){ + return await getContentOptions(tableName); + } + else if (query === 'getAllPublicSampleData') { + return await getAllPublicSampleData(tableName); + } + } + else if (resource === '/admin') { + return await getAllSamples(tableName); } - } } else if (httpMethod === 'DELETE') { return await deleteItem(tableName, event.queryStringParameters['sample-id']); } else if (httpMethod === 'OPTIONS') { @@ -119,6 +133,37 @@ async function getUser(tableName, sampleId) { } } +async function getCensoredUser(tableName, sampleId) { + const params = { + TableName: tableName, + ProjectionExpression: 'censoredContact', + Key: { 'sample-id': sampleId }, + }; + + try { + const { Item } = await dynamodb.get(params).promise(); + if (Item) { + return { + statusCode: 200, + headers: headers, + body: JSON.stringify(Item), + }; + } else { + return { + statusCode: 404, + headers: headers, + body: JSON.stringify({ message: 'Item not found' }), + }; + } + } catch (error) { + return { + statusCode: 500, + headers: headers, + body: JSON.stringify({ message: 'Failed to retrieve item', error }), + }; + } +} + async function getSample(tableName, sampleId) { const params = { TableName: tableName, @@ -170,6 +215,72 @@ async function getAllSamples(tableName) { } } +async function getAllPublicSampleData(tableName) { + const columns = ['date-received', 'expected-content', 'test-results', 'status']; + const status = 'Complete'; + + const columnsModified = columns.map(column => { + return `#${column.replace(/-/g, '_')}`; + }); + + const expressionAttributeNames = {}; + for (let i = 0; i < columns.length; i++) { + expressionAttributeNames[columnsModified[i]] = columns[i]; + } + + const params = { + TableName: tableName, + ProjectionExpression: columnsModified.join(', '), + ExpressionAttributeNames: expressionAttributeNames, + FilterExpression: '#status = :status', // Use an alias for "status" + ExpressionAttributeValues: { + ':status': status, + }, + }; + + try { + const { Items } = await dynamodb.scan(params).promise(); + return { + statusCode: 200, + headers: headers, + body: JSON.stringify(Items), + }; + } catch (error) { + return { + statusCode: 500, + headers: headers, + body: JSON.stringify({ message: 'Failed to retrieve items', error }), + }; + } +} + +async function getContentOptions(tableName) { + const expressionAttributeNames = { + '#expected_content': 'expected-content', + }; + + const params = { + TableName: tableName, + ProjectionExpression: '#expected_content', + ExpressionAttributeNames: expressionAttributeNames, + }; + + try { + const { Items } = await dynamodb.scan(params).promise(); + return { + statusCode: 200, + headers: headers, + body: JSON.stringify(Items), + }; + } catch (error) { + return { + statusCode: 500, + headers: headers, + body: JSON.stringify({ message: 'Failed to retrieve items', error }), + }; + } +} + async function deleteItem(tableName, sampleId) { const params = { TableName: tableName, diff --git a/cdk/lambdas/otpapihandler/otpapihandler.mjs b/cdk/lambdas/otpapihandler/otpapihandler.mjs index 98ff556..7be4516 100644 --- a/cdk/lambdas/otpapihandler/otpapihandler.mjs +++ b/cdk/lambdas/otpapihandler/otpapihandler.mjs @@ -7,7 +7,7 @@ const SENDER = process.env.EMAIL_ADDRESS; const TABLE = process.env.OTP_TABLE; const headers = { - "Access-Control-Allow-Headers" : "Content-Type", + "Access-Control-Allow-Headers" : "Content-Type, x-api-key", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS" } diff --git a/cdk/lambdas/sendnotif/sendnotif.mjs b/cdk/lambdas/sendnotif/sendnotif.mjs index 956b97b..b691b67 100644 --- a/cdk/lambdas/sendnotif/sendnotif.mjs +++ b/cdk/lambdas/sendnotif/sendnotif.mjs @@ -4,9 +4,7 @@ import { DynamoDBClient, GetItemCommand, PutItemCommand } from "@aws-sdk/client- const REGION = process.env.AWS_REGION; const ADMIN_EMAIL = process.env.EMAIL_ADDRESS; - -const API_KEY = process.env.REACT_APP_API_KEY; - +const USERTABLE = process.env.USERTABLE; export const handler = async(event) => { console.log(event.Records[0].dynamodb); @@ -31,14 +29,12 @@ export const handler = async(event) => { if(newStatus != 'Complete') {console.log('[ERROR]: invalid status'); return;} console.log('checking users table'); - - const userTableResp = await axios.get(DB_APIurl + `users?tableName=harm-reduction-users&sample-id=${newImg['sample-id'].S}`, { - headers: { - 'x-api-key': API_KEY, - } + const getUserCMD = new GetItemCommand({ + TableName: USERTABLE, + Key: {"sample-id": newImg['sample-id']}, }); - const contact = userTableResp.data.contact; - + const contactResp = await dynamoClient.send(getUserCMD); + const contact = contactResp.Item['contact'].S; if(checkEmailOrPhone(contact) == 'neither') {console.log('[ERROR]: invalid contact'); return;} let sendMsgResp = 'null'; @@ -51,18 +47,15 @@ export const handler = async(event) => { sendMsgResp = await sendSNS(contact, 'Update from UBC Harm Reduction', completeBodyText); } let expirytime = Math.floor((Date.now()/1000) + 5 * 60).toString(); - - const userTablePurgeResp = await axios.put(DB_APIurl + `users?tableName=harm-reduction-users`, { - "sample-id" : userTableResp.data['sample-id'], - "contact" : userTableResp.data['contact'], - "purge": expirytime - }, - { - headers: { - 'x-api-key': API_KEY, + const updatePurgeCMD = new PutItemCommand({ + TableName: USERTABLE, + Item: { + "sample-id": newImg['sample-id'], + "contact": {S: contact}, + "purge": {N: expirytime} } - }); - + }) + const userTablePurgeResp = await dynamoClient.Send(updatePurgeCMD); return sendMsgResp; }catch(err){ diff --git a/cdk/lib/cdk-stack.ts b/cdk/lib/cdk-stack.ts index 1835872..a02fbd5 100644 --- a/cdk/lib/cdk-stack.ts +++ b/cdk/lib/cdk-stack.ts @@ -70,6 +70,45 @@ export class CdkStack extends cdk.Stack { environment: {'EMAIL_ADDRESS': ''} }); + // Cognito + const adminPool = new cognito.UserPool(this, 'adminuserpool', { + userPoolName: 'harmreduction-adminpool', + signInCaseSensitive: false, + selfSignUpEnabled: false, + mfa: cognito.Mfa.OFF, + passwordPolicy: { + minLength: 8, + requireLowercase: true, + requireUppercase: true, + requireDigits: true, + requireSymbols: true, + tempPasswordValidity: cdk.Duration.days(3), + }, + accountRecovery: cognito.AccountRecovery.NONE, + deviceTracking: { + challengeRequiredOnNewDevice: false, + deviceOnlyRememberedOnUserPrompt: false + }, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const adminPoolClient = adminPool.addClient('adminpoolclient', { + authFlows: { + userPassword: true, + userSrp: true, + } + }); + + new cdk.CfnOutput(this, 'CognitoClientID', { + value: adminPoolClient.userPoolClientId, + description: 'Cognito user pool Client ID' + }); + + new cdk.CfnOutput(this, 'CognitoUserPoolID', { + value: adminPool.userPoolId, + description: 'Cognito user pool ID' + }); + const prdLogGroup = new logs.LogGroup(this, "PrdLogs"); const OTPapi = new apigateway.RestApi(this, 'OTPapi', { @@ -106,8 +145,14 @@ export class CdkStack extends cdk.Stack { }, }); + const cognitoAuthorizer = new apigateway.CognitoUserPoolsAuthorizer(this, 'CognitoAuthorizer', { + cognitoUserPools: [adminPool], + identitySource: 'method.request.header.Authorization', + }); + const DBSample = DBapi.root.addResource('samples'); const DBUser = DBapi.root.addResource('users'); + const DBAdmin = DBapi.root.addResource('admin'); DBSample.addMethod('POST', new apigateway.LambdaIntegration(DBApiHandler, {proxy: true}), {apiKeyRequired: true}); DBSample.addMethod('GET', new apigateway.LambdaIntegration(DBApiHandler, {proxy: true}), {apiKeyRequired: true}); @@ -122,6 +167,11 @@ export class CdkStack extends cdk.Stack { DBUser.addMethod('PUT', new apigateway.LambdaIntegration(DBApiHandler, {proxy: true}), {apiKeyRequired: true}); DBUser.addMethod('DELETE', new apigateway.LambdaIntegration(DBApiHandler, {proxy: true}), {apiKeyRequired: true}); // DBUser.addMethod('OPTIONS', new apigateway.LambdaIntegration(DBApiHandler, {proxy: true})); + DBAdmin.addMethod('GET', new apigateway.LambdaIntegration(DBApiHandler, {proxy: true}), { + authorizationType: apigateway.AuthorizationType.COGNITO, + authorizer: cognitoAuthorizer, + apiKeyRequired: true + }); const methodSettingProperty: apigateway.CfnDeployment.MethodSettingProperty = { cacheDataEncrypted: false, @@ -203,39 +253,6 @@ export class CdkStack extends cdk.Stack { batchSize: 1, })) - // Cognito - const adminPool = new cognito.UserPool(this, 'adminuserpool', { - userPoolName: 'harmreduction-adminpool', - signInCaseSensitive: false, - selfSignUpEnabled: false, - mfa: cognito.Mfa.OFF, - passwordPolicy: { - minLength: 8, - requireLowercase: true, - requireUppercase: true, - requireDigits: true, - requireSymbols: true, - tempPasswordValidity: cdk.Duration.days(3), - }, - accountRecovery: cognito.AccountRecovery.NONE, - deviceTracking: { - challengeRequiredOnNewDevice: false, - deviceOnlyRememberedOnUserPrompt: false - }, - removalPolicy: cdk.RemovalPolicy.DESTROY, - }); - - const adminPoolClient = adminPool.addClient('adminpoolclient', { - authFlows: { - userPassword: true - } - }); - - new cdk.CfnOutput(this, 'CognitoClientID', { - value: adminPoolClient.userPoolClientId, - description: 'Cognito user pool Client ID' - }); - // Store the gateway ARN for use with our WAF stack const apiGatewayARN = `arn:aws:apigateway:${Stack.of(this).region}::/restapis/${DBapi.restApiId}/stages/${DBapi.deploymentStage.stageName}` diff --git a/docs/deployment.md b/docs/deployment.md index d55615a..db1414a 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -109,6 +109,7 @@ Then, add the following variables |REACT_APP_DB_API_URL|paste name of the DB API url here| |REACT_APP_OTP_API_URL|paste name of the OTP API url here| |REACT_APP_COGCLIENT|paste CognitoClientID here| +|REACT_APP_USERPOOLID|paste CognitoUserPoolID here| |REACT_APP_API_KEY|paste ApiKeyOutput here| Once you have added the variables, your screen should look something like the image below, click save to save your changes diff --git a/docs/userguide.md b/docs/userguide.md index 21d3d9d..ebc0b86 100644 --- a/docs/userguide.md +++ b/docs/userguide.md @@ -6,23 +6,12 @@ | Index | Description | | :---- | :---------- | -| [Home Page](#home-page) | Landing page for users | | [Public Table Page](#public-table-page) | Viewing all sample available to the public | | [Individual Sample Page](#individual-sample-page) | Viewing a sample specified by sample ID | | [Admin Login Page](#admin-login-page) | Login page for lab admin | | [Admin Table Page](#admin-table-page) | Table with info privy only to the admin | ||| -## Home Page - -The home page serves as a landing page for users. It consists of a static section and a navbar with three links: home page, public samples page, specific sample page. A throuhgout description of the other two pages are available below. - -![alt text](./images/home.png) - -### 1. Contents - -The contents of the page can be edited in the github repo. However, there is no text editor available to manage this content, it is recommended to have some react/html experience if you intend to make edits to this section. - ## Public Table Page The Public Table Page is a table that displays all the samples that have been through the drug-testing cycle. It does not display any sensitive information as this table can be accessed by anyone on the website. diff --git a/package-lock.json b/package-lock.json index 6078ed9..aed9d94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@mui/icons-material": "^5.11.16", "@mui/material": "^5.13.2", "@mui/x-date-pickers": "^6.9.2", + "amazon-cognito-identity-js": "^6.3.7", "aws-amplify": "^5.3.3", "axios": "^1.4.0", "buffer": "^6.0.3", @@ -179,6 +180,53 @@ "url": "0.11.0" } }, + "node_modules/@aws-amplify/auth/node_modules/@aws-crypto/sha256-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-1.2.2.tgz", + "integrity": "sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==", + "dependencies": { + "@aws-crypto/util": "^1.2.2", + "@aws-sdk/types": "^3.1.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-amplify/auth/node_modules/@aws-crypto/util": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-1.2.2.tgz", + "integrity": "sha512-H8PjG5WJ4wz0UXAFXeJjWCW1vkvIJ3qUUD+rGRwJ2/hj+xT58Qle2MTql/2MGzkU+1JLAFuR6aJpLAjHwhmwwg==", + "dependencies": { + "@aws-sdk/types": "^3.1.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-amplify/auth/node_modules/amazon-cognito-identity-js": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.3.1.tgz", + "integrity": "sha512-PxBdufgS8uZShrcIFAsRjmqNXsh/4fXOWUGQOUhKLHWWK1pcp/y+VeFF48avXIWefM8XwsT3JlN6m9J2eHt4LA==", + "dependencies": { + "@aws-crypto/sha256-js": "1.2.2", + "buffer": "4.9.2", + "fast-base64-decode": "^1.0.0", + "isomorphic-unfetch": "^3.0.0", + "js-cookie": "^2.2.1" + } + }, + "node_modules/@aws-amplify/auth/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/@aws-amplify/auth/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/@aws-amplify/auth/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -276,6 +324,48 @@ "zen-push": "0.2.1" } }, + "node_modules/@aws-amplify/datastore/node_modules/@aws-crypto/sha256-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-1.2.2.tgz", + "integrity": "sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==", + "dependencies": { + "@aws-crypto/util": "^1.2.2", + "@aws-sdk/types": "^3.1.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-amplify/datastore/node_modules/@aws-crypto/util": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-1.2.2.tgz", + "integrity": "sha512-H8PjG5WJ4wz0UXAFXeJjWCW1vkvIJ3qUUD+rGRwJ2/hj+xT58Qle2MTql/2MGzkU+1JLAFuR6aJpLAjHwhmwwg==", + "dependencies": { + "@aws-sdk/types": "^3.1.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-amplify/datastore/node_modules/amazon-cognito-identity-js": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.3.1.tgz", + "integrity": "sha512-PxBdufgS8uZShrcIFAsRjmqNXsh/4fXOWUGQOUhKLHWWK1pcp/y+VeFF48avXIWefM8XwsT3JlN6m9J2eHt4LA==", + "dependencies": { + "@aws-crypto/sha256-js": "1.2.2", + "buffer": "4.9.2", + "fast-base64-decode": "^1.0.0", + "isomorphic-unfetch": "^3.0.0", + "js-cookie": "^2.2.1" + } + }, + "node_modules/@aws-amplify/datastore/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "node_modules/@aws-amplify/datastore/node_modules/idb": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/idb/-/idb-5.0.6.tgz", @@ -290,6 +380,16 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/@aws-amplify/datastore/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/@aws-amplify/datastore/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/@aws-amplify/datastore/node_modules/uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -20520,9 +20620,9 @@ } }, "node_modules/amazon-cognito-identity-js": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.3.1.tgz", - "integrity": "sha512-PxBdufgS8uZShrcIFAsRjmqNXsh/4fXOWUGQOUhKLHWWK1pcp/y+VeFF48avXIWefM8XwsT3JlN6m9J2eHt4LA==", + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.3.7.tgz", + "integrity": "sha512-tSjnM7KyAeOZ7UMah+oOZ6cW4Gf64FFcc7BE2l7MTcp7ekAPrXaCbpcW2xEpH1EiDS4cPcAouHzmCuc2tr72vQ==", "dependencies": { "@aws-crypto/sha256-js": "1.2.2", "buffer": "4.9.2", diff --git a/package.json b/package.json index 8a07062..122349b 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@mui/icons-material": "^5.11.16", "@mui/material": "^5.13.2", "@mui/x-date-pickers": "^6.9.2", + "amazon-cognito-identity-js": "^6.3.7", "aws-amplify": "^5.3.3", "axios": "^1.4.0", "buffer": "^6.0.3", diff --git a/src/components/admintable.js b/src/components/admintable.js index b2d8b5a..7e1814a 100644 --- a/src/components/admintable.js +++ b/src/components/admintable.js @@ -45,7 +45,7 @@ const StyledTableCell = styled(TableCell)(({ theme }) => ({ }, })); -const SampleTable = () => { +const SampleTable = ({ jwtToken }) => { const [samples, setSamples] = useState([]); const [initialSamples, setInitialSamples] = useState([]); const [editableRows, setEditableRows] = useState([]); @@ -82,10 +82,11 @@ const SampleTable = () => { const fetchSamples = async () => { try { const response = await axios.get( - DB_APIurl + 'samples?tableName=harm-reduction-samples', + DB_APIurl + 'admin?tableName=harm-reduction-samples', { headers: { 'x-api-key': API_KEY, + 'Authorization': jwtToken, } } @@ -147,7 +148,7 @@ const SampleTable = () => { newTestingMethod ) => { try { - await axios.put( + const updateSampleInfo = await axios.put( DB_APIurl + `samples?tableName=harm-reduction-samples`, { 'sample-id': sampleId, @@ -465,6 +466,7 @@ const SampleTable = () => { setSearchQueryDateReceived(dayjs(newValue).format('YYYY-MM-DD'))} slotProps={{ diff --git a/src/pages/admin.js b/src/pages/admin.js index ca4b384..baada7b 100644 --- a/src/pages/admin.js +++ b/src/pages/admin.js @@ -1,29 +1,97 @@ -import {useState} from 'react'; +import { useState, useEffect } from 'react'; import { Box, Alert } from "@mui/material" import TextField from '@mui/material/TextField'; import Button from '@mui/material/Button'; - -import { authUser } from '../utils/loginworker.js' import AdminTable from '../components/admintable.js' +import { AuthenticationDetails, CognitoUser, CognitoUserPool } from 'amazon-cognito-identity-js'; + const Admin = () => { const [showError, setShowError] = useState(false); const [loginStatus, setLoginStatus] = useState(false); const [pageState, setPageState] = useState(0); + const [jwtToken, setJwtToken] = useState(''); let username; let password; - const adminSignin = async () => { - const authResp = await authUser(username, password); + const poolData = { + UserPoolId: process.env.REACT_APP_USER_POOL_ID, + ClientId: process.env.REACT_APP_COGCLIENT, + }; + + const userPool = new CognitoUserPool(poolData); + + const checkExistingSession = () => { + // Check for existing session when the component mounts + const cognitoUser = userPool.getCurrentUser(); + + if (cognitoUser) { + cognitoUser.getSession((err, session) => { + if (err) { + setLoginStatus(false); + } else { + const jwtToken = session.getIdToken().getJwtToken(); + setJwtToken(jwtToken); + setLoginStatus(true); + } + }); + } + }; + + useEffect(() => { + checkExistingSession(); + }, []); // Empty dependency array ensures this effect runs only once when the component mounts - if(!authResp.AccessToken){ - setShowError(true); - return; - } - setLoginStatus(true); + const adminSignin = async () => { + const authenticationData = { + Username: username, + Password: password, + }; + + const authenticationDetails = new AuthenticationDetails(authenticationData); + + const userData = { + Username: username, + Pool: userPool, + }; + + const cognitoUser = new CognitoUser(userData); + + cognitoUser.authenticateUser(authenticationDetails, { + onSuccess: (session) => { + const jwtToken = session.getIdToken().getJwtToken(); + setJwtToken(jwtToken); // Add this state to store the JWT token + setLoginStatus(true); + }, + onFailure: (err) => { + setShowError(true); + }, + }); } + const adminSignout = () => { + const cognitoUser = userPool.getCurrentUser(); + + if (cognitoUser) { + cognitoUser.signOut(); + setLoginStatus(false); + } + }; + + const LogoutButton = () => { + return ( + + ); + }; + const LoginPage = () => { return( { justifyContent="center" alignItems="center" minHeight="60vh" - style={{marginTop: '20px'}} + style={{ marginTop: '20px' }} > - {!loginStatus && } - {(loginStatus && (pageState == 0)) && } + {!loginStatus && } + {(loginStatus && (pageState === 0)) && ( +
+
+ +
+ +
+ )}
) } diff --git a/src/pages/publictable.js b/src/pages/publictable.js index 5e98d43..9836de1 100644 --- a/src/pages/publictable.js +++ b/src/pages/publictable.js @@ -56,7 +56,7 @@ const SampleTable = () => { const fetchSamples = async () => { try { const response = await axios.get( - DB_APIurl + 'samples?tableName=harm-reduction-samples', + DB_APIurl + 'samples?query=getAllPublicSampleData&tableName=harm-reduction-samples', { headers: { 'x-api-key': API_KEY, @@ -101,6 +101,7 @@ const SampleTable = () => { setSearchQueryDateReceived(dayjs(newValue).format('YYYY-MM-DD'))} slotProps={{ diff --git a/src/pages/tracksample.js b/src/pages/tracksample.js index 2677590..a3d6784 100644 --- a/src/pages/tracksample.js +++ b/src/pages/tracksample.js @@ -30,9 +30,12 @@ const TrackSample = () => { const [pageState, setPageState] = useState(0); const [referenceID, setReferenceID] = useState(''); const [newContact, setNewContact] = useState(''); + const [newCensoredContact, setNewCensoredContact] = useState(''); const [contactMethod, setContactMethod] = useState('email') const [displayError, setDisplayError] = useState(false); + const [displaySampleIDError, setDisplaySampleIDError] = useState(false); + const [displayExpectedContentsError, setDisplayExpectedContentsError] = useState(false); const [disableButton, setDisableButton] = useState(false); const [displaySavedMsg, setDisplaySavedMsg] = useState(false); const [displayContactEdit, setDisplayContactEdit] = useState(false); @@ -46,15 +49,17 @@ const TrackSample = () => { const [sampleUsed, setSampleUsed] = useState(false); const [sampleTable, setSampleTable] = useState([]); const [contentOptions, setContentOptions] = useState([]); + const [sampleContactInfo, setSampleContactInfo] = useState(''); + const [sampleContactType, setSampleContactType] = useState(''); + const [expectedContentsField, setExpectedContentsField] = useState(''); let trackingID; let contactField; let enteredOTP; - let expectedContentsField = ''; const getOptions = async () => { try{ - const resp = await axios.get(DB_APIurl + 'samples?tableName=harm-reduction-samples', { + const resp = await axios.get(DB_APIurl + 'samples?query=getContentOptions&tableName=harm-reduction-samples', { headers: { 'x-api-key': API_KEY, } @@ -68,19 +73,30 @@ const TrackSample = () => { } const trackSample = async () => { - let sampleID = trackingID.trim(); - sampleID = sampleID.toUpperCase(); - if(!(/^[a-zA-Z0-9\s]{1,12}$/.test(sampleID))){ - setDisplayError(true); - return; - } - getOptions(); + setDisplayError(false); + setDisplayExpectedContentsError(false); + setDisplaySampleIDError(false); + setDisplaySavedMsg(false); try{ - const resp = await axios.get(DB_APIurl + `samples?tableName=harm-reduction-samples&sample-id=${sampleID}`, { + let sampleID = trackingID.trim(); + sampleID = sampleID.toUpperCase(); + if(!(/^[a-zA-Z0-9\s]{1,12}$/.test(sampleID))){ + setDisplaySampleIDError(true); + return; + } + getOptions(); + + const resp = await axios.get(DB_APIurl + `samples?query=getSample&tableName=harm-reduction-samples&sample-id=${sampleID}`, { headers: { 'x-api-key': API_KEY, } }); + + if (resp && !resp.error) { + // Call getContactInfo only if resp exists and there are no errors + getContactInfo(sampleID); + } + setSampleID(resp.data['sample-id']); (resp.data['status'] === 'Manual Testing Required') ? setSampleStatus('Pending') : setSampleStatus(resp.data['status']); setSampleDate(resp.data['date-received']); @@ -95,25 +111,88 @@ const TrackSample = () => { setDisplayContactEdit(false); setDisplayContactVerify(false); - setDisplayError(false); + setDisplaySampleIDError(false); setPageState(1); }catch(err){ - setDisplayError(true); + setDisplaySampleIDError(true); } } + const getContactInfo = async(sampleID) => { + try{ + setSampleContactInfo(''); + setSampleContactType(''); + + const contactResp = await axios.get(DB_APIurl + `samples?query=getCensoredUser&tableName=harm-reduction-users&sample-id=${sampleID}`, { + headers: { + 'x-api-key': API_KEY, + } + }); + + setSampleContactInfo(contactResp.data['censoredContact']); + setSampleContactType(checkCensoredEmailOrPhone(contactResp.data['censoredContact'])); + } + catch(err){ + if(err.response && err.response.status === 404){ + // No contact info available + } + } + } + + function checkEmailOrPhone(string) { + const emailPattern = /^[\w\.-]+@[\w\.-]+\.\w+$/; + const phonePattern = /^(\+\d{1,3}\s?)?(\(\d{1,4}\)|\d{1,4})[-.\s]?\d{1,4}[-.\s]?\d{1,9}$/; + + if(emailPattern.test(string)) return 'email'; + if(phonePattern.test(string)) return 'phone'; + return 'neither' + } + + function checkCensoredEmailOrPhone(string) { + const emailPattern = /^[\w\.-]+@[\w\.-]+\.\w+$/; + const phonePattern = /^(\+\d{1,3}\s?)?(\(\d{1,4}\)|\d{1,4})[-.\s]?\d{1,4}[-.\s]?\d{1,9}$/; + + if(string.length > 0 && string.includes('@')) return 'email'; + else if(string.length > 0) return 'phone'; + return 'neither' + } + + function censorContactInfo(string){ + const contactType = checkEmailOrPhone(string); + if (contactType === 'email') { + const parts = string.split('@'); + const username = parts[0]; + const domain = parts[1]; + const censoredUsername = username.substring(0, 1) + '*'.repeat(username.length - 2) + username.substring(username.length - 1); + const censoredDomain = domain.substring(0, 1) + '*'.repeat(domain.length - 1); + return censoredUsername + '@' + censoredDomain; + } else if (contactType === 'phone') { + const allCensored = string.replace(/./g, '*'); + const partialCensored = allCensored.substr(0,allCensored.length-2) + const lastTwoDigits = string.substr(-2); + return partialCensored + lastTwoDigits; + } + return 'Invalid contact information'; + } + const saveMetadata = async () => { + // Check if expectedContentsField is empty + if (!expectedContentsField) { + setDisplayExpectedContentsError(true); + return; + } + setDisplayGetMetadata(false); try{ - const getresp = await axios.get(DB_APIurl + `samples?tableName=harm-reduction-samples&sample-id=${sampleID}`, { + const getresp = await axios.get(DB_APIurl + `samples?query=getSample&tableName=harm-reduction-samples&sample-id=${sampleID}`, { headers: { 'x-api-key': API_KEY, } }); const item = getresp.data; - const resp = await axios.put(DB_APIurl + `samples?tableName=samples`,{ + const resp = await axios.put(DB_APIurl + `samples?tableName=harm-reduction-samples`,{ "status": item['status'], "sample-id": item["sample-id"], "vial-id": item["vial-id"], @@ -130,9 +209,10 @@ const TrackSample = () => { headers: { 'x-api-key': API_KEY, } - }) - expectedContentsField=''; + }); + setExpectedContentsField(''); setSampleUsed(false); + setDisplayExpectedContentsError(false); }catch(err){ console.log(err); } @@ -140,52 +220,65 @@ const TrackSample = () => { } const editContact = async () => { - let recipient = contactField.trim(); - const emailRegex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/; - const phoneRegex = /^\+?[0-9]{1,3}[-.\s]?\(?[0-9]{1,3}\)?[-.\s]?[0-9]{1,4}[-.\s]?[0-9]{1,4}$/; - const valid = (contactMethod === 'email') ? emailRegex.test(recipient) : phoneRegex.test(recipient); - if(!valid){ - setDisplayError(true); - return; - } - - const OTPInfo = await axios.post(OTP_APIurl + `otp?action=send`,{ - "recipient": recipient, - "contactbyemail": (contactMethod == 'email'), - }, - { - headers: { - 'x-api-key': API_KEY, + try{ + let recipient = contactField.trim(); + const emailRegex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/; + const phoneRegex = /^\+?[0-9]{1,3}[-.\s]?\(?[0-9]{1,3}\)?[-.\s]?[0-9]{1,4}[-.\s]?[0-9]{1,4}$/; + const valid = (contactMethod === 'email') ? emailRegex.test(recipient) : phoneRegex.test(recipient); + if(!valid){ + setDisplayError(true); + return; } - }); - setNewContact(contactField); - setReferenceID(OTPInfo.data.refID); - - setDisplayError(false); - contactField = ''; - setDisplayContactEdit(false); - setDisplayContactVerify(true); - } + const OTPInfo = await axios.post(OTP_APIurl + `otp?action=send`,{ + "recipient": recipient, + "contactbyemail": (contactMethod == 'email'), + }, + { + headers: { + 'x-api-key': API_KEY, + } + }); - const verifyContact = async () => { - let OTP = enteredOTP.trim(); - if(!(/^.{6}$/.test(enteredOTP))){ + setNewContact(contactField); + setNewCensoredContact(censorContactInfo(contactField)) + setReferenceID(OTPInfo.data.refID); + + setDisplayError(false); + contactField = ''; + setDisplayContactEdit(false); + setDisplayContactVerify(true); + } + catch(err){ setDisplayError(true); return; } - - const verifyResp = await axios.post(OTP_APIurl + `otp?action=verify`, { - "recipient": newContact, - "userOTP": OTP, - "userRefID": referenceID - }, { - headers: { - 'x-api-key': API_KEY, + } + + const verifyContact = async () => { + try{ + let OTP = enteredOTP.trim(); + if(!(/^.{6}$/.test(enteredOTP))){ + setDisplayError(true); + return; } - }) + + const verifyResp = await axios.post(OTP_APIurl + `otp?action=verify`, { + "recipient": newContact, + "userOTP": OTP, + "userRefID": referenceID + }, { + headers: { + 'x-api-key': API_KEY, + } + }) - if(!verifyResp.data.valid){ + if(!verifyResp.data.valid){ + setDisplayError(true); + return; + } + } + catch(err){ setDisplayError(true); return; } @@ -194,6 +287,7 @@ const TrackSample = () => { const updateContactResp = await axios.put(DB_APIurl + `users?tableName=harm-reduction-users`, { "sample-id": sampleID, "contact": newContact, + "censoredContact": newCensoredContact }, { headers: { @@ -231,7 +325,7 @@ const TrackSample = () => { sx={{mt:4}} > Enter the sample ID provided on the package in the box below - {displayError && The sample ID you entered is invalid} + {displaySampleIDError && The sample ID you entered is invalid} {trackingID=event.target.value}} @@ -285,6 +379,8 @@ const TrackSample = () => { Sample ID: {`${sampleID}`} Status: {`${sampleStatus}`} Date Received: {`${sampleDate}`} + Contact Info: {sampleContactInfo ? sampleContactInfo : 'N/A'} + Contact Type: {sampleContactType ? sampleContactType.charAt(0).toUpperCase() + sampleContactType.slice(1) : 'N/A'} {displaySavedMsg && ( Your contact information has been saved successfully @@ -335,7 +431,6 @@ const TrackSample = () => { sx={{ boxShadow: 3, width: 400, - height: 200, bgcolor: (theme) => (theme.palette.mode === 'dark' ? '#101010' : '#fff'), color: (theme) => theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800', @@ -350,6 +445,7 @@ const TrackSample = () => { alignItems="flex-start" > Search for another sample + {displaySampleIDError && The sample ID you entered is invalid} { alignItems="center" > Submit additional information about this sample + {displayExpectedContentsError && Please enter a value for Expected Contents} setExpectedContentsField(value)} renderInput={(params) => ( { sx={{width: WIDTH - 100}} > Has this sample been used? - {setSampleUsed(event.target.checked)}}/> + {setSampleUsed(event.target.checked)}}/> { @@ -528,89 +633,175 @@ const TrackSample = () => { } const ContactDisplay = () => { + const [isEditable, setIsEditable] = useState(false); + const [buttonText, setButtonText] = useState('Edit'); const WIDTH = isMobile ? 400 : 700; const { value, onChange, ...otherProps } = []; - return( + + const handleEditClick = () => { + if (isEditable) { + setIsEditable(false); + setButtonText('Edit'); + } else { + setIsEditable(true); + setButtonText('Cancel'); + } + }; + + return ( (theme.palette.mode === 'dark' ? '#101010' : '#fff'), - color: (theme) => - theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800', - p: 1, - m: 2, - borderRadius: 2, - textAlign: 'center', - }} - display="flex" - flexDirection="column" - justifyContent="flex-start" - alignItems="center" + sx={{ + boxShadow: 3, + width: WIDTH, + height: 'auto', + bgcolor: (theme) => (theme.palette.mode === 'dark' ? '#101010' : '#fff'), + color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'), + p: 1, + m: 2, + borderRadius: 2, + textAlign: 'center', + }} + display="flex" + flexDirection="column" + justifyContent="flex-start" + alignItems="center" > - Enter your contact info below and receive notifications on the status of this sample - {setContactMethod(newContactMethod)}} - aria-label="text alignment" - sx={{m:2, mt: 0}} - > - - Email - - - SMS - - - {displayError && The contact info you entered is invalid} - {(contactMethod == 'email') && {contactField=event.target.value}} - id="trackinginput" - label="Enter your email here" - variant="outlined" - sx={{ mb:2, width: WIDTH - 100}} - />} - {(contactMethod == 'sms') && + Enter your contact info below and receive notifications on the status of this sample + + { + setContactMethod(newContactMethod); + }} + aria-label="text alignment" + sx={{ m: 2, mt: 0 }} + > + + Email + + + SMS + + + {displayError && ( + + The contact info you entered is invalid + + )} +
+ {(sampleContactInfo && contactMethod == 'email' && sampleContactType == 'email') ? ( + { + contactField = event.target.value; + }} + id="trackinginput" + label="Enter your email here" + defaultValue={sampleContactInfo} + variant="outlined" + sx={{ width: WIDTH - 100 }} + disabled={!isEditable} + /> + ) : + (contactMethod == 'email') && ( + { + contactField = event.target.value; + }} + id="trackinginput" + label="Enter your email here" + variant="outlined" + sx={{ width: WIDTH - 100 }} + /> + )} + {(sampleContactInfo && contactMethod == 'sms' && sampleContactType == 'phone') ? ( + contactField=event.target.value} - maskChar='' + onChange={(event) => (contactField = event.target.value)} + maskChar="" id="trackinginput" + > + {(inputProps) => ( + + )} + + ) : + (contactMethod == 'sms') && ( + (contactField = event.target.value)} + maskChar="" + id="trackinginput" > - {() => ( + {() => ( - )} - } - + )} + {(sampleContactInfo && contactMethod == 'sms' && sampleContactType == 'phone') && ( // Check if sampleContactInfo is not empty + + )} + {(sampleContactInfo && contactMethod == 'email' && sampleContactType == 'email') && ( // Check if sampleContactInfo is not empty + + )} +
+ + - - + Save + + +
- ) + ); } const ContactVerify = () => { diff --git a/src/utils/loginworker.js b/src/utils/loginworker.js deleted file mode 100644 index b0795e5..0000000 --- a/src/utils/loginworker.js +++ /dev/null @@ -1,26 +0,0 @@ -import { CognitoIdentityProviderClient, InitiateAuthCommand } from "@aws-sdk/client-cognito-identity-provider"; - -const REGION = process.env.REACT_APP_AWS_REGION; - -export const authUser = async (username, password) => { - const CLIENTID = process.env.REACT_APP_COGCLIENT; - - const cognitoIdpClient = new CognitoIdentityProviderClient({region: REGION}); - const initAuthCMD = new InitiateAuthCommand({ - AuthFlow: "USER_PASSWORD_AUTH", - AuthParameters: { - "USERNAME": username, - "PASSWORD": password - }, - ClientId: CLIENTID, - }) - - try{ - const initAuthResp = await cognitoIdpClient.send(initAuthCMD); - return initAuthResp.AuthenticationResult; - }catch(err){ - console.log(err); - return err; - } -} -