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>
This commit is contained in:
parent
b8ca88c6df
commit
3df8810412
@ -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
|
||||||
|
@ -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 {
|
||||||
|
alias,
|
||||||
|
id,
|
||||||
|
permissions,
|
||||||
|
grantee: GranteeUser?.id,
|
||||||
|
type: 'PRIVATE'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
alias: access.alias,
|
alias,
|
||||||
grantee: access.GranteeUser?.id,
|
id,
|
||||||
id: access.id,
|
permissions,
|
||||||
type: 'RESTRICTED_VIEW'
|
grantee: 'Public',
|
||||||
|
type: 'PUBLIC'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
);
|
||||||
return {
|
|
||||||
alias: access.alias,
|
|
||||||
grantee: 'Public',
|
|
||||||
id: access.id,
|
|
||||||
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 {
|
||||||
|
@ -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';
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
) {
|
) {
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
<ion-icon class="mr-1" name="lock-closed-outline" />
|
@if (element.permissions.includes('READ')) {
|
||||||
<ng-container i18n>Restricted View</ng-container>
|
<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" />
|
||||||
|
<ng-container i18n>Restricted view</ng-container>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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 = '*****';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
@ -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
Normal file
1
libs/common/src/lib/types/access-type.type.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type AccessType = 'PRIVATE' | 'PUBLIC';
|
@ -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,
|
||||||
|
@ -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[];
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user