From 2eb169221621d3a4428b0452af6ccc7e8050b895 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Mon, 17 Aug 2026 14:59:01 +0200 Subject: [PATCH] feat: expose current user and preference endpoints --- backend/apps/api/src/api.module.ts | 3 +- .../api/src/users/users.controller.spec.ts | 95 +++++++++++++++++++ .../apps/api/src/users/users.controller.ts | 66 +++++++++++++ backend/apps/api/src/users/users.module.ts | 10 ++ 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 backend/apps/api/src/users/users.controller.spec.ts create mode 100644 backend/apps/api/src/users/users.controller.ts create mode 100644 backend/apps/api/src/users/users.module.ts diff --git a/backend/apps/api/src/api.module.ts b/backend/apps/api/src/api.module.ts index f25558b..dddd396 100644 --- a/backend/apps/api/src/api.module.ts +++ b/backend/apps/api/src/api.module.ts @@ -4,9 +4,10 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { HealthModule } from './health/health.module'; import { VersionModule } from './version/version.module'; +import { UsersApiModule } from './users/users.module'; @Module({ - imports: [ConfigurationModule, HealthModule, VersionModule], + imports: [ConfigurationModule, HealthModule, VersionModule, UsersApiModule], controllers: [AppController], providers: [AppService], }) diff --git a/backend/apps/api/src/users/users.controller.spec.ts b/backend/apps/api/src/users/users.controller.spec.ts new file mode 100644 index 0000000..e836f81 --- /dev/null +++ b/backend/apps/api/src/users/users.controller.spec.ts @@ -0,0 +1,95 @@ +import { UsersController } from './users.controller'; +import type { AuthenticatedUser } from '../../../../libs/auth/src'; + +describe('UsersController', () => { + const currentUser: AuthenticatedUser = { + id: 'u1', + externalSubjectId: 'sub-1', + displayName: 'Alex', + email: 'a@example.com', + }; + + it('GET /users/me returns the current user profile', async () => { + const usersService = { + findById: jest.fn().mockResolvedValue({ + id: 'u1', + externalSubjectId: 'sub-1', + displayName: 'Alex', + email: 'a@example.com', + createdAt: new Date('2026-01-01'), + updatedAt: new Date('2026-01-02'), + }), + }; + const preferencesService = { getOrDefault: jest.fn(), upsert: jest.fn() }; + const controller = new UsersController( + usersService as never, + preferencesService as never, + ); + + const result = await controller.me(currentUser); + + expect(usersService.findById).toHaveBeenCalledWith('u1'); + expect(result).toEqual({ + id: 'u1', + displayName: 'Alex', + email: 'a@example.com', + createdAt: new Date('2026-01-01'), + updatedAt: new Date('2026-01-02'), + }); + }); + + it('GET /users/me/preferences returns the effective preference', async () => { + const preference = { + userId: 'u1', + preferredPace: null, + preferredBudgetLevel: null, + maxWalkingDistanceKm: null, + preferredStartTime: null, + childFriendlyPreferred: false, + interests: [], + notes: null, + }; + const usersService = { findById: jest.fn() }; + const preferencesService = { + getOrDefault: jest.fn().mockResolvedValue(preference), + upsert: jest.fn(), + }; + const controller = new UsersController( + usersService as never, + preferencesService as never, + ); + + await expect(controller.getPreferences(currentUser)).resolves.toEqual( + preference, + ); + expect(preferencesService.getOrDefault).toHaveBeenCalledWith('u1'); + }); + + it('PUT /users/me/preferences round-trips the dto through the service', async () => { + const dto = { childFriendlyPreferred: true }; + const updated = { + userId: 'u1', + preferredPace: null, + preferredBudgetLevel: null, + maxWalkingDistanceKm: null, + preferredStartTime: null, + childFriendlyPreferred: true, + interests: [], + notes: null, + }; + const usersService = { findById: jest.fn() }; + const preferencesService = { + getOrDefault: jest.fn(), + upsert: jest.fn().mockResolvedValue(updated), + }; + const controller = new UsersController( + usersService as never, + preferencesService as never, + ); + + await expect( + controller.updatePreferences(currentUser, dto), + ).resolves.toEqual(updated); + expect(preferencesService.upsert).toHaveBeenCalledWith('u1', dto); + }); +}); diff --git a/backend/apps/api/src/users/users.controller.ts b/backend/apps/api/src/users/users.controller.ts new file mode 100644 index 0000000..3bf3115 --- /dev/null +++ b/backend/apps/api/src/users/users.controller.ts @@ -0,0 +1,66 @@ +import { + Body, + Controller, + Get, + NotFoundException, + Put, + UseGuards, +} from '@nestjs/common'; +import { OidcAuthGuard } from '../../../../libs/auth/src'; +import type { AuthenticatedUser } from '../../../../libs/auth/src'; +import { + UsersService, + UserPreferencesService, +} from '../../../../libs/users/src'; +import type { + UpdateUserPreferenceDto, + UserPreference, +} from '../../../../libs/users/src'; +import { CurrentUser } from '../auth/current-user.decorator'; + +interface UserProfileResponse { + id: string; + displayName: string; + email: string; + createdAt: Date; + updatedAt: Date; +} + +@Controller('users/me') +@UseGuards(OidcAuthGuard) +export class UsersController { + constructor( + private readonly usersService: UsersService, + private readonly preferencesService: UserPreferencesService, + ) {} + + @Get() + async me( + @CurrentUser() currentUser: AuthenticatedUser, + ): Promise { + const user = await this.usersService.findById(currentUser.id); + if (!user) throw new NotFoundException('User not found'); + return { + id: user.id, + displayName: user.displayName, + email: user.email, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }; + } + + @Get('preferences') + getPreferences( + @CurrentUser() currentUser: AuthenticatedUser, + ): Promise { + return this.preferencesService.getOrDefault(currentUser.id); + } + + @Put('preferences') + updatePreferences( + @CurrentUser() currentUser: AuthenticatedUser, + @Body() dto: UpdateUserPreferenceDto, + ): Promise { + return this.preferencesService.upsert(currentUser.id, dto); + } +} diff --git a/backend/apps/api/src/users/users.module.ts b/backend/apps/api/src/users/users.module.ts new file mode 100644 index 0000000..0438c82 --- /dev/null +++ b/backend/apps/api/src/users/users.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { AuthModule } from '../../../../libs/auth/src'; +import { UsersLibModule } from '../../../../libs/users/src'; +import { UsersController } from './users.controller'; + +@Module({ + imports: [AuthModule, UsersLibModule], + controllers: [UsersController], +}) +export class UsersApiModule {}