feat(quick-260630-gbh-01): avatar storage endpoints + enriched /auth/me

- Add avatarPath String? column to User model (migration: add_user_avatar)
- POST /users/me/avatar: 2MB limit, image/png/jpeg/webp allowlist, writes to user-files/avatars/{userId}.{ext}
- GET /users/me/avatar: streams avatar with Cache-Control: no-store
- AuthService.getMe(): returns isLocalUser + hasAvatar without leaking passwordHash/ldapDn
- AuthController GET /auth/me: now returns enriched profile via getMe()
This commit is contained in:
2026-06-30 11:57:33 +02:00
parent dcba4b9977
commit 0fba45d2c2
5 changed files with 159 additions and 3 deletions
+4 -3
View File
@@ -53,11 +53,12 @@ export class AuthController {
/**
* GET /auth/me
* Returns the current user from JWT (session check).
* Returns enriched user profile: public fields + isLocalUser + hasAvatar.
* T-gbh-03: passwordHash and ldapDn are never serialised in the response.
*/
@Get('me')
me(@CurrentUser() user: any) {
return user;
async me(@CurrentUser() user: any) {
return this.authService.getMe(user.id);
}
/**
+34
View File
@@ -182,6 +182,40 @@ export class AuthService {
this.logger.log(`Password reset completed for user ${resetToken.userId}`);
}
/**
* Return enriched profile for the currently authenticated user.
* T-gbh-03: Only public fields + isLocalUser/hasAvatar returned — never
* passwordHash or ldapDn.
*/
async getMe(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
username: true,
displayName: true,
role: true,
tenantId: true,
mustChangePassword: true,
passwordHash: true,
ldapDn: true,
avatarPath: true,
},
});
if (!user) {
return null;
}
const { passwordHash, ldapDn, avatarPath, ...publicFields } = user;
return {
...publicFields,
isLocalUser: !!passwordHash && !ldapDn,
hasAvatar: !!avatarPath,
};
}
/**
* Change password for the currently logged-in user.
* Verifies current password before allowing change.