Browse Source

Feature/Add support to grant private access with permissions (#2870)

* Add support to grant private access with permissions

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
pull/2930/head
Francisco Silva 12 months ago
committed by GitHub
parent
commit
3df8810412
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 23
      apps/api/src/app/access/access.controller.ts
  3. 7
      apps/api/src/app/access/create-access.dto.ts
  4. 34
      apps/api/src/app/portfolio/portfolio.controller.ts
  5. 21
      apps/api/src/app/user/user.service.ts
  6. 18
      apps/api/src/interceptors/redact-values-in-response.interceptor.ts
  7. 12
      apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
  8. 7
      apps/client/src/app/components/access-table/access-table.component.html
  9. 1
      apps/client/src/app/components/home-overview/home-overview.component.ts
  10. 8
      apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html
  11. 4
      apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts
  12. 16
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  13. 10
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  14. 17
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html
  15. 3
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts
  16. 5
      apps/client/src/app/components/user-account-access/user-account-access.component.ts
  17. 6
      libs/common/src/lib/interfaces/access.interface.ts
  18. 1
      libs/common/src/lib/types/access-type.type.ts
  19. 2
      libs/common/src/lib/types/index.ts
  20. 3
      libs/common/src/lib/types/user-with-settings.type.ts
  21. 2
      libs/ui/src/lib/value/value.component.html

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added support to grant private access with permissions (experimental)
- Added `permissions` to the `Access` model - Added `permissions` to the `Access` model
### Changed ### Changed

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

@ -42,23 +42,27 @@ export class AccessController {
where: { userId: this.request.user.id } where: { userId: this.request.user.id }
}); });
return accessesWithGranteeUser.map((access) => { return accessesWithGranteeUser.map(
if (access.GranteeUser) { ({ alias, GranteeUser, id, permissions }) => {
if (GranteeUser) {
return { return {
alias: access.alias, alias,
grantee: access.GranteeUser?.id, id,
id: access.id, permissions,
type: 'RESTRICTED_VIEW' grantee: GranteeUser?.id,
type: 'PRIVATE'
}; };
} }
return { return {
alias: access.alias, alias,
id,
permissions,
grantee: 'Public', grantee: 'Public',
id: access.id,
type: 'PUBLIC' type: 'PUBLIC'
}; };
}); }
);
} }
@HasPermission(permissions.createAccess) @HasPermission(permissions.createAccess)
@ -83,6 +87,7 @@ export class AccessController {
GranteeUser: data.granteeUserId GranteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } } ? { connect: { id: data.granteeUserId } }
: undefined, : undefined,
permissions: data.permissions,
User: { connect: { id: this.request.user.id } } User: { connect: { id: this.request.user.id } }
}); });
} catch { } catch {

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

@ -1,4 +1,5 @@
import { IsOptional, IsString, IsUUID } from 'class-validator'; import { AccessPermission } from '@prisma/client';
import { IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
export class CreateAccessDto { export class CreateAccessDto {
@IsOptional() @IsOptional()
@ -9,7 +10,7 @@ export class CreateAccessDto {
@IsUUID() @IsUUID()
granteeUserId?: string; granteeUserId?: string;
@IsEnum(AccessPermission, { each: true })
@IsOptional() @IsOptional()
@IsString() permissions?: AccessPermission[];
type?: 'PUBLIC';
} }

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

@ -74,6 +74,11 @@ export class PortfolioController {
): Promise<PortfolioDetails & { hasError: boolean }> { ): Promise<PortfolioDetails & { hasError: boolean }> {
let hasDetails = true; let hasDetails = true;
let hasError = false; let hasError = false;
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
hasDetails = this.request.user.subscription.type === 'Premium'; hasDetails = this.request.user.subscription.type === 'Premium';
@ -108,7 +113,7 @@ export class PortfolioController {
let portfolioSummary = summary; let portfolioSummary = summary;
if ( if (
impersonationId || hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
const totalInvestment = Object.values(holdings) const totalInvestment = Object.values(holdings)
@ -148,7 +153,7 @@ export class PortfolioController {
if ( if (
hasDetails === false || hasDetails === false ||
impersonationId || hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
portfolioSummary = nullifyValuesInObject(summary, [ portfolioSummary = nullifyValuesInObject(summary, [
@ -164,6 +169,7 @@ export class PortfolioController {
'excludedAccountsAndActivities', 'excludedAccountsAndActivities',
'fees', 'fees',
'fireWealth', 'fireWealth',
'interest',
'items', 'items',
'liabilities', 'liabilities',
'netWorth', 'netWorth',
@ -216,6 +222,12 @@ export class PortfolioController {
@Query('range') dateRange: DateRange = 'max', @Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string @Query('tags') filterByTags?: string
): Promise<PortfolioDividends> { ): Promise<PortfolioDividends> {
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts, filterByAccounts,
filterByAssetClasses, filterByAssetClasses,
@ -230,7 +242,7 @@ export class PortfolioController {
}); });
if ( if (
impersonationId || hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
const maxDividend = dividends.reduce( const maxDividend = dividends.reduce(
@ -266,6 +278,12 @@ export class PortfolioController {
@Query('range') dateRange: DateRange = 'max', @Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string @Query('tags') filterByTags?: string
): Promise<PortfolioInvestments> { ): Promise<PortfolioInvestments> {
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts, filterByAccounts,
filterByAssetClasses, filterByAssetClasses,
@ -281,7 +299,7 @@ export class PortfolioController {
}); });
if ( if (
impersonationId || hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
const maxInvestment = investments.reduce( const maxInvestment = investments.reduce(
@ -329,6 +347,12 @@ export class PortfolioController {
@Query('tags') filterByTags?: string, @Query('tags') filterByTags?: string,
@Query('withExcludedAccounts') withExcludedAccounts = false @Query('withExcludedAccounts') withExcludedAccounts = false
): Promise<PortfolioPerformanceResponse> { ): Promise<PortfolioPerformanceResponse> {
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts, filterByAccounts,
filterByAssetClasses, filterByAssetClasses,
@ -344,7 +368,7 @@ export class PortfolioController {
}); });
if ( if (
impersonationId || hasReadRestrictedAccessPermission ||
this.request.user.Settings.settings.viewMode === 'ZEN' || this.request.user.Settings.settings.viewMode === 'ZEN' ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {

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

@ -105,6 +105,24 @@ export class UserService {
return usersWithAdminRole.length > 0; return usersWithAdminRole.length > 0;
} }
public hasReadRestrictedAccessPermission({
impersonationId,
user
}: {
impersonationId: string;
user: UserWithSettings;
}) {
if (!impersonationId) {
return false;
}
const access = user.Access?.find(({ id }) => {
return id === impersonationId;
});
return access?.permissions?.includes('READ_RESTRICTED') ?? true;
}
public isRestrictedView(aUser: UserWithSettings) { public isRestrictedView(aUser: UserWithSettings) {
return aUser.Settings.settings.isRestrictedView ?? false; return aUser.Settings.settings.isRestrictedView ?? false;
} }
@ -113,6 +131,7 @@ export class UserService {
userWhereUniqueInput: Prisma.UserWhereUniqueInput userWhereUniqueInput: Prisma.UserWhereUniqueInput
): Promise<UserWithSettings | null> { ): Promise<UserWithSettings | null> {
const { const {
Access,
accessToken, accessToken,
Account, Account,
Analytics, Analytics,
@ -127,6 +146,7 @@ export class UserService {
updatedAt updatedAt
} = await this.prismaService.user.findUnique({ } = await this.prismaService.user.findUnique({
include: { include: {
Access: true,
Account: { Account: {
include: { Platform: true } include: { Platform: true }
}, },
@ -138,6 +158,7 @@ export class UserService {
}); });
const user: UserWithSettings = { const user: UserWithSettings = {
Access,
accessToken, accessToken,
Account, Account,
authChallenge, authChallenge,

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

@ -1,6 +1,7 @@
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { redactAttributes } from '@ghostfolio/api/helper/object.helper'; import { redactAttributes } from '@ghostfolio/api/helper/object.helper';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { UserWithSettings } from '@ghostfolio/common/types';
import { import {
CallHandler, CallHandler,
ExecutionContext, ExecutionContext,
@ -22,13 +23,20 @@ export class RedactValuesInResponseInterceptor<T>
): Observable<any> { ): Observable<any> {
return next.handle().pipe( return next.handle().pipe(
map((data: any) => { map((data: any) => {
const request = context.switchToHttp().getRequest(); const { headers, user }: { headers: Headers; user: UserWithSettings } =
const hasImpersonationId = context.switchToHttp().getRequest();
!!request.headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
const impersonationId =
headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
const hasReadRestrictedPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user
});
if ( if (
hasImpersonationId || hasReadRestrictedPermission ||
this.userService.isRestrictedView(request.user) this.userService.isRestrictedView(user)
) { ) {
data = redactAttributes({ data = redactAttributes({
object: data, object: data,

12
apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts

@ -62,9 +62,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
}, this.configurationService.get('REQUEST_TIMEOUT')); }, this.configurationService.get('REQUEST_TIMEOUT'));
return got( return got(
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol.split( `${TrackinsightDataEnhancerService.baseUrl}/funds/${
'.' symbol.split('.')?.[0]
)?.[0]}.json`, }.json`,
{ {
// @ts-ignore // @ts-ignore
signal: abortController.signal signal: abortController.signal
@ -104,9 +104,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
}, this.configurationService.get('REQUEST_TIMEOUT')); }, this.configurationService.get('REQUEST_TIMEOUT'));
return got( return got(
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol.split( `${TrackinsightDataEnhancerService.baseUrl}/holdings/${
'.' symbol.split('.')?.[0]
)?.[0]}.json`, }.json`,
{ {
// @ts-ignore // @ts-ignore
signal: abortController.signal signal: abortController.signal

7
apps/client/src/app/components/access-table/access-table.component.html

@ -17,8 +17,13 @@
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th> <th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th>
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell> <td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
<div class="align-items-center d-flex"> <div class="align-items-center d-flex">
@if (element.permissions.includes('READ')) {
<ion-icon class="mr-1" name="lock-open-outline" />
<ng-container i18n>View</ng-container>
} @else if (element.permissions.includes('READ_RESTRICTED')) {
<ion-icon class="mr-1" name="lock-closed-outline" /> <ion-icon class="mr-1" name="lock-closed-outline" />
<ng-container i18n>Restricted View</ng-container> <ng-container i18n>Restricted view</ng-container>
}
</div> </div>
</td> </td>
</ng-container> </ng-container>

1
apps/client/src/app/components/home-overview/home-overview.component.ts

@ -74,7 +74,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
}); });
this.showDetails = this.showDetails =
!this.hasImpersonationId &&
!this.user.settings.isRestrictedView && !this.user.settings.isRestrictedView &&
this.user.settings.viewMode !== 'ZEN'; this.user.settings.viewMode !== 'ZEN';

8
apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html

@ -1,14 +1,12 @@
<div class="container p-0"> <div class="container p-0">
<div class="no-gutters row"> <div class="no-gutters row">
<div <div class="status-container text-muted text-right">
class="status-container text-muted text-right"
(click)="onShowErrors()"
>
@if (errors?.length > 0 && !isLoading) { @if (errors?.length > 0 && !isLoading) {
<ion-icon <ion-icon
i18n-title i18n-title
name="time-outline" name="time-outline"
title="Oops! Our data provider partner is experiencing the hiccups." title="Oops! A data provider is experiencing the hiccups."
(click)="onShowErrors()"
/> />
} }
</div> </div>

4
apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts

@ -58,7 +58,7 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
duration: 1, duration: 1,
separator: getNumberFormatGroup(this.locale) separator: getNumberFormatGroup(this.locale)
}).start(); }).start();
} else if (this.performance?.currentValue === null) { } else if (this.showDetails === false) {
new CountUp( new CountUp(
'value', 'value',
this.performance?.currentNetPerformancePercent * 100, this.performance?.currentNetPerformancePercent * 100,
@ -69,6 +69,8 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
separator: getNumberFormatGroup(this.locale) separator: getNumberFormatGroup(this.locale)
} }
).start(); ).start();
} else {
this.value.nativeElement.innerHTML = '*****';
} }
} }
} }

16
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -10,8 +10,8 @@
[hidden]="summary?.ordersCount === null" [hidden]="summary?.ordersCount === null"
> >
<div class="flex-grow-1 ml-3 text-truncate" i18n> <div class="flex-grow-1 ml-3 text-truncate" i18n>
{{ summary?.ordersCount }} {summary?.ordersCount, plural, =1 {transaction} {{ summary?.ordersCount }}
other {transactions}} {summary?.ordersCount, plural, =1 {transaction} other {transactions}}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -71,7 +71,11 @@
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
<div class="align-items-center d-flex flex-grow-1 ml-3 text-truncate"> <div class="align-items-center d-flex flex-grow-1 ml-3 text-truncate">
<ng-container i18n>Gross Performance</ng-container> <ng-container i18n>Gross Performance</ng-container>
<abbr class="initialism ml-2 text-muted" title="Time-Weighted Rate of Return">(TWR)</abbr> <abbr
class="initialism ml-2 text-muted"
title="Time-Weighted Rate of Return"
>(TWR)</abbr
>
</div> </div>
<div class="flex-column flex-wrap justify-content-end"> <div class="flex-column flex-wrap justify-content-end">
<gf-value <gf-value
@ -117,7 +121,11 @@
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
<div class="flex-grow-1 text-truncate ml-3"> <div class="flex-grow-1 text-truncate ml-3">
<ng-container i18n>Net Performance</ng-container> <ng-container i18n>Net Performance</ng-container>
<abbr class="initialism ml-2 text-muted" title="Time-Weighted Rate of Return">(TWR)</abbr> <abbr
class="initialism ml-2 text-muted"
title="Time-Weighted Rate of Return"
>(TWR)</abbr
>
</div> </div>
<div class="flex-column flex-wrap justify-content-end"> <div class="flex-column flex-wrap justify-content-end">
<gf-value <gf-value

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

@ -37,19 +37,23 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
ngOnInit() { ngOnInit() {
this.accessForm = this.formBuilder.group({ this.accessForm = this.formBuilder.group({
alias: [this.data.access.alias], alias: [this.data.access.alias],
permissions: [this.data.access.permissions[0], Validators.required],
type: [this.data.access.type, Validators.required], type: [this.data.access.type, Validators.required],
userId: [this.data.access.grantee, Validators.required] userId: [this.data.access.grantee, Validators.required]
}); });
this.accessForm.get('type').valueChanges.subscribe((value) => { this.accessForm.get('type').valueChanges.subscribe((accessType) => {
const permissionsControl = this.accessForm.get('permissions');
const userIdControl = this.accessForm.get('userId'); const userIdControl = this.accessForm.get('userId');
if (value === 'PRIVATE') { if (accessType === 'PRIVATE') {
permissionsControl.setValidators(Validators.required);
userIdControl.setValidators(Validators.required); userIdControl.setValidators(Validators.required);
} else { } else {
userIdControl.clearValidators(); userIdControl.clearValidators();
} }
permissionsControl.updateValueAndValidity();
userIdControl.updateValueAndValidity(); userIdControl.updateValueAndValidity();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
@ -64,7 +68,7 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
const access: CreateAccessDto = { const access: CreateAccessDto = {
alias: this.accessForm.controls['alias'].value, alias: this.accessForm.controls['alias'].value,
granteeUserId: this.accessForm.controls['userId'].value, granteeUserId: this.accessForm.controls['userId'].value,
type: this.accessForm.controls['type'].value permissions: [this.accessForm.controls['permissions'].value]
}; };
this.dataService this.dataService

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

@ -30,9 +30,20 @@
@if (accessForm.controls['type'].value === 'PRIVATE') { @if (accessForm.controls['type'].value === 'PRIVATE') {
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label <mat-label i18n>Permission</mat-label>
>Ghostfolio <ng-container i18n>User ID</ng-container></mat-label <mat-select formControlName="permissions">
> <mat-option i18n value="READ_RESTRICTED">Restricted view</mat-option>
@if(data?.user?.settings?.isExperimentalFeatures) {
<mat-option i18n value="READ">View</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label>
Ghostfolio <ng-container i18n>User ID</ng-container>
</mat-label>
<input <input
formControlName="userId" formControlName="userId"
matInput matInput

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

@ -1,5 +1,6 @@
import { Access } from '@ghostfolio/common/interfaces'; import { Access, User } from '@ghostfolio/common/interfaces';
export interface CreateOrUpdateAccessDialogParams { export interface CreateOrUpdateAccessDialogParams {
access: Access; access: Access;
user: User;
} }

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

@ -7,7 +7,6 @@ import {
} from '@angular/core'; } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Access, User } from '@ghostfolio/common/interfaces'; import { Access, User } from '@ghostfolio/common/interfaces';
@ -105,8 +104,10 @@ export class UserAccountAccessComponent implements OnDestroy, OnInit {
data: { data: {
access: { access: {
alias: '', alias: '',
permissions: ['READ_RESTRICTED'],
type: 'PRIVATE' type: 'PRIVATE'
} },
user: this.user
}, },
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType === 'mobile' ? '100vw' : '50rem'

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

@ -1,6 +1,10 @@
import { AccessType } from '@ghostfolio/common/types';
import { AccessPermission } from '@prisma/client';
export interface Access { export interface Access {
alias?: string; alias?: string;
grantee?: string; grantee?: string;
id: string; id: string;
type: 'PRIVATE' | 'PUBLIC' | 'RESTRICTED_VIEW'; permissions: AccessPermission[];
type: AccessType;
} }

1
libs/common/src/lib/types/access-type.type.ts

@ -0,0 +1 @@
export type AccessType = 'PRIVATE' | 'PUBLIC';

2
libs/common/src/lib/types/index.ts

@ -1,3 +1,4 @@
import type { AccessType } from './access-type.type';
import type { AccessWithGranteeUser } from './access-with-grantee-user.type'; import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
import type { AccountWithPlatform } from './account-with-platform.type'; import type { AccountWithPlatform } from './account-with-platform.type';
import type { AccountWithValue } from './account-with-value.type'; import type { AccountWithValue } from './account-with-value.type';
@ -18,6 +19,7 @@ import type { UserWithSettings } from './user-with-settings.type';
import type { ViewMode } from './view-mode.type'; import type { ViewMode } from './view-mode.type';
export type { export type {
AccessType,
AccessWithGranteeUser, AccessWithGranteeUser,
AccountWithPlatform, AccountWithPlatform,
AccountWithValue, AccountWithValue,

3
libs/common/src/lib/types/user-with-settings.type.ts

@ -1,10 +1,11 @@
import { UserSettings } from '@ghostfolio/common/interfaces'; import { UserSettings } from '@ghostfolio/common/interfaces';
import { SubscriptionOffer } from '@ghostfolio/common/types'; import { SubscriptionOffer } from '@ghostfolio/common/types';
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type'; import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
import { Account, Settings, User } from '@prisma/client'; import { Access, Account, Settings, User } from '@prisma/client';
// TODO: Compare with User interface // TODO: Compare with User interface
export type UserWithSettings = User & { export type UserWithSettings = User & {
Access: Access[];
Account: Account[]; Account: Account[];
activityCount: number; activityCount: number;
permissions?: string[]; permissions?: string[];

2
libs/ui/src/lib/value/value.component.html

@ -32,7 +32,7 @@
}" }"
> >
<ng-container *ngIf="value === null"> <ng-container *ngIf="value === null">
<span class="text-monospace text-muted">***</span> <span class="text-monospace text-muted">*****</span>
</ng-container> </ng-container>
<ng-container *ngIf="value !== null"> <ng-container *ngIf="value !== null">
{{ formattedValue }} {{ formattedValue }}

Loading…
Cancel
Save