Skip to content

Commit

Permalink
feat: add delete account api
Browse files Browse the repository at this point in the history
  • Loading branch information
Jiin Kim authored and Jiin Kim committed Jan 13, 2025
1 parent 12bbcc0 commit ee021e5
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 0 deletions.
16 changes: 16 additions & 0 deletions cdk/lib/lambdaStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const policies = {
'cognito-idp:AdminInitiateAuth',
'cognito-idp:AdminCreateUser',
'cognito-idp:ListUsers',
'cognito-idp:AdminDeleteUser',
],
resources: [settings.cognito.userPoolArn],
}),
Expand Down Expand Up @@ -217,5 +218,20 @@ export class LambdaStack extends cdk.Stack {
// path: /student/schools
const getSchoolsIntegration = new apigateway.LambdaIntegration(getSchoolsLambda);
studentResource.addResource('schools').addMethod('GET', getSchoolsIntegration);

// =================================================================
// Delete User Lambda
// =================================================================
const deleteUserLambda = new NodejsFunction(this, 'DeleteUserHandler', {
...nodeJsFunctionProps,
entry: path.join(__dirname, '../src/lambda/handlers/deleteUser.ts'),
bundling,
});
deleteUserLambda.addToRolePolicy(policies.cognito.getUser);
deleteUserLambda.addToRolePolicy(policies.cognito.userManagement);

// path: /student/delete
const deleteUserIntegration = new apigateway.LambdaIntegration(deleteUserLambda);
studentResource.addResource('delete-account').addMethod('DELETE', deleteUserIntegration);
}
}
53 changes: 53 additions & 0 deletions cdk/src/docs/deleteUser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
paths:
/student/delete-account:
delete:
summary: Delete User Account
description: Deletes the authenticated user's account from the system.
security:
- BearerAuth: []
parameters:
- in: header
name: Authorization
required: true
description: Bearer token for authentication
schema:
type: string
pattern: '^Bearer\s[\w-]+\.[\w-]+\.[\w-]+$'
example: 'Bearer eyJhbGciOiJIUzI1NiIs...'
responses:
'200':
description: User successfully deleted
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: "User successfully deleted"
'401':
description: Unauthorized - Invalid or missing access token
content:
application/json:
schema:
type: object
properties:
error:
type: string
example: "Invalid or expired access token"
'500':
description: Internal server error
content:
application/json:
schema:
type: object
properties:
error:
type: string
example: "Failed to delete user"
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
37 changes: 37 additions & 0 deletions cdk/src/lambda/handlers/deleteUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { APIGatewayProxyHandler } from 'aws-lambda';
import { successResponse, errorResponse, wrapHandler } from '../handlerUtil';
import { getUserDetailsFromAccessToken } from '../../service/cognito';
import { deleteUserFromCognito } from '../../service/cognito';

const deleteUser: APIGatewayProxyHandler = async event => {
try {
// Get access token from Authorization header
const authHeader = event.headers.Authorization || event.headers.authorization;
if (!authHeader) {
return errorResponse('Authorization header is required', 401);
}

// Extract the token (remove "Bearer " if present)
const accessToken = authHeader.replace(/^Bearer\s/, '');

try {
// Verify access token and get user details
const userDetails = await getUserDetailsFromAccessToken(accessToken);
// Delete user from Cognito
await deleteUserFromCognito(userDetails.username);

return successResponse({
message: 'User successfully deleted',
});
} catch (error) {
if (error instanceof Error && error.message === 'Access token is invalid or has expired.') {
return errorResponse('Invalid or expired access token', 401);
}
throw error;
}
} catch (error) {
return errorResponse(error instanceof Error ? error.message : 'Failed to delete user', 500);
}
};

export const handler = wrapHandler(deleteUser);
12 changes: 12 additions & 0 deletions cdk/src/service/cognito.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AuthenticationResultType,
ListUsersCommand,
GetUserCommandOutput,
AdminDeleteUserCommand,
} from '@aws-sdk/client-cognito-identity-provider';
import { settings } from '../settings';

Expand Down Expand Up @@ -165,3 +166,14 @@ export async function resetUserPassword(email: string, newPassword: string): Pro

await cognito.send(command);
}

export async function deleteUserFromCognito(username: string): Promise<void> {
const { cognito, userPoolId } = getCognitoClient();

const command = new AdminDeleteUserCommand({
UserPoolId: userPoolId,
Username: username,
});

await cognito.send(command);
}
63 changes: 63 additions & 0 deletions cdk/test/lambda/handlers/deleteUser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { APIGatewayProxyEvent, Context, Callback } from 'aws-lambda';
import { handler } from '../../../src/lambda/handlers/deleteUser';
import { getUserDetailsFromAccessToken, deleteUserFromCognito } from '../../../src/service/cognito';
import * as handlerUtil from '../../../src/lambda/handlerUtil';

jest.mock('../../../src/service/cognito');

describe('deleteUser', () => {
const mockUserDetails = {
username: 'testuser',
email: '[email protected]',
};

const invokeHandler = async (event: Partial<APIGatewayProxyEvent>) => {
const context = {} as Context;
const callback = {} as Callback;
const defaultEvent = {
headers: {},
queryStringParameters: {},
};
return handler({ ...defaultEvent, ...event } as APIGatewayProxyEvent, context, callback);
};

beforeEach(() => {
jest.resetAllMocks();
(getUserDetailsFromAccessToken as jest.Mock).mockResolvedValue(mockUserDetails);
(deleteUserFromCognito as jest.Mock).mockResolvedValue(undefined);
jest.spyOn(handlerUtil, 'successResponse');
jest.spyOn(handlerUtil, 'errorResponse');
});

it('should call errorResponse when authorization header is missing', async () => {
await invokeHandler({});
expect(handlerUtil.errorResponse).toHaveBeenCalledWith('Authorization header is required', 401);
});

it('should call errorResponse when access token is invalid', async () => {
(getUserDetailsFromAccessToken as jest.Mock).mockRejectedValue(new Error('Access token is invalid or has expired.'));
await invokeHandler({
headers: { Authorization: 'Bearer invalid_token' },
});
expect(handlerUtil.errorResponse).toHaveBeenCalledWith('Invalid or expired access token', 401);
});

it('should delete user successfully with valid token', async () => {
await invokeHandler({
headers: { Authorization: 'Bearer valid_token' },
});
expect(getUserDetailsFromAccessToken).toHaveBeenCalledWith('valid_token');
expect(deleteUserFromCognito).toHaveBeenCalledWith(mockUserDetails.username);
expect(handlerUtil.successResponse).toHaveBeenCalledWith({
message: 'User successfully deleted',
});
});

it('should handle unexpected errors during deletion', async () => {
(deleteUserFromCognito as jest.Mock).mockRejectedValue(new Error('Unexpected error'));
await invokeHandler({
headers: { Authorization: 'Bearer valid_token' },
});
expect(handlerUtil.errorResponse).toHaveBeenCalledWith('Unexpected error', 500);
});
});

0 comments on commit ee021e5

Please sign in to comment.