Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pull] main from ghostfolio:main #523

Merged
merged 7 commits into from
Mar 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

- Improved the usability of the user account registration

### Changed

- Improved the usability of the user account registration
- Improved the usability of the _Copy AI prompt to clipboard_ actions on the analysis page (experimental)
- Formatted the name in the _Financial Modeling Prep_ service
- Removed the exchange rates from the overview of the admin control panel
- Improved the language localization for German (`de`)
- Upgraded `angular` from version `19.0.5` to `19.2.1`
- Upgraded `Nx` from version `20.3.2` to `20.5.0`
- Upgraded `prettier` from version `3.5.1` to `3.5.3`
- Upgraded `prisma` from version `6.4.1` to `6.5.0`

### Fixed

- Fixed an issue with serving _Storybook_ related to the `contentSecurityPolicy`

## 2.145.1 - 2025-03-10

Expand Down
28 changes: 0 additions & 28 deletions apps/api/src/app/admin/admin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
DEFAULT_CURRENCY,
PROPERTY_CURRENCIES,
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_IS_USER_SIGNUP_ENABLED
Expand All @@ -37,7 +36,6 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import {
AssetClass,
AssetSubClass,
DataSource,
Prisma,
PrismaClient,
Property,
Expand Down Expand Up @@ -132,39 +130,13 @@ export class AdminService {
}

public async get(): Promise<AdminData> {
const exchangeRates = this.exchangeRateDataService
.getCurrencies()
.filter((currency) => {
return currency !== DEFAULT_CURRENCY;
})
.map((currency) => {
const label1 = DEFAULT_CURRENCY;
const label2 = currency;

return {
label1,
label2,
dataSource:
DataSource[
this.configurationService.get('DATA_SOURCE_EXCHANGE_RATES')
],
symbol: `${label1}${label2}`,
value: this.exchangeRateDataService.toCurrency(
1,
DEFAULT_CURRENCY,
currency
)
};
});

const [settings, transactionCount, userCount] = await Promise.all([
this.propertyService.get(),
this.prismaService.order.count(),
this.countUsersWithAnalytics()
]);

return {
exchangeRates,
settings,
transactionCount,
userCount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class TransformDataSourceInRequestInterceptor<T>
const request = http.getRequest();

if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
if (request.body.activities) {
if (request.body?.activities) {
request.body.activities = request.body.activities.map((activity) => {
if (DataSource[activity.dataSource]) {
return activity;
Expand Down
35 changes: 21 additions & 14 deletions apps/api/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { STORYBOOK_PATH } from '@ghostfolio/common/config';

import {
Logger,
LogLevel,
Expand All @@ -7,6 +9,7 @@ import {
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import type { NestExpressApplication } from '@nestjs/platform-express';
import { NextFunction, Request, Response } from 'express';
import helmet from 'helmet';

import { AppModule } from './app/app.module';
Expand Down Expand Up @@ -50,20 +53,24 @@ async function bootstrap() {
app.useBodyParser('json', { limit: '10mb' });

if (configService.get<string>('ENABLE_FEATURE_SUBSCRIPTION') === 'true') {
app.use(
helmet({
contentSecurityPolicy: {
directives: {
connectSrc: ["'self'", 'https://js.stripe.com'], // Allow connections to Stripe
frameSrc: ["'self'", 'https://js.stripe.com'], // Allow loading frames from Stripe
scriptSrc: ["'self'", "'unsafe-inline'", 'https://js.stripe.com'], // Allow inline scripts and scripts from Stripe
scriptSrcAttr: ["'self'", "'unsafe-inline'"], // Allow inline event handlers
styleSrc: ["'self'", "'unsafe-inline'"] // Allow inline styles
}
},
crossOriginOpenerPolicy: false // Disable Cross-Origin-Opener-Policy header (for Internet Identity)
})
);
app.use((req: Request, res: Response, next: NextFunction) => {
if (req.path.startsWith(STORYBOOK_PATH)) {
next();
} else {
helmet({
contentSecurityPolicy: {
directives: {
connectSrc: ["'self'", 'https://js.stripe.com'], // Allow connections to Stripe
frameSrc: ["'self'", 'https://js.stripe.com'], // Allow loading frames from Stripe
scriptSrc: ["'self'", "'unsafe-inline'", 'https://js.stripe.com'], // Allow inline scripts and scripts from Stripe
scriptSrcAttr: ["'self'", "'unsafe-inline'"], // Allow inline event handlers
styleSrc: ["'self'", "'unsafe-inline'"] // Allow inline styles
}
},
crossOriginOpenerPolicy: false // Disable Cross-Origin-Opener-Policy header (for Internet Identity)
})(req, res, next);
}
});
}

app.use(HtmlTemplateMiddleware);
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/middlewares/html-template.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
import {
DEFAULT_LANGUAGE_CODE,
DEFAULT_ROOT_URL,
STORYBOOK_PATH,
SUPPORTED_LANGUAGE_CODES
} from '@ghostfolio/common/config';
import { DATE_FORMAT, interpolate } from '@ghostfolio/common/helper';
Expand Down Expand Up @@ -129,7 +130,7 @@ export const HtmlTemplateMiddleware = async (

if (
path.startsWith('/api/') ||
path.startsWith('/development/storybook') ||
path.startsWith(STORYBOOK_PATH) ||
isFileRequest(path) ||
!environment.production
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
IDataProviderHistoricalResponse,
IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { REPLACE_NAME_PARTS } from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import {
DataProviderInfo,
Expand Down Expand Up @@ -186,7 +187,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
response.isin = assetProfile.isin;
}

response.name = assetProfile.companyName;
response.name = this.formatName({ name: assetProfile.companyName });

if (assetProfile.website) {
response.url = assetProfile.website;
Expand Down Expand Up @@ -398,7 +399,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
assetSubClass: undefined, // TODO
dataProviderInfo: this.getDataProviderInfo(),
dataSource: this.getName(),
name: companyName
name: this.formatName({ name: companyName })
};
});
} else {
Expand All @@ -414,12 +415,12 @@ export class FinancialModelingPrepService implements DataProviderInterface {
items = result.map(({ currency, name, symbol }) => {
return {
currency,
name,
symbol,
assetClass: undefined, // TODO
assetSubClass: undefined, // TODO
dataProviderInfo: this.getDataProviderInfo(),
dataSource: this.getName()
dataSource: this.getName(),
name: this.formatName({ name })
};
});
}
Expand All @@ -438,6 +439,18 @@ export class FinancialModelingPrepService implements DataProviderInterface {
return { items };
}

private formatName({ name }: { name: string }) {
if (name) {
for (const part of REPLACE_NAME_PARTS) {
name = name.replace(part, '');
}

name = name.trim();
}

return name;
}

private getUrl({ version }: { version: number }) {
return `https://financialmodelingprep.com/api/v${version}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
PROPERTY_COUPONS,
PROPERTY_CURRENCIES,
PROPERTY_IS_DATA_GATHERING_ENABLED,
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_IS_USER_SIGNUP_ENABLED,
Expand Down Expand Up @@ -41,8 +40,6 @@ import { takeUntil } from 'rxjs/operators';
export class AdminOverviewComponent implements OnDestroy, OnInit {
public couponDuration: StringValue = '14 days';
public coupons: Coupon[];
public customCurrencies: string[];
public exchangeRates: { label1: string; label2: string; value: number }[];
public hasPermissionForSubscription: boolean;
public hasPermissionForSystemMessage: boolean;
public hasPermissionToToggleReadOnlyMode: boolean;
Expand Down Expand Up @@ -138,19 +135,6 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
});
}

public onDeleteCurrency(aCurrency: string) {
this.notificationService.confirm({
confirmFn: () => {
const currencies = this.customCurrencies.filter((currency) => {
return currency !== aCurrency;
});
this.putAdminSetting({ key: PROPERTY_CURRENCIES, value: currencies });
},
confirmType: ConfirmationDialogType.Warn,
title: $localize`Do you really want to delete this currency?`
});
}

public onDeleteSystemMessage() {
this.notificationService.confirm({
confirmFn: () => {
Expand Down Expand Up @@ -231,25 +215,17 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
this.adminService
.fetchAdminData()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(
({ exchangeRates, settings, transactionCount, userCount, version }) => {
this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? [];
this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[];
this.exchangeRates = exchangeRates;
this.isDataGatheringEnabled =
settings[PROPERTY_IS_DATA_GATHERING_ENABLED] === false
? false
: true;
this.systemMessage = settings[
PROPERTY_SYSTEM_MESSAGE
] as SystemMessage;
this.transactionCount = transactionCount;
this.userCount = userCount;
this.version = version;
.subscribe(({ settings, transactionCount, userCount, version }) => {
this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? [];
this.isDataGatheringEnabled =
settings[PROPERTY_IS_DATA_GATHERING_ENABLED] === false ? false : true;
this.systemMessage = settings[PROPERTY_SYSTEM_MESSAGE] as SystemMessage;
this.transactionCount = transactionCount;
this.userCount = userCount;
this.version = version;

this.changeDetectorRef.markForCheck();
}
);
this.changeDetectorRef.markForCheck();
});
}

private generateCouponCode(aLength: number) {
Expand Down
67 changes: 0 additions & 67 deletions apps/client/src/app/components/admin-overview/admin-overview.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,73 +30,6 @@
}
</div>
</div>
<div class="align-items-start d-flex my-3">
<div class="w-50" i18n>Exchange Rates</div>
<div class="w-50">
<table>
@for (exchangeRate of exchangeRates; track exchangeRate) {
<tr>
<td>
<gf-value [locale]="user?.settings?.locale" [value]="1" />
</td>
<td class="pl-1">{{ exchangeRate.label1 }}</td>
<td class="px-1">=</td>
<td align="right">
<gf-value
class="d-inline-block"
[locale]="user?.settings?.locale"
[precision]="4"
[value]="exchangeRate.value"
/>
</td>
<td class="pl-1">{{ exchangeRate.label2 }}</td>
<td>
<button
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="exchangeRateActionsMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-horizontal" />
</button>
<mat-menu
#exchangeRateActionsMenu="matMenu"
class="h-100 mx-1 no-min-width px-2"
xPosition="before"
>
<a
mat-menu-item
[queryParams]="{
assetProfileDialog: true,
dataSource: exchangeRate.dataSource,
symbol: exchangeRate.symbol
}"
[routerLink]="['/admin', 'market-data']"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
<span i18n>Edit</span>
</span>
</a>
@if (customCurrencies.includes(exchangeRate.label2)) {
<hr class="m-0" />
<button
mat-menu-item
(click)="onDeleteCurrency(exchangeRate.label2)"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Delete</span>
</span>
</button>
}
</mat-menu>
</td>
</tr>
}
</table>
</div>
</div>
<div class="d-flex my-3">
<div class="w-50" i18n>User Signup</div>
<div class="w-50">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
</button>
<mat-menu
#assistantMenu="matMenu"
class="assistant"
class="no-max-width"
xPosition="before"
[overlapTrigger]="true"
(closed)="assistantElement?.setIsOpen(false)"
Expand Down
Loading
Loading