Skip to content

Commit

Permalink
Merge pull request #26 from UBC-CIC/fixes
Browse files Browse the repository at this point in the history
Fixes
  • Loading branch information
Liam-Driscoll authored Nov 16, 2023
2 parents ef66e83 + d5d5e77 commit 750bf36
Show file tree
Hide file tree
Showing 13 changed files with 708 additions and 253 deletions.
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

0 comments on commit 750bf36

Please sign in to comment.