feat: expose current user and preference endpoints
This commit is contained in:
@@ -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],
|
||||||
})
|
})
|
||||||
|
|||||||
95
backend/apps/api/src/users/users.controller.spec.ts
Normal file
95
backend/apps/api/src/users/users.controller.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
66
backend/apps/api/src/users/users.controller.ts
Normal file
66
backend/apps/api/src/users/users.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
backend/apps/api/src/users/users.module.ts
Normal file
10
backend/apps/api/src/users/users.module.ts
Normal 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 {}
|
||||||
Reference in New Issue
Block a user