diff --git a/CHANGELOG.md b/CHANGELOG.md index e9925f740..3c66d1c50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed the `transactionCount` in the portfolio calculator and service - Refreshed the cryptocurrencies list +### Fixed + +- Fixed the accounts of the assistant for the impersonation mode +- Fixed the tags of the assistant for the impersonation mode + ## 2.236.0 - 2026-02-05 ### Changed diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index 397ae016b..6346ce43a 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -1,8 +1,11 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; +import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { DeleteOwnUserDto, UpdateOwnAccessTokenDto, @@ -28,7 +31,8 @@ import { Param, Post, Put, - UseGuards + UseGuards, + UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; @@ -43,6 +47,7 @@ import { UserService } from './user.service'; export class UserController { public constructor( private readonly configurationService: ConfigurationService, + private readonly impersonationService: ImpersonationService, private readonly jwtService: JwtService, private readonly prismaService: PrismaService, private readonly propertyService: PropertyService, @@ -107,13 +112,19 @@ export class UserController { @Get() @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + @UseInterceptors(RedactValuesInResponseInterceptor) public async getUser( - @Headers('accept-language') acceptLanguage: string + @Headers('accept-language') acceptLanguage: string, + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string ): Promise { - return this.userService.getUser( - this.request.user, - acceptLanguage?.split(',')?.[0] - ); + const impersonationUserId = + await this.impersonationService.validateImpersonationId(impersonationId); + + return this.userService.getUser({ + impersonationUserId, + locale: acceptLanguage?.split(',')?.[0], + user: this.request.user + }); } @Post() diff --git a/apps/api/src/app/user/user.module.ts b/apps/api/src/app/user/user.module.ts index 8a21b0a55..7ca68d275 100644 --- a/apps/api/src/app/user/user.module.ts +++ b/apps/api/src/app/user/user.module.ts @@ -1,7 +1,9 @@ import { OrderModule } from '@ghostfolio/api/app/order/order.module'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; +import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { I18nModule } from '@ghostfolio/api/services/i18n/i18n.module'; +import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; @@ -18,6 +20,7 @@ import { UserService } from './user.service'; imports: [ ConfigurationModule, I18nModule, + ImpersonationModule, JwtModule.register({ secret: process.env.JWT_SECRET_KEY, signOptions: { expiresIn: '30 days' } @@ -25,6 +28,7 @@ import { UserService } from './user.service'; OrderModule, PrismaModule, PropertyModule, + RedactValuesInResponseModule, SubscriptionModule, TagModule ], diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 3280fbfac..def0b94d9 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -30,7 +30,7 @@ import { PROPERTY_IS_READ_ONLY_MODE, PROPERTY_SYSTEM_MESSAGE, TAG_ID_EXCLUDE_FROM_ANALYSIS, - locale + locale as defaultLocale } from '@ghostfolio/common/config'; import { User as IUser, @@ -49,7 +49,7 @@ import { Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { Prisma, Role, User } from '@prisma/client'; import { differenceInDays, subDays } from 'date-fns'; -import { sortBy, without } from 'lodash'; +import { without } from 'lodash'; import { createHmac } from 'node:crypto'; @Injectable() @@ -96,10 +96,17 @@ export class UserService { return { accessToken, hashedAccessToken }; } - public async getUser( - { accounts, id, permissions, settings, subscription }: UserWithSettings, - aLocale = locale - ): Promise { + public async getUser({ + impersonationUserId, + locale = defaultLocale, + user + }: { + impersonationUserId: string; + locale?: string; + user: UserWithSettings; + }): Promise { + const { id, permissions, settings, subscription } = user; + const userData = await Promise.all([ this.prismaService.access.findMany({ include: { @@ -108,22 +115,31 @@ export class UserService { orderBy: { alias: 'asc' }, where: { granteeUserId: id } }), + this.prismaService.account.findMany({ + orderBy: { + name: 'asc' + }, + where: { + userId: impersonationUserId || user.id + } + }), this.prismaService.order.count({ - where: { userId: id } + where: { userId: impersonationUserId || user.id } }), this.prismaService.order.findFirst({ orderBy: { date: 'asc' }, - where: { userId: id } + where: { userId: impersonationUserId || user.id } }), - this.tagService.getTagsForUser(id) + this.tagService.getTagsForUser(impersonationUserId || user.id) ]); const access = userData[0]; - const activitiesCount = userData[1]; - const firstActivity = userData[2]; - let tags = userData[3].filter((tag) => { + const accounts = userData[1]; + const activitiesCount = userData[2]; + const firstActivity = userData[3]; + let tags = userData[4].filter((tag) => { return tag.id !== TAG_ID_EXCLUDE_FROM_ANALYSIS; }); @@ -146,7 +162,6 @@ export class UserService { } return { - accounts, activitiesCount, id, permissions, @@ -160,10 +175,13 @@ export class UserService { permissions: accessItem.permissions }; }), + accounts: accounts.sort((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }), dateOfFirstActivity: firstActivity?.date ?? new Date(), settings: { ...(settings.settings as UserSettings), - locale: (settings.settings as UserSettings)?.locale ?? aLocale + locale: (settings.settings as UserSettings)?.locale ?? locale } }; } @@ -516,9 +534,10 @@ export class UserService { currentPermissions.push(permissions.impersonateAllUsers); } - user.accounts = sortBy(user.accounts, ({ name }) => { - return name.toLowerCase(); + user.accounts = user.accounts.sort((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); }); + user.permissions = currentPermissions.sort(); return user; diff --git a/apps/api/src/services/tag/tag.service.ts b/apps/api/src/services/tag/tag.service.ts index eb2d7bfef..f4cbd4cb1 100644 --- a/apps/api/src/services/tag/tag.service.ts +++ b/apps/api/src/services/tag/tag.service.ts @@ -75,12 +75,16 @@ export class TagService { } }); - return tags.map(({ _count, id, name, userId }) => ({ - id, - name, - userId, - isUsed: _count.activities > 0 - })); + return tags + .map(({ _count, id, name, userId }) => ({ + id, + name, + userId, + isUsed: _count.activities > 0 + })) + .sort((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }); } public async getTagsWithActivityCount() {