diff --git a/backendAWS/bin/backend_aws.ts b/backendAWS/bin/backend_aws.ts index bf2d2b0ab050002db5da1edb9aa3371514ac94ee..8fa1412fa373219d09d8b3cc70295fe7d28f7d0b 100644 --- a/backendAWS/bin/backend_aws.ts +++ b/backendAWS/bin/backend_aws.ts @@ -3,7 +3,7 @@ import { AuthenticationStack } from '../lib/authentication_stack'; import { StorageStack } from '../lib/storage_stack'; import { LambdaStack } from '../lib/lambda_stack'; import { ApiGatewayStack } from '../lib/apigateway_stack'; -import { FrontendStack } from '../lib/frontend_stack'; +//import { FrontendStack } from '../lib/frontend_stack'; const app = new cdk.App(); @@ -42,6 +42,6 @@ new ApiGatewayStack(app, 'ContactApp-ApiGatewayStack', { env, }); -new FrontendStack(app, 'ContactApp-FrontendStack', { +/* new FrontendStack(app, 'ContactApp-FrontendStack', { env, -}); +}); */ diff --git a/backendAWS/functions/create-contact/index.ts b/backendAWS/functions/create-contact/index.ts index bbea234c090cc4547343e96359df2bc840af21b4..82f0f57aa702f22a010cae3039edd92689f75fdb 100644 --- a/backendAWS/functions/create-contact/index.ts +++ b/backendAWS/functions/create-contact/index.ts @@ -9,6 +9,9 @@ const bucketName = process.env.BUCKET_NAME; const cloudfrontDomain = process.env.CLOUDFRONT_DOMAIN; export const handler = async (event: any) => { + // Extract Cognito Identity ID (user's ID from Identity Pool) + const userId = event.requestContext.authorizer.claims.sub; // Using API Gateway Cognito authorizer + const body = JSON.parse(event.body); if (!body.image || !body.name || !body.phone || !body.email) { @@ -37,6 +40,7 @@ export const handler = async (event: any) => { // Build DynamoDB contact object const contact = { id: contactId, + userId, // Store the Cognito Identity ID to link the contact to the user name: body.name, phone: body.phone, email: body.email, diff --git a/backendAWS/functions/delete-contact/index.ts b/backendAWS/functions/delete-contact/index.ts index a2d84dd4e9819d0faaa4646ae6709dd387e81b15..80a65cf4072e98c2225bf38b86b8f46d2eeb2119 100644 --- a/backendAWS/functions/delete-contact/index.ts +++ b/backendAWS/functions/delete-contact/index.ts @@ -13,12 +13,21 @@ export const handler = async (event: any) => { return withCors(400, { error: 'Missing required contact ID' }); } + // Extract userId from the claims + const userId = event.requestContext.authorizer.claims.sub; + if (!userId) { + return withCors(403, { error: 'User not authenticated' }); + } + try { - // Fetch the contact to get the image URL + // Fetch the contact to validate ownership const result = await dynamoDb .get({ TableName: tableName!, - Key: { id: contactId }, + Key: { + userId: userId, + id: contactId, + }, }) .promise(); @@ -28,6 +37,13 @@ export const handler = async (event: any) => { return withCors(404, { error: 'Contact not found' }); } + // Ensure the authenticated user owns the contact + if (contact.userId !== userId) { + return withCors(403, { + error: 'You do not have permission to delete this contact', + }); + } + // Extract the S3 key from the image URL const imageUrl = contact.imageUrl; const url = new URL(imageUrl); @@ -47,7 +63,10 @@ export const handler = async (event: any) => { await dynamoDb .delete({ TableName: tableName!, - Key: { id: contactId }, + Key: { + userId: userId, + id: contactId, + }, }) .promise(); diff --git a/backendAWS/functions/get-contact/index.ts b/backendAWS/functions/get-contact/index.ts index e404a85ad8b91c62f7616c955a7cf1a3441f67c9..236cc98959c33a03ab6672b6f6b6e84755b7f6d6 100644 --- a/backendAWS/functions/get-contact/index.ts +++ b/backendAWS/functions/get-contact/index.ts @@ -5,6 +5,12 @@ const dynamoDb = new DynamoDB.DocumentClient(); const tableName = process.env.TABLE_NAME; export const handler = async (event: any) => { + // Extract userId from the claims + const userId = event.requestContext.authorizer.claims.sub; + if (!userId) { + return withCors(403, { error: 'User not authenticated' }); + } + const contactId = event.pathParameters?.contactId; try { @@ -13,7 +19,10 @@ export const handler = async (event: any) => { const result = await dynamoDb .get({ TableName: tableName!, - Key: { id: contactId }, + Key: { + userId: userId, // Add the partition key + id: contactId, // Add the sort key + }, }) .promise(); @@ -21,12 +30,24 @@ export const handler = async (event: any) => { return withCors(404, { error: 'Contact not found' }); } + // Check ownership of the contact + if (result.Item.userId !== userId) { + return withCors(403, { + error: 'You do not have permission to view this contact', + }); + } + return withCors(200, result.Item); } else { - // Fetch all contacts + // Fetch all contacts for the authenticated user const result = await dynamoDb - .scan({ + .query({ TableName: tableName!, + IndexName: 'userId-index', // Ensure you have a GSI on the `userId` attribute + KeyConditionExpression: 'userId = :userId', + ExpressionAttributeValues: { + ':userId': userId, + }, }) .promise(); diff --git a/backendAWS/functions/update-contact/index.ts b/backendAWS/functions/update-contact/index.ts index f117d2ad93061026c5fdeeca5643572f61e6fccd..75653c49ed12371f6c489ea92b7d1d2c625f8407 100644 --- a/backendAWS/functions/update-contact/index.ts +++ b/backendAWS/functions/update-contact/index.ts @@ -10,7 +10,11 @@ const cloudfrontDomain = process.env.CLOUDFRONT_DOMAIN; export const handler = async (event: any) => { const body = JSON.parse(event.body); - console.log('body of Update Lambda', body); + // Extract userId from the claims + const userId = event.requestContext.authorizer.claims.sub; + if (!userId) { + return withCors(403, { error: 'User not authenticated' }); + } if (!body.name || !body.phone || !body.email) { return withCors(400, { error: 'Missing required fields' }); @@ -18,20 +22,37 @@ export const handler = async (event: any) => { const contactId = event.pathParameters?.contactId; + if (!contactId) { + return withCors(400, { error: 'Missing required contact ID' }); + } + try { + // Fetch the contact to validate ownership const contactResult = await dynamoDb .get({ TableName: tableName!, - Key: { id: contactId }, + Key: { + userId: userId, + id: contactId, + }, }) .promise(); - if (!contactResult.Item) { + const contact = contactResult.Item; + + if (!contact) { return withCors(404, { error: 'Contact not found' }); } + // Ensure the authenticated user owns the contact + if (contact.userId !== userId) { + return withCors(403, { + error: 'You do not have permission to update this contact', + }); + } + // Update the contact's image if a new image is provided - let imageUrl = contactResult.Item.imageUrl; + let imageUrl = contact.imageUrl; if (body.image) { const base64Image = body.image.split(',')[1]; const imageBuffer = Buffer.from(base64Image, 'base64'); @@ -52,11 +73,11 @@ export const handler = async (event: any) => { // Update the contact in DynamoDB const updatedContact = { - ...contactResult.Item, + ...contact, name: body.name, phone: body.phone, email: body.email, - isFavorite: body.isFavorite || contactResult.Item.isFavorite, + isFavorite: body.isFavorite || contact.isFavorite, imageUrl, }; diff --git a/backendAWS/lib/authentication_stack.ts b/backendAWS/lib/authentication_stack.ts index 82d2fb7ba7e543b6bcae52bb2b272daef11a5446..6bd341b370cb89a59a222276d96a4ad935093c34 100644 --- a/backendAWS/lib/authentication_stack.ts +++ b/backendAWS/lib/authentication_stack.ts @@ -1,10 +1,13 @@ import * as cdk from 'aws-cdk-lib'; import * as cognito from 'aws-cdk-lib/aws-cognito'; +import * as iam from 'aws-cdk-lib/aws-iam'; //CognitoService export class AuthenticationStack extends cdk.Stack { public readonly userPool: cognito.UserPool; public readonly userPoolClient: cognito.UserPoolClient; + public readonly identityPool: cdk.aws_cognito.CfnIdentityPool; + public readonly authenticatedRole: iam.Role; constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); @@ -35,6 +38,69 @@ export class AuthenticationStack extends cdk.Stack { value: this.userPoolClient.userPoolClientId, }); + // Identity Pool + this.identityPool = new cognito.CfnIdentityPool( + this, + 'ContactsAppIdentityPool', + { + allowUnauthenticatedIdentities: false, + cognitoIdentityProviders: [ + { + clientId: this.userPoolClient.userPoolClientId, + providerName: this.userPool.userPoolProviderName, + }, + ], + } + ); + + new cdk.CfnOutput(this, 'ContactsAppIdentityPoolId', { + value: this.identityPool.ref, + }); + + // Authenticated Role with restricted DynamoDB access + this.authenticatedRole = new iam.Role( + this, + 'ContactsAppAuthenticatedRole', + { + assumedBy: new iam.FederatedPrincipal( + 'cognito-identity.amazonaws.com', + { + StringEquals: { + 'cognito-identity.amazonaws.com:aud': this.identityPool.ref, + }, + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': 'authenticated', + }, + }, + 'sts:AssumeRoleWithWebIdentity' + ), + } + ); + + this.authenticatedRole.addToPolicy( + new iam.PolicyStatement({ + actions: ['dynamodb:Query', 'dynamodb:GetItem', 'dynamodb:PutItem'], + resources: ['arn:aws:dynamodb:*:*:table/Contacts'], + conditions: { + 'ForAllValues:StringEquals': { + 'dynamodb:LeadingKeys': ['${cognito-identity.amazonaws.com:sub}'], + }, + }, + }) + ); + + // Attach Authenticated Role to Identity Pool + new cognito.CfnIdentityPoolRoleAttachment( + this, + 'IdentityPoolRoleAttachment', + { + identityPoolId: this.identityPool.ref, + roles: { + authenticated: this.authenticatedRole.roleArn, + }, + } + ); + cdk.Tags.of(this.userPool).add('Service', 'Auth'); } } diff --git a/backendAWS/lib/lambda_stack.ts b/backendAWS/lib/lambda_stack.ts index 6bdb031c02b8dde09e61fb323210e8d655c93660..55fcd11429f80884f0eb6b03f375ae26b144d12e 100644 --- a/backendAWS/lib/lambda_stack.ts +++ b/backendAWS/lib/lambda_stack.ts @@ -81,7 +81,7 @@ export class LambdaStack extends cdk.Stack { new PolicyStatement({ effect: Effect.ALLOW, resources: [contactsTable.tableArn], - actions: ['dynamodb:Scan', 'dynamodb:GetItem'], + actions: ['dynamodb:Scan', 'dynamodb:GetItem', 'dynamodb:Query'], }) ); diff --git a/backendAWS/lib/storage_stack.ts b/backendAWS/lib/storage_stack.ts index 8dbc00352e1f4a3ff49e826f0b66009444035642..7b0e8bb40c4e76b7a0349614adf99ae3ed19e62b 100644 --- a/backendAWS/lib/storage_stack.ts +++ b/backendAWS/lib/storage_stack.ts @@ -13,12 +13,20 @@ export class StorageStack extends cdk.Stack { super(scope, id, props); //DynamoDB Contacts - this.contactsTable = new dynamodb.Table(this, 'Contacts-app_Table', { - partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, + this.contactsTable = new dynamodb.Table(this, 'ContactsAppTable', { + partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING }, + sortKey: { name: 'id', type: dynamodb.AttributeType.STRING }, // Optional for storing specific records per user tableName: 'Contacts', removalPolicy: cdk.RemovalPolicy.DESTROY, }); + // Add a GSI for querying by `userId` + this.contactsTable.addGlobalSecondaryIndex({ + indexName: 'userId-index', // Name of the GSI + partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING }, // GSI partition key + projectionType: dynamodb.ProjectionType.ALL, // Include all attributes in the GSI (can be optimized) + }); + cdk.Tags.of(this.contactsTable).add('Service', 'Database'); //Contacts Images S3