Browse Source

Update permissions dynamically

pull/520/head
Thomas 4 years ago
parent
commit
0ea08eb060
  1. 16
      apps/api/src/app/access/access.controller.ts
  2. 21
      apps/api/src/app/account/account.controller.ts
  3. 20
      apps/api/src/app/admin/admin.controller.ts
  4. 8
      apps/api/src/app/auth-device/auth-device.controller.ts
  5. 6
      apps/api/src/app/auth/auth.module.ts
  6. 21
      apps/api/src/app/order/order.controller.ts
  7. 36
      apps/api/src/app/user/user.controller.ts
  8. 2
      apps/api/src/app/user/user.module.ts
  9. 31
      apps/api/src/app/user/user.service.ts
  10. 6
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  11. 2
      apps/client/src/app/components/header/header.component.html
  12. 7
      apps/client/src/app/components/transactions-table/transactions-table.component.html
  13. 1
      apps/client/src/app/components/transactions-table/transactions-table.component.ts
  14. 18
      apps/client/src/app/core/http-response.interceptor.ts
  15. 2
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  16. 4
      apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts
  17. 1
      apps/client/src/app/pages/portfolio/transactions/transactions-page.html
  18. 4
      apps/client/src/app/pages/register/register-page.component.ts
  19. 2
      apps/client/src/app/pages/register/register-page.html
  20. 15
      libs/common/src/lib/permissions.ts

16
apps/api/src/app/access/access.controller.ts

@ -1,9 +1,5 @@
import { Access } from '@ghostfolio/common/interfaces';
import {
getPermissions,
hasPermission,
permissions
} from '@ghostfolio/common/permissions';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
@ -66,10 +62,7 @@ export class AccessController {
@Body() data: CreateAccessDto
): Promise<AccessModel> {
if (
!hasPermission(
getPermissions(this.request.user.role),
permissions.createAccess
)
!hasPermission(this.request.user.permissions, permissions.createAccess)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
@ -86,10 +79,7 @@ export class AccessController {
@UseGuards(AuthGuard('jwt'))
public async deleteAccess(@Param('id') id: string): Promise<AccessModule> {
if (
!hasPermission(
getPermissions(this.request.user.role),
permissions.deleteAccess
)
!hasPermission(this.request.user.permissions, permissions.deleteAccess)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),

21
apps/api/src/app/account/account.controller.ts

@ -6,11 +6,7 @@ import {
} from '@ghostfolio/api/helper/object.helper';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { Accounts } from '@ghostfolio/common/interfaces';
import {
getPermissions,
hasPermission,
permissions
} from '@ghostfolio/common/permissions';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
@ -48,10 +44,7 @@ export class AccountController {
@UseGuards(AuthGuard('jwt'))
public async deleteAccount(@Param('id') id: string): Promise<AccountModel> {
if (
!hasPermission(
getPermissions(this.request.user.role),
permissions.deleteAccount
)
!hasPermission(this.request.user.permissions, permissions.deleteAccount)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
@ -143,10 +136,7 @@ export class AccountController {
@Body() data: CreateAccountDto
): Promise<AccountModel> {
if (
!hasPermission(
getPermissions(this.request.user.role),
permissions.createAccount
)
!hasPermission(this.request.user.permissions, permissions.createAccount)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
@ -183,10 +173,7 @@ export class AccountController {
@UseGuards(AuthGuard('jwt'))
public async update(@Param('id') id: string, @Body() data: UpdateAccountDto) {
if (
!hasPermission(
getPermissions(this.request.user.role),
permissions.updateAccount
)
!hasPermission(this.request.user.permissions, permissions.updateAccount)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),

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

@ -6,11 +6,7 @@ import {
AdminMarketData,
AdminMarketDataDetails
} from '@ghostfolio/common/interfaces';
import {
getPermissions,
hasPermission,
permissions
} from '@ghostfolio/common/permissions';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
@ -44,7 +40,7 @@ export class AdminController {
public async getAdminData(): Promise<AdminData> {
if (
!hasPermission(
getPermissions(this.request.user.role),
this.request.user.permissions,
permissions.accessAdminControl
)
) {
@ -62,7 +58,7 @@ export class AdminController {
public async gatherMax(): Promise<void> {
if (
!hasPermission(
getPermissions(this.request.user.role),
this.request.user.permissions,
permissions.accessAdminControl
)
) {
@ -86,7 +82,7 @@ export class AdminController {
): Promise<void> {
if (
!hasPermission(
getPermissions(this.request.user.role),
this.request.user.permissions,
permissions.accessAdminControl
)
) {
@ -106,7 +102,7 @@ export class AdminController {
public async gatherProfileData(): Promise<void> {
if (
!hasPermission(
getPermissions(this.request.user.role),
this.request.user.permissions,
permissions.accessAdminControl
)
) {
@ -126,7 +122,7 @@ export class AdminController {
public async getMarketData(): Promise<AdminMarketData> {
if (
!hasPermission(
getPermissions(this.request.user.role),
this.request.user.permissions,
permissions.accessAdminControl
)
) {
@ -146,7 +142,7 @@ export class AdminController {
): Promise<AdminMarketDataDetails> {
if (
!hasPermission(
getPermissions(this.request.user.role),
this.request.user.permissions,
permissions.accessAdminControl
)
) {
@ -167,7 +163,7 @@ export class AdminController {
) {
if (
!hasPermission(
getPermissions(this.request.user.role),
this.request.user.permissions,
permissions.accessAdminControl
)
) {

8
apps/api/src/app/auth-device/auth-device.controller.ts

@ -1,9 +1,5 @@
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import {
getPermissions,
hasPermission,
permissions
} from '@ghostfolio/common/permissions';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Controller,
@ -29,7 +25,7 @@ export class AuthDeviceController {
public async deleteAuthDevice(@Param('id') id: string): Promise<void> {
if (
!hasPermission(
getPermissions(this.request.user.role),
this.request.user.permissions,
permissions.deleteAuthDevice
)
) {

6
apps/api/src/app/auth/auth.module.ts

@ -1,7 +1,7 @@
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common';
@ -19,7 +19,8 @@ import { JwtStrategy } from './jwt.strategy';
secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '180 days' }
}),
SubscriptionModule
SubscriptionModule,
UserModule
],
providers: [
AuthDeviceService,
@ -28,7 +29,6 @@ import { JwtStrategy } from './jwt.strategy';
GoogleStrategy,
JwtStrategy,
PrismaService,
UserService,
WebAuthService
]
})

21
apps/api/src/app/order/order.controller.ts

@ -1,11 +1,7 @@
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import {
getPermissions,
hasPermission,
permissions
} from '@ghostfolio/common/permissions';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
@ -43,10 +39,7 @@ export class OrderController {
@UseGuards(AuthGuard('jwt'))
public async deleteOrder(@Param('id') id: string): Promise<OrderModel> {
if (
!hasPermission(
getPermissions(this.request.user.role),
permissions.deleteOrder
)
!hasPermission(this.request.user.permissions, permissions.deleteOrder)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
@ -115,10 +108,7 @@ export class OrderController {
@UseGuards(AuthGuard('jwt'))
public async createOrder(@Body() data: CreateOrderDto): Promise<OrderModel> {
if (
!hasPermission(
getPermissions(this.request.user.role),
permissions.createOrder
)
!hasPermission(this.request.user.permissions, permissions.createOrder)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
@ -161,10 +151,7 @@ export class OrderController {
@UseGuards(AuthGuard('jwt'))
public async update(@Param('id') id: string, @Body() data: UpdateOrderDto) {
if (
!hasPermission(
getPermissions(this.request.user.role),
permissions.updateOrder
)
!hasPermission(this.request.user.permissions, permissions.updateOrder)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),

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

@ -1,7 +1,10 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { PROPERTY_IS_READ_ONLY_MODE } from '@ghostfolio/common/config';
import { User } from '@ghostfolio/common/interfaces';
import {
getPermissions,
hasPermission,
hasRole,
permissions
} from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
@ -20,7 +23,7 @@ import {
import { REQUEST } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { AuthGuard } from '@nestjs/passport';
import { Provider } from '@prisma/client';
import { Provider, Role } from '@prisma/client';
import { User as UserModel } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -34,7 +37,9 @@ import { UserService } from './user.service';
@Controller('user')
export class UserController {
public constructor(
private readonly configurationService: ConfigurationService,
private jwtService: JwtService,
private readonly propertyService: PropertyService,
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService
) {}
@ -43,10 +48,7 @@ export class UserController {
@UseGuards(AuthGuard('jwt'))
public async deleteUser(@Param('id') id: string): Promise<UserModel> {
if (
!hasPermission(
getPermissions(this.request.user.role),
permissions.deleteUser
) ||
!hasPermission(this.request.user.permissions, permissions.deleteUser) ||
id === this.request.user.id
) {
throw new HttpException(
@ -68,6 +70,19 @@ export class UserController {
@Post()
public async signupUser(): Promise<UserItem> {
if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) {
const isReadOnlyMode = (await this.propertyService.getByKey(
PROPERTY_IS_READ_ONLY_MODE
)) as boolean;
if (isReadOnlyMode) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
}
const { accessToken, id } = await this.userService.createUser({
provider: Provider.ANONYMOUS
});
@ -85,7 +100,7 @@ export class UserController {
public async updateUserSetting(@Body() data: UpdateUserSettingDto) {
if (
!hasPermission(
getPermissions(this.request.user.role),
this.request.user.permissions,
permissions.updateUserSettings
)
) {
@ -111,7 +126,7 @@ export class UserController {
public async updateUserSettings(@Body() data: UpdateUserSettingsDto) {
if (
!hasPermission(
getPermissions(this.request.user.role),
this.request.user.permissions,
permissions.updateUserSettings
)
) {
@ -127,10 +142,7 @@ export class UserController {
};
if (
hasPermission(
getPermissions(this.request.user.role),
permissions.updateViewMode
)
hasPermission(this.request.user.permissions, permissions.updateViewMode)
) {
userSettings.viewMode = data.viewMode;
}

2
apps/api/src/app/user/user.module.ts

@ -1,6 +1,7 @@
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
@ -13,6 +14,7 @@ import { UserService } from './user.service';
secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '30 days' }
}),
PropertyModule,
SubscriptionModule
],
controllers: [UserController],

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

@ -1,7 +1,12 @@
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { baseCurrency, locale } from '@ghostfolio/common/config';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
PROPERTY_IS_READ_ONLY_MODE,
baseCurrency,
locale
} from '@ghostfolio/common/config';
import { User as IUser, UserWithSettings } from '@ghostfolio/common/interfaces';
import {
getPermissions,
@ -24,6 +29,7 @@ export class UserService {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService,
private readonly subscriptionService: SubscriptionService
) {}
@ -78,19 +84,32 @@ export class UserService {
const user: UserWithSettings = userFromDatabase;
const currentPermissions = getPermissions(userFromDatabase.role);
let currentPermissions = getPermissions(userFromDatabase.role);
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
currentPermissions.push(permissions.accessFearAndGreedIndex);
}
if (
this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE') &&
hasRole(user, Role.ADMIN)
) {
if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) {
if (hasRole(user, Role.ADMIN)) {
currentPermissions.push(permissions.toggleReadOnlyMode);
}
const isReadOnlyMode = (await this.propertyService.getByKey(
PROPERTY_IS_READ_ONLY_MODE
)) as boolean;
if (isReadOnlyMode) {
currentPermissions = currentPermissions.filter((permission) => {
return !(
permission.startsWith('create') ||
permission.startsWith('delete') ||
permission.startsWith('update')
);
});
}
}
user.permissions = currentPermissions;
if (userFromDatabase?.Settings) {

6
apps/client/src/app/components/admin-overview/admin-overview.component.ts

@ -242,6 +242,10 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
value: aValue ? 'true' : ''
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {});
.subscribe(() => {
setTimeout(() => {
window.location.reload();
}, 300);
});
}
}

2
apps/client/src/app/components/header/header.component.html

@ -270,7 +270,7 @@
Sign In
</button>
<a
*ngIf="currentRoute !== 'register'"
*ngIf="currentRoute !== 'register' && !info?.isReadOnlyMode"
class="d-none d-sm-block"
color="primary"
i18n

7
apps/client/src/app/components/transactions-table/transactions-table.component.html

@ -273,7 +273,12 @@
}"
></ngx-skeleton-loader>
<div *ngIf="dataSource.data.length === 0 && !isLoading" class="p-3 text-center">
<div
*ngIf="
dataSource.data.length === 0 && hasPermissionToCreateOrder && !isLoading
"
class="p-3 text-center"
>
<gf-no-transactions-info-indicator
[hasBorder]="false"
></gf-no-transactions-info-indicator>

1
apps/client/src/app/components/transactions-table/transactions-table.component.ts

@ -43,6 +43,7 @@ export class TransactionsTableComponent
{
@Input() baseCurrency: string;
@Input() deviceType: string;
@Input() hasPermissionToCreateOrder: boolean;
@Input() hasPermissionToImportOrders: boolean;
@Input() locale: string;
@Input() showActions: boolean;

18
apps/client/src/app/core/http-response.interceptor.ts

@ -15,22 +15,28 @@ import {
} from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { StatusCodes } from 'http-status-codes';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { TokenStorageService } from '../services/token-storage.service';
import { DataService } from '@ghostfolio/client/services/data.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
@Injectable()
export class HttpResponseInterceptor implements HttpInterceptor {
public info: InfoItem;
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
public constructor(
private dataService: DataService,
private router: Router,
private tokenStorageService: TokenStorageService,
private snackBar: MatSnackBar,
private webAuthnService: WebAuthnService
) {}
) {
this.info = this.dataService.fetchInfo();
}
public intercept(
request: HttpRequest<any>,
@ -63,11 +69,19 @@ export class HttpResponseInterceptor implements HttpInterceptor {
catchError((error: HttpErrorResponse) => {
if (error.status === StatusCodes.FORBIDDEN) {
if (!this.snackBarRef) {
if (this.info.isReadOnlyMode) {
this.snackBarRef = this.snackBar.open(
'This feature is currently unavailable. Please try again later.',
undefined,
{ duration: 6000 }
);
} else {
this.snackBarRef = this.snackBar.open(
'This feature requires a subscription.',
'Upgrade Plan',
{ duration: 6000 }
);
}
this.snackBarRef.afterDismissed().subscribe(() => {
this.snackBarRef = undefined;

2
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -51,7 +51,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (params['createDialog']) {
if (params['createDialog'] && this.hasPermissionToCreateAccount) {
this.openCreateAccountDialog();
} else if (params['editDialog']) {
if (this.accounts) {

4
apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts

@ -61,7 +61,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
this.routeQueryParams = route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (params['createDialog']) {
if (params['createDialog'] && this.hasPermissionToCreateOrder) {
this.openCreateTransactionDialog();
} else if (params['editDialog']) {
if (this.transactions) {
@ -130,7 +130,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
.subscribe((response) => {
this.transactions = response;
if (this.transactions?.length <= 0) {
if (this.hasPermissionToCreateOrder && this.transactions?.length <= 0) {
this.router.navigate([], { queryParams: { createDialog: true } });
}

1
apps/client/src/app/pages/portfolio/transactions/transactions-page.html

@ -5,6 +5,7 @@
<gf-transactions-table
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
[hasPermissionToImportOrders]="hasPermissionToImportOrders"
[locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder && !user.settings.isRestrictedView"

4
apps/client/src/app/pages/register/register-page.component.ts

@ -3,6 +3,7 @@ import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { DataService } from '@ghostfolio/client/services/data.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
import { format } from 'date-fns';
@ -24,6 +25,7 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
public deviceType: string;
public hasPermissionForSocialLogin: boolean;
public historicalDataItems: LineChartItem[];
public info: InfoItem;
private unsubscribeSubject = new Subject<void>();
@ -37,6 +39,8 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
private router: Router,
private tokenStorageService: TokenStorageService
) {
this.info = this.dataService.fetchInfo();
this.tokenStorageService.signOut();
}

2
apps/client/src/app/pages/register/register-page.html

@ -22,7 +22,7 @@
color="primary"
i18n
mat-flat-button
[disabled]="!demoAuthToken"
[disabled]="!demoAuthToken || info?.isReadOnlyMode"
(click)="createAccount()"
>
Create Account

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

@ -1,4 +1,5 @@
import { Role } from '@prisma/client';
import { UserWithSettings } from './interfaces';
export const permissions = {
@ -27,13 +28,6 @@ export const permissions = {
updateViewMode: 'updateViewMode'
};
export function hasPermission(
aPermissions: string[] = [],
aPermission: string
) {
return aPermissions.includes(aPermission);
}
export function getPermissions(aRole: Role): string[] {
switch (aRole) {
case 'ADMIN':
@ -78,6 +72,13 @@ export function getPermissions(aRole: Role): string[] {
}
}
export function hasPermission(
aPermissions: string[] = [],
aPermission: string
) {
return aPermissions.includes(aPermission);
}
export function hasRole(aUser: UserWithSettings, aRole: Role): boolean {
return aUser?.role === aRole;
}

Loading…
Cancel
Save