Browse Source

Merge branch 'main' into Task/refresh-cryptocurrencies-list-20251121

pull/5967/head
Thomas Kaul 1 month ago
committed by GitHub
parent
commit
d9e6030446
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 4
      apps/api/src/app/admin/admin.service.ts
  3. 2
      apps/api/src/app/export/export.controller.ts
  4. 16
      apps/api/src/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor.ts
  5. 54
      apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts
  6. 27
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html
  7. 4
      libs/common/src/lib/interfaces/admin-user.interface.ts

6
CHANGELOG.md

@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Extended the user detail dialog of the admin control panel’s users section by the authentication method
### Changed ### Changed
- Resolved the data source of the `GHOSTFOLIO` data provider in the export functionality
- Resolved the data source of the `GHOSTFOLIO` data provider in the import functionality
- Refreshed the cryptocurrencies list - Refreshed the cryptocurrencies list
## 2.218.0 - 2025-11-20 ## 2.218.0 - 2025-11-20

4
apps/api/src/app/admin/admin.service.ts

@ -876,6 +876,7 @@ export class AdminService {
}, },
createdAt: true, createdAt: true,
id: true, id: true,
provider: true,
role: true, role: true,
subscriptions: { subscriptions: {
orderBy: { orderBy: {
@ -892,7 +893,7 @@ export class AdminService {
}); });
return usersWithAnalytics.map( return usersWithAnalytics.map(
({ _count, analytics, createdAt, id, role, subscriptions }) => { ({ _count, analytics, createdAt, id, provider, role, subscriptions }) => {
const daysSinceRegistration = const daysSinceRegistration =
differenceInDays(new Date(), createdAt) + 1; differenceInDays(new Date(), createdAt) + 1;
const engagement = analytics const engagement = analytics
@ -909,6 +910,7 @@ export class AdminService {
createdAt, createdAt,
engagement, engagement,
id, id,
provider,
role, role,
subscription, subscription,
accountCount: _count.accounts || 0, accountCount: _count.accounts || 0,

2
apps/api/src/app/export/export.controller.ts

@ -1,5 +1,6 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ExportResponse } from '@ghostfolio/common/interfaces'; import { ExportResponse } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
@ -28,6 +29,7 @@ export class ExportController {
@Get() @Get()
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async export( public async export(
@Query('accounts') filterByAccounts?: string, @Query('accounts') filterByAccounts?: string,
@Query('activityIds') filterByActivityIds?: string, @Query('activityIds') filterByActivityIds?: string,

16
apps/api/src/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor.ts

@ -27,9 +27,23 @@ export class TransformDataSourceInRequestInterceptor<T>
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
if (request.body?.activities) { if (request.body?.activities) {
const dataSourceGhostfolioDataProvider = this.configurationService.get(
'DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER'
)?.[0];
request.body.activities = request.body.activities.map((activity) => { request.body.activities = request.body.activities.map((activity) => {
if (DataSource[activity.dataSource]) { if (DataSource[activity.dataSource]) {
return activity; if (
activity.dataSource === 'GHOSTFOLIO' &&
dataSourceGhostfolioDataProvider
) {
return {
...activity,
dataSource: dataSourceGhostfolioDataProvider
};
} else {
return activity;
}
} else { } else {
return { return {
...activity, ...activity,

54
apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts

@ -16,36 +16,56 @@ import { map } from 'rxjs/operators';
export class TransformDataSourceInResponseInterceptor<T> export class TransformDataSourceInResponseInterceptor<T>
implements NestInterceptor<T, any> implements NestInterceptor<T, any>
{ {
private encodedDataSourceMap: {
[dataSource: string]: string;
} = {};
public constructor( public constructor(
private readonly configurationService: ConfigurationService private readonly configurationService: ConfigurationService
) {} ) {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
this.encodedDataSourceMap = Object.keys(DataSource).reduce(
(encodedDataSourceMap, dataSource) => {
if (!['GHOSTFOLIO', 'MANUAL'].includes(dataSource)) {
encodedDataSourceMap[dataSource] = encodeDataSource(
DataSource[dataSource]
);
}
return encodedDataSourceMap;
},
{}
);
}
}
public intercept( public intercept(
_context: ExecutionContext, context: ExecutionContext,
next: CallHandler<T> next: CallHandler<T>
): Observable<any> { ): Observable<any> {
const isExportMode = context.getClass().name === 'ExportController';
return next.handle().pipe( return next.handle().pipe(
map((data: any) => { map((data: any) => {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
const valueMap = this.encodedDataSourceMap;
if (isExportMode) {
for (const dataSource of this.configurationService.get(
'DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER'
)) {
valueMap[dataSource] = 'GHOSTFOLIO';
}
}
data = redactAttributes({ data = redactAttributes({
object: data,
options: [ options: [
{ {
attribute: 'dataSource', valueMap,
valueMap: Object.keys(DataSource).reduce( attribute: 'dataSource'
(valueMap, dataSource) => {
if (!['MANUAL'].includes(dataSource)) {
valueMap[dataSource] = encodeDataSource(
DataSource[dataSource]
);
}
return valueMap;
},
{}
)
} }
], ]
object: data
}); });
} }

27
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html

@ -23,17 +23,38 @@
</div> </div>
<div class="mb-3 row"> <div class="mb-3 row">
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="user?.provider"
>Authentication</gf-value
>
</div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value i18n size="medium" [value]="user?.role">Role</gf-value> <gf-value i18n size="medium" [value]="user?.role">Role</gf-value>
</div> </div>
@if (data.hasPermissionForSubscription) { </div>
@if (data.hasPermissionForSubscription) {
<div class="mb-3 row">
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="user?.subscription ? 'Premium' : 'Basic'"
>Membership</gf-value
>
</div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value i18n size="medium" [value]="user?.country" <gf-value i18n size="medium" [value]="user?.country"
>Country</gf-value >Country</gf-value
> >
</div> </div>
} </div>
</div> }
<div class="mb-3 row"> <div class="mb-3 row">
<div class="col-6 mb-3"> <div class="col-6 mb-3">

4
libs/common/src/lib/interfaces/admin-user.interface.ts

@ -1,4 +1,4 @@
import { Role } from '@prisma/client'; import { Provider, Role, Subscription } from '@prisma/client';
export interface AdminUser { export interface AdminUser {
accountCount: number; accountCount: number;
@ -9,5 +9,7 @@ export interface AdminUser {
engagement: number; engagement: number;
id: string; id: string;
lastActivity: Date; lastActivity: Date;
provider: Provider;
role: Role; role: Role;
subscription?: Subscription;
} }

Loading…
Cancel
Save