Browse Source

Allow user to rotate Security Token

pull/5016/head
csehatt741 1 week ago
committed by Thomas Kaul
parent
commit
35f2ca1aeb
  1. 1
      CHANGELOG.md
  2. 6
      apps/api/src/app/user/update-own-access-token.dto.ts
  3. 38
      apps/api/src/app/user/user.controller.ts
  4. 2
      apps/client/src/app/components/admin-users/admin-users.component.ts
  5. 50
      apps/client/src/app/components/user-account-access/user-account-access.component.ts
  6. 47
      apps/client/src/app/components/user-account-access/user-account-access.html
  7. 11
      apps/client/src/app/components/user-account-access/user-account-access.module.ts
  8. 10
      apps/client/src/app/services/data.service.ts
  9. 2
      libs/common/src/lib/permissions.ts
  10. 7593
      package-lock.json

1
CHANGELOG.md

@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Set up the language localization for the static portfolio analysis rule: _Account Cluster Risks_ (Current Investment)
- Extended the data providers management of the admin control panel by the online status
- Added support for generating a new _Security Token_ via the user's account access panel
### Changed

6
apps/api/src/app/user/update-own-access-token.dto.ts

@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class UpdateOwnAccessTokenDto {
@IsString()
accessToken: string;
}

38
apps/api/src/app/user/user.controller.ts

@ -33,6 +33,7 @@ import { merge, size } from 'lodash';
import { DeleteOwnUserDto } from './delete-own-user.dto';
import { UserItem } from './interfaces/user-item.interface';
import { UpdateOwnAccessTokenDto } from './update-own-access-token.dto';
import { UpdateUserSettingDto } from './update-user-setting.dto';
import { UserService } from './user.service';
@ -94,7 +95,7 @@ export class UserController {
@HasPermission(permissions.accessAdminControl)
@Post(':id/access-token')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async generateAccessToken(
public async updateUserAccessToken(
@Param('id') id: string
): Promise<AccessTokenResponse> {
const { accessToken, hashedAccessToken } =
@ -110,6 +111,41 @@ export class UserController {
return { accessToken };
}
@HasPermission(permissions.updateOwnAccess)
@Post('access-token')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updateOwnAccessToken(
@Body() data: UpdateOwnAccessTokenDto
): Promise<AccessTokenResponse> {
const currentHashedAccessToken = this.userService.createAccessToken({
password: data.accessToken,
salt: this.configurationService.get('ACCESS_TOKEN_SALT')
});
const [user] = await this.userService.users({
where: { accessToken: currentHashedAccessToken, id: this.request.user.id }
});
if (!user) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const { accessToken, hashedAccessToken } =
this.userService.generateAccessToken({
userId: this.request.user.id
});
await this.prismaService.user.update({
data: { accessToken: hashedAccessToken },
where: { id: this.request.user.id }
});
return { accessToken };
}
@Get()
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getUser(

2
apps/client/src/app/components/admin-users/admin-users.component.ts

@ -147,7 +147,7 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
this.notificationService.confirm({
confirmFn: () => {
this.dataService
.generateAccessToken(aUserId)
.updateUserAccessToken(aUserId)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ accessToken }) => {
this.notificationService.alert({

50
apps/client/src/app/components/user-account-access/user-account-access.component.ts

@ -1,5 +1,6 @@
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { DataService } from '@ghostfolio/client/services/data.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Access, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -11,12 +12,15 @@ import {
OnDestroy,
OnInit
} from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { EMPTY, Subject } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators';
import { ConfirmationDialogType } from '../../core/notification/confirmation-dialog/confirmation-dialog.type';
import { NotificationService } from '../../core/notification/notification.service';
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog/create-or-update-access-dialog.component';
@Component({
@ -33,6 +37,10 @@ export class UserAccountAccessComponent implements OnDestroy, OnInit {
public deviceType: string;
public hasPermissionToCreateAccess: boolean;
public hasPermissionToDeleteAccess: boolean;
public isAccessTokenHidden = true;
public updateOwnAccessTokenForm = this.formBuilder.group({
accessToken: ['', Validators.required]
});
public user: User;
private unsubscribeSubject = new Subject<void>();
@ -42,8 +50,11 @@ export class UserAccountAccessComponent implements OnDestroy, OnInit {
private dataService: DataService,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private formBuilder: FormBuilder,
private notificationService: NotificationService,
private route: ActivatedRoute,
private router: Router,
private tokenStorageService: TokenStorageService,
private userService: UserService
) {
const { globalPermissions } = this.dataService.fetchInfo();
@ -99,6 +110,41 @@ export class UserAccountAccessComponent implements OnDestroy, OnInit {
});
}
public onGenerateAccessToken() {
this.notificationService.confirm({
confirmFn: () => {
this.dataService
.updateOwnAccessToken({
accessToken: this.updateOwnAccessTokenForm.get('accessToken').value
})
.pipe(
catchError(() => {
this.notificationService.alert({
title: $localize`Oops! Incorrect Security Token.`
});
return EMPTY;
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe(({ accessToken }) => {
this.notificationService.alert({
discardFn: () => {
this.tokenStorageService.signOut();
this.userService.remove();
document.location.href = `/${document.documentElement.lang}`;
},
message: accessToken,
title: $localize`Security token`
});
});
},
confirmType: ConfirmationDialogType.Warn,
title: $localize`Do you really want to generate a new security token?`
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();

47
apps/client/src/app/components/user-account-access/user-account-access.html

@ -1,3 +1,50 @@
<form
class="w-100 mb-3"
[formGroup]="updateOwnAccessTokenForm"
(ngSubmit)="onGenerateAccessToken()"
>
<div class="container">
<h1 class="d-flex align-items-center justify-content-center h3 mb-3">
<span i18n>Security Token</span>
</h1>
<div class="d-flex align-items-center justify-content-center">
<mat-form-field
appearance="outline"
class="without-hint w-50"
[hideRequiredMarker]="true"
>
<mat-label i18n>Security Token</mat-label>
<input
formControlName="accessToken"
matInput
[type]="isAccessTokenHidden ? 'password' : 'text'"
/>
<button
mat-button
matSuffix
type="button"
(click)="isAccessTokenHidden = !isAccessTokenHidden"
>
<ion-icon
[name]="isAccessTokenHidden ? 'eye-outline' : 'eye-off-outline'"
/>
</button>
</mat-form-field>
<div class="pl-2">
<button
color="warn"
mat-flat-button
type="submit"
[disabled]="
!(updateOwnAccessTokenForm.dirty && updateOwnAccessTokenForm.valid)
"
>
<span i18n>Generate</span>
</button>
</div>
</div>
</div>
</form>
<div class="container">
@if (accessesGet.length > 0) {
<h1 class="h3 mb-3 text-center" i18n>Received Access</h1>

11
apps/client/src/app/components/user-account-access/user-account-access.module.ts

@ -2,9 +2,12 @@ import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/acce
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { RouterModule } from '@angular/router';
import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-dialog/create-or-update-access-dialog.module';
@ -20,7 +23,11 @@ import { UserAccountAccessComponent } from './user-account-access.component';
GfPremiumIndicatorComponent,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule,
RouterModule
]
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfUserAccountAccessModule {}

10
apps/client/src/app/services/data.service.ts

@ -16,6 +16,7 @@ import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
import { DeleteOwnUserDto } from '@ghostfolio/api/app/user/delete-own-user.dto';
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
import { UpdateOwnAccessTokenDto } from '@ghostfolio/api/app/user/update-own-access-token.dto';
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
@ -703,13 +704,20 @@ export class DataService {
return this.http.get<WatchlistResponse>('/api/v1/watchlist');
}
public generateAccessToken(aUserId: string) {
public updateUserAccessToken(aUserId: string) {
return this.http.post<AccessTokenResponse>(
`/api/v1/user/${aUserId}/access-token`,
{}
);
}
public updateOwnAccessToken(aAccessToken: UpdateOwnAccessTokenDto) {
return this.http.post<AccessTokenResponse>(
`/api/v1/user/access-token`,
aAccessToken
);
}
public loginAnonymous(accessToken: string) {
return this.http.post<OAuthResponse>('/api/v1/auth/anonymous', {
accessToken

2
libs/common/src/lib/permissions.ts

@ -52,6 +52,7 @@ export const permissions = {
updateMarketData: 'updateMarketData',
updateMarketDataOfOwnAssetProfile: 'updateMarketDataOfOwnAssetProfile',
updateOrder: 'updateOrder',
updateOwnAccess: 'updateOwnAccess',
updatePlatform: 'updatePlatform',
updateTag: 'updateTag',
updateUserSettings: 'updateUserSettings',
@ -136,6 +137,7 @@ export function getPermissions(aRole: Role): string[] {
permissions.updateAuthDevice,
permissions.updateMarketDataOfOwnAssetProfile,
permissions.updateOrder,
permissions.updateOwnAccess,
permissions.updateUserSettings,
permissions.updateViewMode
];

7593
package-lock.json

File diff suppressed because it is too large
Loading…
Cancel
Save