From ed4dd79c72db8668e71155e33bde2942c0e0ee6d Mon Sep 17 00:00:00 2001
From: Sanjeev Sharma <29539278+thesanjeevsharma@users.noreply.github.com>
Date: Sat, 2 Dec 2023 01:42:41 +0530
Subject: [PATCH 1/4] Add cash balances table to account detail dialog (#2549)
* Add cash balances table to account detail dialog
* Update changelog
---------
Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
---
CHANGELOG.md | 4 ++
.../account-detail-dialog.component.ts | 12 ++--
.../account-detail-dialog.html | 25 +++++---
.../account-detail-dialog.module.ts | 4 ++
.../portfolio/activities/activities-page.html | 2 +-
apps/client/src/app/services/data.service.ts | 7 +++
.../account-balances.component.html | 36 +++++++++++
.../account-balances.component.scss | 5 ++
.../account-balances.component.ts | 63 +++++++++++++++++++
.../account-balances.module.ts | 15 +++++
10 files changed, 161 insertions(+), 12 deletions(-)
create mode 100644 libs/ui/src/lib/account-balances/account-balances.component.html
create mode 100644 libs/ui/src/lib/account-balances/account-balances.component.scss
create mode 100644 libs/ui/src/lib/account-balances/account-balances.component.ts
create mode 100644 libs/ui/src/lib/account-balances/account-balances.module.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 729b4713b..2aa09c05a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- Added a historical cash balances table to the account detail dialog
+
+### Changed
+
- Respected the `withExcludedAccounts` flag in the account balance time series
## 2.27.1 - 2023-11-28
diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
index 284bfcca2..b3a916da9 100644
--- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
+++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
@@ -29,14 +29,15 @@ import { AccountDetailDialogParams } from './interfaces/interfaces';
styleUrls: ['./account-detail-dialog.component.scss']
})
export class AccountDetailDialog implements OnDestroy, OnInit {
+ public activities: OrderWithAccount[];
public balance: number;
public currency: string;
public equity: number;
public hasImpersonationId: boolean;
public historicalDataItems: HistoricalDataItem[];
+ public isLoadingActivities: boolean;
public isLoadingChart: boolean;
public name: string;
- public orders: OrderWithAccount[];
public platformName: string;
public transactionCount: number;
public user: User;
@@ -64,6 +65,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
}
public ngOnInit() {
+ this.isLoadingActivities = true;
this.isLoadingChart = true;
this.dataService
@@ -103,7 +105,9 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => {
- this.orders = activities;
+ this.activities = activities;
+
+ this.isLoadingActivities = false;
this.changeDetectorRef.markForCheck();
});
@@ -153,8 +157,8 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
public onExport() {
this.dataService
.fetchExport(
- this.orders.map((order) => {
- return order.id;
+ this.activities.map(({ id }) => {
+ return id;
})
)
.pipe(takeUntil(this.unsubscribeSubject))
diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
index 02d1c917e..7e92eca85 100644
--- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
+++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
@@ -31,7 +31,7 @@
>
-
+
-
-
-
Activities
+
+
+ Activities
-
-
+
+
+ Cash Balances
+
+
+
diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts
index c3d45b6ce..83ac5b6ea 100644
--- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts
+++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts
@@ -2,9 +2,11 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
+import { MatTabsModule } from '@angular/material/tabs';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
+import { GfAccountBalancesModule } from '@ghostfolio/ui/account-balances/account-balances.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@@ -15,6 +17,7 @@ import { AccountDetailDialog } from './account-detail-dialog.component';
declarations: [AccountDetailDialog],
imports: [
CommonModule,
+ GfAccountBalancesModule,
GfActivitiesTableModule,
GfDialogFooterModule,
GfDialogHeaderModule,
@@ -22,6 +25,7 @@ import { AccountDetailDialog } from './account-detail-dialog.component';
GfValueModule,
MatButtonModule,
MatDialogModule,
+ MatTabsModule,
NgxSkeletonLoaderModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.html b/apps/client/src/app/pages/portfolio/activities/activities-page.html
index a5c9201a0..8c2cf9bd5 100644
--- a/apps/client/src/app/pages/portfolio/activities/activities-page.html
+++ b/apps/client/src/app/pages/portfolio/activities/activities-page.html
@@ -1,5 +1,5 @@
-
+
Activities
(`/api/v1/account/${aAccountId}`);
}
+ public fetchAccountBalances(aAccountId: string) {
+ return this.http.get(
+ `/api/v1/account/${aAccountId}/balances`
+ );
+ }
+
public fetchAccounts() {
return this.http.get('/api/v1/account');
}
diff --git a/libs/ui/src/lib/account-balances/account-balances.component.html b/libs/ui/src/lib/account-balances/account-balances.component.html
new file mode 100644
index 000000000..81f8a8192
--- /dev/null
+++ b/libs/ui/src/lib/account-balances/account-balances.component.html
@@ -0,0 +1,36 @@
+
+
+
+ Date
+ |
+
+
+ |
+
+
+
+
+ Value
+ |
+
+
+
+
+ |
+
+
+
+
+
diff --git a/libs/ui/src/lib/account-balances/account-balances.component.scss b/libs/ui/src/lib/account-balances/account-balances.component.scss
new file mode 100644
index 000000000..b5b58f67e
--- /dev/null
+++ b/libs/ui/src/lib/account-balances/account-balances.component.scss
@@ -0,0 +1,5 @@
+@import 'apps/client/src/styles/ghostfolio-style';
+
+:host {
+ display: block;
+}
diff --git a/libs/ui/src/lib/account-balances/account-balances.component.ts b/libs/ui/src/lib/account-balances/account-balances.component.ts
new file mode 100644
index 000000000..c552519d6
--- /dev/null
+++ b/libs/ui/src/lib/account-balances/account-balances.component.ts
@@ -0,0 +1,63 @@
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ Input,
+ OnDestroy,
+ OnInit,
+ ViewChild
+} from '@angular/core';
+import { MatSort } from '@angular/material/sort';
+import { MatTableDataSource } from '@angular/material/table';
+import { DataService } from '@ghostfolio/client/services/data.service';
+import { AccountBalancesResponse } from '@ghostfolio/common/interfaces';
+import { get } from 'lodash';
+import { Subject, takeUntil } from 'rxjs';
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ selector: 'gf-account-balances',
+ styleUrls: ['./account-balances.component.scss'],
+ templateUrl: './account-balances.component.html'
+})
+export class AccountBalancesComponent implements OnDestroy, OnInit {
+ @Input() accountId: string;
+ @Input() locale: string;
+
+ @ViewChild(MatSort) sort: MatSort;
+
+ public dataSource: MatTableDataSource<
+ AccountBalancesResponse['balances'][0]
+ > = new MatTableDataSource();
+ public displayedColumns: string[] = ['date', 'value'];
+
+ private unsubscribeSubject = new Subject();
+
+ public constructor(
+ private changeDetectorRef: ChangeDetectorRef,
+ private dataService: DataService
+ ) {}
+
+ public ngOnInit() {
+ this.fetchBalances();
+ }
+
+ public ngOnDestroy() {
+ this.unsubscribeSubject.next();
+ this.unsubscribeSubject.complete();
+ }
+
+ private fetchBalances() {
+ this.dataService
+ .fetchAccountBalances(this.accountId)
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe(({ balances }) => {
+ this.dataSource = new MatTableDataSource(balances);
+
+ this.dataSource.sort = this.sort;
+ this.dataSource.sortingDataAccessor = get;
+
+ this.changeDetectorRef.markForCheck();
+ });
+ }
+}
diff --git a/libs/ui/src/lib/account-balances/account-balances.module.ts b/libs/ui/src/lib/account-balances/account-balances.module.ts
new file mode 100644
index 000000000..cc8fb9677
--- /dev/null
+++ b/libs/ui/src/lib/account-balances/account-balances.module.ts
@@ -0,0 +1,15 @@
+import { CommonModule } from '@angular/common';
+import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
+import { MatSortModule } from '@angular/material/sort';
+import { MatTableModule } from '@angular/material/table';
+import { GfValueModule } from '@ghostfolio/ui/value';
+
+import { AccountBalancesComponent } from './account-balances.component';
+
+@NgModule({
+ declarations: [AccountBalancesComponent],
+ exports: [AccountBalancesComponent],
+ imports: [CommonModule, GfValueModule, MatSortModule, MatTableModule],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+})
+export class GfAccountBalancesModule {}
From 4eedf64a3cbc28530a637ae5c2bbcc07b3a74c4c Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 2 Dec 2023 10:00:00 +0100
Subject: [PATCH 2/4] Update OSS Friends (#2701)
---
apps/client/src/assets/oss-friends.json | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/apps/client/src/assets/oss-friends.json b/apps/client/src/assets/oss-friends.json
index a95f8e709..d22f4c030 100644
--- a/apps/client/src/assets/oss-friends.json
+++ b/apps/client/src/assets/oss-friends.json
@@ -1,5 +1,5 @@
{
- "createdAt": "2023-11-17T00:00:00.000Z",
+ "createdAt": "2023-11-30T00:00:00.000Z",
"data": [
{
"name": "BoxyHQ",
@@ -16,6 +16,11 @@
"description": "Centralize community, product, and customer data to understand which companies are engaging with your open source project.",
"href": "https://www.crowd.dev"
},
+ {
+ "name": "DevHunt",
+ "description": "Find the best Dev Tools upvoted by the community every week.",
+ "href": "https://devhunt.org"
+ },
{
"name": "Documenso",
"description": "The Open-Source DocuSign Alternative. We aim to earn your trust by enabling you to self-host the platform and examine its inner workings.",
@@ -59,7 +64,7 @@
{
"name": "Hook0",
"description": "Open-Source Webhooks-as-a-service (WaaS) that makes it easy for developers to send webhooks.",
- "href": "https://www.hook0.com/"
+ "href": "https://www.hook0.com"
},
{
"name": "HTMX",
@@ -89,7 +94,7 @@
{
"name": "Papermark",
"description": "Open-Source Docsend Alternative to securely share documents with real-time analytics.",
- "href": "https://www.papermark.io/"
+ "href": "https://www.papermark.io"
},
{
"name": "Requestly",
@@ -109,7 +114,7 @@
{
"name": "Shelf.nu",
"description": "Open Source Asset and Equipment tracking software that lets you create QR asset labels, manage and overview your assets across locations.",
- "href": "https://www.shelf.nu/"
+ "href": "https://www.shelf.nu"
},
{
"name": "Sniffnet",
From 1d796a959738f6f3f61fce2253855b40be500ceb Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 2 Dec 2023 10:20:05 +0100
Subject: [PATCH 3/4] Bugfix/change intraday data gathering to operate
synchronously (#2705)
* Change intraday data gathering to operate synchronously
* Update changelog
---
CHANGELOG.md | 4 ++++
apps/api/src/services/data-provider/data-provider.service.ts | 2 +-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2aa09c05a..a865536aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Respected the `withExcludedAccounts` flag in the account balance time series
+### Fixed
+
+- Changed the mechanism of the `INTRADAY` data gathering to operate synchronously avoiding database deadlocks
+
## 2.27.1 - 2023-11-28
### Changed
diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts
index ef5143475..cd5874ca7 100644
--- a/apps/api/src/services/data-provider/data-provider.service.ts
+++ b/apps/api/src/services/data-provider/data-provider.service.ts
@@ -346,7 +346,7 @@ export class DataProviderService {
);
try {
- this.marketDataService.updateMany({
+ await this.marketDataService.updateMany({
data: Object.keys(response)
.filter((symbol) => {
return (
From 813e73a0a3902a29a3ade79ed05e3ec91bbae556 Mon Sep 17 00:00:00 2001
From: underwater
Date: Sat, 2 Dec 2023 10:21:19 +0100
Subject: [PATCH 4/4] Introduce HasPermission annotation (#2693)
* Introduce HasPermission annotation
* Update changelog
---
CHANGELOG.md | 1 +
.../decorators/has-permission.decorator.ts | 6 ++
.../src/guards/has-permission.guard.spec.ts | 55 +++++++++++++++++++
apps/api/src/guards/has-permission.guard.ts | 37 +++++++++++++
4 files changed, 99 insertions(+)
create mode 100644 apps/api/src/decorators/has-permission.decorator.ts
create mode 100644 apps/api/src/guards/has-permission.guard.spec.ts
create mode 100644 apps/api/src/guards/has-permission.guard.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a865536aa..2a5a5dc5b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added a historical cash balances table to the account detail dialog
+- Introduced a `HasPermission` annotation for endpoints
### Changed
diff --git a/apps/api/src/decorators/has-permission.decorator.ts b/apps/api/src/decorators/has-permission.decorator.ts
new file mode 100644
index 000000000..dc65cf82e
--- /dev/null
+++ b/apps/api/src/decorators/has-permission.decorator.ts
@@ -0,0 +1,6 @@
+import { SetMetadata } from '@nestjs/common';
+export const HAS_PERMISSION_KEY = 'has_permission';
+
+export function HasPermission(permission: string) {
+ return SetMetadata(HAS_PERMISSION_KEY, permission);
+}
diff --git a/apps/api/src/guards/has-permission.guard.spec.ts b/apps/api/src/guards/has-permission.guard.spec.ts
new file mode 100644
index 000000000..7f5f90de9
--- /dev/null
+++ b/apps/api/src/guards/has-permission.guard.spec.ts
@@ -0,0 +1,55 @@
+import { HttpException } from '@nestjs/common';
+import { Reflector } from '@nestjs/core';
+import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
+import { Test, TestingModule } from '@nestjs/testing';
+
+import { HasPermissionGuard } from './has-permission.guard';
+
+describe('HasPermissionGuard', () => {
+ let guard: HasPermissionGuard;
+ let reflector: Reflector;
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [HasPermissionGuard, Reflector]
+ }).compile();
+
+ guard = module.get(HasPermissionGuard);
+ reflector = module.get(Reflector);
+ });
+
+ function setupReflectorSpy(returnValue: string) {
+ jest.spyOn(reflector, 'get').mockReturnValue(returnValue);
+ }
+
+ function createMockExecutionContext(permissions: string[]) {
+ return new ExecutionContextHost([
+ {
+ user: {
+ permissions // Set user permissions based on the argument
+ }
+ }
+ ]);
+ }
+
+ it('should deny access if the user does not have any permission', () => {
+ setupReflectorSpy('required-permission');
+ const noPermissions = createMockExecutionContext([]);
+
+ expect(() => guard.canActivate(noPermissions)).toThrow(HttpException);
+ });
+
+ it('should deny access if the user has the wrong permission', () => {
+ setupReflectorSpy('required-permission');
+ const wrongPermission = createMockExecutionContext(['wrong-permission']);
+
+ expect(() => guard.canActivate(wrongPermission)).toThrow(HttpException);
+ });
+
+ it('should allow access if the user has the required permission', () => {
+ setupReflectorSpy('required-permission');
+ const rightPermission = createMockExecutionContext(['required-permission']);
+
+ expect(guard.canActivate(rightPermission)).toBe(true);
+ });
+});
diff --git a/apps/api/src/guards/has-permission.guard.ts b/apps/api/src/guards/has-permission.guard.ts
new file mode 100644
index 000000000..298992d06
--- /dev/null
+++ b/apps/api/src/guards/has-permission.guard.ts
@@ -0,0 +1,37 @@
+import { HAS_PERMISSION_KEY } from '@ghostfolio/api/decorators/has-permission.decorator';
+import { hasPermission } from '@ghostfolio/common/permissions';
+import {
+ CanActivate,
+ ExecutionContext,
+ HttpException,
+ Injectable
+} from '@nestjs/common';
+import { Reflector } from '@nestjs/core';
+import { StatusCodes, getReasonPhrase } from 'http-status-codes';
+
+@Injectable()
+export class HasPermissionGuard implements CanActivate {
+ public constructor(private reflector: Reflector) {}
+
+ public canActivate(context: ExecutionContext): boolean {
+ const requiredPermission = this.reflector.get(
+ HAS_PERMISSION_KEY,
+ context.getHandler()
+ );
+
+ if (!requiredPermission) {
+ return true; // No specific permissions required
+ }
+
+ const { user } = context.switchToHttp().getRequest();
+
+ if (!user || !hasPermission(user.permissions, requiredPermission)) {
+ throw new HttpException(
+ getReasonPhrase(StatusCodes.FORBIDDEN),
+ StatusCodes.FORBIDDEN
+ );
+ }
+
+ return true;
+ }
+}