Germán Martín 16 hours ago
committed by GitHub
parent
commit
1ea3c18a61
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 40
      apps/api/src/app/access/access-filter.dto.ts
  3. 76
      apps/api/src/app/access/access.controller.ts
  4. 5
      apps/api/src/app/access/create-access.dto.ts
  5. 5
      apps/api/src/app/access/update-access.dto.ts
  6. 143
      apps/api/src/app/endpoints/public/public.controller.ts
  7. 232
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  8. 18
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html
  9. 17
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.scss
  10. 15
      apps/client/src/app/components/user-account-access/user-account-access.component.ts
  11. 14
      libs/common/src/lib/interfaces/access.interface.ts
  12. 4
      libs/common/src/lib/interfaces/index.ts

1
CHANGELOG.md

@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added a close holding button to the holding detail dialog
- Added the _Sponsors_ section to the about page
- Extended the user detail dialog in the users section of the admin control panel
- Enable filtering (by account, holdings, tag or asset class) in public access
### Changed

40
apps/api/src/app/access/access-filter.dto.ts

@ -0,0 +1,40 @@
import { DataSource } from '@prisma/client';
import { Type } from 'class-transformer';
import {
IsArray,
IsEnum,
IsOptional,
IsString,
ValidateNested
} from 'class-validator';
class HoldingFilterDto {
@IsEnum(DataSource)
dataSource: DataSource;
@IsString()
symbol: string;
}
export class AccessFilterDto {
@IsArray()
@IsOptional()
@IsString({ each: true })
accountIds?: string[];
@IsArray()
@IsOptional()
@IsString({ each: true })
assetClasses?: string[];
@IsArray()
@IsOptional()
@Type(() => HoldingFilterDto)
@ValidateNested({ each: true })
holdings?: HoldingFilterDto[];
@IsArray()
@IsOptional()
@IsString({ each: true })
tagIds?: string[];
}

76
apps/api/src/app/access/access.controller.ts

@ -1,7 +1,7 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { Access } from '@ghostfolio/common/interfaces';
import { Access, AccessSettings } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
@ -19,7 +19,7 @@ import {
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Access as AccessModel } from '@prisma/client';
import { Access as AccessModel, Prisma } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AccessService } from './access.service';
@ -46,22 +46,30 @@ export class AccessController {
});
return accessesWithGranteeUser.map(
({ alias, granteeUser, id, permissions }) => {
({
alias,
granteeUser,
id,
permissions: accessPermissions,
settings
}) => {
if (granteeUser) {
return {
alias,
id,
permissions,
grantee: granteeUser?.id,
id,
permissions: accessPermissions,
settings: settings as AccessSettings,
type: 'PRIVATE'
};
}
return {
alias,
id,
permissions,
grantee: 'Public',
id,
permissions: accessPermissions,
settings: settings as AccessSettings,
type: 'PUBLIC'
};
}
@ -85,12 +93,17 @@ export class AccessController {
}
try {
const settings: AccessSettings = data.filter
? { filter: data.filter }
: {};
return this.accessService.createAccess({
alias: data.alias || undefined,
granteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } }
: undefined,
permissions: data.permissions,
settings: settings as Prisma.InputJsonValue,
user: { connect: { id: this.request.user.id } }
});
} catch {
@ -101,27 +114,6 @@ export class AccessController {
}
}
@Delete(':id')
@HasPermission(permissions.deleteAccess)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteAccess(@Param('id') id: string): Promise<AccessModel> {
const originalAccess = await this.accessService.access({
id,
userId: this.request.user.id
});
if (!originalAccess) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.accessService.deleteAccess({
id
});
}
@HasPermission(permissions.updateAccess)
@Put(':id')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@ -152,13 +144,18 @@ export class AccessController {
}
try {
const settings: AccessSettings = data.filter
? { filter: data.filter }
: {};
return this.accessService.updateAccess({
data: {
alias: data.alias,
granteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } }
: { disconnect: true },
permissions: data.permissions
permissions: data.permissions,
settings: settings as Prisma.InputJsonValue
},
where: { id }
});
@ -169,4 +166,25 @@ export class AccessController {
);
}
}
@Delete(':id')
@HasPermission(permissions.deleteAccess)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteAccess(@Param('id') id: string): Promise<AccessModel> {
const originalAccess = await this.accessService.access({
id,
userId: this.request.user.id
});
if (!originalAccess) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.accessService.deleteAccess({
id
});
}
}

5
apps/api/src/app/access/create-access.dto.ts

@ -1,3 +1,5 @@
import { AccessFilter } from '@ghostfolio/common/interfaces';
import { AccessPermission } from '@prisma/client';
import { IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
@ -6,6 +8,9 @@ export class CreateAccessDto {
@IsString()
alias?: string;
@IsOptional()
filter?: AccessFilter;
@IsOptional()
@IsUUID()
granteeUserId?: string;

5
apps/api/src/app/access/update-access.dto.ts

@ -1,3 +1,5 @@
import { AccessFilter } from '@ghostfolio/common/interfaces';
import { AccessPermission } from '@prisma/client';
import { IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
@ -6,6 +8,9 @@ export class UpdateAccessDto {
@IsString()
alias?: string;
@IsOptional()
filter?: AccessFilter;
@IsOptional()
@IsUUID()
granteeUserId?: string;

143
apps/api/src/app/endpoints/public/public.controller.ts

@ -8,7 +8,11 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { getSum } from '@ghostfolio/common/helper';
import { PublicPortfolioResponse } from '@ghostfolio/common/interfaces';
import {
AccessSettings,
Filter,
PublicPortfolioResponse
} from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
@ -20,7 +24,7 @@ import {
UseInterceptors
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Type as ActivityType } from '@prisma/client';
import { Type as ActivityType, AssetSubClass } from '@prisma/client';
import { Big } from 'big.js';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -61,6 +65,62 @@ export class PublicController {
hasDetails = user.subscription.type === 'Premium';
}
// Get filter configuration from access settings
const accessSettings = (access.settings ?? {}) as AccessSettings;
const accessFilter = accessSettings.filter;
// Convert access filter to portfolio filters
const portfolioFilters: Filter[] = [];
if (accessFilter) {
// Add account filters
if (accessFilter.accountIds?.length > 0) {
portfolioFilters.push(
...accessFilter.accountIds.map((accountId) => ({
id: accountId,
type: 'ACCOUNT' as const
}))
);
}
// Add asset class filters
if (accessFilter.assetClasses?.length > 0) {
portfolioFilters.push(
...accessFilter.assetClasses.map((assetClass) => ({
id: assetClass,
type: 'ASSET_CLASS' as const
}))
);
}
// Add tag filters
if (accessFilter.tagIds?.length > 0) {
portfolioFilters.push(
...accessFilter.tagIds.map((tagId) => ({
id: tagId,
type: 'TAG' as const
}))
);
}
// Add holding filters (symbol + dataSource)
// Each holding needs both DATA_SOURCE and SYMBOL filters
if (accessFilter.holdings?.length > 0) {
for (const { dataSource, symbol } of accessFilter.holdings) {
portfolioFilters.push(
{
id: dataSource,
type: 'DATA_SOURCE' as const
},
{
id: symbol,
type: 'SYMBOL' as const
}
);
}
}
}
const [
{ createdAt, holdings, markets },
{ performance: performance1d },
@ -68,6 +128,7 @@ export class PublicController {
{ performance: performanceYtd }
] = await Promise.all([
this.portfolioService.getDetails({
filters: portfolioFilters.length > 0 ? portfolioFilters : undefined,
impersonationId: access.userId,
userId: user.id,
withMarkets: true
@ -75,13 +136,52 @@ export class PublicController {
...['1d', 'max', 'ytd'].map((dateRange) => {
return this.portfolioService.getPerformance({
dateRange,
filters: portfolioFilters.length > 0 ? portfolioFilters : undefined,
impersonationId: undefined,
userId: user.id
});
})
]);
const baseCurrency =
user.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY;
const filteredHoldings = Object.fromEntries(
Object.entries(holdings).filter(([, holding]) => {
// Remove only cash holdings that match the base currency
const isCash = holding.assetSubClass === AssetSubClass.CASH;
const isBaseCurrency = holding.symbol === baseCurrency;
return !(isCash && isBaseCurrency);
})
);
// Use filters for activities, but exclude DATA_SOURCE/SYMBOL filters
// if there are multiple holdings (the service can't handle multiple symbol filters)
const hasMultipleHoldingFilters =
accessFilter?.holdings && accessFilter.holdings.length > 1;
const activityFilters = portfolioFilters.filter((filter) => {
// Always include ACCOUNT, ASSET_CLASS, TAG filters
if (
filter.type === 'ACCOUNT' ||
filter.type === 'ASSET_CLASS' ||
filter.type === 'TAG'
) {
return true;
}
// Include DATA_SOURCE and SYMBOL only if there's a single holding filter
if (
!hasMultipleHoldingFilters &&
(filter.type === 'DATA_SOURCE' || filter.type === 'SYMBOL')
) {
return true;
}
return false;
});
const { activities } = await this.orderService.getOrders({
filters: activityFilters.length > 0 ? activityFilters : undefined,
includeDrafts: false,
sortColumn: 'date',
sortDirection: 'desc',
@ -92,7 +192,6 @@ export class PublicController {
withExcludedAccountsAndActivities: false
});
// Experimental
const latestActivities = this.configurationService.get(
'ENABLE_FEATURE_SUBSCRIPTION'
)
@ -129,11 +228,6 @@ export class PublicController {
const publicPortfolioResponse: PublicPortfolioResponse = {
createdAt,
hasDetails,
latestActivities,
markets,
alias: access.alias,
holdings: {},
performance: {
'1d': {
relativeChange:
@ -147,23 +241,32 @@ export class PublicController {
relativeChange:
performanceYtd.netPerformancePercentageWithCurrencyEffect
}
}
},
alias: access.alias,
hasDetails,
holdings: {},
latestActivities,
markets
};
const totalValue = getSum(
Object.values(holdings).map(({ currency, marketPrice, quantity }) => {
return new Big(
this.exchangeRateDataService.toCurrency(
quantity * marketPrice,
currency,
this.request.user?.settings?.settings.baseCurrency ??
DEFAULT_CURRENCY
)
);
})
Object.values(filteredHoldings).map(
({ currency, marketPrice, quantity }) => {
return new Big(
this.exchangeRateDataService.toCurrency(
quantity * marketPrice,
currency,
this.request.user?.settings?.settings.baseCurrency ??
DEFAULT_CURRENCY
)
);
}
)
).toNumber();
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
for (const [symbol, portfolioPosition] of Object.entries(
filteredHoldings
)) {
publicPortfolioResponse.holdings[symbol] = {
allocationInPercentage:
portfolioPosition.valueInBaseCurrency / totalValue,

232
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts

@ -1,8 +1,15 @@
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { UpdateAccessDto } from '@ghostfolio/api/app/access/update-access.dto';
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
import { DataService } from '@ghostfolio/client/services/data.service';
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
import {
AssetProfileIdentifier,
Filter,
PortfolioPosition
} from '@ghostfolio/common/interfaces';
import { AccountWithPlatform } from '@ghostfolio/common/types';
import {
GfPortfolioFilterFormComponent,
PortfolioFilterFormValue
} from '@ghostfolio/ui/portfolio-filter-form';
import {
ChangeDetectionStrategy,
@ -13,6 +20,7 @@ import {
OnInit
} from '@angular/core';
import {
AbstractControl,
FormBuilder,
FormGroup,
FormsModule,
@ -28,9 +36,13 @@ import {
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { AccessPermission } from '@prisma/client';
import { StatusCodes } from 'http-status-codes';
import { EMPTY, Subject, catchError, takeUntil } from 'rxjs';
import { NotificationService } from '../../../core/notification/notification.service';
import { DataService } from '../../../services/data.service';
import { validateObjectForForm } from '../../../util/form.util';
import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
@Component({
@ -38,6 +50,7 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
host: { class: 'h-100' },
imports: [
FormsModule,
GfPortfolioFilterFormComponent,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
@ -54,16 +67,22 @@ export class GfCreateOrUpdateAccessDialogComponent
{
public accessForm: FormGroup;
public mode: 'create' | 'update';
public showFilterPanel = false;
public accounts: AccountWithPlatform[] = [];
public assetClasses: Filter[] = [];
public holdings: PortfolioPosition[] = [];
public tags: Filter[] = [];
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) private data: CreateOrUpdateAccessDialogParams,
public dialogRef: MatDialogRef<GfCreateOrUpdateAccessDialogComponent>,
private dataService: DataService,
private formBuilder: FormBuilder,
private notificationService: NotificationService
private notificationService: NotificationService,
public dialogRef: MatDialogRef<GfCreateOrUpdateAccessDialogComponent>
) {
this.mode = this.data.access?.id ? 'update' : 'create';
}
@ -73,14 +92,20 @@ export class GfCreateOrUpdateAccessDialogComponent
this.accessForm = this.formBuilder.group({
alias: [this.data.access.alias],
filters: [null],
granteeUserId: [
this.data.access.grantee,
isPublic ? null : Validators.required
isPublic
? null
: [(control: AbstractControl) => Validators.required(control)]
],
permissions: [
this.data.access.permissions[0],
[(control: AbstractControl) => Validators.required(control)]
],
permissions: [this.data.access.permissions[0], Validators.required],
type: [
{ disabled: this.mode === 'update', value: this.data.access.type },
Validators.required
[(control: AbstractControl) => Validators.required(control)]
]
});
@ -89,17 +114,28 @@ export class GfCreateOrUpdateAccessDialogComponent
const permissionsControl = this.accessForm.get('permissions');
if (accessType === 'PRIVATE') {
granteeUserIdControl.setValidators(Validators.required);
granteeUserIdControl.setValidators([
(control: AbstractControl) => Validators.required(control)
]);
this.showFilterPanel = false;
this.accessForm.get('filters')?.setValue(null);
} else {
granteeUserIdControl.clearValidators();
granteeUserIdControl.setValue(null);
permissionsControl.setValue(this.data.access.permissions[0]);
this.showFilterPanel = true;
this.loadFilterData();
}
granteeUserIdControl.updateValueAndValidity();
this.changeDetectorRef.markForCheck();
});
if (isPublic) {
this.showFilterPanel = true;
this.loadFilterData();
}
}
public onCancel() {
@ -119,11 +155,164 @@ export class GfCreateOrUpdateAccessDialogComponent
this.unsubscribeSubject.complete();
}
private buildFilterObject():
| {
accountIds?: string[];
assetClasses?: string[];
holdings?: AssetProfileIdentifier[];
tagIds?: string[];
}
| undefined {
const filterValue = this.accessForm.get('filters')
?.value as PortfolioFilterFormValue | null;
if (
!filterValue ||
(!filterValue.account &&
!filterValue.assetClass &&
!filterValue.holding &&
!filterValue.tag)
) {
return undefined;
}
const filter: {
accountIds?: string[];
assetClasses?: string[];
holdings?: AssetProfileIdentifier[];
tagIds?: string[];
} = {};
if (filterValue.account) {
filter.accountIds = [filterValue.account];
}
if (filterValue.assetClass) {
filter.assetClasses = [filterValue.assetClass];
}
if (filterValue.holding) {
filter.holdings = [
{
dataSource: filterValue.holding.dataSource,
symbol: filterValue.holding.symbol
}
];
}
if (filterValue.tag) {
filter.tagIds = [filterValue.tag];
}
return filter;
}
private loadFilterData() {
const existingFilter = this.data.access.settings?.filter;
this.dataService
.fetchAccounts()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.accounts = response.accounts;
this.updateFiltersFormControl(existingFilter);
});
this.dataService
.fetchPortfolioDetails({})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
if (response.holdings) {
this.holdings = Object.values(response.holdings);
const assetClassesSet = new Set<string>();
Object.values(response.holdings).forEach((holding) => {
if (holding.assetClass) {
assetClassesSet.add(holding.assetClass);
}
});
this.assetClasses = Array.from(assetClassesSet).map((ac) => ({
id: ac,
label: ac,
type: 'ASSET_CLASS' as const
}));
this.updateFiltersFormControl(existingFilter);
}
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchTags()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.tags = response.map((tag) => ({
id: tag.id,
label: tag.name,
type: 'TAG' as const
}));
this.updateFiltersFormControl(existingFilter);
this.changeDetectorRef.markForCheck();
});
}
private updateFiltersFormControl(
existingFilter:
| {
accountIds?: string[];
assetClasses?: string[];
holdings?: AssetProfileIdentifier[];
tagIds?: string[];
}
| undefined
) {
if (!existingFilter) {
return;
}
const filterValue: Partial<PortfolioFilterFormValue> = {};
if (existingFilter.accountIds?.[0] && this.accounts.length > 0) {
filterValue.account = existingFilter.accountIds[0];
}
if (existingFilter.assetClasses?.[0] && this.assetClasses.length > 0) {
filterValue.assetClass = existingFilter.assetClasses[0];
}
if (existingFilter.holdings?.[0] && this.holdings.length > 0) {
const holdingData = existingFilter.holdings[0];
const holding = this.holdings.find(
(h) =>
h.dataSource === holdingData.dataSource &&
h.symbol === holdingData.symbol
);
if (holding) {
filterValue.holding = holding;
}
}
if (existingFilter.tagIds?.[0] && this.tags.length > 0) {
filterValue.tag = existingFilter.tagIds[0];
}
if (Object.keys(filterValue).length > 0) {
this.accessForm.get('filters')?.setValue(filterValue);
this.changeDetectorRef.markForCheck();
}
}
private async createAccess() {
const filter = this.showFilterPanel ? this.buildFilterObject() : undefined;
const access: CreateAccessDto = {
alias: this.accessForm.get('alias').value,
granteeUserId: this.accessForm.get('granteeUserId').value,
permissions: [this.accessForm.get('permissions').value]
alias: this.accessForm.get('alias')?.value as string,
filter: filter,
granteeUserId: this.accessForm.get('granteeUserId')?.value as string,
permissions: [
this.accessForm.get('permissions')?.value as AccessPermission
]
};
try {
@ -136,8 +325,8 @@ export class GfCreateOrUpdateAccessDialogComponent
this.dataService
.postAccess(access)
.pipe(
catchError((error) => {
if (error.status === StatusCodes.BAD_REQUEST) {
catchError((error: { status?: number }) => {
if (error.status === (StatusCodes.BAD_REQUEST as number)) {
this.notificationService.alert({
title: $localize`Oops! Could not grant access.`
});
@ -156,11 +345,16 @@ export class GfCreateOrUpdateAccessDialogComponent
}
private async updateAccess() {
const filter = this.showFilterPanel ? this.buildFilterObject() : undefined;
const access: UpdateAccessDto = {
alias: this.accessForm.get('alias').value,
granteeUserId: this.accessForm.get('granteeUserId').value,
alias: this.accessForm.get('alias')?.value as string,
filter: filter,
granteeUserId: this.accessForm.get('granteeUserId')?.value as string,
id: this.data.access.id,
permissions: [this.accessForm.get('permissions').value]
permissions: [
this.accessForm.get('permissions')?.value as AccessPermission
]
};
try {
@ -173,8 +367,8 @@ export class GfCreateOrUpdateAccessDialogComponent
this.dataService
.putAccess(access)
.pipe(
catchError(({ status }) => {
if (status.status === StatusCodes.BAD_REQUEST) {
catchError((error: { status?: number }) => {
if (error.status === (StatusCodes.BAD_REQUEST as number)) {
this.notificationService.alert({
title: $localize`Oops! Could not update access.`
});

18
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html

@ -59,6 +59,24 @@
</mat-form-field>
</div>
}
@if (showFilterPanel) {
<div class="mt-4">
<h2 class="filter-settings-title" i18n>Filter Settings (Optional)</h2>
<p class="filter-settings-description text-muted" i18n>
Configure which accounts, holdings, tags, and asset classes will be
visible in this public access. Leave empty to show all data.
</p>
<gf-portfolio-filter-form
formControlName="filters"
[accounts]="accounts"
[assetClasses]="assetClasses"
[holdings]="holdings"
[tags]="tags"
/>
</div>
}
</div>
<div class="justify-content-end" mat-dialog-actions>
<button mat-button type="button" (click)="onCancel()">

17
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.scss

@ -4,4 +4,21 @@
.mat-mdc-dialog-content {
max-height: unset;
}
ion-icon {
&.rotate-90 {
transform: rotate(90deg);
}
}
.filter-settings-title {
font-size: 1.125rem;
font-weight: 500;
margin-bottom: 1rem;
}
.filter-settings-description {
font-size: 0.875rem;
margin-bottom: 1rem;
}
}

15
apps/client/src/app/components/user-account-access/user-account-access.component.ts

@ -17,7 +17,12 @@ import {
OnDestroy,
OnInit
} from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import {
AbstractControl,
FormBuilder,
ReactiveFormsModule,
Validators
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
@ -61,7 +66,10 @@ export class GfUserAccountAccessComponent implements OnDestroy, OnInit {
public hasPermissionToUpdateOwnAccessToken: boolean;
public isAccessTokenHidden = true;
public updateOwnAccessTokenForm = this.formBuilder.group({
accessToken: ['', Validators.required]
accessToken: [
'',
[(control: AbstractControl) => Validators.required(control)]
]
});
public user: User;
@ -117,7 +125,7 @@ export class GfUserAccountAccessComponent implements OnDestroy, OnInit {
if (params['createDialog']) {
this.openCreateAccessDialog();
} else if (params['editDialog'] && params['accessId']) {
this.openUpdateAccessDialog(params['accessId']);
this.openUpdateAccessDialog(params['accessId'] as string);
}
});
@ -235,6 +243,7 @@ export class GfUserAccountAccessComponent implements OnDestroy, OnInit {
grantee: access.grantee === 'Public' ? null : access.grantee,
id: access.id,
permissions: access.permissions,
settings: access.settings,
type: access.type
}
},

14
libs/common/src/lib/interfaces/access.interface.ts

@ -2,10 +2,24 @@ import { AccessType } from '@ghostfolio/common/types';
import { AccessPermission } from '@prisma/client';
import { AssetProfileIdentifier } from './asset-profile-identifier.interface';
export interface AccessFilter {
accountIds?: string[];
assetClasses?: string[];
holdings?: AssetProfileIdentifier[];
tagIds?: string[];
}
export interface AccessSettings {
filter?: AccessFilter;
}
export interface Access {
alias?: string;
grantee?: string;
id: string;
permissions: AccessPermission[];
settings?: AccessSettings;
type: AccessType;
}

4
libs/common/src/lib/interfaces/index.ts

@ -1,4 +1,4 @@
import type { Access } from './access.interface';
import type { Access, AccessFilter, AccessSettings } from './access.interface';
import type { AccountBalance } from './account-balance.interface';
import type { AdminData } from './admin-data.interface';
import type { AdminJobs } from './admin-jobs.interface';
@ -81,6 +81,8 @@ import type { XRayRulesSettings } from './x-ray-rules-settings.interface';
export {
Access,
AccessFilter,
AccessSettings,
AccessTokenResponse,
AccountBalance,
AccountBalancesResponse,

Loading…
Cancel
Save