mirror of https://github.com/ghostfolio/ghostfolio
88 changed files with 5870 additions and 4132 deletions
@ -0,0 +1,49 @@ |
|||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
|||
import { |
|||
DATE_FORMAT, |
|||
getYesterday, |
|||
interpolate |
|||
} from '@ghostfolio/common/helper'; |
|||
|
|||
import { Controller, Get, Res, VERSION_NEUTRAL, Version } from '@nestjs/common'; |
|||
import { format } from 'date-fns'; |
|||
import { Response } from 'express'; |
|||
import { readFileSync } from 'fs'; |
|||
import { join } from 'path'; |
|||
|
|||
import { SitemapService } from './sitemap.service'; |
|||
|
|||
@Controller('sitemap.xml') |
|||
export class SitemapController { |
|||
public sitemapXml = ''; |
|||
|
|||
public constructor( |
|||
private readonly configurationService: ConfigurationService, |
|||
private readonly sitemapService: SitemapService |
|||
) { |
|||
try { |
|||
this.sitemapXml = readFileSync( |
|||
join(__dirname, 'assets', 'sitemap.xml'), |
|||
'utf8' |
|||
); |
|||
} catch {} |
|||
} |
|||
|
|||
@Get() |
|||
@Version(VERSION_NEUTRAL) |
|||
public getSitemapXml(@Res() response: Response) { |
|||
const currentDate = format(getYesterday(), DATE_FORMAT); |
|||
|
|||
response.setHeader('content-type', 'application/xml'); |
|||
response.send( |
|||
interpolate(this.sitemapXml, { |
|||
currentDate, |
|||
personalFinanceTools: this.configurationService.get( |
|||
'ENABLE_FEATURE_SUBSCRIPTION' |
|||
) |
|||
? this.sitemapService.getPersonalFinanceTools({ currentDate }) |
|||
: '' |
|||
}) |
|||
); |
|||
} |
|||
} |
@ -1,11 +1,14 @@ |
|||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; |
|||
import { I18nModule } from '@ghostfolio/api/services/i18n/i18n.module'; |
|||
|
|||
import { Module } from '@nestjs/common'; |
|||
|
|||
import { SitemapController } from './sitemap.controller'; |
|||
import { SitemapService } from './sitemap.service'; |
|||
|
|||
@Module({ |
|||
controllers: [SitemapController], |
|||
imports: [ConfigurationModule] |
|||
imports: [ConfigurationModule, I18nModule], |
|||
providers: [SitemapService] |
|||
}) |
|||
export class SitemapModule {} |
@ -0,0 +1,47 @@ |
|||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
|||
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service'; |
|||
import { SUPPORTED_LANGUAGE_CODES } from '@ghostfolio/common/config'; |
|||
import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools'; |
|||
|
|||
import { Injectable } from '@nestjs/common'; |
|||
|
|||
@Injectable() |
|||
export class SitemapService { |
|||
public constructor( |
|||
private readonly configurationService: ConfigurationService, |
|||
private readonly i18nService: I18nService |
|||
) {} |
|||
|
|||
public getPersonalFinanceTools({ currentDate }: { currentDate: string }) { |
|||
const rootUrl = this.configurationService.get('ROOT_URL'); |
|||
|
|||
return personalFinanceTools |
|||
.map(({ alias, key }) => { |
|||
return SUPPORTED_LANGUAGE_CODES.map((languageCode) => { |
|||
const resourcesPath = this.i18nService.getTranslation({ |
|||
languageCode, |
|||
id: 'routes.resources' |
|||
}); |
|||
|
|||
const personalFinanceToolsPath = this.i18nService.getTranslation({ |
|||
languageCode, |
|||
id: 'routes.resources.personalFinanceTools' |
|||
}); |
|||
|
|||
const openSourceAlternativeToPath = this.i18nService.getTranslation({ |
|||
languageCode, |
|||
id: 'routes.resources.personalFinanceTools.openSourceAlternativeTo' |
|||
}); |
|||
|
|||
return [ |
|||
' <url>', |
|||
` <loc>${rootUrl}/${languageCode}/${resourcesPath}/${personalFinanceToolsPath}/${openSourceAlternativeToPath}-${alias ?? key}</loc>`, |
|||
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`, |
|||
' </url>' |
|||
].join('\n'); |
|||
}); |
|||
}) |
|||
.flat() |
|||
.join('\n'); |
|||
} |
|||
} |
@ -1,80 +0,0 @@ |
|||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
|||
import { |
|||
DATE_FORMAT, |
|||
getYesterday, |
|||
interpolate |
|||
} from '@ghostfolio/common/helper'; |
|||
import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools'; |
|||
|
|||
import { Controller, Get, Res, VERSION_NEUTRAL, Version } from '@nestjs/common'; |
|||
import { format } from 'date-fns'; |
|||
import { Response } from 'express'; |
|||
import { readFileSync } from 'fs'; |
|||
import { join } from 'path'; |
|||
|
|||
@Controller('sitemap.xml') |
|||
export class SitemapController { |
|||
public sitemapXml = ''; |
|||
|
|||
public constructor( |
|||
private readonly configurationService: ConfigurationService |
|||
) { |
|||
try { |
|||
this.sitemapXml = readFileSync( |
|||
join(__dirname, 'assets', 'sitemap.xml'), |
|||
'utf8' |
|||
); |
|||
} catch {} |
|||
} |
|||
|
|||
@Get() |
|||
@Version(VERSION_NEUTRAL) |
|||
public async getSitemapXml(@Res() response: Response): Promise<void> { |
|||
const currentDate = format(getYesterday(), DATE_FORMAT); |
|||
|
|||
response.setHeader('content-type', 'application/xml'); |
|||
response.send( |
|||
interpolate(this.sitemapXml, { |
|||
currentDate, |
|||
personalFinanceTools: this.configurationService.get( |
|||
'ENABLE_FEATURE_SUBSCRIPTION' |
|||
) |
|||
? personalFinanceTools |
|||
.map(({ alias, key }) => { |
|||
return [ |
|||
'<url>', |
|||
` <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-${alias ?? key}</loc>`, |
|||
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`, |
|||
'</url>', |
|||
'<url>', |
|||
` <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-${alias ?? key}</loc>`, |
|||
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`, |
|||
'</url>', |
|||
'<url>', |
|||
` <loc>https://ghostfol.io/es/recursos/personal-finance-tools/alternativa-de-software-libre-a-${alias ?? key}</loc>`, |
|||
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`, |
|||
'</url>', |
|||
'<url>', |
|||
` <loc>https://ghostfol.io/fr/ressources/personal-finance-tools/alternative-open-source-a-${alias ?? key}</loc>`, |
|||
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`, |
|||
'</url>', |
|||
'<url>', |
|||
` <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-${alias ?? key}</loc>`, |
|||
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`, |
|||
'</url>', |
|||
'<url>', |
|||
` <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-${alias ?? key}</loc>`, |
|||
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`, |
|||
'</url>', |
|||
'<url>', |
|||
` <loc>https://ghostfol.io/pt/recursos/personal-finance-tools/alternativa-de-software-livre-ao-${alias ?? key}</loc>`, |
|||
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`, |
|||
'</url>' |
|||
].join('\n'); |
|||
}) |
|||
.join('\n') |
|||
: '' |
|||
}) |
|||
); |
|||
} |
|||
} |
File diff suppressed because it is too large
@ -1,60 +0,0 @@ |
|||
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; |
|||
import { DataService } from '@ghostfolio/client/services/data.service'; |
|||
import { PROPERTY_API_KEY_GHOSTFOLIO } from '@ghostfolio/common/config'; |
|||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; |
|||
|
|||
import { Component, Inject } from '@angular/core'; |
|||
import { MatButtonModule } from '@angular/material/button'; |
|||
import { |
|||
MAT_DIALOG_DATA, |
|||
MatDialogModule, |
|||
MatDialogRef |
|||
} from '@angular/material/dialog'; |
|||
|
|||
import { GfDialogFooterModule } from '../../dialog-footer/dialog-footer.module'; |
|||
import { GfDialogHeaderModule } from '../../dialog-header/dialog-header.module'; |
|||
import { GhostfolioPremiumApiDialogParams } from './interfaces/interfaces'; |
|||
|
|||
@Component({ |
|||
imports: [ |
|||
GfDialogFooterModule, |
|||
GfDialogHeaderModule, |
|||
GfPremiumIndicatorComponent, |
|||
MatButtonModule, |
|||
MatDialogModule |
|||
], |
|||
selector: 'gf-ghostfolio-premium-api-dialog', |
|||
styleUrls: ['./ghostfolio-premium-api-dialog.scss'], |
|||
templateUrl: './ghostfolio-premium-api-dialog.html' |
|||
}) |
|||
export class GfGhostfolioPremiumApiDialogComponent { |
|||
public constructor( |
|||
@Inject(MAT_DIALOG_DATA) public data: GhostfolioPremiumApiDialogParams, |
|||
private dataService: DataService, |
|||
public dialogRef: MatDialogRef<GfGhostfolioPremiumApiDialogComponent>, |
|||
private notificationService: NotificationService |
|||
) {} |
|||
|
|||
public onCancel() { |
|||
this.dialogRef.close(); |
|||
} |
|||
|
|||
public onSetGhostfolioApiKey() { |
|||
this.notificationService.prompt({ |
|||
confirmFn: (value) => { |
|||
const ghostfolioApiKey = value?.trim(); |
|||
|
|||
if (ghostfolioApiKey) { |
|||
this.dataService |
|||
.putAdminSetting(PROPERTY_API_KEY_GHOSTFOLIO, { |
|||
value: ghostfolioApiKey |
|||
}) |
|||
.subscribe(() => { |
|||
this.dialogRef.close(); |
|||
}); |
|||
} |
|||
}, |
|||
title: $localize`Please enter your Ghostfolio API key.` |
|||
}); |
|||
} |
|||
} |
@ -1,49 +0,0 @@ |
|||
<gf-dialog-header |
|||
mat-dialog-title |
|||
position="center" |
|||
title="Ghostfolio Premium Data Provider" |
|||
[deviceType]="data.deviceType" |
|||
(closeButtonClicked)="onCancel()" |
|||
/> |
|||
|
|||
<div class="text-center" mat-dialog-content> |
|||
<p class="gf-text-wrap-balance"> |
|||
Early access to the official |
|||
<a |
|||
class="align-items-center d-inline-flex" |
|||
target="_blank" |
|||
[href]="data.pricingUrl" |
|||
>Ghostfolio Premium |
|||
<gf-premium-indicator class="d-inline-block ml-1" [enableLink]="false" /> |
|||
</a> |
|||
data provider <strong>for self-hosters</strong>, offering |
|||
<strong>80’000+ tickers</strong> from over <strong>50 exchanges</strong>, is |
|||
ready now! |
|||
</p> |
|||
<div> |
|||
<a |
|||
color="primary" |
|||
href="mailto:hi@ghostfol.io?Subject=Ghostfolio Premium Data Provider&body=Hello%0D%0DI am interested in the Ghostfolio Premium data provider. Could you please give me early access so I can try it for some time?%0D%0DKind regards" |
|||
i18n |
|||
mat-flat-button |
|||
>Get Early Access</a |
|||
> |
|||
<div> |
|||
<small class="text-muted" i18n>or</small> |
|||
</div> |
|||
<button |
|||
color="accent" |
|||
i18n |
|||
mat-stroked-button |
|||
(click)="onSetGhostfolioApiKey()" |
|||
> |
|||
I have an API key |
|||
</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<gf-dialog-footer |
|||
mat-dialog-actions |
|||
[deviceType]="data.deviceType" |
|||
(closeButtonClicked)="onCancel()" |
|||
/> |
@ -1,2 +0,0 @@ |
|||
:host { |
|||
} |
@ -1,4 +0,0 @@ |
|||
export interface GhostfolioPremiumApiDialogParams { |
|||
deviceType: string; |
|||
pricingUrl: string; |
|||
} |
@ -0,0 +1,15 @@ |
|||
@if (status$ | async; as status) { |
|||
@if (status.isHealthy) { |
|||
<span class="text-success" i18n>Available</span> |
|||
} @else { |
|||
<span class="text-danger" i18n>Unavailable</span> |
|||
} |
|||
} @else { |
|||
<ngx-skeleton-loader |
|||
animation="pulse" |
|||
[theme]="{ |
|||
height: '1.5rem', |
|||
width: '100%' |
|||
}" |
|||
/> |
|||
} |
@ -0,0 +1,51 @@ |
|||
import { DataService } from '@ghostfolio/client/services/data.service'; |
|||
|
|||
import { CommonModule } from '@angular/common'; |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
Component, |
|||
Input, |
|||
OnDestroy, |
|||
OnInit |
|||
} from '@angular/core'; |
|||
import type { DataSource } from '@prisma/client'; |
|||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; |
|||
import { catchError, map, type Observable, of, Subject, takeUntil } from 'rxjs'; |
|||
|
|||
import { DataProviderStatus } from './interfaces/interfaces'; |
|||
|
|||
@Component({ |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
imports: [CommonModule, NgxSkeletonLoaderModule], |
|||
selector: 'gf-data-provider-status', |
|||
standalone: true, |
|||
templateUrl: './data-provider-status.component.html' |
|||
}) |
|||
export class GfDataProviderStatusComponent implements OnDestroy, OnInit { |
|||
@Input() dataSource: DataSource; |
|||
|
|||
public status$: Observable<DataProviderStatus>; |
|||
|
|||
private unsubscribeSubject = new Subject<void>(); |
|||
|
|||
public constructor(private dataService: DataService) {} |
|||
|
|||
public ngOnInit() { |
|||
this.status$ = this.dataService |
|||
.fetchDataProviderHealth(this.dataSource) |
|||
.pipe( |
|||
catchError(() => { |
|||
return of({ isHealthy: false }); |
|||
}), |
|||
map(() => { |
|||
return { isHealthy: true }; |
|||
}), |
|||
takeUntil(this.unsubscribeSubject) |
|||
); |
|||
} |
|||
|
|||
public ngOnDestroy() { |
|||
this.unsubscribeSubject.next(); |
|||
this.unsubscribeSubject.complete(); |
|||
} |
|||
} |
@ -0,0 +1,3 @@ |
|||
export interface DataProviderStatus { |
|||
isHealthy: boolean; |
|||
} |
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,3 @@ |
|||
export interface DataEnhancerHealthResponse { |
|||
status: string; |
|||
} |
@ -0,0 +1,3 @@ |
|||
export interface DataProviderHealthResponse { |
|||
status: string; |
|||
} |
@ -1,3 +1,3 @@ |
|||
import { Access, User } from '@prisma/client'; |
|||
|
|||
export type AccessWithGranteeUser = Access & { GranteeUser?: User }; |
|||
export type AccessWithGranteeUser = Access & { granteeUser?: User }; |
|||
|
@ -1,3 +1,3 @@ |
|||
import { Account, Platform } from '@prisma/client'; |
|||
|
|||
export type AccountWithPlatform = Account & { Platform?: Platform }; |
|||
export type AccountWithPlatform = Account & { platform?: Platform }; |
|||
|
Loading…
Reference in new issue