mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
15 changed files with 272 additions and 18 deletions
@ -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); |
||||
|
} |
@ -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>(HasPermissionGuard); |
||||
|
reflector = module.get<Reflector>(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); |
||||
|
}); |
||||
|
}); |
@ -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<string>( |
||||
|
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; |
||||
|
} |
||||
|
} |
@ -0,0 +1,36 @@ |
|||||
|
<table |
||||
|
class="gf-table w-100" |
||||
|
mat-table |
||||
|
matSort |
||||
|
matSortActive="date" |
||||
|
matSortDirection="desc" |
||||
|
[dataSource]="dataSource" |
||||
|
> |
||||
|
<ng-container matColumnDef="date"> |
||||
|
<th *matHeaderCellDef class="px-2" mat-header-cell mat-sort-header> |
||||
|
<ng-container i18n>Date</ng-container> |
||||
|
</th> |
||||
|
<td *matCellDef="let element" class="px-2" mat-cell> |
||||
|
<gf-value [isDate]="true" [locale]="locale" [value]="element?.date" /> |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container matColumnDef="value"> |
||||
|
<th *matHeaderCellDef class="px-2 text-right" mat-header-cell> |
||||
|
<ng-container i18n>Value</ng-container> |
||||
|
</th> |
||||
|
<td *matCellDef="let element" class="px-2" mat-cell> |
||||
|
<div class="d-flex justify-content-end"> |
||||
|
<gf-value |
||||
|
[isCurrency]="true" |
||||
|
[locale]="locale" |
||||
|
[unit]="element?.Account?.currency" |
||||
|
[value]="element?.value" |
||||
|
></gf-value> |
||||
|
</div> |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> |
||||
|
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr> |
||||
|
</table> |
@ -0,0 +1,5 @@ |
|||||
|
@import 'apps/client/src/styles/ghostfolio-style'; |
||||
|
|
||||
|
:host { |
||||
|
display: block; |
||||
|
} |
@ -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<void>(); |
||||
|
|
||||
|
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(); |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -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 {} |
Loading…
Reference in new issue