fix(backend): enforce user ownership when deleting PAT (#5916)

Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Mir Arif Hasan 2026-03-03 00:05:19 +06:00 committed by GitHub
parent d6ea86dcca
commit 1f4ae3dd88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 55 additions and 17 deletions

View file

@ -51,8 +51,14 @@ export class AccessTokenController {
@Delete('revoke') @Delete('revoke')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
async deletePAT(@Query('id') id: string) { async deletePAT(@GqlUser() user: AuthUser, @Query('id') id: string) {
const result = await this.accessTokenService.deletePAT(id); if (!id) {
throw new BadRequestException(
createCLIErrorResponse(ACCESS_TOKENS_INVALID_DATA_ID),
);
}
const result = await this.accessTokenService.deletePAT(id, user.uid);
if (E.isLeft(result)) throwHTTPErr(result.left); if (E.isLeft(result)) throwHTTPErr(result.left);
return result.right; return result.right;

View file

@ -112,11 +112,17 @@ describe('AccessTokenService', () => {
describe('deletePAT', () => { describe('deletePAT', () => {
test('should throw ACCESS_TOKEN_NOT_FOUND if Access Token is not found', async () => { test('should throw ACCESS_TOKEN_NOT_FOUND if Access Token is not found', async () => {
mockPrisma.personalAccessToken.delete.mockRejectedValueOnce( mockPrisma.personalAccessToken.deleteMany.mockResolvedValueOnce({
'RecordNotFound', count: 0,
); });
const result = await accessTokenService.deletePAT(userAccessToken.id); const result = await accessTokenService.deletePAT(
userAccessToken.id,
user.uid,
);
expect(mockPrisma.personalAccessToken.deleteMany).toHaveBeenCalledWith({
where: { id: userAccessToken.id, userUid: user.uid },
});
expect(result).toEqualLeft({ expect(result).toEqualLeft({
message: ACCESS_TOKEN_NOT_FOUND, message: ACCESS_TOKEN_NOT_FOUND,
statusCode: HttpStatus.NOT_FOUND, statusCode: HttpStatus.NOT_FOUND,
@ -124,13 +130,37 @@ describe('AccessTokenService', () => {
}); });
test('should successfully delete a new Access Token', async () => { test('should successfully delete a new Access Token', async () => {
mockPrisma.personalAccessToken.delete.mockResolvedValueOnce( mockPrisma.personalAccessToken.deleteMany.mockResolvedValueOnce({
userAccessToken, count: 1,
); });
const result = await accessTokenService.deletePAT(userAccessToken.id); const result = await accessTokenService.deletePAT(
userAccessToken.id,
user.uid,
);
expect(mockPrisma.personalAccessToken.deleteMany).toHaveBeenCalledWith({
where: { id: userAccessToken.id, userUid: user.uid },
});
expect(result).toEqualRight(true); expect(result).toEqualRight(true);
}); });
test('should throw ACCESS_TOKEN_NOT_FOUND when token belongs to a different user', async () => {
mockPrisma.personalAccessToken.deleteMany.mockResolvedValueOnce({
count: 0,
});
const result = await accessTokenService.deletePAT(
userAccessToken.id,
'different-user-uid',
);
expect(mockPrisma.personalAccessToken.deleteMany).toHaveBeenCalledWith({
where: { id: userAccessToken.id, userUid: 'different-user-uid' },
});
expect(result).toEqualLeft({
message: ACCESS_TOKEN_NOT_FOUND,
statusCode: HttpStatus.NOT_FOUND,
});
});
}); });
describe('listAllUserPAT', () => { describe('listAllUserPAT', () => {

View file

@ -103,20 +103,22 @@ export class AccessTokenService {
* Delete a Personal Access Token * Delete a Personal Access Token
* *
* @param accessTokenID ID of the Personal Access Token * @param accessTokenID ID of the Personal Access Token
* @param userUid UID of the user requesting the deletion
* @returns Either of true or error message * @returns Either of true or error message
*/ */
async deletePAT(accessTokenID: string) { async deletePAT(accessTokenID: string, userUid: string) {
try { const { count } = await this.prisma.personalAccessToken.deleteMany({
await this.prisma.personalAccessToken.delete({ where: { id: accessTokenID, userUid },
where: { id: accessTokenID }, });
});
return E.right(true); if (count === 0) {
} catch {
return E.left({ return E.left({
message: ACCESS_TOKEN_NOT_FOUND, message: ACCESS_TOKEN_NOT_FOUND,
statusCode: HttpStatus.NOT_FOUND, statusCode: HttpStatus.NOT_FOUND,
}); });
} }
return E.right(true);
} }
/** /**