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 { 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],
|
||||
})
|
||||
|
||||
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