feat: expose current user and preference endpoints

This commit is contained in:
Bastian Wagner
2026-08-17 14:59:01 +02:00
parent 5abc7cbe29
commit 2eb1692216
4 changed files with 173 additions and 1 deletions

View File

@@ -4,9 +4,10 @@ import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { HealthModule } from './health/health.module'; import { HealthModule } from './health/health.module';
import { VersionModule } from './version/version.module'; import { VersionModule } from './version/version.module';
import { UsersApiModule } from './users/users.module';
@Module({ @Module({
imports: [ConfigurationModule, HealthModule, VersionModule], imports: [ConfigurationModule, HealthModule, VersionModule, UsersApiModule],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],
}) })

View File

@@ -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);
});
});

View File

@@ -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<UserProfileResponse> {
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<UserPreference> {
return this.preferencesService.getOrDefault(currentUser.id);
}
@Put('preferences')
updatePreferences(
@CurrentUser() currentUser: AuthenticatedUser,
@Body() dto: UpdateUserPreferenceDto,
): Promise<UserPreference> {
return this.preferencesService.upsert(currentUser.id, dto);
}
}

View File

@@ -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 {}