Browse Source

Task/guard system tags in admin control panel (#7532)

* Guard system tags against deletion and renaming

* Update changelog
pull/7543/head
Thomas Kaul 3 days ago
committed by GitHub
parent
commit
b812bc7421
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 5
      apps/api/src/app/endpoints/tags/tags.controller.ts
  3. 12
      apps/client/src/app/components/admin-tag/admin-tag.component.html
  4. 9
      apps/client/src/app/components/admin-tag/admin-tag.component.ts
  5. 8
      libs/common/src/lib/config.ts
  6. 9
      libs/common/src/lib/helper.ts

4
CHANGELOG.md

@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added the platform logo to the account selector in the create or update activity dialog - Added the platform logo to the account selector in the create or update activity dialog
### Changed
- Guarded the system tags against deletion and renaming in the tag management of the admin control panel
## 3.42.0 - 2026-08-04 ## 3.42.0 - 2026-08-04
### Changed ### Changed

5
apps/api/src/app/endpoints/tags/tags.controller.ts

@ -2,6 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { CreateTagDto, UpdateTagDto } from '@ghostfolio/common/dtos'; import { CreateTagDto, UpdateTagDto } from '@ghostfolio/common/dtos';
import { isSystemTag } from '@ghostfolio/common/helper';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { RequestWithUser } from '@ghostfolio/common/types'; import { RequestWithUser } from '@ghostfolio/common/types';
@ -69,7 +70,7 @@ export class TagsController {
id id
}); });
if (!originalTag) { if (!originalTag || isSystemTag(originalTag)) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN), getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN StatusCodes.FORBIDDEN
@ -94,7 +95,7 @@ export class TagsController {
id id
}); });
if (!originalTag) { if (!originalTag || isSystemTag(originalTag)) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN), getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN StatusCodes.FORBIDDEN

12
apps/client/src/app/components/admin-tag/admin-tag.component.html

@ -83,7 +83,11 @@
<ion-icon name="ellipsis-horizontal" /> <ion-icon name="ellipsis-horizontal" />
</button> </button>
<mat-menu #tagMenu="matMenu" xPosition="before"> <mat-menu #tagMenu="matMenu" xPosition="before">
<button mat-menu-item (click)="onUpdateTag(element)"> <button
mat-menu-item
[disabled]="isSystemTag(element)"
(click)="onUpdateTag(element)"
>
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" /> <ion-icon class="mr-2" name="create-outline" />
<span><ng-container i18n>Edit</ng-container>...</span> <span><ng-container i18n>Edit</ng-container>...</span>
@ -92,7 +96,11 @@
<hr class="m-0" /> <hr class="m-0" />
<button <button
mat-menu-item mat-menu-item
[disabled]="element.accountCount > 0 || element.activityCount > 0" [disabled]="
element.accountCount > 0 ||
element.activityCount > 0 ||
isSystemTag(element)
"
(click)="onDeleteTag(element.id)" (click)="onDeleteTag(element.id)"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">

9
apps/client/src/app/components/admin-tag/admin-tag.component.ts

@ -2,7 +2,11 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { CreateTagDto, UpdateTagDto } from '@ghostfolio/common/dtos'; import { CreateTagDto, UpdateTagDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { getLocale, getLowercase } from '@ghostfolio/common/helper'; import {
getLocale,
getLowercase,
isSystemTag
} from '@ghostfolio/common/helper';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { NotificationService } from '@ghostfolio/ui/notifications'; import { NotificationService } from '@ghostfolio/ui/notifications';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
@ -67,6 +71,7 @@ export class GfAdminTagComponent implements OnInit {
'activities', 'activities',
'actions' 'actions'
]; ];
protected readonly isSystemTag = isSystemTag;
protected readonly pageSize = DEFAULT_PAGE_SIZE; protected readonly pageSize = DEFAULT_PAGE_SIZE;
protected tags: Tag[]; protected tags: Tag[];
protected readonly translate = translate; protected readonly translate = translate;
@ -99,7 +104,7 @@ export class GfAdminTagComponent implements OnInit {
return id === params['tagId']; return id === params['tagId'];
}); });
if (tag) { if (tag && !isSystemTag(tag)) {
this.openUpdateTagDialog(tag); this.openUpdateTagDialog(tag);
} }
} else { } else {

8
libs/common/src/lib/config.ts

@ -338,10 +338,16 @@ export const SUPPORTED_LANGUAGE_CODES = [
'zh' 'zh'
] as const; ] as const;
export const TAG_ID_DEMO = 'efa08cb3-9b9d-4974-ac68-db13a19c4874';
export const TAG_ID_EMERGENCY_FUND = '4452656d-9fa4-4bd0-ba38-70492e31d180'; export const TAG_ID_EMERGENCY_FUND = '4452656d-9fa4-4bd0-ba38-70492e31d180';
export const TAG_ID_EXCLUDE_FROM_ANALYSIS = export const TAG_ID_EXCLUDE_FROM_ANALYSIS =
'f2e868af-8333-459f-b161-cbc6544c24bd'; 'f2e868af-8333-459f-b161-cbc6544c24bd';
export const TAG_ID_DEMO = 'efa08cb3-9b9d-4974-ac68-db13a19c4874';
export const TAG_IDS_SYSTEM = [
TAG_ID_DEMO,
TAG_ID_EMERGENCY_FUND,
TAG_ID_EXCLUDE_FROM_ANALYSIS
];
export const THROTTLE_DAILY_KEY = 'daily'; export const THROTTLE_DAILY_KEY = 'daily';
export const THROTTLE_DAILY_TTL = ms('1 day'); export const THROTTLE_DAILY_TTL = ms('1 day');

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

@ -43,7 +43,8 @@ import {
ghostfolioFearAndGreedIndexSymbolStocks, ghostfolioFearAndGreedIndexSymbolStocks,
ghostfolioPrefix, ghostfolioPrefix,
SEARCH_QUERY_MINIMUM_LENGTH, SEARCH_QUERY_MINIMUM_LENGTH,
TAG_ID_EXCLUDE_FROM_ANALYSIS TAG_ID_EXCLUDE_FROM_ANALYSIS,
TAG_IDS_SYSTEM
} from './config'; } from './config';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
@ -557,6 +558,12 @@ export function isSplitRatio({
); );
} }
export function isSystemTag(tag?: { id: string }) {
return TAG_IDS_SYSTEM.some((id) => {
return id === tag?.id;
});
}
export function isValidCustomAssetProfileSymbol(aSymbol: string) { export function isValidCustomAssetProfileSymbol(aSymbol: string) {
return hasGhostfolioPrefix(aSymbol) || isUUID(aSymbol); return hasGhostfolioPrefix(aSymbol) || isUUID(aSymbol);
} }

Loading…
Cancel
Save