Feature/modernize tags endpoint (#4317)

* Modernize tags endpoint

* Update changelog
This commit is contained in:
Thomas Kaul 2025-02-15 10:00:14 +01:00 committed by GitHub
parent 4f76ee6758
commit f1ab3ff8e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 147 additions and 159 deletions

View File

@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Asia-Pacific Markets) - Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Asia-Pacific Markets)
- Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Japan) - Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Japan)
- Added support to create custom tags in the holding detail dialog - Added support to create custom tags in the holding detail dialog (experimental)
- Extended the tags selector component by a `readonly` attribute - Extended the tags selector component by a `readonly` attribute
- Extended the tags selector component to support creating custom tags - Extended the tags selector component to support creating custom tags
- Extended the holding detail dialog by the historical market data editor (experimental) - Extended the holding detail dialog by the historical market data editor (experimental)

View File

@ -36,6 +36,7 @@ import { ApiKeysModule } from './endpoints/api-keys/api-keys.module';
import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module'; import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module';
import { MarketDataModule } from './endpoints/market-data/market-data.module'; import { MarketDataModule } from './endpoints/market-data/market-data.module';
import { PublicModule } from './endpoints/public/public.module'; import { PublicModule } from './endpoints/public/public.module';
import { TagsModule } from './endpoints/tags/tags.module';
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module'; import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
import { ExportModule } from './export/export.module'; import { ExportModule } from './export/export.module';
import { HealthModule } from './health/health.module'; import { HealthModule } from './health/health.module';
@ -49,7 +50,6 @@ import { RedisCacheModule } from './redis-cache/redis-cache.module';
import { SitemapModule } from './sitemap/sitemap.module'; import { SitemapModule } from './sitemap/sitemap.module';
import { SubscriptionModule } from './subscription/subscription.module'; import { SubscriptionModule } from './subscription/subscription.module';
import { SymbolModule } from './symbol/symbol.module'; import { SymbolModule } from './symbol/symbol.module';
import { TagModule } from './tag/tag.module';
import { UserModule } from './user/user.module'; import { UserModule } from './user/user.module';
@Module({ @Module({
@ -124,7 +124,7 @@ import { UserModule } from './user/user.module';
SitemapModule, SitemapModule,
SubscriptionModule, SubscriptionModule,
SymbolModule, SymbolModule,
TagModule, TagsModule,
TwitterBotModule, TwitterBotModule,
UserModule UserModule
], ],

View File

@ -1,5 +1,6 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
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 { 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';
@ -21,23 +22,15 @@ import { Tag } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { CreateTagDto } from './create-tag.dto'; import { CreateTagDto } from './create-tag.dto';
import { TagService } from './tag.service';
import { UpdateTagDto } from './update-tag.dto'; import { UpdateTagDto } from './update-tag.dto';
@Controller('tag') @Controller('tags')
export class TagController { export class TagsController {
public constructor( public constructor(
@Inject(REQUEST) private readonly request: RequestWithUser, @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly tagService: TagService private readonly tagService: TagService
) {} ) {}
@Get()
@HasPermission(permissions.readTags)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getTags() {
return this.tagService.getTagsWithActivityCount();
}
@Post() @Post()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async createTag(@Body() data: CreateTagDto): Promise<Tag> { public async createTag(@Body() data: CreateTagDto): Promise<Tag> {
@ -70,6 +63,31 @@ export class TagController {
return this.tagService.createTag(data); return this.tagService.createTag(data);
} }
@Delete(':id')
@HasPermission(permissions.deleteTag)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteTag(@Param('id') id: string) {
const originalTag = await this.tagService.getTag({
id
});
if (!originalTag) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.tagService.deleteTag({ id });
}
@Get()
@HasPermission(permissions.readTags)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getTags() {
return this.tagService.getTagsWithActivityCount();
}
@HasPermission(permissions.updateTag) @HasPermission(permissions.updateTag)
@Put(':id') @Put(':id')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@ -94,22 +112,4 @@ export class TagController {
} }
}); });
} }
@Delete(':id')
@HasPermission(permissions.deleteTag)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteTag(@Param('id') id: string) {
const originalTag = await this.tagService.getTag({
id
});
if (!originalTag) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.tagService.deleteTag({ id });
}
} }

View File

@ -0,0 +1,12 @@
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common';
import { TagsController } from './tags.controller';
@Module({
controllers: [TagsController],
imports: [PrismaModule, TagModule]
})
export class TagsModule {}

View File

@ -1,14 +0,0 @@
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common';
import { TagController } from './tag.controller';
import { TagService } from './tag.service';
@Module({
controllers: [TagController],
exports: [TagService],
imports: [PrismaModule],
providers: [TagService]
})
export class TagModule {}

View File

@ -1,81 +0,0 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { Injectable } from '@nestjs/common';
import { Prisma, Tag } from '@prisma/client';
@Injectable()
export class TagService {
public constructor(private readonly prismaService: PrismaService) {}
public async createTag(data: Prisma.TagCreateInput) {
return this.prismaService.tag.create({
data
});
}
public async deleteTag(where: Prisma.TagWhereUniqueInput): Promise<Tag> {
return this.prismaService.tag.delete({ where });
}
public async getTag(
tagWhereUniqueInput: Prisma.TagWhereUniqueInput
): Promise<Tag> {
return this.prismaService.tag.findUnique({
where: tagWhereUniqueInput
});
}
public async getTags({
cursor,
orderBy,
skip,
take,
where
}: {
cursor?: Prisma.TagWhereUniqueInput;
orderBy?: Prisma.TagOrderByWithRelationInput;
skip?: number;
take?: number;
where?: Prisma.TagWhereInput;
} = {}) {
return this.prismaService.tag.findMany({
cursor,
orderBy,
skip,
take,
where
});
}
public async getTagsWithActivityCount() {
const tagsWithOrderCount = await this.prismaService.tag.findMany({
include: {
_count: {
select: { orders: true }
}
}
});
return tagsWithOrderCount.map(({ _count, id, name, userId }) => {
return {
id,
name,
userId,
activityCount: _count.orders
};
});
}
public async updateTag({
data,
where
}: {
data: Prisma.TagUpdateInput;
where: Prisma.TagWhereUniqueInput;
}): Promise<Tag> {
return this.prismaService.tag.update({
data,
where
});
}
}

View File

@ -1,11 +1,52 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Prisma, Tag } from '@prisma/client';
@Injectable() @Injectable()
export class TagService { export class TagService {
public constructor(private readonly prismaService: PrismaService) {} public constructor(private readonly prismaService: PrismaService) {}
public async createTag(data: Prisma.TagCreateInput) {
return this.prismaService.tag.create({
data
});
}
public async deleteTag(where: Prisma.TagWhereUniqueInput): Promise<Tag> {
return this.prismaService.tag.delete({ where });
}
public async getTag(
tagWhereUniqueInput: Prisma.TagWhereUniqueInput
): Promise<Tag> {
return this.prismaService.tag.findUnique({
where: tagWhereUniqueInput
});
}
public async getTags({
cursor,
orderBy,
skip,
take,
where
}: {
cursor?: Prisma.TagWhereUniqueInput;
orderBy?: Prisma.TagOrderByWithRelationInput;
skip?: number;
take?: number;
where?: Prisma.TagWhereInput;
} = {}) {
return this.prismaService.tag.findMany({
cursor,
orderBy,
skip,
take,
where
});
}
public async getTagsForUser(userId: string) { public async getTagsForUser(userId: string) {
const tags = await this.prismaService.tag.findMany({ const tags = await this.prismaService.tag.findMany({
include: { include: {
@ -41,4 +82,36 @@ export class TagService {
isUsed: _count.orders > 0 isUsed: _count.orders > 0
})); }));
} }
public async getTagsWithActivityCount() {
const tagsWithOrderCount = await this.prismaService.tag.findMany({
include: {
_count: {
select: { orders: true }
}
}
});
return tagsWithOrderCount.map(({ _count, id, name, userId }) => {
return {
id,
name,
userId,
activityCount: _count.orders
};
});
}
public async updateTag({
data,
where
}: {
data: Prisma.TagUpdateInput;
where: Prisma.TagWhereUniqueInput;
}): Promise<Tag> {
return this.prismaService.tag.update({
data,
where
});
}
} }

View File

@ -1,8 +1,7 @@
import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto'; import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto';
import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto'; import { UpdateTagDto } from '@ghostfolio/api/app/endpoints/tags/update-tag.dto';
import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type'; import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type';
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
import { AdminService } from '@ghostfolio/client/services/admin.service';
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';
@ -43,7 +42,6 @@ export class AdminTagComponent implements OnInit, OnDestroy {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
@ -100,7 +98,7 @@ export class AdminTagComponent implements OnInit, OnDestroy {
} }
private deleteTag(aId: string) { private deleteTag(aId: string) {
this.adminService this.dataService
.deleteTag(aId) .deleteTag(aId)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe({ .subscribe({
@ -116,7 +114,7 @@ export class AdminTagComponent implements OnInit, OnDestroy {
} }
private fetchTags() { private fetchTags() {
this.adminService this.dataService
.fetchTags() .fetchTags()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((tags) => { .subscribe((tags) => {
@ -148,7 +146,7 @@ export class AdminTagComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((tag: CreateTagDto | null) => { .subscribe((tag: CreateTagDto | null) => {
if (tag) { if (tag) {
this.adminService this.dataService
.postTag(tag) .postTag(tag)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe({ .subscribe({
@ -184,7 +182,7 @@ export class AdminTagComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((tag: UpdateTagDto | null) => { .subscribe((tag: UpdateTagDto | null) => {
if (tag) { if (tag) {
this.adminService this.dataService
.putTag(tag) .putTag(tag)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe({ .subscribe({

View File

@ -1,5 +1,5 @@
import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto'; import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto';
import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto'; import { UpdateTagDto } from '@ghostfolio/api/app/endpoints/tags/update-tag.dto';
import { validateObjectForForm } from '@ghostfolio/client/util/form.util'; import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
import { import {

View File

@ -2,7 +2,6 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interf
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module'; import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { AdminService } from '@ghostfolio/client/services/admin.service';
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 { NUMERICAL_PRECISION_THRESHOLD } from '@ghostfolio/common/config'; import { NUMERICAL_PRECISION_THRESHOLD } from '@ghostfolio/common/config';
@ -133,7 +132,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
public dialogRef: MatDialogRef<GfHoldingDetailDialogComponent>, public dialogRef: MatDialogRef<GfHoldingDetailDialogComponent>,
@ -162,7 +160,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
}); });
if (newTag && this.hasPermissionToCreateOwnTag) { if (newTag && this.hasPermissionToCreateOwnTag) {
this.adminService this.dataService
.postTag({ ...newTag, userId: this.user.id }) .postTag({ ...newTag, userId: this.user.id })
.pipe( .pipe(
switchMap((createdTag) => { switchMap((createdTag) => {

View File

@ -388,7 +388,9 @@
</mat-tab-group> </mat-tab-group>
<gf-tags-selector <gf-tags-selector
[hasPermissionToCreateTag]="hasPermissionToCreateOwnTag" [hasPermissionToCreateTag]="
hasPermissionToCreateOwnTag && user?.settings?.isExperimentalFeatures
"
[readonly]="!data.hasPermissionToUpdateOrder" [readonly]="!data.hasPermissionToUpdateOrder"
[tags]="activityForm.get('tags')?.value" [tags]="activityForm.get('tags')?.value"
[tagsAvailable]="tagsAvailable" [tagsAvailable]="tagsAvailable"

View File

@ -1,8 +1,6 @@
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto'; import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto'; import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto'; import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto';
import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { import {
HEADER_KEY_SKIP_INTERCEPTOR, HEADER_KEY_SKIP_INTERCEPTOR,
@ -25,7 +23,7 @@ import {
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort'; import { SortDirection } from '@angular/material/sort';
import { DataSource, MarketData, Platform, Tag } from '@prisma/client'; import { DataSource, MarketData, Platform } from '@prisma/client';
import { JobStatus } from 'bull'; import { JobStatus } from 'bull';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { switchMap } from 'rxjs'; import { switchMap } from 'rxjs';
@ -75,10 +73,6 @@ export class AdminService {
); );
} }
public deleteTag(aId: string) {
return this.http.delete<void>(`/api/v1/tag/${aId}`);
}
public executeJob(aId: string) { public executeJob(aId: string) {
return this.http.get<void>(`/api/v1/admin/queue/job/${aId}/execute`); return this.http.get<void>(`/api/v1/admin/queue/job/${aId}/execute`);
} }
@ -155,10 +149,6 @@ export class AdminService {
return this.http.get<Platform[]>('/api/v1/platform'); return this.http.get<Platform[]>('/api/v1/platform');
} }
public fetchTags() {
return this.http.get<Tag[]>('/api/v1/tag');
}
public fetchUsers({ public fetchUsers({
skip, skip,
take = DEFAULT_PAGE_SIZE take = DEFAULT_PAGE_SIZE
@ -261,10 +251,6 @@ export class AdminService {
return this.http.post<Platform>(`/api/v1/platform`, aPlatform); return this.http.post<Platform>(`/api/v1/platform`, aPlatform);
} }
public postTag(aTag: CreateTagDto) {
return this.http.post<Tag>(`/api/v1/tag`, aTag);
}
public putPlatform(aPlatform: UpdatePlatformDto) { public putPlatform(aPlatform: UpdatePlatformDto) {
return this.http.put<Platform>( return this.http.put<Platform>(
`/api/v1/platform/${aPlatform.id}`, `/api/v1/platform/${aPlatform.id}`,
@ -272,10 +258,6 @@ export class AdminService {
); );
} }
public putTag(aTag: UpdateTagDto) {
return this.http.put<Tag>(`/api/v1/tag/${aTag.id}`, aTag);
}
public testMarketData({ public testMarketData({
dataSource, dataSource,
scraperConfiguration, scraperConfiguration,

View File

@ -4,6 +4,8 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto
import { TransferBalanceDto } from '@ghostfolio/api/app/account/transfer-balance.dto'; import { TransferBalanceDto } from '@ghostfolio/api/app/account/transfer-balance.dto';
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto'; import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
import { UpdateBulkMarketDataDto } from '@ghostfolio/api/app/admin/update-bulk-market-data.dto'; import { UpdateBulkMarketDataDto } from '@ghostfolio/api/app/admin/update-bulk-market-data.dto';
import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto';
import { UpdateTagDto } from '@ghostfolio/api/app/endpoints/tags/update-tag.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { import {
Activities, Activities,
@ -308,6 +310,10 @@ export class DataService {
return this.http.delete<any>(`/api/v1/user`, { body: aData }); return this.http.delete<any>(`/api/v1/user`, { body: aData });
} }
public deleteTag(aId: string) {
return this.http.delete<void>(`/api/v1/tags/${aId}`);
}
public deleteUser(aId: string) { public deleteUser(aId: string) {
return this.http.delete<any>(`/api/v1/user/${aId}`); return this.http.delete<any>(`/api/v1/user/${aId}`);
} }
@ -662,6 +668,10 @@ export class DataService {
); );
} }
public fetchTags() {
return this.http.get<Tag[]>('/api/v1/tags');
}
public loginAnonymous(accessToken: string) { public loginAnonymous(accessToken: string) {
return this.http.post<OAuthResponse>('/api/v1/auth/anonymous', { return this.http.post<OAuthResponse>('/api/v1/auth/anonymous', {
accessToken accessToken
@ -709,6 +719,10 @@ export class DataService {
return this.http.post<OrderModel>('/api/v1/order', aOrder); return this.http.post<OrderModel>('/api/v1/order', aOrder);
} }
public postTag(aTag: CreateTagDto) {
return this.http.post<Tag>(`/api/v1/tags`, aTag);
}
public postUser() { public postUser() {
return this.http.post<UserItem>('/api/v1/user', {}); return this.http.post<UserItem>('/api/v1/user', {});
} }
@ -736,6 +750,10 @@ export class DataService {
return this.http.put<UserItem>(`/api/v1/order/${aOrder.id}`, aOrder); return this.http.put<UserItem>(`/api/v1/order/${aOrder.id}`, aOrder);
} }
public putTag(aTag: UpdateTagDto) {
return this.http.put<Tag>(`/api/v1/tags/${aTag.id}`, aTag);
}
public putUserSetting(aData: UpdateUserSettingDto) { public putUserSetting(aData: UpdateUserSettingDto) {
return this.http.put<User>('/api/v1/user/setting', aData); return this.http.put<User>('/api/v1/user/setting', aData);
} }