Browse Source

Restrict support for filtering to public access

pull/7753/head
Thomas Kaul 21 hours ago
parent
commit
dda8256617
  1. 36
      apps/api/src/app/access/access.controller.ts
  2. 13
      apps/api/src/app/access/access.service.ts
  3. 18
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  4. 15
      libs/common/src/lib/helper.spec.ts
  5. 4
      libs/common/src/lib/helper.ts

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

@ -4,7 +4,10 @@ import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos'; import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos';
import { SubscriptionType } from '@ghostfolio/common/enums'; import { SubscriptionType } from '@ghostfolio/common/enums';
import { isValidGranteeOfAccess } from '@ghostfolio/common/helper'; import {
canApplyFiltersToAccess,
isValidGranteeOfAccess
} from '@ghostfolio/common/helper';
import { Access, AccessSettings } from '@ghostfolio/common/interfaces'; import { Access, AccessSettings } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import { getScopesOfAccess } from '@ghostfolio/common/scopes'; import { getScopesOfAccess } from '@ghostfolio/common/scopes';
@ -53,6 +56,8 @@ export class AccessController {
const { alias, expiresAt, granteeUser, id, lastUsedAt, settings, type } = const { alias, expiresAt, granteeUser, id, lastUsedAt, settings, type } =
accessItem; accessItem;
const { filters } = (settings ?? {}) as AccessSettings;
return { return {
alias, alias,
expiresAt, expiresAt,
@ -61,7 +66,7 @@ export class AccessController {
type, type,
grantee: granteeUser?.id, grantee: granteeUser?.id,
scopes: getScopesOfAccess(accessItem), scopes: getScopesOfAccess(accessItem),
settings: settings as AccessSettings settings: canApplyFiltersToAccess({ type }) ? { filters } : {}
}; };
}); });
} }
@ -104,6 +109,13 @@ export class AccessController {
); );
} }
if (data.filters?.length && !canApplyFiltersToAccess({ type })) {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
try { try {
return await this.accessService.createAccess({ return await this.accessService.createAccess({
type, type,
@ -116,10 +128,7 @@ export class AccessController {
type, type,
scopes: data.scopes scopes: data.scopes
}), }),
settings: this.accessService.buildSettings({ settings: this.accessService.buildSettings(data.filters),
type,
filters: data.filters
}),
user: { connect: { id: this.request.user.id } } user: { connect: { id: this.request.user.id } }
}); });
} catch { } catch {
@ -195,6 +204,16 @@ export class AccessController {
); );
} }
if (
data.filters?.length &&
!canApplyFiltersToAccess({ type: originalAccess.type })
) {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
try { try {
return await this.accessService.updateAccess({ return await this.accessService.updateAccess({
data: { data: {
@ -207,10 +226,7 @@ export class AccessController {
scopes: data.scopes ?? originalAccess.scopes, scopes: data.scopes ?? originalAccess.scopes,
type: originalAccess.type type: originalAccess.type
}), }),
settings: this.accessService.buildSettings({ settings: this.accessService.buildSettings(data.filters)
filters: data.filters,
type: originalAccess.type
})
}, },
where: { id } where: { id }
}); });

13
apps/api/src/app/access/access.service.ts

@ -3,7 +3,7 @@ import { AccessSettings, Filter } from '@ghostfolio/common/interfaces';
import { AccessWithGranteeUser } from '@ghostfolio/common/types'; import { AccessWithGranteeUser } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Access, AccessType, Prisma } from '@prisma/client'; import { Access, Prisma } from '@prisma/client';
import { isBefore, isToday, isValid } from 'date-fns'; import { isBefore, isToday, isValid } from 'date-fns';
@Injectable() @Injectable()
@ -41,15 +41,8 @@ export class AccessService {
}); });
} }
public buildSettings({ public buildSettings(filters?: Filter[]) {
filters, const settings: AccessSettings = filters?.length ? { filters } : {};
type
}: {
filters?: Filter[];
type: AccessType;
}) {
const settings: AccessSettings =
type === 'PUBLIC' && filters?.length ? { filters } : {};
return settings as Prisma.InputJsonValue; return settings as Prisma.InputJsonValue;
} }

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

@ -1,6 +1,7 @@
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_LOCALE } from '@ghostfolio/common/config'; import { DEFAULT_LOCALE } from '@ghostfolio/common/config';
import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos'; import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos';
import { canApplyFiltersToAccess } from '@ghostfolio/common/helper';
import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { import {
@ -52,6 +53,7 @@ import {
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { AccessType } from '@prisma/client';
import { addYears, endOfDay, isBefore, isValid, startOfDay } from 'date-fns'; import { addYears, endOfDay, isBefore, isValid, startOfDay } from 'date-fns';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import { EMPTY, catchError } from 'rxjs'; import { EMPTY, catchError } from 'rxjs';
@ -112,7 +114,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
} }
public get canApplyFilters() { public get canApplyFilters() {
return this.isPublicAccess && this.hasExperimentalFeatures; return (
canApplyFiltersToAccess({ type: this.accessType }) &&
this.hasExperimentalFeatures
);
} }
public get canGrantMcpAccess() { public get canGrantMcpAccess() {
@ -177,7 +182,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
this.accessForm this.accessForm
.get('type') .get('type')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)) ?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((accessType) => { .subscribe((accessType: AccessType) => {
const granteeUserIdControl = this.accessForm.get('granteeUserId'); const granteeUserIdControl = this.accessForm.get('granteeUserId');
if (accessType === 'PRIVATE') { if (accessType === 'PRIVATE') {
@ -191,8 +196,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
this.accessForm.get('accessLevel')?.setValue('READ_RESTRICTED'); this.accessForm.get('accessLevel')?.setValue('READ_RESTRICTED');
} }
if (accessType !== 'PUBLIC') { if (!canApplyFiltersToAccess({ type: accessType })) {
// Only a public access can be limited to a part of the portfolio
this.accessForm.get('filters')?.setValue(null); this.accessForm.get('filters')?.setValue(null);
} }
@ -208,8 +212,12 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
return this.accessForm?.get('accessLevel')?.value as AccessLevel; return this.accessForm?.get('accessLevel')?.value as AccessLevel;
} }
protected get accessType(): AccessType {
return this.accessForm?.get('type')?.value as AccessType;
}
protected get isPublicAccess() { protected get isPublicAccess() {
return this.accessForm?.get('type')?.value === 'PUBLIC'; return this.accessType === 'PUBLIC';
} }
protected get showExpiresAtErrorMessage() { protected get showExpiresAtErrorMessage() {

15
libs/common/src/lib/helper.spec.ts

@ -3,6 +3,7 @@ import {
TAG_ID_EXCLUDE_FROM_ANALYSIS TAG_ID_EXCLUDE_FROM_ANALYSIS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { import {
canApplyFiltersToAccess,
extractNumberFromString, extractNumberFromString,
getCountryCodeFromCurrency, getCountryCodeFromCurrency,
getNumberFormatGroup, getNumberFormatGroup,
@ -19,6 +20,20 @@ import {
import { UserSettings } from '@ghostfolio/common/interfaces'; import { UserSettings } from '@ghostfolio/common/interfaces';
describe('Helper', () => { describe('Helper', () => {
describe('Can apply filters to access', () => {
it('An access of the model context protocol', () => {
expect(canApplyFiltersToAccess({ type: 'MCP' })).toEqual(false);
});
it('A private access', () => {
expect(canApplyFiltersToAccess({ type: 'PRIVATE' })).toEqual(false);
});
it('A public access', () => {
expect(canApplyFiltersToAccess({ type: 'PUBLIC' })).toEqual(true);
});
});
describe('Extract number from string', () => { describe('Extract number from string', () => {
it('Get decimal number', () => { it('Get decimal number', () => {
expect(extractNumberFromString({ value: '999.99' })).toEqual(999.99); expect(extractNumberFromString({ value: '999.99' })).toEqual(999.99);

4
libs/common/src/lib/helper.ts

@ -174,6 +174,10 @@ export function calculateMovingAverage({
.toNumber(); .toNumber();
} }
export function canApplyFiltersToAccess({ type }: { type: AccessType }) {
return type === 'PUBLIC';
}
export function canDeleteAssetProfile({ export function canDeleteAssetProfile({
activitiesCount, activitiesCount,
isBenchmark, isBenchmark,

Loading…
Cancel
Save