mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
81 changed files with 3583 additions and 2619 deletions
@ -0,0 +1,12 @@ |
|||||
|
import { SetMetadata } from '@nestjs/common'; |
||||
|
|
||||
|
export const ALLOW_DURING_IMPERSONATION_KEY = 'allow_during_impersonation'; |
||||
|
|
||||
|
/** |
||||
|
* Marks a controller or a route which modifies data of the authenticated user |
||||
|
* instead of data of the impersonated user, hence it stays available while an |
||||
|
* impersonation is active |
||||
|
*/ |
||||
|
export function AllowDuringImpersonation() { |
||||
|
return SetMetadata(ALLOW_DURING_IMPERSONATION_KEY, true); |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
import type { |
||||
|
ImpersonationContext, |
||||
|
RequestWithUser |
||||
|
} from '@ghostfolio/common/types'; |
||||
|
|
||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common'; |
||||
|
|
||||
|
/** |
||||
|
* Provides the impersonation context of the request, which requires the |
||||
|
* ImpersonationGuard to be applied to the route |
||||
|
*/ |
||||
|
export const Impersonation = createParamDecorator( |
||||
|
(_data: unknown, context: ExecutionContext): ImpersonationContext => { |
||||
|
const { impersonation, user } = context |
||||
|
.switchToHttp() |
||||
|
.getRequest<RequestWithUser>(); |
||||
|
|
||||
|
return ( |
||||
|
impersonation ?? { |
||||
|
isActive: false, |
||||
|
userId: user?.id, |
||||
|
userSettings: user?.settings?.settings ?? {} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
); |
||||
@ -0,0 +1,53 @@ |
|||||
|
import { ALLOW_DURING_IMPERSONATION_KEY } from '@ghostfolio/api/decorators/allow-during-impersonation.decorator'; |
||||
|
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; |
||||
|
|
||||
|
import { |
||||
|
CanActivate, |
||||
|
ExecutionContext, |
||||
|
HttpException, |
||||
|
Injectable |
||||
|
} from '@nestjs/common'; |
||||
|
import { Reflector } from '@nestjs/core'; |
||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; |
||||
|
|
||||
|
/** |
||||
|
* Blocks write requests while an impersonation is active, so that data of the |
||||
|
* authenticated user cannot be changed from a view presenting data of the |
||||
|
* impersonated user. The header is evaluated instead of the resolved context to |
||||
|
* fail closed, also for an identifier which cannot be resolved. |
||||
|
*/ |
||||
|
@Injectable() |
||||
|
export class ImpersonationWriteGuard implements CanActivate { |
||||
|
public constructor(private readonly reflector: Reflector) {} |
||||
|
|
||||
|
public canActivate(context: ExecutionContext): boolean { |
||||
|
if (context.getType() !== 'http') { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
const request = context.switchToHttp().getRequest(); |
||||
|
|
||||
|
if (request.method === 'GET') { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
if (!request.headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()]) { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
const isAllowedDuringImpersonation = |
||||
|
this.reflector.getAllAndOverride<boolean>( |
||||
|
ALLOW_DURING_IMPERSONATION_KEY, |
||||
|
[context.getHandler(), context.getClass()] |
||||
|
); |
||||
|
|
||||
|
if (isAllowedDuringImpersonation) { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
throw new HttpException( |
||||
|
getReasonPhrase(StatusCodes.FORBIDDEN), |
||||
|
StatusCodes.FORBIDDEN |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; |
||||
|
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; |
||||
|
import type { RequestWithUser } from '@ghostfolio/common/types'; |
||||
|
|
||||
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class ImpersonationGuard implements CanActivate { |
||||
|
public constructor( |
||||
|
private readonly impersonationService: ImpersonationService |
||||
|
) {} |
||||
|
|
||||
|
public async canActivate(context: ExecutionContext) { |
||||
|
const request = context.switchToHttp().getRequest<RequestWithUser>(); |
||||
|
|
||||
|
request.impersonation = await this.impersonationService.resolve({ |
||||
|
impersonationId: request.headers?.[ |
||||
|
HEADER_KEY_IMPERSONATION.toLowerCase() |
||||
|
] as string, |
||||
|
user: request.user |
||||
|
}); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
@ -1,9 +1,3 @@ |
|||||
:host { |
:host { |
||||
display: block; |
display: block; |
||||
|
|
||||
.mat-button-toggle-group { |
|
||||
.mat-button-toggle-appearance-standard { |
|
||||
--mat-button-toggle-height: 1.5rem; |
|
||||
} |
|
||||
} |
|
||||
} |
} |
||||
|
|||||
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -1,4 +0,0 @@ |
|||||
export interface ToggleOption { |
|
||||
label: string; |
|
||||
value: string; |
|
||||
} |
|
||||
@ -0,0 +1,13 @@ |
|||||
|
import { UserSettings } from '@ghostfolio/common/interfaces'; |
||||
|
|
||||
|
/** |
||||
|
* Describes whose data a request presents. The user id and the settings belong |
||||
|
* to the impersonated user while an impersonation is active and to the |
||||
|
* authenticated user otherwise, so a handler can use them unconditionally. |
||||
|
*/ |
||||
|
export interface ImpersonationContext { |
||||
|
accessId?: string; |
||||
|
isActive: boolean; |
||||
|
userId: string; |
||||
|
userSettings: UserSettings; |
||||
|
} |
||||
@ -1,3 +1,9 @@ |
|||||
import { UserWithSettings } from '@ghostfolio/common/types'; |
import { |
||||
|
ImpersonationContext, |
||||
|
UserWithSettings |
||||
|
} from '@ghostfolio/common/types'; |
||||
|
|
||||
export type RequestWithUser = Request & { user: UserWithSettings }; |
export type RequestWithUser = Request & { |
||||
|
impersonation?: ImpersonationContext; |
||||
|
user: UserWithSettings; |
||||
|
}; |
||||
|
|||||
@ -0,0 +1,9 @@ |
|||||
|
interface BaseToggleOption { |
||||
|
value: string; |
||||
|
} |
||||
|
|
||||
|
export type ToggleOption = BaseToggleOption & |
||||
|
( |
||||
|
| { iconName: string; label?: never; title: string } |
||||
|
| { iconName?: never; label: string; title?: never } |
||||
|
); |
||||
Loading…
Reference in new issue