Feature/add logo endpoint (#1506)
* Add logo endpoint * Update changelog
This commit is contained in:
parent
49dcade964
commit
e87b93f19c
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added the date of the first activity to the positions table
|
- Added the date of the first activity to the positions table
|
||||||
|
- Added an endpoint to fetch the logo of an asset or a platform
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ import { ExportModule } from './export/export.module';
|
|||||||
import { FrontendMiddleware } from './frontend.middleware';
|
import { FrontendMiddleware } from './frontend.middleware';
|
||||||
import { ImportModule } from './import/import.module';
|
import { ImportModule } from './import/import.module';
|
||||||
import { InfoModule } from './info/info.module';
|
import { InfoModule } from './info/info.module';
|
||||||
|
import { LogoModule } from './logo/logo.module';
|
||||||
import { OrderModule } from './order/order.module';
|
import { OrderModule } from './order/order.module';
|
||||||
import { PortfolioModule } from './portfolio/portfolio.module';
|
import { PortfolioModule } from './portfolio/portfolio.module';
|
||||||
import { SubscriptionModule } from './subscription/subscription.module';
|
import { SubscriptionModule } from './subscription/subscription.module';
|
||||||
@ -58,6 +59,7 @@ import { UserModule } from './user/user.module';
|
|||||||
ExportModule,
|
ExportModule,
|
||||||
ImportModule,
|
ImportModule,
|
||||||
InfoModule,
|
InfoModule,
|
||||||
|
LogoModule,
|
||||||
OrderModule,
|
OrderModule,
|
||||||
PortfolioModule,
|
PortfolioModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
@ -30,8 +30,8 @@ export class BenchmarkController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':dataSource/:symbol/:startDateString')
|
@Get(':dataSource/:symbol/:startDateString')
|
||||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
public async getBenchmarkMarketDataBySymbol(
|
public async getBenchmarkMarketDataBySymbol(
|
||||||
@Param('dataSource') dataSource: DataSource,
|
@Param('dataSource') dataSource: DataSource,
|
||||||
@Param('startDateString') startDateString: string,
|
@Param('startDateString') startDateString: string,
|
||||||
|
54
apps/api/src/app/logo/logo.controller.ts
Normal file
54
apps/api/src/app/logo/logo.controller.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
Query,
|
||||||
|
Res,
|
||||||
|
UseInterceptors
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
|
import { LogoService } from './logo.service';
|
||||||
|
|
||||||
|
@Controller('logo')
|
||||||
|
export class LogoController {
|
||||||
|
public constructor(private readonly logoService: LogoService) {}
|
||||||
|
|
||||||
|
@Get(':dataSource/:symbol')
|
||||||
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
|
public async getLogoByDataSourceAndSymbol(
|
||||||
|
@Param('dataSource') dataSource: DataSource,
|
||||||
|
@Param('symbol') symbol: string,
|
||||||
|
@Res() response: Response
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const buffer = await this.logoService.getLogoByDataSourceAndSymbol({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
});
|
||||||
|
|
||||||
|
response.contentType('image/png');
|
||||||
|
response.send(buffer);
|
||||||
|
} catch {
|
||||||
|
response.status(HttpStatus.NOT_FOUND).send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
public async getLogoByUrl(
|
||||||
|
@Query('url') url: string,
|
||||||
|
@Res() response: Response
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const buffer = await this.logoService.getLogoByUrl(url);
|
||||||
|
|
||||||
|
response.contentType('image/png');
|
||||||
|
response.send(buffer);
|
||||||
|
} catch {
|
||||||
|
response.status(HttpStatus.NOT_FOUND).send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
apps/api/src/app/logo/logo.module.ts
Normal file
13
apps/api/src/app/logo/logo.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { LogoController } from './logo.controller';
|
||||||
|
import { LogoService } from './logo.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [LogoController],
|
||||||
|
imports: [ConfigurationModule, SymbolProfileModule],
|
||||||
|
providers: [LogoService]
|
||||||
|
})
|
||||||
|
export class LogoModule {}
|
55
apps/api/src/app/logo/logo.service.ts
Normal file
55
apps/api/src/app/logo/logo.service.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
|
import { HttpException, Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
|
import * as bent from 'bent';
|
||||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LogoService {
|
||||||
|
public constructor(
|
||||||
|
private readonly symbolProfileService: SymbolProfileService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async getLogoByDataSourceAndSymbol({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: UniqueAsset) {
|
||||||
|
if (!DataSource[dataSource]) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||||
|
StatusCodes.NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([
|
||||||
|
{ dataSource, symbol }
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!assetProfile) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||||
|
StatusCodes.NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getBuffer(assetProfile.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLogoByUrl(aUrl: string) {
|
||||||
|
return this.getBuffer(aUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBuffer(aUrl: string) {
|
||||||
|
const get = bent(
|
||||||
|
`https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`,
|
||||||
|
'GET',
|
||||||
|
'buffer',
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
'User-Agent': 'request'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return get();
|
||||||
|
}
|
||||||
|
}
|
@ -373,6 +373,7 @@ export class PortfolioController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('public/:accessId')
|
@Get('public/:accessId')
|
||||||
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
public async getPublic(
|
public async getPublic(
|
||||||
@Param('accessId') accessId
|
@Param('accessId') accessId
|
||||||
): Promise<PortfolioPublicDetails> {
|
): Promise<PortfolioPublicDetails> {
|
||||||
@ -422,6 +423,7 @@ export class PortfolioController {
|
|||||||
allocationCurrent: portfolioPosition.value / totalValue,
|
allocationCurrent: portfolioPosition.value / totalValue,
|
||||||
countries: hasDetails ? portfolioPosition.countries : [],
|
countries: hasDetails ? portfolioPosition.countries : [],
|
||||||
currency: hasDetails ? portfolioPosition.currency : undefined,
|
currency: hasDetails ? portfolioPosition.currency : undefined,
|
||||||
|
dataSource: portfolioPosition.dataSource,
|
||||||
dateOfFirstActivity: portfolioPosition.dateOfFirstActivity,
|
dateOfFirstActivity: portfolioPosition.dateOfFirstActivity,
|
||||||
markets: hasDetails ? portfolioPosition.markets : undefined,
|
markets: hasDetails ? portfolioPosition.markets : undefined,
|
||||||
name: portfolioPosition.name,
|
name: portfolioPosition.name,
|
||||||
|
@ -10,9 +10,9 @@
|
|||||||
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
|
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
|
||||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||||
<gf-symbol-icon
|
<gf-symbol-icon
|
||||||
*ngIf="element.url"
|
[dataSource]="element.dataSource"
|
||||||
|
[symbol]="element.symbol"
|
||||||
[tooltip]="element.name"
|
[tooltip]="element.name"
|
||||||
[url]="element.url"
|
|
||||||
></gf-symbol-icon>
|
></gf-symbol-icon>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<img
|
<img
|
||||||
*ngIf="url"
|
*ngIf="src"
|
||||||
src="https://www.google.com/s2/favicons?domain={{ url }}&sz=64"
|
onerror="this.style.display='none'"
|
||||||
[ngClass]="{ large: size === 'large' }"
|
[ngClass]="{ large: size === 'large' }"
|
||||||
|
[src]="src"
|
||||||
[title]="tooltip ? tooltip : ''"
|
[title]="tooltip ? tooltip : ''"
|
||||||
/>
|
/>
|
||||||
|
@ -2,8 +2,9 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
Input,
|
Input,
|
||||||
OnInit
|
OnChanges
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-symbol-icon',
|
selector: 'gf-symbol-icon',
|
||||||
@ -11,12 +12,22 @@ import {
|
|||||||
templateUrl: './symbol-icon.component.html',
|
templateUrl: './symbol-icon.component.html',
|
||||||
styleUrls: ['./symbol-icon.component.scss']
|
styleUrls: ['./symbol-icon.component.scss']
|
||||||
})
|
})
|
||||||
export class SymbolIconComponent implements OnInit {
|
export class SymbolIconComponent implements OnChanges {
|
||||||
|
@Input() dataSource: DataSource;
|
||||||
@Input() size: 'large';
|
@Input() size: 'large';
|
||||||
|
@Input() symbol: string;
|
||||||
@Input() tooltip: string;
|
@Input() tooltip: string;
|
||||||
@Input() url: string;
|
@Input() url: string;
|
||||||
|
|
||||||
|
public src: string;
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnChanges() {
|
||||||
|
if (this.dataSource && this.symbol) {
|
||||||
|
this.src = `../api/v1/logo/${this.dataSource}/${this.symbol}`;
|
||||||
|
} else if (this.url) {
|
||||||
|
this.src = `../api/v1/logo?url=${this.url}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ export interface PortfolioPublicDetails {
|
|||||||
| 'allocationCurrent'
|
| 'allocationCurrent'
|
||||||
| 'countries'
|
| 'countries'
|
||||||
| 'currency'
|
| 'currency'
|
||||||
|
| 'dataSource'
|
||||||
| 'dateOfFirstActivity'
|
| 'dateOfFirstActivity'
|
||||||
| 'markets'
|
| 'markets'
|
||||||
| 'name'
|
| 'name'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user