diff --git a/.env b/.env index e96c8b6b2..b5ae7ec70 100644 --- a/.env +++ b/.env @@ -12,5 +12,24 @@ POSTGRES_PASSWORD=password ACCESS_TOKEN_SALT=GHOSTFOLIO ALPHA_VANTAGE_API_KEY= DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer +DATA_SOURCES=["GHOSTFOLIO","GOOGLE_SHEETS","YAHOO"] +ENABLE_FEATURE_BLOG=true +ENABLE_FEATURE_CUSTOM_SYMBOLS=true +ENABLE_FEATURE_FEAR_AND_GREED_INDEX=true +ENABLE_FEATURE_SOCIAL_LOGIN=true +ENABLE_FEATURE_STATISTICS=true +ENABLE_FEATURE_SUBSCRIPTION=true +ENABLE_FEATURE_SYSTEM_MESSAGE=true +GOOGLE_CLIENT_ID=43371500610-uqv0gt8h5l6v2uo69c53o0h1ssfkbbni.apps.googleusercontent.com +GOOGLE_SECRET=0bOB89maN5JA43vqXkzvuHDs +GOOGLE_SHEETS_ACCOUNT=market-data@ghostfolio-337512.iam.gserviceaccount.com +GOOGLE_SHEETS_ID=1sSAsqhtleACpj0BQIrMz48TGGCXYM5RO80SeHtz_Seg +GOOGLE_SHEETS_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQChsFi+CYhF3CFI\nfccrucx1ZIXY3ZEVQ2R+gIhUsoGBkxoDU/oreuFB/xTYBSvIbVsub9KpLLRFKboi\nuRRs4UWodJGUZzHxYnUN5lIFA5V6hNVCzd137QKjJM7FatSiGEM/OcNqOKNkOCUs\nZlHQIkzHXtmAAA4JrpEQzNe0qdYMcYhoWG079/CvbBVx3y/90eX8ue2C1l1fDm1J\nB3yJj3GgiClH+gMRvP6om4bp+dZJ8CYDje2fTfnlZ99MxK6rQxWu/Eou6Ws8S6sf\nMhD8nCTSlG822Fuk8RLrzxHlvCFKAFEztLtCo7v56v3iTMW5sOplQa78/T7Mlpzh\njYq7YAJlAgMBAAECggEAIselaylvQgG+OhLuMSJsD9Nx9CqC7xU6THjW+osUTvxG\nw/EANvKdej4FrIr+NkSJsNU2dhQK6fa2FoqD0YDqpDgA6bCB/neskLMasP/qmzpw\nCkjwqv+VSeUcwjv+5ag87OB+v75TrTbjjierUQ9Tvy4QsJcybdQ6WagKfU7sH2xD\n8mNV4gVEYzBbWfG29v1gIdVmxKdZbfkCFB1hcvv8ZFwNq9NBIT8tbLEFfnrD9tw1\nYiK6y/efSfZ9OZ+2kIBntI0MR9jysg7tD+sDHC9Q+O8XPzwi/x4yLdFyI8uTivYA\nq/GdvnDXmH0WfuVGP8Hs059TbW7xNaMDX3cczVu4gQKBgQDTE3K4jYFE+NgXnCjD\nXUqEuOwFtEX12isA4SvrxZzw9CM7VR3yG/i43/O6B2roX0niVHPTi/n+cy2bUPy6\nXt5pO+qR6n4OsH/BpvP2vj7RNb4OaCbVstQQGvpOrNrXc678F+LwnquHek17rbV9\nXqVLYbbNd7iZNk468ZE9GeMV5QKBgQDEGgX76j8mSDH7zfdh/IO1oWOY91NdL1uh\nOsp4vPv4dhbMIPDebSyE316Zq6JwNGbD2RW3sUSRtL21z2jGzN0aqSTRUNTzFZIr\n5CLmIrxxjsoy6YkQZOx4OL4QWulj+inzdgpDsPA++HbAeGl7SJi9/2wLiqn2BNWL\nukBsjEpygQKBgGPj9Uu/s+iXN3Tc8zGZqdVryk7cxKsX53gQGAAJUj952l6O5pAY\nirm7SpXEQuTbi5Sv0OzRdqrjiTbSuffdQ7Zbo6QQbD25a4yS3SvtVr8dhuc8hPxn\nGBLTIZgwF5UU6z/kcgLbpGOGDrs0NwqwytsE0EUmnlbrq1Qb1FctNBm9AoGAGeLB\nhXZvbZM8HdwbWrDlhfVO22NSesuEkezby0JPFIYqDjoO8Z2Bsex2ZVyVrbANHK8s\nQbpBreYo4LYHQ67JRPqs5ICCC7B+QhL0VGKjc24A3OWc9TANUvVSiYAmrM7Z+MxN\nIJBbtkRAELoUWnTDzNjJn2BnfRU4RyCH3oxKS4ECgYEAgDj5jWTpFj7jX6L2OKgU\ntWWnZwW3v9PVEKFM0UTalIEL0FA99TUwJ2dxkdSagNHftVNIOj2SbKdXK+QLgVRn\naGO85QZ2IDgiOiPfuqUfmPsnk6WsZRhRSbUzdzel/ZfgLCbpErStFfnuPDCrIz7W\nIQf4jR6u3q8L6cmnKa6e0u0=\n-----END PRIVATE KEY-----\n" +IS_DEVELOPMENT_MODE=false JWT_SECRET_KEY=123456 +#MAX_ORDERS_TO_IMPORT=20 PORT=3333 +RAKUTEN_RAPID_API_KEY=db994bceaamsh39c922e3ac79677p17da8ajsnd69256103cbd +STRIPE_PUBLIC_KEY=pk_test_0OKelyHqlD6IHLq7cVuSCYWR +STRIPE_SECRET_KEY=sk_test_uZgtp2vZNugwkfALI0LQhGn8 +WEB_AUTH_RP_ID=ghostfol.io diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts index 3b3cd8da6..55efc0249 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts @@ -211,15 +211,14 @@ export class PositionDetailDialog implements OnDestroy, OnInit { ) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((data) => { - downloadAsFile( - data, - `ghostfolio-export-${this.SymbolProfile?.symbol}-${format( + downloadAsFile({ + content: data, + fileName: `ghostfolio-export-${this.SymbolProfile?.symbol}-${format( parseISO(data.meta.date), 'yyyyMMddHHmm' )}.json`, - 'text/plain', - 'json' - ); + format: 'json' + }); }); } diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index fcfe7b8dd..61683fd75 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -7,15 +7,16 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interf import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { DataService } from '@ghostfolio/client/services/data.service'; +import { IcsService } from '@ghostfolio/client/services/ics/ics.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { downloadAsFile } from '@ghostfolio/common/helper'; -import { Export, User } from '@ghostfolio/common/interfaces'; +import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import { DataSource, Order as OrderModel, Type } from '@prisma/client'; +import { DataSource, Order as OrderModel } from '@prisma/client'; import { format, parseISO } from 'date-fns'; -import { capitalize, isArray } from 'lodash'; +import { isArray } from 'lodash'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject, Subscription } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -50,6 +51,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { private dataService: DataService, private deviceService: DeviceDetectorService, private dialog: MatDialog, + private icsService: IcsService, private impersonationStorageService: ImpersonationStorageService, private importTransactionsService: ImportTransactionsService, private route: ActivatedRoute, @@ -156,15 +158,14 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { delete activity.id; } - downloadAsFile( - data, - `ghostfolio-export-${format( + downloadAsFile({ + content: data, + fileName: `ghostfolio-export-${format( parseISO(data.meta.date), 'yyyyMMddHHmm' )}.json`, - 'text/plain', - 'json' - ); + format: 'json' + }); }); } @@ -173,15 +174,16 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { .fetchExport(activityIds) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((data) => { - downloadAsFile( - this.getIcsContent(data.activities), - `ghostfolio-drafts-${format( + downloadAsFile({ + content: this.icsService.transformActivitiesToIcsContent( + data.activities + ), + fileName: `ghostfolio-drafts-${format( parseISO(data.meta.date), 'yyyyMMddHHmm' )}.ics`, - 'text/plain', - 'string' - ); + format: 'string' + }); }); } @@ -315,49 +317,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { this.unsubscribeSubject.complete(); } - private getIcsContent(aActivities: Export['activities']) { - const header = [ - 'BEGIN:VCALENDAR', - 'VERSION:2.0', - 'PRODID:-//Ghostfolio//NONSGML v1.0//EN' - ]; - const events = aActivities.map((activity) => { - return this.getEvent({ - date: parseISO(activity.date), - id: activity.id, - symbol: activity.symbol, - type: activity.type - }); - }); - const footer = ['END:VCALENDAR']; - - return [...header, ...events, ...footer].join('\n'); - } - - private getEvent({ - date, - id, - symbol, - type - }: { - date: Date; - id: string; - symbol: string; - type: Type; - }) { - const today = format(new Date(), 'yyyyMMdd'); - - return [ - 'BEGIN:VEVENT', - `UID:${id}`, - `DTSTAMP:${today}T000000`, - `DTSTART;VALUE=DATE:${format(date, 'yyyyMMdd')}`, - `DTEND;VALUE=DATE:${format(date, 'yyyyMMdd')}`, - `SUMMARY:${capitalize(type)} ${symbol}`, - 'END:VEVENT' - ].join('\n'); - } - private handleImportError({ activities, error diff --git a/apps/client/src/app/services/ics/ics.service.ts b/apps/client/src/app/services/ics/ics.service.ts new file mode 100644 index 000000000..7329ea663 --- /dev/null +++ b/apps/client/src/app/services/ics/ics.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@angular/core'; +import { capitalize } from '@ghostfolio/common/helper'; +import { Export } from '@ghostfolio/common/interfaces'; +import { Type } from '@prisma/client'; +import { format, parseISO } from 'date-fns'; + +@Injectable({ + providedIn: 'root' +}) +export class IcsService { + private readonly ICS_DATE_FORMAT = 'yyyyMMdd'; + + public constructor() {} + + public transformActivitiesToIcsContent( + aActivities: Export['activities'] + ): string { + const header = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//Ghostfolio//NONSGML v1.0//EN' + ]; + const events = aActivities.map((activity) => { + return this.getEvent({ + date: parseISO(activity.date), + id: activity.id, + symbol: activity.symbol, + type: activity.type + }); + }); + const footer = ['END:VCALENDAR']; + + return [...header, ...events, ...footer].join('\n'); + } + + private getEvent({ + date, + id, + symbol, + type + }: { + date: Date; + id: string; + symbol: string; + type: Type; + }) { + const today = format(new Date(), this.ICS_DATE_FORMAT); + + return [ + 'BEGIN:VEVENT', + `UID:${id}`, + `DTSTAMP:${today}T000000`, + `DTSTART;VALUE=DATE:${format(date, this.ICS_DATE_FORMAT)}`, + `DTEND;VALUE=DATE:${format(date, this.ICS_DATE_FORMAT)}`, + `SUMMARY:${capitalize(type)} ${symbol}`, + 'END:VEVENT' + ].join('\n'); + } +} diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 9406b4df2..ad47abfdd 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -12,24 +12,28 @@ export function decodeDataSource(encodedDataSource: string) { return Buffer.from(encodedDataSource, 'hex').toString(); } -export function downloadAsFile( - aContent: unknown, - aFileName: string, - aContentType: string, - aType: 'json' | 'string' -) { +export function downloadAsFile({ + content, + contentType = 'text/plain', + fileName, + format +}: { + content: unknown; + contentType?: string; + fileName: string; + format: 'json' | 'string'; +}) { const a = document.createElement('a'); - let content = aContent; - if (aType === 'json') { - content = JSON.stringify(aContent, undefined, ' '); + if (format === 'json') { + content = JSON.stringify(content, undefined, ' '); } const file = new Blob([content], { - type: aContentType + type: contentType }); a.href = URL.createObjectURL(file); - a.download = aFileName; + a.download = fileName; a.click(); }