Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #26

Merged
merged 20 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 122 additions & 11 deletions cdk/lambdas/dbapihandler/dbapihandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion cdk/lambdas/otpapihandler/otpapihandler.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
35 changes: 14 additions & 21 deletions cdk/lambdas/sendnotif/sendnotif.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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';
Expand All @@ -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){
Expand Down
83 changes: 50 additions & 33 deletions cdk/lib/cdk-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down Expand Up @@ -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});
Expand All @@ -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,
Expand Down Expand Up @@ -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}`

Expand Down
1 change: 1 addition & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 0 additions & 11 deletions docs/userguide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading