From 6dea9093ba07bdee19ed8947ab81429e8353bcc1 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 19 Oct 2021 18:27:50 +0200 Subject: [PATCH] Feature/add public portfolio (#426) * Setup public portfolio page * Update changelog --- CHANGELOG.md | 8 + apps/api/src/app/access/access.controller.ts | 12 +- apps/api/src/app/access/access.module.ts | 3 +- apps/api/src/app/access/access.service.ts | 11 ++ .../src/app/portfolio/portfolio.controller.ts | 63 +++++- .../api/src/app/portfolio/portfolio.module.ts | 2 + .../src/app/portfolio/portfolio.service.ts | 47 +++-- apps/client/src/app/app-routing.module.ts | 7 + .../access-table/access-table.component.html | 10 +- .../access-table/access-table.component.ts | 1 + apps/client/src/app/core/auth.guard.ts | 1 + .../public/public-page-routing.module.ts | 15 ++ .../app/pages/public/public-page.component.ts | 183 ++++++++++++++++++ .../src/app/pages/public/public-page.html | 38 ++++ .../app/pages/public/public-page.module.ts | 23 +++ .../src/app/pages/public/public-page.scss | 12 ++ apps/client/src/app/services/data.service.ts | 7 + .../src/lib/interfaces/access.interface.ts | 2 + libs/common/src/lib/interfaces/index.ts | 2 + .../portfolio-public-details.interface.ts | 10 + .../migration.sql | 5 + prisma/schema.prisma | 4 +- 22 files changed, 439 insertions(+), 27 deletions(-) create mode 100644 apps/client/src/app/pages/public/public-page-routing.module.ts create mode 100644 apps/client/src/app/pages/public/public-page.component.ts create mode 100644 apps/client/src/app/pages/public/public-page.html create mode 100644 apps/client/src/app/pages/public/public-page.module.ts create mode 100644 apps/client/src/app/pages/public/public-page.scss create mode 100644 libs/common/src/lib/interfaces/portfolio-public-details.interface.ts create mode 100644 prisma/migrations/20211018203042_changed_grantee_user_to_optional_in_access/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index fbc03f98f..5d32dca33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added a public page to share your portfolio + ### Changed - Improved the skeleton loader size of the portfolio proportion chart component +### Todo + +- Apply data migration (`yarn prisma migrate deploy`) + ## 1.62.0 - 17.10.2021 ### Added diff --git a/apps/api/src/app/access/access.controller.ts b/apps/api/src/app/access/access.controller.ts index a738c70d7..b9a1f9b21 100644 --- a/apps/api/src/app/access/access.controller.ts +++ b/apps/api/src/app/access/access.controller.ts @@ -24,8 +24,18 @@ export class AccessController { }); return accessesWithGranteeUser.map((access) => { + if (access.GranteeUser) { + return { + granteeAlias: access.GranteeUser?.alias, + id: access.id, + type: 'RESTRICTED_VIEW' + }; + } + return { - granteeAlias: access.GranteeUser.alias + granteeAlias: 'Public', + id: access.id, + type: 'PUBLIC' }; }); } diff --git a/apps/api/src/app/access/access.module.ts b/apps/api/src/app/access/access.module.ts index beaf98cad..303989b21 100644 --- a/apps/api/src/app/access/access.module.ts +++ b/apps/api/src/app/access/access.module.ts @@ -5,8 +5,9 @@ import { AccessController } from './access.controller'; import { AccessService } from './access.service'; @Module({ - imports: [], controllers: [AccessController], + exports: [AccessService], + imports: [], providers: [AccessService, PrismaService] }) export class AccessModule {} diff --git a/apps/api/src/app/access/access.service.ts b/apps/api/src/app/access/access.service.ts index 49e2c308d..c09c9d28a 100644 --- a/apps/api/src/app/access/access.service.ts +++ b/apps/api/src/app/access/access.service.ts @@ -7,6 +7,17 @@ import { Prisma } from '@prisma/client'; export class AccessService { public constructor(private readonly prismaService: PrismaService) {} + public async access( + accessWhereInput: Prisma.AccessWhereInput + ): Promise { + return this.prismaService.access.findFirst({ + include: { + GranteeUser: true + }, + where: accessWhereInput + }); + } + public async accesses(params: { include?: Prisma.AccessInclude; skip?: number; diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 62406107d..b07579cf2 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -1,3 +1,4 @@ +import { AccessService } from '@ghostfolio/api/app/access/access.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; import { hasNotDefinedValuesInObject, @@ -5,9 +6,11 @@ import { } from '@ghostfolio/api/helper/object.helper'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; +import { baseCurrency } from '@ghostfolio/common/config'; import { PortfolioDetails, PortfolioPerformance, + PortfolioPublicDetails, PortfolioReport, PortfolioSummary } from '@ghostfolio/common/interfaces'; @@ -39,6 +42,7 @@ import { PortfolioService } from './portfolio.service'; @Controller('portfolio') export class PortfolioController { public constructor( + private readonly accessService: AccessService, private readonly configurationService: ConfigurationService, private readonly exchangeRateDataService: ExchangeRateDataService, private readonly portfolioService: PortfolioService, @@ -145,7 +149,11 @@ export class PortfolioController { } const { accounts, holdings, hasErrors } = - await this.portfolioService.getDetails(impersonationId, range); + await this.portfolioService.getDetails( + impersonationId, + this.request.user.id, + range + ); if (hasErrors || hasNotDefinedValuesInObject(holdings)) { res.status(StatusCodes.ACCEPTED); @@ -252,6 +260,59 @@ export class PortfolioController { return res.json(result); } + @Get('public/:accessId') + public async getPublic( + @Param('accessId') accessId, + @Res() res: Response + ): Promise { + const access = await this.accessService.access({ id: accessId }); + + if (!access) { + res.status(StatusCodes.NOT_FOUND); + return res.json({ accounts: {}, holdings: {} }); + } + + const { hasErrors, holdings } = await this.portfolioService.getDetails( + access.userId, + access.userId + ); + + const portfolioPublicDetails: PortfolioPublicDetails = { + holdings: {} + }; + + if (hasErrors || hasNotDefinedValuesInObject(holdings)) { + res.status(StatusCodes.ACCEPTED); + } + + const totalValue = Object.values(holdings) + .filter((holding) => { + return holding.assetClass === 'EQUITY'; + }) + .map((portfolioPosition) => { + return this.exchangeRateDataService.toCurrency( + portfolioPosition.quantity * portfolioPosition.marketPrice, + portfolioPosition.currency, + this.request.user?.Settings?.currency ?? baseCurrency + ); + }) + .reduce((a, b) => a + b, 0); + + for (const [symbol, portfolioPosition] of Object.entries(holdings)) { + if (portfolioPosition.assetClass === 'EQUITY') { + portfolioPublicDetails.holdings[symbol] = { + allocationCurrent: portfolioPosition.allocationCurrent, + countries: [], + name: portfolioPosition.name, + sectors: [], + value: portfolioPosition.value / totalValue + }; + } + } + + return res.json(portfolioPublicDetails); + } + @Get('summary') @UseGuards(AuthGuard('jwt')) public async getSummary( diff --git a/apps/api/src/app/portfolio/portfolio.module.ts b/apps/api/src/app/portfolio/portfolio.module.ts index e8161e925..112f0e4ef 100644 --- a/apps/api/src/app/portfolio/portfolio.module.ts +++ b/apps/api/src/app/portfolio/portfolio.module.ts @@ -1,3 +1,4 @@ +import { AccessModule } from '@ghostfolio/api/app/access/access.module'; import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { OrderModule } from '@ghostfolio/api/app/order/order.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module'; @@ -18,6 +19,7 @@ import { RulesService } from './rules.service'; @Module({ imports: [ + AccessModule, ConfigurationModule, DataGatheringModule, DataProviderModule, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 78e9f27df..2f5f77bca 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1,3 +1,5 @@ +// TODO /////////// + import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface'; import { OrderService } from '@ghostfolio/api/app/order/order.service'; @@ -21,7 +23,11 @@ import { ImpersonationService } from '@ghostfolio/api/services/impersonation.ser import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; -import { UNKNOWN_KEY, ghostfolioCashSymbol } from '@ghostfolio/common/config'; +import { + UNKNOWN_KEY, + baseCurrency, + ghostfolioCashSymbol +} from '@ghostfolio/common/config'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { PortfolioDetails, @@ -78,7 +84,7 @@ export class PortfolioService { public async getInvestments( aImpersonationId: string ): Promise { - const userId = await this.getUserId(aImpersonationId); + const userId = await this.getUserId(aImpersonationId, this.request.user.id); const portfolioCalculator = new PortfolioCalculator( this.currentRateService, @@ -106,7 +112,7 @@ export class PortfolioService { aImpersonationId: string, aDateRange: DateRange = 'max' ): Promise { - const userId = await this.getUserId(aImpersonationId); + const userId = await this.getUserId(aImpersonationId, this.request.user.id); const portfolioCalculator = new PortfolioCalculator( this.currentRateService, @@ -148,11 +154,12 @@ export class PortfolioService { public async getDetails( aImpersonationId: string, + aUserId: string, aDateRange: DateRange = 'max' ): Promise { - const userId = await this.getUserId(aImpersonationId); + const userId = await this.getUserId(aImpersonationId, aUserId); - const userCurrency = this.request.user.Settings.currency; + const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; const portfolioCalculator = new PortfolioCalculator( this.currentRateService, userCurrency @@ -265,7 +272,7 @@ export class PortfolioService { aImpersonationId: string, aSymbol: string ): Promise { - const userId = await this.getUserId(aImpersonationId); + const userId = await this.getUserId(aImpersonationId, this.request.user.id); const orders = (await this.orderService.getOrders({ userId })).filter( (order) => order.symbol === aSymbol @@ -484,7 +491,7 @@ export class PortfolioService { aImpersonationId: string, aDateRange: DateRange = 'max' ): Promise<{ hasErrors: boolean; positions: Position[] }> { - const userId = await this.getUserId(aImpersonationId); + const userId = await this.getUserId(aImpersonationId, this.request.user.id); const portfolioCalculator = new PortfolioCalculator( this.currentRateService, @@ -555,7 +562,7 @@ export class PortfolioService { aImpersonationId: string, aDateRange: DateRange = 'max' ): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { - const userId = await this.getUserId(aImpersonationId); + const userId = await this.getUserId(aImpersonationId, this.request.user.id); const portfolioCalculator = new PortfolioCalculator( this.currentRateService, @@ -628,8 +635,8 @@ export class PortfolioService { } public async getReport(impersonationId: string): Promise { - const userId = await this.getUserId(impersonationId); - const baseCurrency = this.request.user.Settings.currency; + const currency = this.request.user.Settings.currency; + const userId = await this.getUserId(impersonationId, this.request.user.id); const { orders, transactionPoints } = await this.getTransactionPoints({ userId @@ -643,7 +650,7 @@ export class PortfolioService { const portfolioCalculator = new PortfolioCalculator( this.currentRateService, - this.request.user.Settings.currency + currency ); portfolioCalculator.setTransactionPoints(transactionPoints); @@ -659,7 +666,7 @@ export class PortfolioService { const accounts = await this.getAccounts( orders, portfolioItemsNow, - baseCurrency, + currency, userId ); return { @@ -679,7 +686,7 @@ export class PortfolioService { accounts ) ], - { baseCurrency } + { baseCurrency: currency } ), currencyClusterRisk: await this.rulesService.evaluate( [ @@ -700,7 +707,7 @@ export class PortfolioService { currentPositions ) ], - { baseCurrency } + { baseCurrency: currency } ), fees: await this.rulesService.evaluate( [ @@ -710,7 +717,7 @@ export class PortfolioService { this.getFees(orders) ) ], - { baseCurrency } + { baseCurrency: currency } ) } }; @@ -718,7 +725,7 @@ export class PortfolioService { public async getSummary(aImpersonationId: string): Promise { const currency = this.request.user.Settings.currency; - const userId = await this.getUserId(aImpersonationId); + const userId = await this.getUserId(aImpersonationId, this.request.user.id); const performanceInformation = await this.getPerformance(aImpersonationId); @@ -820,7 +827,7 @@ export class PortfolioService { return { transactionPoints: [], orders: [] }; } - const userCurrency = this.request.user.Settings.currency; + const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ currency: order.currency, dataSource: order.dataSource, @@ -920,14 +927,14 @@ export class PortfolioService { return accounts; } - private async getUserId(aImpersonationId: string) { + private async getUserId(aImpersonationId: string, aUserId: string) { const impersonationUserId = await this.impersonationService.validateImpersonationId( aImpersonationId, - this.request.user.id + aUserId ); - return impersonationUserId || this.request.user.id; + return impersonationUserId || aUserId; } private getTotalByType( diff --git a/apps/client/src/app/app-routing.module.ts b/apps/client/src/app/app-routing.module.ts index 5ad41c077..31521e686 100644 --- a/apps/client/src/app/app-routing.module.ts +++ b/apps/client/src/app/app-routing.module.ts @@ -52,6 +52,13 @@ const routes: Routes = [ loadChildren: () => import('./pages/home/home-page.module').then((m) => m.HomePageModule) }, + { + path: 'p', + loadChildren: () => + import('./pages/public/public-page.module').then( + (m) => m.PublicPageModule + ) + }, { path: 'portfolio', loadChildren: () => diff --git a/apps/client/src/app/components/access-table/access-table.component.html b/apps/client/src/app/components/access-table/access-table.component.html index 387394b62..468267acf 100644 --- a/apps/client/src/app/components/access-table/access-table.component.html +++ b/apps/client/src/app/components/access-table/access-table.component.html @@ -9,8 +9,14 @@ Type - - Restricted View + + + {{ baseUrl }}/p/{{ element.id }} + + + + Restricted View + diff --git a/apps/client/src/app/components/access-table/access-table.component.ts b/apps/client/src/app/components/access-table/access-table.component.ts index f1a844a01..4136197b8 100644 --- a/apps/client/src/app/components/access-table/access-table.component.ts +++ b/apps/client/src/app/components/access-table/access-table.component.ts @@ -17,6 +17,7 @@ import { Access } from '@ghostfolio/common/interfaces'; export class AccessTableComponent implements OnChanges, OnInit { @Input() accesses: Access[]; + public baseUrl = window.location.origin; public dataSource: MatTableDataSource; public displayedColumns = ['granteeAlias', 'type']; diff --git a/apps/client/src/app/core/auth.guard.ts b/apps/client/src/app/core/auth.guard.ts index 7035f7167..c90ffbe4c 100644 --- a/apps/client/src/app/core/auth.guard.ts +++ b/apps/client/src/app/core/auth.guard.ts @@ -18,6 +18,7 @@ export class AuthGuard implements CanActivate { '/about', '/de/blog', '/en/blog', + '/p', '/pricing', '/register', '/resources' diff --git a/apps/client/src/app/pages/public/public-page-routing.module.ts b/apps/client/src/app/pages/public/public-page-routing.module.ts new file mode 100644 index 000000000..2a648c0af --- /dev/null +++ b/apps/client/src/app/pages/public/public-page-routing.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; + +import { PublicPageComponent } from './public-page.component'; + +const routes: Routes = [ + { path: ':id', component: PublicPageComponent, canActivate: [AuthGuard] } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class PublicPageRoutingModule {} diff --git a/apps/client/src/app/pages/public/public-page.component.ts b/apps/client/src/app/pages/public/public-page.component.ts new file mode 100644 index 000000000..9108a7d1f --- /dev/null +++ b/apps/client/src/app/pages/public/public-page.component.ts @@ -0,0 +1,183 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DataService } from '@ghostfolio/client/services/data.service'; +import { UNKNOWN_KEY } from '@ghostfolio/common/config'; +import { + PortfolioPosition, + PortfolioPublicDetails +} from '@ghostfolio/common/interfaces'; +import { StatusCodes } from 'http-status-codes'; +import { DeviceDetectorService } from 'ngx-device-detector'; +import { EMPTY, Subject } from 'rxjs'; +import { catchError, takeUntil } from 'rxjs/operators'; + +@Component({ + host: { class: 'mb-5' }, + selector: 'gf-public-page', + styleUrls: ['./public-page.scss'], + templateUrl: './public-page.html' +}) +export class PublicPageComponent implements OnInit { + public continents: { + [code: string]: { name: string; value: number }; + }; + public countries: { + [code: string]: { name: string; value: number }; + }; + public deviceType: string; + public portfolioPublicDetails: PortfolioPublicDetails; + public positions: { + [symbol: string]: Pick; + }; + public sectors: { + [name: string]: { name: string; value: number }; + }; + public symbols: { + [name: string]: { name: string; symbol: string; value: number }; + }; + + private id: string; + private unsubscribeSubject = new Subject(); + + /** + * @constructor + */ + public constructor( + private activatedRoute: ActivatedRoute, + private changeDetectorRef: ChangeDetectorRef, + private dataService: DataService, + private deviceService: DeviceDetectorService, + private router: Router + ) { + this.activatedRoute.params.subscribe((params) => { + this.id = params['id']; + }); + } + + /** + * Initializes the controller + */ + public ngOnInit() { + this.deviceType = this.deviceService.getDeviceInfo().deviceType; + + this.dataService + .fetchPortfolioPublic(this.id) + .pipe( + takeUntil(this.unsubscribeSubject), + catchError((error) => { + if (error.status === StatusCodes.NOT_FOUND) { + console.error(error); + this.router.navigate(['/']); + } + + return EMPTY; + }) + ) + .subscribe((portfolioPublicDetails) => { + this.portfolioPublicDetails = portfolioPublicDetails; + + this.initializeAnalysisData(); + + this.changeDetectorRef.markForCheck(); + }); + } + + public initializeAnalysisData() { + this.continents = { + [UNKNOWN_KEY]: { + name: UNKNOWN_KEY, + value: 0 + } + }; + this.countries = { + [UNKNOWN_KEY]: { + name: UNKNOWN_KEY, + value: 0 + } + }; + this.positions = {}; + this.sectors = { + [UNKNOWN_KEY]: { + name: UNKNOWN_KEY, + value: 0 + } + }; + this.symbols = { + [UNKNOWN_KEY]: { + name: UNKNOWN_KEY, + symbol: UNKNOWN_KEY, + value: 0 + } + }; + + for (const [symbol, position] of Object.entries( + this.portfolioPublicDetails.holdings + )) { + const value = position.allocationCurrent; + + this.positions[symbol] = { + value, + name: position.name + }; + + if (position.countries.length > 0) { + for (const country of position.countries) { + const { code, continent, name, weight } = country; + + if (this.continents[continent]?.value) { + this.continents[continent].value += weight * position.value; + } else { + this.continents[continent] = { + name: continent, + value: weight * this.portfolioPublicDetails.holdings[symbol].value + }; + } + + if (this.countries[code]?.value) { + this.countries[code].value += weight * position.value; + } else { + this.countries[code] = { + name, + value: weight * this.portfolioPublicDetails.holdings[symbol].value + }; + } + } + } else { + this.continents[UNKNOWN_KEY].value += + this.portfolioPublicDetails.holdings[symbol].value; + + this.countries[UNKNOWN_KEY].value += + this.portfolioPublicDetails.holdings[symbol].value; + } + + if (position.sectors.length > 0) { + for (const sector of position.sectors) { + const { name, weight } = sector; + + if (this.sectors[name]?.value) { + this.sectors[name].value += weight * position.value; + } else { + this.sectors[name] = { + name, + value: weight * this.portfolioPublicDetails.holdings[symbol].value + }; + } + } + } else { + this.sectors[UNKNOWN_KEY].value += + this.portfolioPublicDetails.holdings[symbol].value; + } + + this.symbols[symbol] = { + symbol, + name: position.name, + value: position.value + }; + } + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } +} diff --git a/apps/client/src/app/pages/public/public-page.html b/apps/client/src/app/pages/public/public-page.html new file mode 100644 index 000000000..9bbd7403a --- /dev/null +++ b/apps/client/src/app/pages/public/public-page.html @@ -0,0 +1,38 @@ +
+
+
+

Portfolio

+
+
+
+
+ + + + + +
+
+
+
+

+ Would you like to refine your + personal investment strategy? +

+

+ Ghostfolio empowers you to keep track of your wealth. +

+ +
+
+
diff --git a/apps/client/src/app/pages/public/public-page.module.ts b/apps/client/src/app/pages/public/public-page.module.ts new file mode 100644 index 000000000..27b5eb6cf --- /dev/null +++ b/apps/client/src/app/pages/public/public-page.module.ts @@ -0,0 +1,23 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module'; + +import { PublicPageRoutingModule } from './public-page-routing.module'; +import { PublicPageComponent } from './public-page.component'; + +@NgModule({ + declarations: [PublicPageComponent], + exports: [], + imports: [ + CommonModule, + GfPortfolioProportionChartModule, + MatButtonModule, + MatCardModule, + PublicPageRoutingModule + ], + providers: [], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class PublicPageModule {} diff --git a/apps/client/src/app/pages/public/public-page.scss b/apps/client/src/app/pages/public/public-page.scss new file mode 100644 index 000000000..5a02cb0d3 --- /dev/null +++ b/apps/client/src/app/pages/public/public-page.scss @@ -0,0 +1,12 @@ +:host { + color: rgb(var(--dark-primary-text)); + display: block; + + gf-portfolio-proportion-chart { + max-width: 80vh; + } +} + +:host-context(.is-dark-theme) { + color: rgb(var(--light-primary-text)); +} diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 30f64d660..e1c297108 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -22,6 +22,7 @@ import { InfoItem, PortfolioDetails, PortfolioPerformance, + PortfolioPublicDetails, PortfolioReport, PortfolioSummary, User @@ -164,6 +165,12 @@ export class DataService { }); } + public fetchPortfolioPublic(aId: string) { + return this.http.get( + `/api/portfolio/public/${aId}` + ); + } + public fetchPortfolioReport() { return this.http.get('/api/portfolio/report'); } diff --git a/libs/common/src/lib/interfaces/access.interface.ts b/libs/common/src/lib/interfaces/access.interface.ts index 8ab6f522b..b1b827f7d 100644 --- a/libs/common/src/lib/interfaces/access.interface.ts +++ b/libs/common/src/lib/interfaces/access.interface.ts @@ -1,3 +1,5 @@ export interface Access { granteeAlias: string; + id: string; + type: 'PUBLIC' | 'RESTRICTED_VIEW'; } diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index 15357d306..d11782f8f 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -7,6 +7,7 @@ import { PortfolioItem } from './portfolio-item.interface'; import { PortfolioOverview } from './portfolio-overview.interface'; import { PortfolioPerformance } from './portfolio-performance.interface'; import { PortfolioPosition } from './portfolio-position.interface'; +import { PortfolioPublicDetails } from './portfolio-public-details.interface'; import { PortfolioReportRule } from './portfolio-report-rule.interface'; import { PortfolioReport } from './portfolio-report.interface'; import { PortfolioSummary } from './portfolio-summary.interface'; @@ -26,6 +27,7 @@ export { PortfolioOverview, PortfolioPerformance, PortfolioPosition, + PortfolioPublicDetails, PortfolioReport, PortfolioReportRule, PortfolioSummary, diff --git a/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts b/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts new file mode 100644 index 000000000..7f09e2ac6 --- /dev/null +++ b/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts @@ -0,0 +1,10 @@ +import { PortfolioPosition } from '@ghostfolio/common/interfaces'; + +export interface PortfolioPublicDetails { + holdings: { + [symbol: string]: Pick< + PortfolioPosition, + 'allocationCurrent' | 'countries' | 'name' | 'sectors' | 'value' + >; + }; +} diff --git a/prisma/migrations/20211018203042_changed_grantee_user_to_optional_in_access/migration.sql b/prisma/migrations/20211018203042_changed_grantee_user_to_optional_in_access/migration.sql new file mode 100644 index 000000000..468544129 --- /dev/null +++ b/prisma/migrations/20211018203042_changed_grantee_user_to_optional_in_access/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Access" ALTER COLUMN "granteeUserId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "Account" ALTER COLUMN "currency" DROP NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6eb2e04f6..b008492dd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,8 +14,8 @@ generator client { model Access { createdAt DateTime @default(now()) - GranteeUser User @relation(fields: [granteeUserId], name: "accessGet", references: [id]) - granteeUserId String + GranteeUser User? @relation(fields: [granteeUserId], name: "accessGet", references: [id]) + granteeUserId String? id String @default(uuid()) updatedAt DateTime @updatedAt User User @relation(fields: [userId], name: "accessGive", references: [id])