Browse Source

Minor improvements

pull/4458/head
Thomas Kaul 4 weeks ago
parent
commit
a44897a539
  1. 8
      apps/api/src/app/auth/auth.service.ts
  2. 20
      apps/api/src/app/user/user.controller.ts
  3. 49
      apps/api/src/app/user/user.service.ts
  4. 18
      apps/client/src/app/components/admin-users/admin-users.component.ts
  5. 2
      apps/client/src/app/components/admin-users/admin-users.html
  6. 4
      apps/client/src/app/services/data.service.ts

8
apps/api/src/app/auth/auth.service.ts

@ -20,10 +20,10 @@ export class AuthService {
public async validateAnonymousLogin(accessToken: string): Promise<string> { public async validateAnonymousLogin(accessToken: string): Promise<string> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
const hashedAccessToken = this.userService.createAccessToken( const hashedAccessToken = this.userService.createAccessToken({
accessToken, password: accessToken,
this.configurationService.get('ACCESS_TOKEN_SALT') salt: this.configurationService.get('ACCESS_TOKEN_SALT')
); });
const [user] = await this.userService.users({ const [user] = await this.userService.users({
where: { accessToken: hashedAccessToken } where: { accessToken: hashedAccessToken }

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

@ -1,6 +1,7 @@
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 { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; 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 { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { import {
AccessTokenResponse, AccessTokenResponse,
@ -40,6 +41,7 @@ export class UserController {
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
@Inject(REQUEST) private readonly request: RequestWithUser, @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService private readonly userService: UserService
@ -51,10 +53,10 @@ export class UserController {
public async deleteOwnUser( public async deleteOwnUser(
@Body() data: DeleteOwnUserDto @Body() data: DeleteOwnUserDto
): Promise<UserModel> { ): Promise<UserModel> {
const hashedAccessToken = this.userService.createAccessToken( const hashedAccessToken = this.userService.createAccessToken({
data.accessToken, password: data.accessToken,
this.configurationService.get('ACCESS_TOKEN_SALT') salt: this.configurationService.get('ACCESS_TOKEN_SALT')
); });
const [user] = await this.userService.users({ const [user] = await this.userService.users({
where: { accessToken: hashedAccessToken, id: this.request.user.id } where: { accessToken: hashedAccessToken, id: this.request.user.id }
@ -89,16 +91,22 @@ export class UserController {
}); });
} }
@Post(':id/access-token')
@HasPermission(permissions.accessAdminControl) @HasPermission(permissions.accessAdminControl)
@Post(':id/access-token')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async generateAccessToken( public async generateAccessToken(
@Param('id') id: string @Param('id') id: string
): Promise<AccessTokenResponse> { ): Promise<AccessTokenResponse> {
const { accessToken } = await this.userService.generateAccessToken({ const { accessToken, hashedAccessToken } =
this.userService.generateAccessToken({
userId: id userId: id
}); });
await this.prismaService.user.update({
data: { accessToken: hashedAccessToken },
where: { id }
});
return { accessToken }; return { accessToken };
} }

49
apps/api/src/app/user/user.service.ts

@ -67,13 +67,33 @@ export class UserService {
return this.prismaService.user.count(args); return this.prismaService.user.count(args);
} }
public createAccessToken(password: string, salt: string): string { public createAccessToken({
password,
salt
}: {
password: string;
salt: string;
}): string {
const hash = createHmac('sha512', salt); const hash = createHmac('sha512', salt);
hash.update(password); hash.update(password);
return hash.digest('hex'); return hash.digest('hex');
} }
public generateAccessToken({ userId }: { userId: string }) {
const accessToken = this.createAccessToken({
password: userId,
salt: getRandomString(10)
});
const hashedAccessToken = this.createAccessToken({
password: accessToken,
salt: this.configurationService.get('ACCESS_TOKEN_SALT')
});
return { accessToken, hashedAccessToken };
}
public async getUser( public async getUser(
{ Account, id, permissions, Settings, subscription }: UserWithSettings, { Account, id, permissions, Settings, subscription }: UserWithSettings,
aLocale = locale aLocale = locale
@ -464,10 +484,15 @@ export class UserService {
} }
if (data.provider === 'ANONYMOUS') { if (data.provider === 'ANONYMOUS') {
const { accessToken } = await this.generateAccessToken({ const { accessToken, hashedAccessToken } = this.generateAccessToken({
userId: user.id userId: user.id
}); });
await this.prismaService.user.update({
data: { accessToken: hashedAccessToken },
where: { id: user.id }
});
return { ...user, accessToken }; return { ...user, accessToken };
} }
@ -573,24 +598,4 @@ export class UserService {
return settings; return settings;
} }
public async generateAccessToken({
userId
}: {
userId: string;
}): Promise<{ accessToken: string }> {
const accessToken = this.createAccessToken(userId, getRandomString(10));
const hashedAccessToken = this.createAccessToken(
accessToken,
this.configurationService.get('ACCESS_TOKEN_SALT')
);
await this.prismaService.user.update({
data: { accessToken: hashedAccessToken },
where: { id: userId }
});
return { accessToken };
}
} }

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

@ -1,3 +1,4 @@
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { getDateFormatString, getEmojiFlag } from '@ghostfolio/common/helper'; import { getDateFormatString, getEmojiFlag } from '@ghostfolio/common/helper';
import { AdminUsers, InfoItem, User } from '@ghostfolio/common/interfaces'; import { AdminUsers, InfoItem, User } from '@ghostfolio/common/interfaces';
@ -29,9 +30,9 @@ import { UserService } from '../../services/user/user.service';
@Component({ @Component({
selector: 'gf-admin-users', selector: 'gf-admin-users',
standalone: false,
styleUrls: ['./admin-users.scss'], styleUrls: ['./admin-users.scss'],
templateUrl: './admin-users.html', templateUrl: './admin-users.html'
standalone: false
}) })
export class AdminUsersComponent implements OnDestroy, OnInit { export class AdminUsersComponent implements OnDestroy, OnInit {
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ -56,6 +57,7 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
private dataService: DataService, private dataService: DataService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private notificationService: NotificationService, private notificationService: NotificationService,
private tokenStorageService: TokenStorageService,
private userService: UserService private userService: UserService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.fetchInfo();
@ -141,14 +143,22 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
}); });
} }
public onGenerateAccessToken(aId: string) { public onGenerateAccessToken(aUserId: string) {
this.notificationService.confirm({ this.notificationService.confirm({
confirmFn: () => { confirmFn: () => {
this.dataService this.dataService
.generateAccessToken(aId) .generateAccessToken(aUserId)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ accessToken }) => { .subscribe(({ accessToken }) => {
this.notificationService.alert({ this.notificationService.alert({
discardFn: () => {
if (aUserId === this.user.id) {
this.tokenStorageService.signOut();
this.userService.remove();
document.location.href = `/${document.documentElement.lang}`;
}
},
message: accessToken, message: accessToken,
title: $localize`Security token` title: $localize`Security token`
}); });

2
apps/client/src/app/components/admin-users/admin-users.html

@ -239,7 +239,6 @@
<span i18n>Impersonate User</span> <span i18n>Impersonate User</span>
</span> </span>
</button> </button>
<hr class="m-0" />
} }
<button <button
mat-menu-item mat-menu-item
@ -250,6 +249,7 @@
<span i18n>Generate Security Token</span> <span i18n>Generate Security Token</span>
</span> </span>
</button> </button>
<hr class="m-0" />
<button <button
mat-menu-item mat-menu-item
[disabled]="element.id === user?.id" [disabled]="element.id === user?.id"

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

@ -692,9 +692,9 @@ export class DataService {
return this.http.get<Tag[]>('/api/v1/tags'); return this.http.get<Tag[]>('/api/v1/tags');
} }
public generateAccessToken(aId: string) { public generateAccessToken(aUserId: string) {
return this.http.post<AccessTokenResponse>( return this.http.post<AccessTokenResponse>(
`/api/v1/user/${aId}/access-token`, `/api/v1/user/${aUserId}/access-token`,
{} {}
); );
} }

Loading…
Cancel
Save