From b54d428cde8b706128a0d8a3f39b32a260ea69f6 Mon Sep 17 00:00:00 2001 From: ghzhost Date: Sun, 16 Aug 2026 00:28:42 +0000 Subject: [PATCH] fix(users): authorize stellar-address updates by caller ID (#39) - Validate that authenticated user (req.user.userId) matches the URL :id param - Throw 403 ForbiddenException when caller attempts to update another user's stellar address - Add unit tests for UsersController verifying authorized updates and 403 rejections --- src/users/users.controller.spec.ts | 74 ++++++++++++++++++++++++++++++ src/users/users.controller.ts | 27 ++++++++++- 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 src/users/users.controller.spec.ts diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts new file mode 100644 index 0000000..55fe263 --- /dev/null +++ b/src/users/users.controller.spec.ts @@ -0,0 +1,74 @@ +import { ForbiddenException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthenticatedRequest, UsersController } from './users.controller'; +import { UsersService } from './users.service'; +import { User } from '../common/entities'; + +describe('UsersController', () => { + let controller: UsersController; + + const mockUsersService = { + list: jest.fn(), + findById: jest.fn(), + setStellarAddress: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UsersController], + providers: [ + { + provide: UsersService, + useValue: mockUsersService, + }, + ], + }).compile(); + + controller = module.get(UsersController); + jest.clearAllMocks(); + }); + + describe('setStellarAddress', () => { + it('allows a user to update their own stellar address', async () => { + const userId = 'user-123'; + const dto = { + stellarAddress: + 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5', + }; + const req = { + user: { userId: 'user-123', username: 'alice' }, + } as unknown as AuthenticatedRequest; + const updatedUser = { + id: userId, + stellarAddress: dto.stellarAddress, + } as User; + + mockUsersService.setStellarAddress.mockResolvedValue(updatedUser); + + const result = await controller.setStellarAddress(userId, dto, req); + + expect(mockUsersService.setStellarAddress).toHaveBeenCalledWith( + userId, + dto.stellarAddress, + ); + expect(result).toEqual(updatedUser); + }); + + it('rejects update when authenticated user does not match the target id (403 Forbidden)', () => { + const targetUserId = 'user-target-456'; + const dto = { + stellarAddress: + 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5', + }; + const req = { + user: { userId: 'user-attacker-123', username: 'eve' }, + } as unknown as AuthenticatedRequest; + + expect(() => + controller.setStellarAddress(targetUserId, dto, req), + ).toThrow(ForbiddenException); + + expect(mockUsersService.setStellarAddress).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 37a22bb..e033b5a 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -1,9 +1,28 @@ -import { Body, Controller, Get, Param, Patch, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + ForbiddenException, + Get, + Param, + Patch, + Req, + UseGuards, +} from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { Request } from 'express'; import { IsString } from 'class-validator'; import { UsersService } from './users.service'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +export interface AuthenticatedUser { + userId: string; + username: string; +} + +export interface AuthenticatedRequest extends Request { + user?: AuthenticatedUser; +} + class SetStellarAddressDto { @IsString() stellarAddress: string; @@ -30,7 +49,13 @@ export class UsersController { setStellarAddress( @Param('id') id: string, @Body() dto: SetStellarAddressDto, + @Req() req: AuthenticatedRequest, ) { + if (req.user?.userId !== id) { + throw new ForbiddenException( + 'You are not authorized to update another user stellar address', + ); + } return this.usersService.setStellarAddress(id, dto.stellarAddress); } }