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')
@UseGuards(JwtAuthGuard)
async deletePAT(@Query('id') id: string) {
const result = await this.accessTokenService.deletePAT(id);
async deletePAT(@GqlUser() user: AuthUser, @Query('id') id: string) {
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);
return result.right;

View file

@ -112,11 +112,17 @@ describe('AccessTokenService', () => {
describe('deletePAT', () => {
test('should throw ACCESS_TOKEN_NOT_FOUND if Access Token is not found', async () => {
mockPrisma.personalAccessToken.delete.mockRejectedValueOnce(
'RecordNotFound',
);
mockPrisma.personalAccessToken.deleteMany.mockResolvedValueOnce({
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({
message: ACCESS_TOKEN_NOT_FOUND,
statusCode: HttpStatus.NOT_FOUND,
@ -124,13 +130,37 @@ describe('AccessTokenService', () => {
});
test('should successfully delete a new Access Token', async () => {
mockPrisma.personalAccessToken.delete.mockResolvedValueOnce(
userAccessToken,
);
mockPrisma.personalAccessToken.deleteMany.mockResolvedValueOnce({
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);
});
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', () => {

View file

@ -103,20 +103,22 @@ export class AccessTokenService {
* Delete a 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
*/
async deletePAT(accessTokenID: string) {
try {
await this.prisma.personalAccessToken.delete({
where: { id: accessTokenID },
});
return E.right(true);
} catch {
async deletePAT(accessTokenID: string, userUid: string) {
const { count } = await this.prisma.personalAccessToken.deleteMany({
where: { id: accessTokenID, userUid },
});
if (count === 0) {
return E.left({
message: ACCESS_TOKEN_NOT_FOUND,
statusCode: HttpStatus.NOT_FOUND,
});
}
return E.right(true);
}
/**