Browse Source

Merge branch 'main' into feature/upgrade-ngx-skeleton-loader-to-version-7.0.0

pull/1758/head
Thomas Kaul 3 years ago
committed by GitHub
parent
commit
40900e7a0e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/build-code.yml
  2. 5
      CHANGELOG.md
  3. 4
      Dockerfile
  4. 12
      README.md
  5. 5
      apps/api/src/app/account/account.controller.ts
  6. 36
      apps/api/src/app/admin/admin.service.ts
  7. 27
      apps/api/src/app/auth/jwt.strategy.ts
  8. 24
      apps/api/src/app/info/info.service.ts
  9. 3
      apps/api/src/app/order/order.controller.ts
  10. 15
      apps/api/src/app/portfolio/portfolio.controller.ts
  11. 7
      apps/api/src/app/user/create-user.dto.ts
  12. 4
      apps/api/src/app/user/user.controller.ts
  13. 8
      apps/api/src/app/user/user.service.ts
  14. 4
      apps/api/src/interceptors/redact-values-in-response.interceptor.ts
  15. 2
      apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts
  16. 4
      apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts
  17. 13
      apps/client/src/app/components/admin-users/admin-users.html
  18. 28
      apps/client/src/app/core/auth.interceptor.ts
  19. 4
      apps/client/src/app/pages/landing/landing-page.component.ts
  20. 32
      apps/client/src/app/pages/landing/landing-page.html
  21. 2
      apps/client/src/app/pages/register/register-page.component.ts
  22. 1
      apps/client/src/app/pages/register/register-page.html
  23. 4
      apps/client/src/app/services/data.service.ts
  24. 15
      apps/client/src/app/services/user/user.service.ts
  25. 7
      libs/common/src/lib/config.ts
  26. 426
      libs/common/src/lib/timezone-cities-to-countries.ts
  27. 1
      package.json
  28. 299
      prisma/seed.js
  29. 5
      yarn.lock

2
.github/workflows/build-code.yml

@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
node_version:
- 18
- 16
steps:
- name: Checkout code
uses: actions/checkout@v3

5
CHANGELOG.md

@ -9,8 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Simplified the database seeding
- Upgraded `ngx-skeleton-loader` from version `5.0.0` to `7.0.0`
### Fixed
- Downgraded `Node.js` from version `18` to `16` (Dockerfile) to resolve `SIGSEGV` (segmentation fault) during the `prisma` database migrations (see https://github.com/prisma/prisma/issues/10649)
## 1.241.0 - 2023-03-01
### Changed

4
Dockerfile

@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM node:18-slim as builder
FROM --platform=$BUILDPLATFORM node:16-slim as builder
# Build application and add additional files
WORKDIR /ghostfolio
@ -50,7 +50,7 @@ COPY package.json /ghostfolio/dist/apps/api
RUN yarn database:generate-typings
# Image to run, copy everything needed from builder
FROM node:18-slim
FROM node:16-slim
RUN apt update && apt install -y \
openssl \
&& rm -rf /var/lib/apt/lists/*

12
README.md

@ -126,13 +126,10 @@ docker-compose --env-file ./.env -f docker/docker-compose.build.yml build
docker-compose --env-file ./.env -f docker/docker-compose.build.yml up -d
```
#### Fetch Historical Data
Open http://localhost:3333 in your browser and accomplish these steps:
#### Setup
1. Open http://localhost:3333 in your browser
1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`)
1. Go to the _Market Data_ tab in the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
1. Click _Sign out_ and check out the _Live Demo_
#### Upgrade Version
@ -159,11 +156,10 @@ Please follow the instructions of the Ghostfolio [Unraid Community App](https://
1. Run `yarn install`
1. Run `yarn build:dev` to build the source code including the assets
1. Run `docker-compose --env-file ./.env -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
1. Run `yarn database:setup` to initialize the database schema and populate your database with (example) data
1. Run `yarn database:setup` to initialize the database schema
1. Start the server and the client (see [_Development_](#Development))
1. Open http://localhost:4200/en in your browser
1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`)
1. Go to the _Market Data_ tab in the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
1. Click _Sign out_ and check out the _Live Demo_
### Start Server

5
apps/api/src/app/account/account.controller.ts

@ -1,6 +1,7 @@
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { Accounts } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type {
@ -83,7 +84,7 @@ export class AccountController {
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(RedactValuesInResponseInterceptor)
public async getAllAccounts(
@Headers('impersonation-id') impersonationId
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId
): Promise<Accounts> {
const impersonationUserId =
await this.impersonationService.validateImpersonationId(
@ -101,7 +102,7 @@ export class AccountController {
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(RedactValuesInResponseInterceptor)
public async getAccountById(
@Headers('impersonation-id') impersonationId,
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
@Param('id') id: string
): Promise<AccountWithValue> {
const impersonationUserId =

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

@ -231,12 +231,27 @@ export class AdminService {
}
private async getUsersWithAnalytics(): Promise<AdminData['users']> {
const usersWithAnalytics = await this.prismaService.user.findMany({
orderBy: {
let orderBy: any = {
createdAt: 'desc'
};
let where;
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
orderBy = {
Analytics: {
updatedAt: 'desc'
}
},
};
where = {
NOT: {
Analytics: null
}
};
}
const usersWithAnalytics = await this.prismaService.user.findMany({
orderBy,
where,
select: {
_count: {
select: { Account: true, Order: true }
@ -252,19 +267,16 @@ export class AdminService {
id: true,
Subscription: true
},
take: 30,
where: {
NOT: {
Analytics: null
}
}
take: 30
});
return usersWithAnalytics.map(
({ _count, Analytics, createdAt, id, Subscription }) => {
const daysSinceRegistration =
differenceInDays(new Date(), createdAt) + 1;
const engagement = Analytics.activityCount / daysSinceRegistration;
const engagement = Analytics
? Analytics.activityCount / daysSinceRegistration
: undefined;
const subscription = this.configurationService.get(
'ENABLE_FEATURE_SUBSCRIPTION'
@ -278,8 +290,8 @@ export class AdminService {
id,
subscription,
accountCount: _count.Account || 0,
country: Analytics.country,
lastActivity: Analytics.updatedAt,
country: Analytics?.country,
lastActivity: Analytics?.updatedAt,
transactionCount: _count.Order || 0
};
}

27
apps/api/src/app/auth/jwt.strategy.ts

@ -1,33 +1,46 @@
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { HEADER_KEY_TIMEZONE } from '@ghostfolio/common/config';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import * as countriesAndTimezones from 'countries-and-timezones';
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
public constructor(
readonly configurationService: ConfigurationService,
private readonly configurationService: ConfigurationService,
private readonly prismaService: PrismaService,
private readonly userService: UserService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
passReqToCallback: true,
secretOrKey: configurationService.get('JWT_SECRET_KEY')
});
}
public async validate({ id }: { id: string }) {
public async validate(request: Request, { id }: { id: string }) {
try {
const timezone = request.headers[HEADER_KEY_TIMEZONE.toLowerCase()];
const user = await this.userService.user({ id });
if (user) {
await this.prismaService.analytics.upsert({
create: { User: { connect: { id: user.id } } },
update: { activityCount: { increment: 1 }, updatedAt: new Date() },
where: { userId: user.id }
});
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
const country =
countriesAndTimezones.getCountryForTimezone(timezone)?.id;
await this.prismaService.analytics.upsert({
create: { country, User: { connect: { id: user.id } } },
update: {
country,
activityCount: { increment: 1 },
updatedAt: new Date()
},
where: { userId: user.id }
});
}
return user;
} else {

24
apps/api/src/app/info/info.service.ts

@ -6,12 +6,12 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import {
DEMO_USER_ID,
PROPERTY_COUNTRIES_OF_SUBSCRIBERS,
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_SLACK_COMMUNITY_USERS,
PROPERTY_STRIPE_CONFIG,
PROPERTY_SYSTEM_MESSAGE,
PROPERTY_DEMO_USER_ID,
ghostfolioFearAndGreedIndexDataSource
} from '@ghostfolio/common/config';
import {
@ -59,9 +59,7 @@ export class InfoService {
}
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true
) {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
info.fearAndGreedDataSource = encodeDataSource(
ghostfolioFearAndGreedIndexDataSource
);
@ -120,7 +118,7 @@ export class InfoService {
baseCurrency: this.configurationService.get('BASE_CURRENCY'),
benchmarks: await this.benchmarkService.getBenchmarkAssetProfiles(),
currencies: this.exchangeRateDataService.getCurrencies(),
demoAuthToken: this.getDemoAuthToken(),
demoAuthToken: await this.getDemoAuthToken(),
statistics: await this.getStatistics(),
subscriptions: await this.getSubscriptions(),
tags: await this.tagService.get()
@ -248,10 +246,18 @@ export class InfoService {
)) as string;
}
private getDemoAuthToken() {
return this.jwtService.sign({
id: DEMO_USER_ID
});
private async getDemoAuthToken() {
const demoUserId = (await this.propertyService.getByKey(
PROPERTY_DEMO_USER_ID
)) as string;
if (demoUserId) {
return this.jwtService.sign({
id: demoUserId
});
}
return undefined;
}
private async getStatistics() {

3
apps/api/src/app/order/order.controller.ts

@ -3,6 +3,7 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
@ -66,7 +67,7 @@ export class OrderController {
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getAllOrders(
@Headers('impersonation-id') impersonationId,
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('tags') filterByTags?: string

15
apps/api/src/app/portfolio/portfolio.controller.ts

@ -10,6 +10,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import {
PortfolioDetails,
PortfolioDividends,
@ -65,7 +66,7 @@ export class PortfolioController {
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getDetails(
@Headers('impersonation-id') impersonationId: string,
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max',
@ -189,7 +190,7 @@ export class PortfolioController {
@Get('dividends')
@UseGuards(AuthGuard('jwt'))
public async getDividends(
@Headers('impersonation-id') impersonationId: string,
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('groupBy') groupBy?: GroupBy,
@ -239,7 +240,7 @@ export class PortfolioController {
@Get('investments')
@UseGuards(AuthGuard('jwt'))
public async getInvestments(
@Headers('impersonation-id') impersonationId: string,
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('groupBy') groupBy?: GroupBy,
@ -291,7 +292,7 @@ export class PortfolioController {
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@Version('2')
public async getPerformanceV2(
@Headers('impersonation-id') impersonationId: string,
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max',
@ -360,7 +361,7 @@ export class PortfolioController {
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getPositions(
@Headers('impersonation-id') impersonationId: string,
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max',
@ -451,7 +452,7 @@ export class PortfolioController {
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@UseGuards(AuthGuard('jwt'))
public async getPosition(
@Headers('impersonation-id') impersonationId: string,
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('dataSource') dataSource,
@Param('symbol') symbol
): Promise<PortfolioPositionDetail> {
@ -474,7 +475,7 @@ export class PortfolioController {
@Get('report')
@UseGuards(AuthGuard('jwt'))
public async getReport(
@Headers('impersonation-id') impersonationId: string
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string
): Promise<PortfolioReport> {
const report = await this.portfolioService.getReport(impersonationId);

7
apps/api/src/app/user/create-user.dto.ts

@ -1,7 +0,0 @@
import { IsOptional, IsString } from 'class-validator';
export class CreateUserDto {
@IsString()
@IsOptional()
country?: string;
}

4
apps/api/src/app/user/user.controller.ts

@ -22,7 +22,6 @@ import { User as UserModel } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { size } from 'lodash';
import { CreateUserDto } from './create-user.dto';
import { UserItem } from './interfaces/user-item.interface';
import { UpdateUserSettingDto } from './update-user-setting.dto';
import { UserService } from './user.service';
@ -66,7 +65,7 @@ export class UserController {
}
@Post()
public async signupUser(@Body() data: CreateUserDto): Promise<UserItem> {
public async signupUser(): Promise<UserItem> {
const isUserSignupEnabled =
await this.propertyService.isUserSignupEnabled();
@ -80,7 +79,6 @@ export class UserController {
const hasAdmin = await this.userService.hasAdmin();
const { accessToken, id, role } = await this.userService.createUser({
country: data.country,
data: { role: hasAdmin ? 'USER' : 'ADMIN' }
});

8
apps/api/src/app/user/user.service.ts

@ -18,8 +18,6 @@ import { Injectable } from '@nestjs/common';
import { Prisma, Role, User } from '@prisma/client';
import { sortBy } from 'lodash';
import { CreateUserDto } from './create-user.dto';
const crypto = require('crypto');
@Injectable()
@ -234,9 +232,10 @@ export class UserService {
}
public async createUser({
country,
data
}: CreateUserDto & { data: Prisma.UserCreateInput }): Promise<User> {
}: {
data: Prisma.UserCreateInput;
}): Promise<User> {
if (!data?.provider) {
data.provider = 'ANONYMOUS';
}
@ -264,7 +263,6 @@ export class UserService {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
await this.prismaService.analytics.create({
data: {
country,
User: { connect: { id: user.id } }
}
});

4
apps/api/src/interceptors/redact-values-in-response.interceptor.ts

@ -1,5 +1,6 @@
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { redactAttributes } from '@ghostfolio/api/helper/object.helper';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import {
CallHandler,
ExecutionContext,
@ -22,7 +23,8 @@ export class RedactValuesInResponseInterceptor<T>
return next.handle().pipe(
map((data: any) => {
const request = context.switchToHttp().getRequest();
const hasImpersonationId = !!request.headers?.['impersonation-id'];
const hasImpersonationId =
!!request.headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
if (
hasImpersonationId ||

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

@ -24,7 +24,7 @@ export class TransformDataSourceInRequestInterceptor<T>
const http = context.switchToHttp();
const request = http.getRequest();
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true) {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
if (request.body.dataSource) {
request.body.dataSource = decodeDataSource(request.body.dataSource);
}

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

@ -26,9 +26,7 @@ export class TransformDataSourceInResponseInterceptor<T>
): Observable<any> {
return next.handle().pipe(
map((data: any) => {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true
) {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
data = redactAttributes({
options: [
{

13
apps/client/src/app/components/admin-users/admin-users.html

@ -28,7 +28,13 @@
>
<ng-container i18n>Engagement per Day</ng-container>
</th>
<th class="mat-header-cell px-1 py-2" i18n>Last Request</th>
<th
*ngIf="hasPermissionForSubscription"
class="mat-header-cell px-1 py-2"
i18n
>
Last Request
</th>
<th class="mat-header-cell px-1 py-2"></th>
</tr>
</thead>
@ -86,7 +92,10 @@
[value]="userItem.engagement"
></gf-value>
</td>
<td class="mat-cell px-1 py-2">
<td
*ngIf="hasPermissionForSubscription"
class="mat-cell px-1 py-2"
>
{{ formatDistanceToNow(userItem.lastActivity) }}
</td>
<td class="mat-cell px-1 py-2">

28
apps/client/src/app/core/auth.interceptor.ts

@ -5,14 +5,16 @@ import {
HttpRequest
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import {
HEADER_KEY_IMPERSONATION,
HEADER_KEY_TIMEZONE,
HEADER_KEY_TOKEN
} from '@ghostfolio/common/config';
import { Observable } from 'rxjs';
import { ImpersonationStorageService } from '../services/impersonation-storage.service';
import { TokenStorageService } from '../services/token-storage.service';
const IMPERSONATION_KEY = 'Impersonation-Id';
const TOKEN_HEADER_KEY = 'Authorization';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
public constructor(
@ -24,21 +26,27 @@ export class AuthInterceptor implements HttpInterceptor {
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
let authReq = req;
let request = req;
let headers = request.headers.set(
HEADER_KEY_TIMEZONE,
Intl?.DateTimeFormat().resolvedOptions().timeZone
);
const token = this.tokenStorageService.getToken();
const impersonationId = this.impersonationStorageService.getId();
if (token !== null) {
let headers = req.headers.set(TOKEN_HEADER_KEY, `Bearer ${token}`);
headers = headers.set(HEADER_KEY_TOKEN, `Bearer ${token}`);
const impersonationId = this.impersonationStorageService.getId();
if (impersonationId !== null) {
headers = headers.set(IMPERSONATION_KEY, impersonationId);
headers = headers.set(HEADER_KEY_IMPERSONATION, impersonationId);
}
authReq = req.clone({ headers });
}
return next.handle(authReq);
request = request.clone({ headers });
return next.handle(request);
}
}

4
apps/client/src/app/pages/landing/landing-page.component.ts

@ -17,8 +17,8 @@ export class LandingPageComponent implements OnDestroy, OnInit {
[code: string]: { value: number };
} = {};
public currentYear = format(new Date(), 'yyyy');
public demoAuthToken: string;
public deviceType: string;
public hasPermissionForDemo: boolean;
public hasPermissionForStatistics: boolean;
public hasPermissionForSubscription: boolean;
public hasPermissionToCreateUser: boolean;
@ -54,6 +54,7 @@ export class LandingPageComponent implements OnDestroy, OnInit {
) {
const {
countriesOfSubscribers = [],
demoAuthToken,
globalPermissions,
statistics
} = this.dataService.fetchInfo();
@ -64,6 +65,7 @@ export class LandingPageComponent implements OnDestroy, OnInit {
};
}
this.hasPermissionForDemo = !!demoAuthToken;
this.hasPermissionForStatistics = hasPermission(
globalPermissions,
permissions.enableStatistics

32
apps/client/src/app/pages/landing/landing-page.html

@ -40,12 +40,18 @@
>
Get Started
</a>
<div class="d-inline-block mx-3 text-muted">or</div></ng-container
>
<a class="d-inline-block" mat-stroked-button [routerLink]="['/demo']">
Live Demo
</a>
</ng-container>
<ng-container *ngIf="hasPermissionForDemo">
<div
*ngIf="hasPermissionToCreateUser"
class="d-inline-block mx-3 text-muted"
>
or
</div>
<a class="d-inline-block" mat-stroked-button [routerLink]="['/demo']">
Live Demo
</a>
</ng-container>
</div>
</div>
</div>
@ -379,16 +385,20 @@
<div class="col">
<h2 class="h4 mb-1 text-center">Are <strong>you</strong> ready?</h2>
<p class="lead mb-3 text-center">
Join now or check out the example account
Join now<ng-container *ngIf="hasPermissionForDemo">
or check out the example account</ng-container
>
</p>
<div class="py-2 text-center">
<a color="primary" mat-flat-button [routerLink]="['/register']">
Get Started
</a>
<div class="d-inline-block mx-3 text-muted">or</div>
<a class="d-inline-block" mat-stroked-button [routerLink]="['/demo']">
Live Demo
</a>
<ng-container *ngIf="hasPermissionForDemo">
<div class="d-inline-block mx-3 text-muted">or</div>
<a class="d-inline-block" mat-stroked-button [routerLink]="['/demo']">
Live Demo
</a>
</ng-container>
</div>
</div>
</div>

2
apps/client/src/app/pages/register/register-page.component.ts

@ -63,7 +63,7 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
public async createAccount() {
this.dataService
.postUser({ country: this.userService.getCountry() })
.postUser()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ accessToken, authToken, role }) => {
this.openShowAccessTokenDialog(accessToken, authToken, role);

1
apps/client/src/app/pages/register/register-page.html

@ -21,7 +21,6 @@
class="d-inline-block"
color="primary"
mat-flat-button
[disabled]="!demoAuthToken"
(click)="createAccount()"
>
<ng-container i18n>Create Account</ng-container>

4
apps/client/src/app/services/data.service.ts

@ -405,8 +405,8 @@ export class DataService {
return this.http.post<OrderModel>(`/api/v1/order`, aOrder);
}
public postUser({ country }: { country: string }) {
return this.http.post<UserItem>(`/api/v1/user`, { country });
public postUser() {
return this.http.post<UserItem>(`/api/v1/user`, {});
}
public putAccount(aAccount: UpdateAccountDto) {

15
apps/client/src/app/services/user/user.service.ts

@ -6,7 +6,6 @@ import { SubscriptionInterstitialDialogParams } from '@ghostfolio/client/compone
import { SubscriptionInterstitialDialog } from '@ghostfolio/client/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component';
import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { timezoneCitiesToCountries } from '@ghostfolio/common/timezone-cities-to-countries';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject, of } from 'rxjs';
import { throwError } from 'rxjs';
@ -46,20 +45,6 @@ export class UserService extends ObservableStore<UserStoreState> {
}
}
public getCountry() {
let country: string;
if (Intl) {
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const timeZoneArray = timeZone.split('/');
const city = timeZoneArray[timeZoneArray.length - 1];
country = timezoneCitiesToCountries[city];
}
return country;
}
public remove() {
this.setState({ user: null }, UserStoreActions.RemoveUser);
}

7
libs/common/src/lib/config.ts

@ -2,8 +2,6 @@ import { DataSource } from '@prisma/client';
import { JobOptions, JobStatus } from 'bull';
import ms from 'ms';
export const DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f';
export const ghostfolioPrefix = 'GF';
export const ghostfolioScraperApiSymbolPrefix = `_${ghostfolioPrefix}_`;
export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`;
@ -69,12 +67,17 @@ export const GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS: JobOptions = {
}
};
export const HEADER_KEY_IMPERSONATION = 'Impersonation-Id';
export const HEADER_KEY_TIMEZONE = 'Timezone';
export const HEADER_KEY_TOKEN = 'Authorization';
export const MAX_CHART_ITEMS = 365;
export const PROPERTY_BENCHMARKS = 'BENCHMARKS';
export const PROPERTY_COUNTRIES_OF_SUBSCRIBERS = 'COUNTRIES_OF_SUBSCRIBERS';
export const PROPERTY_COUPONS = 'COUPONS';
export const PROPERTY_CURRENCIES = 'CURRENCIES';
export const PROPERTY_DEMO_USER_ID = 'DEMO_USER_ID';
export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE';
export const PROPERTY_IS_USER_SIGNUP_ENABLED = 'IS_USER_SIGNUP_ENABLED';
export const PROPERTY_SLACK_COMMUNITY_USERS = 'SLACK_COMMUNITY_USERS';

426
libs/common/src/lib/timezone-cities-to-countries.ts

@ -1,426 +0,0 @@
export const timezoneCitiesToCountries = {
Abidjan: 'CI',
Accra: 'GH',
Adak: 'US',
Addis_Ababa: 'ET',
Adelaide: 'AU',
Aden: 'YE',
Algiers: 'DZ',
Almaty: 'KZ',
Amman: 'JO',
Amsterdam: 'NL',
Anadyr: 'RU',
Anchorage: 'US',
Andorra: 'AD',
Anguilla: 'AI',
Antananarivo: 'MG',
Antigua: 'AG',
Apia: 'WS',
Aqtau: 'KZ',
Aqtobe: 'KZ',
Araguaina: 'BR',
Aruba: 'AW',
Ashgabat: 'TM',
Asmara: 'ER',
Astrakhan: 'RU',
Asuncion: 'PY',
Athens: 'GR',
Atikokan: 'CA',
Atyrau: 'KZ',
Auckland: 'NZ',
Azores: 'PT',
Baghdad: 'IQ',
Bahia: 'BR',
Bahia_Banderas: 'MX',
Bahrain: 'BH',
Baku: 'AZ',
Bamako: 'ML',
Bangkok: 'TH',
Bangui: 'CF',
Banjul: 'GM',
Barbados: 'BB',
Barnaul: 'RU',
Beirut: 'LB',
Belem: 'BR',
Belgrade: 'RS',
Belize: 'BZ',
Berlin: 'DE',
Bermuda: 'BM',
Beulah: 'US',
Bishkek: 'KG',
Bissau: 'GW',
'Blanc-Sablon': 'CA',
Blantyre: 'MW',
Boa_Vista: 'BR',
Bogota: 'CO',
Boise: 'US',
Bougainville: 'PG',
Bratislava: 'SK',
Brazzaville: 'CG',
Brisbane: 'AU',
Broken_Hill: 'AU',
Brunei: 'BN',
Brussels: 'BE',
Bucharest: 'RO',
Budapest: 'HU',
Buenos_Aires: 'AR',
Bujumbura: 'BI',
Busingen: 'DE',
Cairo: 'EG',
Cambridge_Bay: 'CA',
Campo_Grande: 'BR',
Canary: 'ES',
Cancun: 'MX',
Cape_Verde: 'CV',
Caracas: 'VE',
Casablanca: 'MA',
Casey: 'AQ',
Catamarca: 'AR',
Cayenne: 'GF',
Cayman: 'KY',
Center: 'US',
Ceuta: 'ES',
Chagos: 'IO',
Chatham: 'NZ',
Chicago: 'US',
Chihuahua: 'MX',
Chisinau: 'MD',
Chita: 'RU',
Choibalsan: 'MN',
Christmas: 'CX',
Chuuk: 'FM',
Cocos: 'CC',
Colombo: 'LK',
Comoro: 'KM',
Conakry: 'GN',
Copenhagen: 'DK',
Cordoba: 'AR',
Costa_Rica: 'CR',
Creston: 'CA',
Cuiaba: 'BR',
Curacao: 'CW',
Dakar: 'SN',
Damascus: 'SY',
Danmarkshavn: 'GL',
Dar_es_Salaam: 'TZ',
Darwin: 'AU',
Davis: 'AQ',
Dawson: 'CA',
Dawson_Creek: 'CA',
Denver: 'US',
Detroit: 'US',
Dhaka: 'BD',
Dili: 'TL',
Djibouti: 'DJ',
Dominica: 'DM',
Douala: 'CM',
Dubai: 'AE',
Dublin: 'IE',
DumontDUrville: 'AQ',
Dushanbe: 'TJ',
Easter: 'CL',
Edmonton: 'CA',
Efate: 'VU',
Eirunepe: 'BR',
El_Aaiun: 'EH',
El_Salvador: 'SV',
Eucla: 'AU',
Fakaofo: 'TK',
Famagusta: 'CY',
Faroe: 'FO',
Fiji: 'FJ',
Fort_Nelson: 'CA',
Fortaleza: 'BR',
Freetown: 'SL',
Funafuti: 'TV',
Gaborone: 'BW',
Galapagos: 'EC',
Gambier: 'PF',
Gaza: 'PS',
Gibraltar: 'GI',
Glace_Bay: 'CA',
Goose_Bay: 'CA',
Grand_Turk: 'TC',
Grenada: 'GD',
Guadalcanal: 'SB',
Guadeloupe: 'GP',
Guam: 'GU',
Guatemala: 'GT',
Guayaquil: 'EC',
Guernsey: 'GG',
Guyana: 'GY',
Halifax: 'CA',
Harare: 'ZW',
Havana: 'CU',
Hebron: 'PS',
Helsinki: 'FI',
Hermosillo: 'MX',
Ho_Chi_Minh: 'VN',
Hobart: 'AU',
Hong_Kong: 'HK',
Honolulu: 'US',
Hovd: 'MN',
Indianapolis: 'US',
Inuvik: 'CA',
Iqaluit: 'CA',
Irkutsk: 'RU',
Isle_of_Man: 'IM',
Istanbul: 'TR',
Jakarta: 'ID',
Jamaica: 'JM',
Jayapura: 'ID',
Jersey: 'JE',
Jerusalem: 'IL',
Johannesburg: 'ZA',
Juba: 'SS',
Jujuy: 'AR',
Juneau: 'US',
Kabul: 'AF',
Kaliningrad: 'RU',
Kamchatka: 'RU',
Kampala: 'UG',
Kanton: 'KI',
Karachi: 'PK',
Kathmandu: 'NP',
Kerguelen: 'TF',
Khandyga: 'RU',
Khartoum: 'SD',
Kiev: 'UA',
Kigali: 'RW',
Kinshasa: 'CD',
Kiritimati: 'KI',
Kirov: 'RU',
Knox: 'US',
Kolkata: 'IN',
Kosrae: 'FM',
Kralendijk: 'NL',
Krasnoyarsk: 'RU',
Kuala_Lumpur: 'MY',
Kuching: 'MY',
Kuwait: 'KW',
Kwajalein: 'MH',
La_Paz: 'BO',
La_Rioja: 'AR',
Lagos: 'NG',
Libreville: 'GA',
Lima: 'PE',
Lindeman: 'AU',
Lisbon: 'PT',
Ljubljana: 'SI',
Lome: 'TG',
London: 'GB',
Longyearbyen: 'SJ',
Lord_Howe: 'AU',
Los_Angeles: 'US',
Louisville: 'US',
Lower_Princes: 'SX',
Luanda: 'AO',
Lubumbashi: 'CD',
Lusaka: 'ZM',
Luxembourg: 'LU',
Macau: 'MO',
Maceio: 'BR',
Macquarie: 'AU',
Madeira: 'PT',
Madrid: 'ES',
Magadan: 'RU',
Mahe: 'SC',
Majuro: 'MH',
Makassar: 'ID',
Malabo: 'GQ',
Maldives: 'MV',
Malta: 'MT',
Managua: 'NI',
Manaus: 'BR',
Manila: 'PH',
Maputo: 'MZ',
Marengo: 'US',
Mariehamn: 'AX',
Marigot: 'MF',
Marquesas: 'PF',
Martinique: 'MQ',
Maseru: 'LS',
Matamoros: 'MX',
Mauritius: 'MU',
Mawson: 'AQ',
Mayotte: 'YT',
Mazatlan: 'MX',
Mbabane: 'SZ',
McMurdo: 'AQ',
Melbourne: 'AU',
Mendoza: 'AR',
Menominee: 'US',
Merida: 'MX',
Metlakatla: 'US',
Mexico_City: 'MX',
Midway: 'UM',
Minsk: 'BY',
Miquelon: 'PM',
Mogadishu: 'SO',
Monaco: 'MC',
Moncton: 'CA',
Monrovia: 'LR',
Monterrey: 'MX',
Montevideo: 'UY',
Monticello: 'US',
Montserrat: 'MS',
Moscow: 'RU',
Muscat: 'OM',
Nairobi: 'KE',
Nassau: 'BS',
Nauru: 'NR',
Ndjamena: 'TD',
New_Salem: 'US',
New_York: 'US',
Niamey: 'NE',
Nicosia: 'CY',
Nipigon: 'CA',
Niue: 'NU',
Nome: 'US',
Norfolk: 'NF',
Noronha: 'BR',
Nouakchott: 'MR',
Noumea: 'NC',
Novokuznetsk: 'RU',
Novosibirsk: 'RU',
Nuuk: 'GL',
Ojinaga: 'MX',
Omsk: 'RU',
Oral: 'KZ',
Oslo: 'NO',
Ouagadougou: 'BF',
Pago_Pago: 'AS',
Palau: 'PW',
Palmer: 'AQ',
Panama: 'PA',
Pangnirtung: 'CA',
Paramaribo: 'SR',
Paris: 'FR',
Perth: 'AU',
Petersburg: 'US',
Phnom_Penh: 'KH',
Phoenix: 'US',
Pitcairn: 'PN',
Podgorica: 'ME',
Pohnpei: 'FM',
Pontianak: 'ID',
'Port-au-Prince': 'HT',
Port_Moresby: 'PG',
Port_of_Spain: 'TT',
'Porto-Novo': 'BJ',
Porto_Velho: 'BR',
Prague: 'CZ',
Puerto_Rico: 'PR',
Punta_Arenas: 'CL',
Pyongyang: 'KP',
Qatar: 'QA',
Qostanay: 'KZ',
Qyzylorda: 'KZ',
Rainy_River: 'CA',
Rankin_Inlet: 'CA',
Rarotonga: 'CK',
Recife: 'BR',
Regina: 'CA',
Resolute: 'CA',
Reunion: 'RE',
Reykjavik: 'IS',
Riga: 'LV',
Rio_Branco: 'BR',
Rio_Gallegos: 'AR',
Riyadh: 'SA',
Rome: 'IT',
Rothera: 'AQ',
Saipan: 'MP',
Sakhalin: 'RU',
Salta: 'AR',
Samara: 'RU',
Samarkand: 'UZ',
San_Juan: 'AR',
San_Luis: 'AR',
San_Marino: 'SM',
Santarem: 'BR',
Santiago: 'CL',
Santo_Domingo: 'DO',
Sao_Paulo: 'BR',
Sao_Tome: 'ST',
Sarajevo: 'BA',
Saratov: 'RU',
Scoresbysund: 'GL',
Seoul: 'KR',
Shanghai: 'CN',
Simferopol: 'RU',
Singapore: 'SG',
Sitka: 'US',
Skopje: 'MK',
Sofia: 'BG',
South_Georgia: 'GS',
Srednekolymsk: 'RU',
St_Barthelemy: 'BL',
St_Helena: 'SH',
St_Johns: 'CA',
St_Kitts: 'KN',
St_Lucia: 'LC',
St_Thomas: 'VI',
St_Vincent: 'VC',
Stanley: 'FK',
Stockholm: 'SE',
Swift_Current: 'CA',
Sydney: 'AU',
Syowa: 'AQ',
Tahiti: 'PF',
Taipei: 'TW',
Tallinn: 'EE',
Tarawa: 'KI',
Tashkent: 'UZ',
Tbilisi: 'GE',
Tegucigalpa: 'HN',
Tehran: 'IR',
Tell_City: 'US',
Thimphu: 'BT',
Thule: 'GL',
Thunder_Bay: 'CA',
Tijuana: 'MX',
Tirane: 'AL',
Tokyo: 'JP',
Tomsk: 'RU',
Tongatapu: 'TO',
Toronto: 'CA',
Tortola: 'VI (UK)',
Tripoli: 'LY',
Troll: 'AQ',
Tucuman: 'AR',
Tunis: 'TN',
Ulaanbaatar: 'MN',
Ulyanovsk: 'RU',
Urumqi: 'CN',
Ushuaia: 'AR',
'Ust-Nera': 'RU',
Uzhgorod: 'UA',
Vaduz: 'LI',
Vancouver: 'CA',
Vatican: 'VA',
Vevay: 'US',
Vienna: 'AT',
Vientiane: 'LA',
Vilnius: 'LT',
Vincennes: 'US',
Vladivostok: 'RU',
Volgograd: 'RU',
Vostok: 'AQ',
Wake: 'UM',
Wallis: 'WF',
Warsaw: 'PL',
Whitehorse: 'CA',
Winamac: 'US',
Windhoek: 'NA',
Winnipeg: 'CA',
Yakutat: 'US',
Yakutsk: 'RU',
Yangon: 'MM',
Yekaterinburg: 'RU',
Yellowknife: 'CA',
Yerevan: 'AM',
Zagreb: 'HR',
Zaporozhye: 'UA',
Zurich: 'CH'
};

1
package.json

@ -100,6 +100,7 @@
"class-transformer": "0.3.2",
"class-validator": "0.13.1",
"color": "4.2.3",
"countries-and-timezones": "3.4.1",
"countries-list": "2.6.1",
"countup.js": "2.3.2",
"date-fns": "2.29.3",

299
prisma/seed.js

@ -1,293 +1,7 @@
const {
AccountType,
DataSource,
PrismaClient,
Role,
Type
} = require('@prisma/client');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function main() {
const platformBitcoinSuisse = await prisma.platform.upsert({
create: {
id: '70b6e475-a2b9-4527-99db-943e4f38ce45',
name: 'Bitcoin Suisse',
url: 'https://www.bitcoinsuisse.com'
},
update: {},
where: { id: '70b6e475-a2b9-4527-99db-943e4f38ce45' }
});
const platformBitpanda = await prisma.platform.upsert({
create: {
id: 'debf9110-498f-4811-b972-7ebbd317e730',
name: 'Bitpanda',
url: 'https://www.bitpanda.com'
},
update: {},
where: { id: 'debf9110-498f-4811-b972-7ebbd317e730' }
});
const platformCoinbase = await prisma.platform.upsert({
create: {
id: '8dc24b88-bb92-4152-af25-fe6a31643e26',
name: 'Coinbase',
url: 'https://www.coinbase.com'
},
update: {},
where: { id: '8dc24b88-bb92-4152-af25-fe6a31643e26' }
});
const platformDegiro = await prisma.platform.upsert({
create: {
id: '94c1a2f4-a666-47be-84cd-4c8952e74c81',
name: 'DEGIRO',
url: 'https://www.degiro.eu'
},
update: {},
where: { id: '94c1a2f4-a666-47be-84cd-4c8952e74c81' }
});
const platformInteractiveBrokers = await prisma.platform.upsert({
create: {
id: '9da3a8a7-4795-43e3-a6db-ccb914189737',
name: 'Interactive Brokers',
url: 'https://www.interactivebrokers.com'
},
update: {},
where: { id: '9da3a8a7-4795-43e3-a6db-ccb914189737' }
});
const platformPostFinance = await prisma.platform.upsert({
create: {
id: '5377d9df-0d25-42c2-9d9b-e4c63166281e',
name: 'PostFinance',
url: 'https://www.postfinance.ch'
},
update: {},
where: { id: '5377d9df-0d25-42c2-9d9b-e4c63166281e' }
});
const platformSwissquote = await prisma.platform.upsert({
create: {
id: '1377d9df-0d25-42c2-9d9b-e4c63156291f',
name: 'Swissquote',
url: 'https://swissquote.com'
},
update: {},
where: { id: '1377d9df-0d25-42c2-9d9b-e4c63156291f' }
});
const userDemo = await prisma.user.upsert({
create: {
accessToken:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjliMTEyYjRkLTNiN2QtNGJhZC05YmRkLTNiMGY3YjRkYWMyZiIsImlhdCI6MTYxODUxMjAxNCwiZXhwIjoxNjIxMTA0MDE0fQ.l3WUxpI0hxuQtdPrD0kd7sem6S2kx_7CrdNvkmlKuWw',
Account: {
create: [
{
accountType: AccountType.SECURITIES,
balance: 0,
currency: 'USD',
id: 'd804de69-0429-42dc-b6ca-b308fd7dd926',
name: 'Coinbase Account',
platformId: platformCoinbase.id
},
{
accountType: AccountType.SECURITIES,
balance: 0,
currency: 'EUR',
id: '65cfb79d-b6c7-4591-9d46-73426bc62094',
name: 'DEGIRO Account',
platformId: platformDegiro.id
},
{
accountType: AccountType.SECURITIES,
balance: 0,
currency: 'USD',
id: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
isDefault: true,
name: 'Interactive Brokers Account',
platformId: platformInteractiveBrokers.id
}
]
},
id: '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f',
role: Role.DEMO
},
update: {},
where: { id: '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f' }
});
await prisma.symbolProfile.createMany({
data: [
{
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
countries: [{ code: 'US', weight: 1 }],
currency: 'USD',
dataSource: DataSource.YAHOO,
id: '2bd26362-136e-411c-b578-334084b4cdcc',
name: 'Amazon.com Inc.',
sectors: [{ name: 'Consumer Cyclical', weight: 1 }],
symbol: 'AMZN'
},
{
assetClass: 'CASH',
assetSubClass: 'CRYPTOCURRENCY',
countries: undefined,
currency: 'USD',
dataSource: DataSource.YAHOO,
id: 'fdc42ea6-1321-44f5-9fb0-d7f1f2cf9b1e',
name: 'Bitcoin USD',
sectors: undefined,
symbol: 'BTCUSD'
},
{
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
countries: [{ code: 'US', weight: 1 }],
currency: 'USD',
dataSource: DataSource.YAHOO,
id: 'd1ee9681-fb21-4f99-a3b7-afd4fc04df2e',
name: 'Tesla Inc.',
sectors: [{ name: 'Consumer Cyclical', weight: 1 }],
symbol: 'TSLA'
},
{
assetClass: 'EQUITY',
assetSubClass: 'ETF',
countries: [
{ code: 'US', weight: 0.9886789999999981 },
{ code: 'NL', weight: 0.000203 },
{ code: 'CA', weight: 0.000362 }
],
currency: 'USD',
dataSource: DataSource.YAHOO,
id: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
name: 'Vanguard Total Stock Market Index Fund ETF',
sectors: [
{ name: 'Technology', weight: 0.31393799999999955 },
{ name: 'Consumer Cyclical', weight: 0.149224 },
{ name: 'Financials', weight: 0.11716100000000002 },
{ name: 'Healthcare', weight: 0.13285199999999994 },
{ name: 'Consumer Staples', weight: 0.053919000000000016 },
{ name: 'Energy', weight: 0.025529999999999997 },
{ name: 'Telecommunications', weight: 0.012579 },
{ name: 'Industrials', weight: 0.09526399999999995 },
{ name: 'Utilities', weight: 0.024791999999999988 },
{ name: 'Materials', weight: 0.027664 },
{ name: 'Real Estate', weight: 0.03239999999999998 },
{ name: 'Communication', weight: 0.0036139999999999996 },
{ name: 'Other', weight: 0.000218 }
],
symbol: 'VTI'
}
],
skipDuplicates: true
});
await prisma.order.createMany({
data: [
{
accountId: '65cfb79d-b6c7-4591-9d46-73426bc62094',
accountUserId: userDemo.id,
date: new Date(Date.UTC(2017, 0, 3, 0, 0, 0)),
fee: 30,
id: 'cf7c0418-8535-4089-ae3d-5dbfa0aec2e1',
quantity: 50,
symbolProfileId: 'd1ee9681-fb21-4f99-a3b7-afd4fc04df2e', // TSLA
type: Type.BUY,
unitPrice: 42.97,
userId: userDemo.id
},
{
accountId: 'd804de69-0429-42dc-b6ca-b308fd7dd926',
accountUserId: userDemo.id,
date: new Date(Date.UTC(2017, 7, 16, 0, 0, 0)),
fee: 29.9,
id: 'a1c5d73a-8631-44e5-ac44-356827a5212c',
quantity: 0.5614682,
symbolProfileId: 'fdc42ea6-1321-44f5-9fb0-d7f1f2cf9b1e', // BTCUSD
type: Type.BUY,
unitPrice: 3562.089535970158,
userId: userDemo.id
},
{
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
accountUserId: userDemo.id,
date: new Date(Date.UTC(2018, 9, 1, 0, 0, 0)),
fee: 80.79,
id: '71c08e2a-4a86-44ae-a890-c337de5d5f9b',
quantity: 5,
symbolProfileId: '2bd26362-136e-411c-b578-334084b4cdcc', // AMZN
type: Type.BUY,
unitPrice: 2021.99,
userId: userDemo.id
},
{
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
accountUserId: userDemo.id,
date: new Date(Date.UTC(2019, 2, 1, 0, 0, 0)),
fee: 19.9,
id: '385f2c2c-d53e-4937-b0e5-e92ef6020d4e',
quantity: 10,
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI
type: Type.BUY,
unitPrice: 144.38,
userId: userDemo.id
},
{
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
accountUserId: userDemo.id,
date: new Date(Date.UTC(2019, 8, 3, 0, 0, 0)),
fee: 19.9,
id: '185f2c2c-d53e-4937-b0e5-a93ef6020d4e',
quantity: 10,
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI
type: Type.BUY,
unitPrice: 147.99,
userId: userDemo.id
},
{
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
accountUserId: userDemo.id,
date: new Date(Date.UTC(2020, 2, 2, 0, 0, 0)),
fee: 19.9,
id: '347b0430-a84f-4031-a0f9-390399066ad6',
quantity: 10,
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI
type: Type.BUY,
unitPrice: 151.41,
userId: userDemo.id
},
{
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
accountUserId: userDemo.id,
date: new Date(Date.UTC(2020, 8, 1, 0, 0, 0)),
fee: 19.9,
id: '67ec3f47-3189-4b63-ba05-60d3a06b302f',
quantity: 10,
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI
type: Type.BUY,
unitPrice: 177.69,
userId: userDemo.id
},
{
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
accountUserId: userDemo.id,
date: new Date(Date.UTC(2020, 2, 1, 0, 0, 0)),
fee: 19.9,
id: 'd01c6fbc-fa8d-47e6-8e80-66f882d2bfd2',
quantity: 10,
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI
type: Type.BUY,
unitPrice: 203.15,
userId: userDemo.id
}
],
skipDuplicates: true
});
await prisma.tag.createMany({
data: [
{
@ -297,17 +11,6 @@ async function main() {
],
skipDuplicates: true
});
console.log({
platformBitcoinSuisse,
platformBitpanda,
platformCoinbase,
platformDegiro,
platformInteractiveBrokers,
platformPostFinance,
platformSwissquote,
userDemo
});
}
main()

5
yarn.lock

@ -9352,6 +9352,11 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1:
path-type "^4.0.0"
yaml "^1.10.0"
countries-and-timezones@3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/countries-and-timezones/-/countries-and-timezones-3.4.1.tgz#0ec2540f57e42f0f740eb2acaede786043347fe1"
integrity sha512-INeHGCony4XUUR8iGL/lmt9s1Oi+n+gFHeJAMfbV5hJfYeDOB8JG1oxz5xFQu5oBZoRCJe/87k1Vzue9DoIauA==
countries-list@2.6.1:
version "2.6.1"
resolved "https://registry.npmjs.org/countries-list/-/countries-list-2.6.1.tgz"

Loading…
Cancel
Save