Browse Source

Task/deprecate admin user endpoints in favor of users endpoints (#8357)

* Deprecate admin user endpoints in favor of users endpoints

* Update changelog
pull/8366/head
Thomas Kaul 4 days ago
committed by GitHub
parent
commit
5f5184689a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 36
      apps/api/src/app/__snapshots__/app.module.spec.ts.snap
  3. 2
      apps/api/src/app/admin/admin.controller.ts
  4. 4
      apps/api/src/app/app.module.ts
  5. 24
      apps/api/src/app/endpoints/users/user.controller.ts
  6. 83
      apps/api/src/app/endpoints/users/users.controller.ts
  7. 13
      apps/api/src/app/endpoints/users/users.module.ts
  8. 18
      apps/api/src/services/user/user.service.ts
  9. 4
      libs/ui/src/lib/services/admin.service.ts
  10. 4
      libs/ui/src/lib/services/data.service.ts

6
CHANGELOG.md

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Deprecated the `api/v1/admin/user`, `api/v1/user/:id` and `api/v1/user/:id/access-token` endpoints in favor of the `api/v1/users` endpoints
## 3.95.0 - 2026-10-31
### Changed

36
apps/api/src/app/__snapshots__/app.module.spec.ts.snap

@ -230,6 +230,15 @@ exports[`AppModule should keep the security of each route 1`] = `
"interceptors": [],
"permission": "deleteUser",
},
"DELETE /users/:id": {
"allowDuringImpersonation": true,
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [],
"permission": "deleteUser",
},
"DELETE /watchlist/:dataSource/:symbol": {
"guards": [
"AuthGuard('jwt')",
@ -891,6 +900,24 @@ exports[`AppModule should keep the security of each route 1`] = `
"RedactValuesInResponseInterceptor",
],
},
"GET /users": {
"allowDuringImpersonation": true,
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [],
"permission": "accessAdminControl",
},
"GET /users/:id": {
"allowDuringImpersonation": true,
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [],
"permission": "accessAdminControl",
},
"GET /watchlist": {
"guards": [
"AuthGuard('jwt')",
@ -1307,6 +1334,15 @@ exports[`AppModule should keep the security of each route 1`] = `
"interceptors": [],
"permission": "updateOwnAccessToken",
},
"POST /users/:id/access-token": {
"allowDuringImpersonation": true,
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [],
"permission": "accessAdminControl",
},
"POST /watchlist": {
"guards": [
"AuthGuard('jwt')",

2
apps/api/src/app/admin/admin.controller.ts

@ -354,6 +354,7 @@ export class AdminController {
return this.adminService.putSetting(key, data.value);
}
/** @deprecated */
@Get('user')
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@ -367,6 +368,7 @@ export class AdminController {
});
}
/** @deprecated */
@Get('user/:id')
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)

4
apps/api/src/app/app.module.ts

@ -63,9 +63,9 @@ import { SitemapModule } from './endpoints/sitemap/sitemap.module';
import { SubscriptionModule } from './endpoints/subscription/subscription.module';
import { SymbolsModule } from './endpoints/symbols/symbols.module';
import { TagsModule } from './endpoints/tags/tags.module';
import { UsersModule } from './endpoints/users/users.module';
import { WatchlistModule } from './endpoints/watchlist/watchlist.module';
import { PortfolioModule } from './portfolio/portfolio.module';
import { UserModule } from './user/user.module';
@Module({
controllers: [AppController],
@ -182,7 +182,7 @@ import { UserModule } from './user/user.module';
};
}
}),
UserModule,
UsersModule,
WatchlistModule
],
providers: [

24
apps/api/src/app/user/user.controller.ts → apps/api/src/app/endpoints/users/user.controller.ts

@ -8,7 +8,6 @@ import { decodeDataSource } from '@ghostfolio/api/helper/data-source.helper';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { UserService } from '@ghostfolio/api/services/user/user.service';
import {
@ -62,7 +61,6 @@ export class UserController {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly jwtService: JwtService,
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService,
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService
@ -91,6 +89,7 @@ export class UserController {
});
}
/** @deprecated */
@Delete(':id')
@HasPermission(permissions.deleteUser)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@ -107,13 +106,14 @@ export class UserController {
});
}
/** @deprecated */
@HasPermission(permissions.accessAdminControl)
@Post(':id/access-token')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updateUserAccessToken(
@Param('id') id: string
): Promise<AccessTokenResponse> {
return this.rotateUserAccessToken(id);
return this.userService.rotateAccessToken({ userId: id });
}
@HasPermission(permissions.updateOwnAccessToken)
@ -127,7 +127,7 @@ export class UserController {
this.request.user.id
);
return this.rotateUserAccessToken(user.id);
return this.userService.rotateAccessToken({ userId: user.id });
}
@Get()
@ -251,22 +251,6 @@ export class UserController {
});
}
private async rotateUserAccessToken(
userId: string
): Promise<AccessTokenResponse> {
const { accessToken, hashedAccessToken } =
this.userService.generateAccessToken({
userId
});
await this.prismaService.user.update({
data: { accessToken: hashedAccessToken },
where: { id: userId }
});
return { accessToken };
}
private async validateAccessToken(
accessToken: string,
userId: string

83
apps/api/src/app/endpoints/users/users.controller.ts

@ -0,0 +1,83 @@
import { AdminService } from '@ghostfolio/api/app/admin/admin.service';
import { AllowDuringImpersonation } from '@ghostfolio/api/decorators/allow-during-impersonation.decorator';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { UserService } from '@ghostfolio/api/services/user/user.service';
import {
AccessTokenResponse,
AdminUserResponse,
AdminUsersResponse
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Controller,
Delete,
Get,
HttpException,
Inject,
Param,
ParseIntPipe,
Post,
Query,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@AllowDuringImpersonation()
@Controller('users')
export class UsersController {
public constructor(
private readonly adminService: AdminService,
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService
) {}
@Delete(':id')
@HasPermission(permissions.deleteUser)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteUser(@Param('id') id: string): Promise<void> {
if (id === this.request.user.id) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
await this.userService.deleteUser({
id
});
}
@Get(':id')
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getUser(@Param('id') id: string): Promise<AdminUserResponse> {
return this.adminService.getUser(id);
}
@Get()
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getUsers(
@Query('skip', new ParseIntPipe({ optional: true })) skip?: number,
@Query('take', new ParseIntPipe({ optional: true })) take?: number
): Promise<AdminUsersResponse> {
return this.adminService.getUsers({
skip,
take
});
}
@HasPermission(permissions.accessAdminControl)
@Post(':id/access-token')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updateUserAccessToken(
@Param('id') id: string
): Promise<AccessTokenResponse> {
return this.userService.rotateAccessToken({ userId: id });
}
}

13
apps/api/src/app/user/user.module.ts → apps/api/src/app/endpoints/users/users.module.ts

@ -1,30 +1,31 @@
import { AdminModule } from '@ghostfolio/api/app/admin/admin.module';
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { UserModule as UserServiceModule } from '@ghostfolio/api/services/user/user.module';
import { UserModule } from '@ghostfolio/api/services/user/user.module';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { UserController } from './user.controller';
import { UsersController } from './users.controller';
@Module({
controllers: [UserController],
controllers: [UserController, UsersController],
imports: [
AdminModule,
ConfigurationModule,
ImpersonationModule,
JwtModule.register({
secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '30 days' }
}),
PrismaModule,
PropertyModule,
RedactValuesInResponseModule,
TransformDataSourceInResponseModule,
UserServiceModule
UserModule
]
})
export class UserModule {}
export class UsersModule {}

18
apps/api/src/services/user/user.service.ts

@ -28,6 +28,7 @@ import {
import { SubscriptionType } from '@ghostfolio/common/enums';
import { resolveUserSettings } from '@ghostfolio/common/helper';
import {
AccessTokenResponse,
User as IUser,
ReferralPartner,
SystemMessage,
@ -689,6 +690,23 @@ export class UserService {
});
}
public async rotateAccessToken({
userId
}: {
userId: string;
}): Promise<AccessTokenResponse> {
const { accessToken, hashedAccessToken } = this.generateAccessToken({
userId
});
await this.prismaService.user.update({
data: { accessToken: hashedAccessToken },
where: { id: userId }
});
return { accessToken };
}
/**
* Keeps the date and the client of the request of the user. The request is
* counted only if the analytics of the user exist already. The country is

4
libs/ui/src/lib/services/admin.service.ts

@ -104,7 +104,7 @@ export class AdminService {
}
public fetchUserById(id: string) {
return this.http.get<AdminUserResponse>(`/api/v1/admin/user/${id}`);
return this.http.get<AdminUserResponse>(`/api/v1/users/${id}`);
}
public fetchUsers({
@ -124,7 +124,7 @@ export class AdminService {
params = params.append('take', take);
}
return this.http.get<AdminUsersResponse>('/api/v1/admin/user', { params });
return this.http.get<AdminUsersResponse>('/api/v1/users', { params });
}
public gatherMax() {

4
libs/ui/src/lib/services/data.service.ts

@ -377,7 +377,7 @@ export class DataService {
}
public deleteUser(aId: string) {
return this.http.delete<UserModel>(`/api/v1/user/${aId}`);
return this.http.delete<void>(`/api/v1/users/${aId}`);
}
public deleteWatchlistItem({ dataSource, symbol }: AssetProfileIdentifier) {
@ -1020,7 +1020,7 @@ export class DataService {
public updateUserAccessToken(aUserId: string) {
return this.http.post<AccessTokenResponse>(
`/api/v1/user/${aUserId}/access-token`,
`/api/v1/users/${aUserId}/access-token`,
{}
);
}

Loading…
Cancel
Save