mirror of
https://github.com/valitydev/dashboard.git
synced 2024-11-06 02:25:23 +00:00
OPS-180,182: Use org party for req-s (#93)
This commit is contained in:
parent
3a7bd802a3
commit
b8ae56c301
2
.gitignore
vendored
2
.gitignore
vendored
@ -49,4 +49,4 @@ Thumbs.db
|
||||
.angular
|
||||
|
||||
# Configs
|
||||
src/*Config.json
|
||||
src/*Config*.json
|
@ -23,7 +23,6 @@
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
<option name="IMPORT_SORT_MEMBERS" value="false" />
|
||||
<option name="USE_PATH_MAPPING" value="DIFFERENT_PATHS" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<VueCodeStyleSettings>
|
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||
|
22
.vscode/launch.json
vendored
22
.vscode/launch.json
vendored
@ -1,22 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"name": "Chrome",
|
||||
"url": "http://localhost:8000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "vscode-edge-devtools.debug",
|
||||
"request": "launch",
|
||||
"name": "Edge",
|
||||
"url": "http://localhost:8000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
14
.vscode/settings.json
vendored
14
.vscode/settings.json
vendored
@ -47,17 +47,5 @@
|
||||
],
|
||||
"cSpell.language": "en,ru",
|
||||
"prettier.prettierPath": "node_modules/prettier",
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.svn": true,
|
||||
"**/.hg": true,
|
||||
"**/CVS": true,
|
||||
"**/.DS_Store": true,
|
||||
"**/node_modules": true,
|
||||
"**/.idea": true,
|
||||
"coverage": true,
|
||||
"**/.eslintcache": true,
|
||||
"build_utils": true,
|
||||
"**/.vscode": true
|
||||
}
|
||||
"tasksStatusbar.taskLabelFilter": "Start",
|
||||
}
|
||||
|
17
.vscode/tasks.json
vendored
Normal file
17
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"problemMatcher": [],
|
||||
"label": "Start dev"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "stage",
|
||||
"problemMatcher": [],
|
||||
"label": "Start stage"
|
||||
}
|
||||
]
|
||||
}
|
38
angular.json
38
angular.json
@ -39,7 +39,14 @@
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/favicon.ico", "src/assets", "src/appConfig.json", "src/authConfig.json"],
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
"src/appConfig.json",
|
||||
"src/authConfig.json",
|
||||
"src/appConfig.stage.json",
|
||||
"src/authConfig.stage.json"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles/core.scss",
|
||||
{
|
||||
@ -88,14 +95,6 @@
|
||||
"outputHashing": "all",
|
||||
"sourceMap": true
|
||||
},
|
||||
"stub-keycloak": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/app/auth/keycloak/index.ts",
|
||||
"with": "src/app/auth/keycloak/index.stub.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
@ -103,6 +102,14 @@
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"stage": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.stage.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
@ -116,8 +123,8 @@
|
||||
"development": {
|
||||
"browserTarget": "dashboard:build:development"
|
||||
},
|
||||
"stub-keycloak": {
|
||||
"browserTarget": "dashboard:build:stub-keycloak"
|
||||
"stage": {
|
||||
"browserTarget": "dashboard:build:development,stage"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
@ -130,7 +137,14 @@
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/favicon.ico", "src/assets", "src/appConfig.json", "src/authConfig.json"],
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
"src/appConfig.json",
|
||||
"src/authConfig.json",
|
||||
"src/appConfig.stage.json",
|
||||
"src/authConfig.stage.json"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles/core.scss",
|
||||
{
|
||||
|
@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"postinstall": "ngcc",
|
||||
"start": "ng serve --port 8000",
|
||||
"start-stub": "npm start -- --configuration=stub-keycloak",
|
||||
"stage": "ng serve --port 8001 --configuration=stage",
|
||||
"fix": "npm run lint-fix && npm run prettier-fix",
|
||||
"build": "ng build --extra-webpack-config webpack.extra.js && npm run transloco:optimize",
|
||||
"test": "ng test",
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { ShopsService as ApiShopsService, Shop } from '@vality/swag-payments';
|
||||
import { defer, Observable, Subject } from 'rxjs';
|
||||
import { startWith, switchMapTo } from 'rxjs/operators';
|
||||
import { defer, Observable, Subject, combineLatest } from 'rxjs';
|
||||
import { startWith, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { ContextService } from '@dsh/app/shared';
|
||||
import { shareReplayRefCount } from '@dsh/operators';
|
||||
|
||||
import { createApi } from '../utils';
|
||||
@ -11,14 +12,20 @@ import { createApi } from '../utils';
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ShopsService extends createApi(ApiShopsService) {
|
||||
shops$: Observable<Shop[]> = defer(() => this.reloadShops$).pipe(
|
||||
startWith(null),
|
||||
switchMapTo(this.getShops()),
|
||||
shops$: Observable<Shop[]> = combineLatest([
|
||||
this.contextService.organization$,
|
||||
defer(() => this.reloadShops$).pipe(startWith(null)),
|
||||
]).pipe(
|
||||
switchMap(([{ party }]) => this.getShopsForParty({ partyID: party })),
|
||||
shareReplayRefCount()
|
||||
);
|
||||
|
||||
private reloadShops$ = new Subject<void>();
|
||||
|
||||
constructor(injector: Injector, private contextService: ContextService) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
reloadShops(): void {
|
||||
this.reloadShops$.next();
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
|
||||
import { KeycloakTokenInfoService } from '@dsh/app/shared/services/keycloak-token-info';
|
||||
import { ContextService } from '@dsh/app/shared';
|
||||
|
||||
import { ApiExtension } from '../create-api';
|
||||
|
||||
@ -9,12 +9,12 @@ import { ApiExtension } from '../create-api';
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PartyIdExtension implements ApiExtension {
|
||||
constructor(private keycloakTokenInfoService: KeycloakTokenInfoService) {}
|
||||
constructor(private contextService: ContextService) {}
|
||||
|
||||
selector() {
|
||||
return this.keycloakTokenInfoService.partyID$.pipe(
|
||||
return this.contextService.organization$.pipe(
|
||||
first(),
|
||||
map((partyID) => ({ partyID }))
|
||||
map(({ party }) => ({ partyID: party }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import * as Sentry from '@sentry/angular';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
|
||||
import { ENV, Env } from '../environments';
|
||||
import { BootstrapService } from './bootstrap.service';
|
||||
import { KeycloakTokenInfoService } from './shared';
|
||||
import { ContextService } from './shared';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
@ -19,12 +19,13 @@ export class AppComponent implements OnInit {
|
||||
constructor(
|
||||
private bootstrapService: BootstrapService,
|
||||
@Inject(ENV) public env: Env,
|
||||
private keycloakTokenInfoService: KeycloakTokenInfoService
|
||||
private contextService: ContextService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.bootstrapService.bootstrap();
|
||||
this.keycloakTokenInfoService.partyID$
|
||||
this.contextService.organization$
|
||||
.pipe(map(({ party }) => party))
|
||||
.pipe(first(), untilDestroyed(this))
|
||||
.subscribe((partyID) => Sentry.setUser({ id: partyID }));
|
||||
}
|
||||
|
@ -1,19 +1,31 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Router } from '@angular/router';
|
||||
import { ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { ErrorService } from '@dsh/app/shared';
|
||||
|
||||
import { KeycloakAuthGuard, KeycloakService } from './keycloak';
|
||||
import { RoleAccessService } from './role-access.service';
|
||||
import { RoleAccessName } from './types/role-access-name';
|
||||
|
||||
@Injectable()
|
||||
export class AppAuthGuardService extends KeycloakAuthGuard {
|
||||
constructor(protected router: Router, protected keycloakAngular: KeycloakService) {
|
||||
constructor(
|
||||
protected router: Router,
|
||||
protected keycloakAngular: KeycloakService,
|
||||
private errorService: ErrorService,
|
||||
private roleAccessService: RoleAccessService
|
||||
) {
|
||||
super(router, keycloakAngular);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async isAccessAllowed(route: ActivatedRouteSnapshot): Promise<boolean> {
|
||||
const isAccessAllowed = Array.isArray(this.roles) && route.data.roles.every((v) => this.roles.includes(v));
|
||||
async isAccessAllowed(route: ActivatedRouteSnapshot): Promise<boolean | UrlTree> {
|
||||
const isAccessAllowed = await firstValueFrom(
|
||||
this.roleAccessService.isAccessAllowed(route.data.roles as RoleAccessName[])
|
||||
);
|
||||
if (!isAccessAllowed) {
|
||||
console.error('Access is denied');
|
||||
this.errorService.error('Access is denied', false);
|
||||
return this.router.createUrlTree(['404']);
|
||||
}
|
||||
return isAccessAllowed;
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { AppAuthGuardService } from './app-auth-guard.service';
|
||||
import { KeycloakAngularModule } from './keycloak';
|
||||
import { IsAccessAllowedPipe } from './is-access-allowed.pipe';
|
||||
|
||||
@NgModule({
|
||||
imports: [KeycloakAngularModule],
|
||||
providers: [AppAuthGuardService],
|
||||
declarations: [IsAccessAllowedPipe],
|
||||
exports: [IsAccessAllowedPipe],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
@ -2,3 +2,8 @@ export * from './auth.module';
|
||||
export * from './app-auth-guard.service';
|
||||
// eslint-disable-next-line import/export
|
||||
export * from './keycloak';
|
||||
export * from './types/role-access-name';
|
||||
export * from './types/role-access';
|
||||
export * from './role-access-groups';
|
||||
export * from './role-access.service';
|
||||
export * from './utils/create-private-route';
|
||||
|
32
src/app/auth/is-access-allowed.pipe.ts
Normal file
32
src/app/auth/is-access-allowed.pipe.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { Pipe, PipeTransform, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||
|
||||
import { RoleAccessService } from './role-access.service';
|
||||
import { RoleAccessName } from './types/role-access-name';
|
||||
|
||||
@Pipe({
|
||||
name: 'isAccessAllowed',
|
||||
})
|
||||
export class IsAccessAllowedPipe implements PipeTransform, OnDestroy {
|
||||
private asyncPipe: AsyncPipe;
|
||||
|
||||
constructor(private roleAccessService: RoleAccessService, ref: ChangeDetectorRef) {
|
||||
this.asyncPipe = new AsyncPipe(ref);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.asyncPipe.ngOnDestroy();
|
||||
}
|
||||
|
||||
transform(
|
||||
roleAccessNames: RoleAccessName[] | keyof typeof RoleAccessName,
|
||||
type: 'every' | 'some' = 'every'
|
||||
): boolean {
|
||||
return this.asyncPipe.transform(
|
||||
this.roleAccessService.isAccessAllowed(
|
||||
Array.isArray(roleAccessNames) ? roleAccessNames : [RoleAccessName[roleAccessNames]],
|
||||
type
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export * from './keycloak.module';
|
||||
export * from './keycloak.service';
|
||||
export * from './keycloak-auth-guard.service';
|
@ -1,18 +0,0 @@
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||
|
||||
import { KeycloakService } from './keycloak.service';
|
||||
|
||||
export abstract class KeycloakAuthGuard implements CanActivate {
|
||||
protected authenticated: boolean;
|
||||
|
||||
protected roles: string[];
|
||||
|
||||
constructor(protected router: Router, protected keycloakAngular: KeycloakService) {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async canActivate(_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean>;
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { KeycloakService } from './keycloak.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [],
|
||||
declarations: [],
|
||||
providers: [KeycloakService],
|
||||
})
|
||||
export class KeycloakAngularModule {}
|
@ -1,94 +0,0 @@
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { KeycloakEvent, KeycloakOptions } from 'keycloak-angular';
|
||||
import { KeycloakInstance, KeycloakLoginOptions, KeycloakProfile } from 'keycloak-js';
|
||||
import { Observable, Observer, Subject } from 'rxjs';
|
||||
|
||||
import { STUB_USER } from './stub-user';
|
||||
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
/* eslint-disable no-console */
|
||||
@Injectable()
|
||||
export class KeycloakService {
|
||||
async init(_options: KeycloakOptions = {}): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async login(_options: KeycloakLoginOptions = {}): Promise<void> {
|
||||
console.log('login');
|
||||
}
|
||||
|
||||
async logout(_redirectUri?: string): Promise<void> {
|
||||
console.log('logout');
|
||||
}
|
||||
|
||||
async register(_options: KeycloakLoginOptions = { action: 'register' }): Promise<void> {}
|
||||
|
||||
isUserInRole(_role: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
getUserRoles(_allRoles: boolean = true): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
async isLoggedIn(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
isTokenExpired(_minValidity: number = 0): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async updateToken(_minValidity: number = 5): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async loadUserProfile(_forceReload: boolean = false): Promise<KeycloakProfile> {
|
||||
return STUB_USER;
|
||||
}
|
||||
|
||||
async getToken(): Promise<string> {
|
||||
return Math.random().toString();
|
||||
}
|
||||
|
||||
getUsername(): string {
|
||||
return STUB_USER.username;
|
||||
}
|
||||
|
||||
clearToken(): void {}
|
||||
|
||||
addTokenToHeader(headersArg?: HttpHeaders): Observable<HttpHeaders> {
|
||||
return Observable.create(async (observer: Observer<any>) => {
|
||||
let headers = headersArg;
|
||||
if (!headers) {
|
||||
headers = new HttpHeaders();
|
||||
}
|
||||
try {
|
||||
const token: string = await this.getToken();
|
||||
headers = headers.set('Authorization', 'Bearer ' + token);
|
||||
observer.next(headers);
|
||||
observer.complete();
|
||||
} catch (error) {
|
||||
observer.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getKeycloakInstance(): KeycloakInstance {
|
||||
return {} as KeycloakInstance;
|
||||
}
|
||||
|
||||
get bearerExcludedUrls(): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
get enableBearerInterceptor(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
get keycloakEvents$(): Subject<KeycloakEvent> {
|
||||
return new Subject();
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
export const STUB_USER: Keycloak.KeycloakProfile = {
|
||||
id: '1',
|
||||
username: 'mock',
|
||||
email: 'mock@rbkmoney.local',
|
||||
firstName: 'Mock',
|
||||
lastName: 'Money',
|
||||
enabled: true,
|
||||
emailVerified: true,
|
||||
totp: true,
|
||||
createdTimestamp: 1,
|
||||
};
|
68
src/app/auth/role-access-groups.ts
Normal file
68
src/app/auth/role-access-groups.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { RoleId } from '@vality/swag-organizations';
|
||||
|
||||
import { RoleAccessGroup } from './types/role-access';
|
||||
import { RoleAccessName } from './types/role-access-name';
|
||||
|
||||
export const ROLE_ACCESS_GROUPS: RoleAccessGroup[] = [
|
||||
{
|
||||
name: RoleAccessName.Payments,
|
||||
children: [
|
||||
{
|
||||
name: RoleAccessName.ViewAnalytics,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Manager],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.ViewInvoices,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Manager, RoleId.Accountant, RoleId.Integrator],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.ViewPayments,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Manager, RoleId.Accountant, RoleId.Integrator],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.ViewRefunds,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Manager, RoleId.Accountant, RoleId.Integrator],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.ViewPayouts,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Manager, RoleId.Accountant],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.ApiKeys,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Integrator],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.Reports,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Accountant],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.Webhooks,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Integrator],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.CreateInvoice,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Manager],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.PaymentLinks,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Manager],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.CreateRefund,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Accountant],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.Wallets,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Accountant, RoleId.Integrator],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.Claims,
|
||||
availableRoles: [RoleId.Administrator],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.ManageOrganizations,
|
||||
availableRoles: [RoleId.Administrator],
|
||||
},
|
||||
];
|
33
src/app/auth/role-access.service.ts
Normal file
33
src/app/auth/role-access.service.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
|
||||
import { ContextService } from '@dsh/app/shared';
|
||||
|
||||
import { ROLE_ACCESS_GROUPS } from './role-access-groups';
|
||||
import { RoleAccess } from './types/role-access';
|
||||
import { RoleAccessName } from './types/role-access-name';
|
||||
|
||||
const ROLE_ACCESSES_OBJECT = Object.fromEntries(
|
||||
ROLE_ACCESS_GROUPS.flatMap((r) => [r, ...(r.children || [])] as RoleAccess[]).map((r) => [r.name, r.availableRoles])
|
||||
);
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RoleAccessService {
|
||||
constructor(private contextService: ContextService) {}
|
||||
|
||||
isAccessAllowed(roleAccessNames: RoleAccessName[], type: 'every' | 'some' = 'every'): Observable<boolean> {
|
||||
if (!roleAccessNames.length) return of(true);
|
||||
return this.contextService.member$.pipe(
|
||||
first(),
|
||||
map((member) => {
|
||||
const memberRoles = member.roles.map((r) => r.roleId);
|
||||
return roleAccessNames[type]((access) =>
|
||||
ROLE_ACCESSES_OBJECT[access]?.some((role) => memberRoles.includes(role))
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
22
src/app/auth/types/role-access-name.ts
Normal file
22
src/app/auth/types/role-access-name.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export enum RoleAccessName {
|
||||
Payments,
|
||||
|
||||
Reports,
|
||||
Webhooks,
|
||||
ApiKeys,
|
||||
PaymentLinks,
|
||||
|
||||
ViewAnalytics,
|
||||
ViewPayments,
|
||||
ViewPayouts,
|
||||
|
||||
ViewInvoices,
|
||||
CreateInvoice,
|
||||
|
||||
ViewRefunds,
|
||||
CreateRefund,
|
||||
|
||||
Wallets,
|
||||
Claims,
|
||||
ManageOrganizations,
|
||||
}
|
13
src/app/auth/types/role-access.ts
Normal file
13
src/app/auth/types/role-access.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { RoleId } from '@vality/swag-organizations';
|
||||
import { Overwrite } from 'utility-types';
|
||||
|
||||
import { RoleAccessName } from './role-access-name';
|
||||
|
||||
export interface RoleAccess {
|
||||
name: RoleAccessName;
|
||||
availableRoles: RoleId[];
|
||||
}
|
||||
|
||||
export interface RoleAccessGroup extends Overwrite<RoleAccess, Partial<Pick<RoleAccess, 'availableRoles'>>> {
|
||||
children?: RoleAccess[];
|
||||
}
|
14
src/app/auth/utils/create-private-route.ts
Normal file
14
src/app/auth/utils/create-private-route.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Route } from '@angular/router';
|
||||
|
||||
import { RoleAccessName, AppAuthGuardService } from '@dsh/app/auth';
|
||||
|
||||
export function createPrivateRoute(route: Route, roles: RoleAccessName[]) {
|
||||
return {
|
||||
...route,
|
||||
canActivate: [...(route?.canActivate ?? []), AppAuthGuardService],
|
||||
data: {
|
||||
...(route?.data ?? {}),
|
||||
roles,
|
||||
},
|
||||
};
|
||||
}
|
@ -37,12 +37,12 @@ export class BootstrapService {
|
||||
private getBootstrapped(): Observable<boolean> {
|
||||
return concat(this.initOrganization(), this.initShop()).pipe(
|
||||
takeLast(1),
|
||||
catchError((err) => {
|
||||
this.errorService.error(
|
||||
new CommonError(this.transloco.translate('app.errors.bootstrapAppFailed', null, 'components'))
|
||||
);
|
||||
return throwError(err);
|
||||
})
|
||||
catchError((err) =>
|
||||
this.transloco.selectTranslate<string>('app.errors.bootstrapAppFailed', null, 'components').pipe(
|
||||
tap((msg) => this.errorService.error(new CommonError(msg))),
|
||||
switchMap(() => throwError(err))
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,9 @@
|
||||
fxLayoutGap="24px"
|
||||
>
|
||||
<ng-container>
|
||||
<span class="dsh-body-2" *ngIf="activeOrg$ | async">{{ (activeOrg$ | async)?.name }}</span>
|
||||
<dsh-menu-item class="dsh-body-2" *ngIf="activeOrg$ | async as activeOrg" (click)="toActiveOrg(activeOrg.id)">{{
|
||||
activeOrg.name
|
||||
}}</dsh-menu-item>
|
||||
<dsh-menu-item (click)="selectActiveOrg()">{{ t('selectActiveOrg') }}</dsh-menu-item>
|
||||
<dsh-menu-item (click)="openOrgList()">{{ t('orgList') }}</dsh-menu-item>
|
||||
</ng-container>
|
||||
|
@ -77,4 +77,9 @@ export class UserComponent {
|
||||
void this.router.navigate(['organization-section', 'organizations']);
|
||||
this.selected.emit();
|
||||
}
|
||||
|
||||
toActiveOrg(activeOrg: string): void {
|
||||
void this.router.navigate(['organization-section', 'organizations'], { fragment: activeOrg });
|
||||
this.selected.emit();
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { environment } from '../environments';
|
||||
import { KeycloakService } from './auth/keycloak';
|
||||
import { ConfigService } from './config';
|
||||
import { IconsService } from './icons';
|
||||
@ -15,12 +16,12 @@ export const initializer =
|
||||
) =>
|
||||
() =>
|
||||
Promise.all([
|
||||
configService.init({ configUrl: '/appConfig.json' }).then(() =>
|
||||
configService.init({ configUrl: environment.appConfigPath }).then(() =>
|
||||
Promise.all([
|
||||
themeManager.init(),
|
||||
initSentry(configService.sentryDsn),
|
||||
keycloakService.init({
|
||||
config: '/authConfig.json',
|
||||
config: environment.authConfigPath,
|
||||
initOptions: {
|
||||
onLoad: 'login-required',
|
||||
checkLoginIframe: true,
|
||||
|
@ -1,24 +1,29 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { RoleAccessName, createPrivateRoute } from '@dsh/app/auth';
|
||||
|
||||
import { ClaimSectionComponent } from './claim-section.component';
|
||||
|
||||
const CLAIM_SECTION_ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ClaimSectionComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'claims',
|
||||
loadChildren: () => import('./claims/claims.module').then((m) => m.ClaimsModule),
|
||||
},
|
||||
{
|
||||
path: 'claims/:claimId',
|
||||
loadChildren: () => import('./claim/claim.module').then((m) => m.ClaimModule),
|
||||
},
|
||||
{ path: '', redirectTo: 'claims', pathMatch: 'full' },
|
||||
],
|
||||
},
|
||||
createPrivateRoute(
|
||||
{
|
||||
path: '',
|
||||
component: ClaimSectionComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'claims',
|
||||
loadChildren: () => import('./claims/claims.module').then((m) => m.ClaimsModule),
|
||||
},
|
||||
{
|
||||
path: 'claims/:claimId',
|
||||
loadChildren: () => import('./claim/claim.module').then((m) => m.ClaimModule),
|
||||
},
|
||||
{ path: '', redirectTo: 'claims', pathMatch: 'full' },
|
||||
],
|
||||
},
|
||||
[RoleAccessName.Claims]
|
||||
),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -1,70 +0,0 @@
|
||||
import { RoleId } from '@vality/swag-organizations';
|
||||
|
||||
import { RoleAccess } from './types/role-access';
|
||||
import { RoleAccessName } from './types/role-access-name';
|
||||
|
||||
export const ROLES_ACCESSES: RoleAccess[] = [
|
||||
{
|
||||
name: RoleAccessName.Payments,
|
||||
isHeader: true,
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.ViewAnalytics,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Manager],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.ViewInvoices,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Manager, RoleId.Accountant, RoleId.Integrator],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.ViewPayments,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Manager, RoleId.Accountant, RoleId.Integrator],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.ViewRefunds,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Manager, RoleId.Accountant, RoleId.Integrator],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.ViewPayouts,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Manager, RoleId.Accountant],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.ViewApiKey,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Integrator],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.ManageReports,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Accountant],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.ManageWebhooks,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Integrator],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.CreateInvoice,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Manager],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.CreatePaymentLink,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Manager],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.CreateRefund,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Accountant],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.Wallets,
|
||||
isHeader: true,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Accountant, RoleId.Integrator],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.Claims,
|
||||
isHeader: true,
|
||||
availableRoles: [RoleId.Administrator],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.ManageOrganizations,
|
||||
isHeader: true,
|
||||
availableRoles: [RoleId.Administrator],
|
||||
},
|
||||
];
|
@ -5,14 +5,18 @@ import { FormBuilder } from '@ngneat/reactive-forms';
|
||||
import { RoleId } from '@vality/swag-organizations';
|
||||
|
||||
import { OrganizationsDictionaryService } from '@dsh/api/organizations';
|
||||
import { RoleAccess, ROLE_ACCESS_GROUPS } from '@dsh/app/auth';
|
||||
import { BaseDialogResponseStatus } from '@dsh/app/shared/components/dialog/base-dialog';
|
||||
import { ROLE_PRIORITY_DESC } from '@dsh/app/shared/components/organization-roles/utils/sort-role-ids';
|
||||
|
||||
import { ROLES_ACCESSES } from './roles-accesses';
|
||||
import { RoleAccessesDictionaryService } from './services/role-accesses-dictionary.service';
|
||||
import { SelectRoleDialogResult } from './types/select-role-dialog-result';
|
||||
import { SelectRoleDialogData } from './types/selected-role-dialog-data';
|
||||
|
||||
interface FlatRoleAccess extends RoleAccess {
|
||||
isHeader: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-select-role-dialog',
|
||||
templateUrl: 'select-role-dialog.component.html',
|
||||
@ -21,7 +25,9 @@ import { SelectRoleDialogData } from './types/selected-role-dialog-data';
|
||||
})
|
||||
export class SelectRoleDialogComponent {
|
||||
roleControl = this.fb.control<RoleId>(null, Validators.required);
|
||||
accesses = ROLES_ACCESSES;
|
||||
accesses: FlatRoleAccess[] = ROLE_ACCESS_GROUPS.map((r) => ({ ...r, isHeader: true })).flatMap(
|
||||
(r) => [r, ...(r.children || [])] as FlatRoleAccess[]
|
||||
);
|
||||
roleIdDict$ = this.organizationsDictionaryService.roleId$;
|
||||
roleAccessDict$ = this.roleAccessesDictionaryService.roleAccessDict$;
|
||||
get rowsGridTemplateColumns() {
|
||||
|
@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { TranslocoService } from '@ngneat/transloco';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { RoleAccessName } from '../types/role-access-name';
|
||||
import { RoleAccessName } from '@dsh/app/auth';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@ -11,29 +11,81 @@ export class RoleAccessesDictionaryService {
|
||||
roleAccessDict$ = this.t.selectTranslation('organization-section').pipe(
|
||||
map(
|
||||
(): Record<RoleAccessName, string> => ({
|
||||
claims: this.t.translate('roleAccessesDictionary.claims', null, 'organization-section'),
|
||||
createInvoice: this.t.translate('roleAccessesDictionary.createInvoice', null, 'organization-section'),
|
||||
createPaymentLink: this.t.translate(
|
||||
[RoleAccessName.Claims]: this.t.translate(
|
||||
'roleAccessesDictionary.claims',
|
||||
null,
|
||||
'organization-section'
|
||||
),
|
||||
[RoleAccessName.CreateInvoice]: this.t.translate(
|
||||
'roleAccessesDictionary.createInvoice',
|
||||
null,
|
||||
'organization-section'
|
||||
),
|
||||
[RoleAccessName.PaymentLinks]: this.t.translate(
|
||||
'roleAccessesDictionary.createPaymentLink',
|
||||
null,
|
||||
'organization-section'
|
||||
),
|
||||
createRefund: this.t.translate('roleAccessesDictionary.createRefund', null, 'organization-section'),
|
||||
manageOrganizations: this.t.translate(
|
||||
[RoleAccessName.CreateRefund]: this.t.translate(
|
||||
'roleAccessesDictionary.createRefund',
|
||||
null,
|
||||
'organization-section'
|
||||
),
|
||||
[RoleAccessName.ManageOrganizations]: this.t.translate(
|
||||
'roleAccessesDictionary.manageOrganizations',
|
||||
null,
|
||||
'organization-section'
|
||||
),
|
||||
manageReports: this.t.translate('roleAccessesDictionary.manageReports', null, 'organization-section'),
|
||||
manageWebhooks: this.t.translate('roleAccessesDictionary.manageWebhooks', null, 'organization-section'),
|
||||
payments: this.t.translate('roleAccessesDictionary.payments', null, 'organization-section'),
|
||||
viewAnalytics: this.t.translate('roleAccessesDictionary.viewAnalytics', null, 'organization-section'),
|
||||
viewApiKey: this.t.translate('roleAccessesDictionary.viewApiKey', null, 'organization-section'),
|
||||
viewInvoices: this.t.translate('roleAccessesDictionary.viewInvoices', null, 'organization-section'),
|
||||
viewPayments: this.t.translate('roleAccessesDictionary.viewPayments', null, 'organization-section'),
|
||||
viewPayouts: this.t.translate('roleAccessesDictionary.viewPayouts', null, 'organization-section'),
|
||||
viewRefunds: this.t.translate('roleAccessesDictionary.viewRefunds', null, 'organization-section'),
|
||||
wallets: this.t.translate('roleAccessesDictionary.wallets', null, 'organization-section'),
|
||||
[RoleAccessName.Reports]: this.t.translate(
|
||||
'roleAccessesDictionary.manageReports',
|
||||
null,
|
||||
'organization-section'
|
||||
),
|
||||
[RoleAccessName.Webhooks]: this.t.translate(
|
||||
'roleAccessesDictionary.manageWebhooks',
|
||||
null,
|
||||
'organization-section'
|
||||
),
|
||||
[RoleAccessName.Payments]: this.t.translate(
|
||||
'roleAccessesDictionary.payments',
|
||||
null,
|
||||
'organization-section'
|
||||
),
|
||||
[RoleAccessName.ViewAnalytics]: this.t.translate(
|
||||
'roleAccessesDictionary.viewAnalytics',
|
||||
null,
|
||||
'organization-section'
|
||||
),
|
||||
[RoleAccessName.ApiKeys]: this.t.translate(
|
||||
'roleAccessesDictionary.viewApiKey',
|
||||
null,
|
||||
'organization-section'
|
||||
),
|
||||
[RoleAccessName.ViewInvoices]: this.t.translate(
|
||||
'roleAccessesDictionary.viewInvoices',
|
||||
null,
|
||||
'organization-section'
|
||||
),
|
||||
[RoleAccessName.ViewPayments]: this.t.translate(
|
||||
'roleAccessesDictionary.viewPayments',
|
||||
null,
|
||||
'organization-section'
|
||||
),
|
||||
[RoleAccessName.ViewPayouts]: this.t.translate(
|
||||
'roleAccessesDictionary.viewPayouts',
|
||||
null,
|
||||
'organization-section'
|
||||
),
|
||||
[RoleAccessName.ViewRefunds]: this.t.translate(
|
||||
'roleAccessesDictionary.viewRefunds',
|
||||
null,
|
||||
'organization-section'
|
||||
),
|
||||
[RoleAccessName.Wallets]: this.t.translate(
|
||||
'roleAccessesDictionary.wallets',
|
||||
null,
|
||||
'organization-section'
|
||||
),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { RoleId } from '@vality/swag-organizations';
|
||||
|
||||
export interface RoleAccessItem {
|
||||
name: string;
|
||||
isHeader?: boolean;
|
||||
availableRoles?: RoleId[];
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
export enum RoleAccessName {
|
||||
Payments = 'payments',
|
||||
ViewAnalytics = 'viewAnalytics',
|
||||
ViewInvoices = 'viewInvoices',
|
||||
ViewPayments = 'viewPayments',
|
||||
ViewRefunds = 'viewRefunds',
|
||||
ViewPayouts = 'viewPayouts',
|
||||
ViewApiKey = 'viewApiKey',
|
||||
ManageReports = 'manageReports',
|
||||
ManageWebhooks = 'manageWebhooks',
|
||||
CreateInvoice = 'createInvoice',
|
||||
CreatePaymentLink = 'createPaymentLink',
|
||||
CreateRefund = 'createRefund',
|
||||
Wallets = 'wallets',
|
||||
Claims = 'claims',
|
||||
ManageOrganizations = 'manageOrganizations',
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { RoleId } from '@vality/swag-organizations';
|
||||
|
||||
export interface RoleAccess {
|
||||
name: string;
|
||||
isHeader?: boolean;
|
||||
availableRoles?: RoleId[];
|
||||
}
|
@ -3,6 +3,7 @@ import { MatDialogRef } from '@angular/material/dialog';
|
||||
import { FormBuilder } from '@ngneat/reactive-forms';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { BehaviorSubject, switchMap } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
import { OrgsService } from '@dsh/api/organizations';
|
||||
import { KeycloakTokenInfoService } from '@dsh/app/shared';
|
||||
@ -32,8 +33,9 @@ export class CreateOrganizationDialogComponent {
|
||||
|
||||
@inProgressTo('inProgress$')
|
||||
create() {
|
||||
return this.keycloakTokenInfoService.partyID$
|
||||
return this.keycloakTokenInfoService.userID$
|
||||
.pipe(
|
||||
first(),
|
||||
switchMap((owner) =>
|
||||
this.organizationsService.createOrg({
|
||||
organization: {
|
||||
|
@ -7,7 +7,7 @@ import { filter, pluck, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { OrgsService } from '@dsh/api/organizations';
|
||||
import { BaseDialogResponseStatus } from '@dsh/app/shared/components/dialog/base-dialog';
|
||||
import { ErrorService, NotificationService } from '@dsh/app/shared/services';
|
||||
import { ErrorService, NotificationService, ContextService } from '@dsh/app/shared/services';
|
||||
import { FetchOrganizationsService } from '@dsh/app/shared/services/fetch-organizations';
|
||||
import { OrganizationManagementService } from '@dsh/app/shared/services/organization-management/organization-management.service';
|
||||
import { ConfirmActionDialogComponent, ConfirmActionDialogResult } from '@dsh/components/popups';
|
||||
@ -29,7 +29,7 @@ export class OrganizationComponent implements OnChanges {
|
||||
|
||||
@Output() changed = new EventEmitter<void>();
|
||||
|
||||
member$ = this.organizationManagementService.currentMember$;
|
||||
member$ = this.contextService.member$;
|
||||
membersCount$ = this.organizationManagementService.members$.pipe(pluck('length'));
|
||||
hasAdminAccess$ = this.organizationManagementService.hasAdminAccess$;
|
||||
isOwner$ = this.organizationManagementService.isOrganizationOwner$;
|
||||
@ -40,7 +40,8 @@ export class OrganizationComponent implements OnChanges {
|
||||
private dialog: MatDialog,
|
||||
private notificationService: NotificationService,
|
||||
private errorService: ErrorService,
|
||||
private fetchOrganizationsService: FetchOrganizationsService
|
||||
private fetchOrganizationsService: FetchOrganizationsService,
|
||||
private contextService: ContextService
|
||||
) {}
|
||||
|
||||
ngOnChanges({ organization }: ComponentChanges<OrganizationComponent>) {
|
||||
|
@ -1,4 +1,11 @@
|
||||
<div>404</div>
|
||||
<button *transloco="let t; scope: 'components'; read: 'components.pageNotFound'" (click)="back()">
|
||||
{{ t('back') }}
|
||||
</button>
|
||||
<div fxLayout="column" fxLayoutGap="16px" fxLayoutAlign="center center">
|
||||
<div class="dsh-display-1">404</div>
|
||||
<button
|
||||
dsh-button
|
||||
color="primary"
|
||||
*transloco="let t; scope: 'components'; read: 'components.pageNotFound'"
|
||||
(click)="back()"
|
||||
>
|
||||
{{ t('back') }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-page-not-found',
|
||||
templateUrl: 'page-not-found.component.html',
|
||||
})
|
||||
export class PageNotFoundComponent {
|
||||
constructor(private router: Router) {}
|
||||
constructor(private location: Location) {}
|
||||
|
||||
back() {
|
||||
this.router.navigate(['']);
|
||||
this.location.back();
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexModule } from '@angular/flex-layout';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
|
||||
import { PageNotFoundRoutingModule } from './page-not-found-routing.module';
|
||||
import { PageNotFoundComponent } from './page-not-found.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [PageNotFoundComponent],
|
||||
imports: [RouterModule, PageNotFoundRoutingModule, TranslocoModule],
|
||||
imports: [RouterModule, PageNotFoundRoutingModule, TranslocoModule, FlexModule, ButtonModule],
|
||||
exports: [PageNotFoundComponent],
|
||||
})
|
||||
export class PageNotFoundModule {}
|
||||
|
@ -1,13 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { RoleAccessName, createPrivateRoute } from '@dsh/app/auth';
|
||||
|
||||
import { AnalyticsComponent } from './analytics.component';
|
||||
|
||||
const OPERATIONS_ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AnalyticsComponent,
|
||||
},
|
||||
createPrivateRoute(
|
||||
{
|
||||
path: '',
|
||||
component: AnalyticsComponent,
|
||||
},
|
||||
[RoleAccessName.ViewAnalytics]
|
||||
),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { createPrivateRoute, RoleAccessName } from '@dsh/app/auth';
|
||||
|
||||
import { IntegrationsComponent } from './integrations.component';
|
||||
|
||||
const ROUTES: Routes = [
|
||||
@ -8,18 +10,27 @@ const ROUTES: Routes = [
|
||||
path: '',
|
||||
component: IntegrationsComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'webhooks',
|
||||
loadChildren: () => import('./webhooks').then((m) => m.WebhooksModule),
|
||||
},
|
||||
{
|
||||
path: 'payment-link',
|
||||
loadChildren: () => import('./payment-link').then((m) => m.PaymentLinkModule),
|
||||
},
|
||||
{
|
||||
path: 'api-key',
|
||||
loadChildren: () => import('./api-key').then((m) => m.ApiKeyModule),
|
||||
},
|
||||
createPrivateRoute(
|
||||
{
|
||||
path: 'webhooks',
|
||||
loadChildren: () => import('./webhooks').then((m) => m.WebhooksModule),
|
||||
},
|
||||
[RoleAccessName.Webhooks]
|
||||
),
|
||||
createPrivateRoute(
|
||||
{
|
||||
path: 'payment-link',
|
||||
loadChildren: () => import('./payment-link').then((m) => m.PaymentLinkModule),
|
||||
},
|
||||
[RoleAccessName.PaymentLinks]
|
||||
),
|
||||
createPrivateRoute(
|
||||
{
|
||||
path: 'api-key',
|
||||
loadChildren: () => import('./api-key').then((m) => m.ApiKeyModule),
|
||||
},
|
||||
[RoleAccessName.ApiKeys]
|
||||
),
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'payment-link',
|
||||
|
@ -8,7 +8,9 @@
|
||||
</div>
|
||||
<div fxLayout="column" fxLayout.gt-sm="row" fxLayoutGap="24px">
|
||||
<ng-container *ngIf="invoice.status === 'unpaid'">
|
||||
<button dsh-button color="accent" (click)="createPaymentLink()">{{ t('createPaymentLink') }}</button>
|
||||
<button dsh-button color="accent" *ngIf="'PaymentLinks' | isAccessAllowed" (click)="createPaymentLink()">
|
||||
{{ t('createPaymentLink') }}
|
||||
</button>
|
||||
<button dsh-button color="warn" (click)="cancelInvoice()">{{ t('cancelInvoice') }}</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="invoice.status === 'paid'">
|
||||
|
@ -22,7 +22,6 @@ import { FulfillInvoiceService } from '../../fulfill-invoice';
|
||||
})
|
||||
export class InvoiceActionsComponent {
|
||||
@Input() invoice: Invoice;
|
||||
|
||||
@Output() refreshData = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
|
@ -7,6 +7,7 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { AuthModule } from '@dsh/app/auth';
|
||||
import {
|
||||
InvoiceDetailsModule as InvoiceInvoiceDetailsModule,
|
||||
PaymentDetailsModule,
|
||||
@ -49,6 +50,7 @@ import { TaxModeToTaxRatePipe } from './pipes/tax-mode-to-tax-rate/tax-mode-to-t
|
||||
RouterModule,
|
||||
MatIconModule,
|
||||
AmountCurrencyModule,
|
||||
AuthModule,
|
||||
],
|
||||
declarations: [
|
||||
InvoiceDetailsComponent,
|
||||
|
@ -6,14 +6,16 @@
|
||||
gdAlignRows="space-between start"
|
||||
gdGap="32px"
|
||||
>
|
||||
<button
|
||||
*transloco="let i; scope: 'payment-section'; read: 'paymentSection.invoices'"
|
||||
dsh-button
|
||||
color="accent"
|
||||
(click)="create()"
|
||||
>
|
||||
{{ i('createButton') }}
|
||||
</button>
|
||||
<ng-container *ngIf="'CreateInvoice' | isAccessAllowed">
|
||||
<button
|
||||
*transloco="let i; scope: 'payment-section'; read: 'paymentSection.invoices'"
|
||||
dsh-button
|
||||
color="accent"
|
||||
(click)="create()"
|
||||
>
|
||||
{{ i('createButton') }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<dsh-invoices-search-filters
|
||||
gdRow.gt-sm="1"
|
||||
[initParams]="params$ | async"
|
||||
|
@ -14,6 +14,7 @@ import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { AuthModule } from '@dsh/app/auth';
|
||||
import { InvoiceDetailsModule } from '@dsh/app/shared/components';
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
import { EmptySearchResultModule } from '@dsh/components/empty-search-result';
|
||||
@ -60,6 +61,7 @@ import { InvoicesComponent } from './invoices.component';
|
||||
InvoiceDetailsModule,
|
||||
InvoicesListModule,
|
||||
ShowMorePanelModule,
|
||||
AuthModule,
|
||||
],
|
||||
declarations: [InvoicesComponent],
|
||||
})
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { createPrivateRoute, RoleAccessName } from '@dsh/app/auth';
|
||||
|
||||
import { OperationsComponent } from './operations.component';
|
||||
|
||||
const OPERATIONS_ROUTES: Routes = [
|
||||
@ -8,18 +10,27 @@ const OPERATIONS_ROUTES: Routes = [
|
||||
path: '',
|
||||
component: OperationsComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'payments',
|
||||
loadChildren: () => import('./payments/payments.module').then((mod) => mod.PaymentsModule),
|
||||
},
|
||||
{
|
||||
path: 'refunds',
|
||||
loadChildren: () => import('./refunds/refunds.module').then((mod) => mod.RefundsModule),
|
||||
},
|
||||
{
|
||||
path: 'invoices',
|
||||
loadChildren: () => import('./invoices/invoices.module').then((mod) => mod.InvoicesModule),
|
||||
},
|
||||
createPrivateRoute(
|
||||
{
|
||||
path: 'payments',
|
||||
loadChildren: () => import('./payments/payments.module').then((mod) => mod.PaymentsModule),
|
||||
},
|
||||
[RoleAccessName.ViewPayments]
|
||||
),
|
||||
createPrivateRoute(
|
||||
{
|
||||
path: 'refunds',
|
||||
loadChildren: () => import('./refunds/refunds.module').then((mod) => mod.RefundsModule),
|
||||
},
|
||||
[RoleAccessName.ViewRefunds]
|
||||
),
|
||||
createPrivateRoute(
|
||||
{
|
||||
path: 'invoices',
|
||||
loadChildren: () => import('./invoices/invoices.module').then((mod) => mod.InvoicesModule),
|
||||
},
|
||||
[RoleAccessName.ViewInvoices]
|
||||
),
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'payments',
|
||||
|
@ -7,7 +7,13 @@
|
||||
<div>
|
||||
{{ t('title') }}
|
||||
</div>
|
||||
<button dsh-button color="accent" [disabled]="!isRefundAvailable" (click)="createRefund()">
|
||||
<button
|
||||
*ngIf="'CreateRefund' | isAccessAllowed"
|
||||
dsh-button
|
||||
color="accent"
|
||||
[disabled]="!isRefundAvailable"
|
||||
(click)="createRefund()"
|
||||
>
|
||||
{{ t('createRefund.title') }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { AuthModule } from '@dsh/app/auth';
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
|
||||
import { CreateRefundModule } from './create-refund';
|
||||
@ -10,7 +11,15 @@ import { RefundsListModule } from './refunds-list';
|
||||
import { RefundsComponent } from './refunds.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, TranslocoModule, FlexLayoutModule, ButtonModule, CreateRefundModule, RefundsListModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
TranslocoModule,
|
||||
FlexLayoutModule,
|
||||
ButtonModule,
|
||||
CreateRefundModule,
|
||||
RefundsListModule,
|
||||
AuthModule,
|
||||
],
|
||||
declarations: [RefundsComponent],
|
||||
exports: [RefundsComponent],
|
||||
})
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { createPrivateRoute, RoleAccessName } from '@dsh/app/auth';
|
||||
|
||||
import { PaymentSectionComponent } from './payment-section.component';
|
||||
|
||||
const PAYMENT_SECTION_ROUTES: Routes = [
|
||||
@ -21,22 +23,31 @@ const PAYMENT_SECTION_ROUTES: Routes = [
|
||||
path: 'shops',
|
||||
loadChildren: () => import('./shops/shops.module').then((m) => m.ShopsModule),
|
||||
},
|
||||
{
|
||||
path: 'analytics',
|
||||
loadChildren: () => import('./analytics/analytics.module').then((m) => m.AnalyticsModule),
|
||||
},
|
||||
createPrivateRoute(
|
||||
{
|
||||
path: 'analytics',
|
||||
loadChildren: () => import('./analytics/analytics.module').then((m) => m.AnalyticsModule),
|
||||
},
|
||||
[RoleAccessName.ViewAnalytics]
|
||||
),
|
||||
{
|
||||
path: 'operations',
|
||||
loadChildren: () => import('./operations/operations.module').then((m) => m.OperationsModule),
|
||||
},
|
||||
{
|
||||
path: 'reports',
|
||||
loadChildren: () => import('./reports/reports.module').then((m) => m.ReportsModule),
|
||||
},
|
||||
{
|
||||
path: 'payouts',
|
||||
loadChildren: () => import('./payouts/payouts.module').then((m) => m.PayoutsModule),
|
||||
},
|
||||
createPrivateRoute(
|
||||
{
|
||||
path: 'reports',
|
||||
loadChildren: () => import('./reports/reports.module').then((m) => m.ReportsModule),
|
||||
},
|
||||
[RoleAccessName.Reports]
|
||||
),
|
||||
createPrivateRoute(
|
||||
{
|
||||
path: 'payouts',
|
||||
loadChildren: () => import('./payouts/payouts.module').then((m) => m.PayoutsModule),
|
||||
},
|
||||
[RoleAccessName.ViewPayouts]
|
||||
),
|
||||
{
|
||||
path: 'integrations',
|
||||
loadChildren: () => import('./integrations/integrations.module').then((m) => m.IntegrationsModule),
|
||||
|
@ -2,16 +2,19 @@
|
||||
<dsh-no-shops-alert *ngIf="noShops$ | async" (action)="navigateToShops()"></dsh-no-shops-alert>
|
||||
<dsh-route-navbar-layout [routeName]="activeSection$ | async">
|
||||
<div fxLayout="row" fxLayout.gt-sm="column">
|
||||
<dsh-navbar-item
|
||||
*ngFor="let item of navbarItemConfig$ | async"
|
||||
[routerLink]="item.routerLink"
|
||||
routerLinkActive
|
||||
#rla="routerLinkActive"
|
||||
[active]="rla.isActive"
|
||||
[icon]="item.icon"
|
||||
(activeChange)="setActiveSection($event, item)"
|
||||
>{{ item.label }}</dsh-navbar-item
|
||||
>
|
||||
<ng-container *ngFor="let item of navbarItemConfig$ | async">
|
||||
<dsh-navbar-item
|
||||
*ngIf="item.roles | isAccessAllowed: 'some'"
|
||||
[routerLink]="item.routerLink"
|
||||
routerLinkActive
|
||||
#rla="routerLinkActive"
|
||||
[active]="rla.isActive"
|
||||
[icon]="item.icon"
|
||||
(activeChange)="setActiveSection($event, item)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</dsh-navbar-item>
|
||||
</ng-container>
|
||||
<dsh-navbar-item
|
||||
*transloco="let t; scope: 'payment-section'; read: 'paymentSection.paymentSection.nav'"
|
||||
withToggle="true"
|
||||
|
@ -4,6 +4,7 @@ import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslocoModule, TRANSLOCO_SCOPE } from '@ngneat/transloco';
|
||||
|
||||
import { AuthModule } from '@dsh/app/auth';
|
||||
import { RouteNavbarLayoutModule } from '@dsh/app/shared/components/route-navbar-layout';
|
||||
import { NavbarItemModule } from '@dsh/components/navigation';
|
||||
|
||||
@ -23,6 +24,7 @@ import { PaymentSectionComponent } from './payment-section.component';
|
||||
RouteNavbarLayoutModule,
|
||||
NavbarItemModule,
|
||||
NoShopsAlertModule,
|
||||
AuthModule,
|
||||
],
|
||||
declarations: [PaymentSectionComponent],
|
||||
exports: [PaymentSectionComponent],
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RoleAccessName } from '@dsh/app/auth';
|
||||
import { BootstrapIconName } from '@dsh/components/indicators';
|
||||
|
||||
export enum NavbarRouterLink {
|
||||
@ -13,6 +14,7 @@ export interface NavbarItemConfig {
|
||||
routerLink: NavbarRouterLink;
|
||||
icon: BootstrapIconName;
|
||||
label: string;
|
||||
roles: RoleAccessName[];
|
||||
}
|
||||
|
||||
export const toNavbarItemConfig = ({
|
||||
@ -30,30 +32,36 @@ export const toNavbarItemConfig = ({
|
||||
routerLink: NavbarRouterLink.Shops,
|
||||
icon: BootstrapIconName.Shop,
|
||||
label: shops,
|
||||
roles: [],
|
||||
},
|
||||
{
|
||||
routerLink: NavbarRouterLink.Analytics,
|
||||
icon: BootstrapIconName.PieChart,
|
||||
label: analytics,
|
||||
roles: [RoleAccessName.ViewAnalytics],
|
||||
},
|
||||
{
|
||||
routerLink: NavbarRouterLink.Operations,
|
||||
icon: BootstrapIconName.LayoutTextSidebarReverse,
|
||||
label: operations,
|
||||
roles: [RoleAccessName.ViewPayments, RoleAccessName.ViewInvoices, RoleAccessName.ViewRefunds],
|
||||
},
|
||||
{
|
||||
routerLink: NavbarRouterLink.Payouts,
|
||||
icon: BootstrapIconName.ArrowRightCircle,
|
||||
label: payouts,
|
||||
roles: [RoleAccessName.ViewPayouts],
|
||||
},
|
||||
{
|
||||
routerLink: NavbarRouterLink.Reports,
|
||||
icon: BootstrapIconName.FileText,
|
||||
label: reports,
|
||||
roles: [],
|
||||
},
|
||||
{
|
||||
routerLink: NavbarRouterLink.Integrations,
|
||||
icon: BootstrapIconName.Plug,
|
||||
label: integrations,
|
||||
roles: [RoleAccessName.PaymentLinks, RoleAccessName.ApiKeys, RoleAccessName.Webhooks],
|
||||
},
|
||||
];
|
||||
|
@ -1,11 +1,16 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Organization } from '@vality/swag-organizations';
|
||||
import { Observable, ReplaySubject, EMPTY, concat, defer } from 'rxjs';
|
||||
import { distinctUntilChanged, switchMap, shareReplay, catchError, map } from 'rxjs/operators';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { Organization, Member, RoleId } from '@vality/swag-organizations';
|
||||
import { Observable, ReplaySubject, EMPTY, concat, defer, combineLatest, of, throwError } from 'rxjs';
|
||||
import { switchMap, shareReplay, catchError, map, tap } from 'rxjs/operators';
|
||||
|
||||
import { OrgsService } from '@dsh/api/organizations';
|
||||
import { OrgsService, MembersService } from '@dsh/api/organizations';
|
||||
|
||||
import { ErrorService } from '../error';
|
||||
import { KeycloakTokenInfoService } from '../keycloak-token-info';
|
||||
|
||||
@UntilDestroy()
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@ -15,28 +20,55 @@ export class ContextService {
|
||||
map(({ organizationId }) => organizationId),
|
||||
catchError((err) => {
|
||||
if (err instanceof HttpErrorResponse && err.status === 404)
|
||||
return this.organizationsService
|
||||
.listOrgMembership({ limit: 1 })
|
||||
.pipe(map(({ result }) => result[0].id));
|
||||
return this.organizationsService.listOrgMembership({ limit: 1 }).pipe(
|
||||
map(({ result }) => result[0].id),
|
||||
tap((id) => this.switchOrganization(id)),
|
||||
switchMap(() => EMPTY)
|
||||
);
|
||||
console.error(err);
|
||||
return EMPTY;
|
||||
})
|
||||
),
|
||||
defer(() => this.switchOrganization$)
|
||||
defer(() => this.switchOrganization$).pipe(
|
||||
switchMap((organizationId) =>
|
||||
this.organizationsService
|
||||
.switchContext({ organizationSwitchRequest: { organizationId } })
|
||||
.pipe(map(() => organizationId))
|
||||
)
|
||||
)
|
||||
).pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((organizationId) =>
|
||||
this.organizationsService
|
||||
.switchContext({ organizationSwitchRequest: { organizationId } })
|
||||
.pipe(map(() => organizationId))
|
||||
),
|
||||
switchMap((orgId) => this.organizationsService.getOrg({ orgId })),
|
||||
shareReplay({ refCount: true, bufferSize: 1 })
|
||||
untilDestroyed(this),
|
||||
shareReplay(1)
|
||||
);
|
||||
member$ = combineLatest([this.organization$, this.keycloakTokenInfoService.userID$]).pipe(
|
||||
switchMap(([{ id: orgId }, userId]) =>
|
||||
this.membersService.getOrgMember({ orgId, userId }).pipe(
|
||||
catchError((error) => {
|
||||
if (error instanceof HttpErrorResponse && error.status === 404) {
|
||||
return of<Member>({
|
||||
id: userId,
|
||||
userEmail: '',
|
||||
roles: [{ id: null, roleId: RoleId.Administrator }],
|
||||
});
|
||||
}
|
||||
this.errorService.error(error);
|
||||
return throwError(error);
|
||||
})
|
||||
)
|
||||
),
|
||||
untilDestroyed(this),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
private switchOrganization$ = new ReplaySubject<string>(1);
|
||||
|
||||
constructor(private organizationsService: OrgsService) {}
|
||||
constructor(
|
||||
private organizationsService: OrgsService,
|
||||
private membersService: MembersService,
|
||||
private keycloakTokenInfoService: KeycloakTokenInfoService,
|
||||
private errorService: ErrorService
|
||||
) {}
|
||||
|
||||
switchOrganization(organizationId: string): void {
|
||||
this.switchOrganization$.next(organizationId);
|
||||
|
@ -2,16 +2,15 @@ import { Injectable } from '@angular/core';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import jwt_decode, { JwtPayload } from 'jwt-decode';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { from, Observable, defer } from 'rxjs';
|
||||
import { map, pluck, shareReplay } from 'rxjs/operators';
|
||||
import { Observable, defer, from } from 'rxjs';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
|
||||
@UntilDestroy()
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class KeycloakTokenInfoService {
|
||||
// Party ID & User ID
|
||||
partyID$: Observable<string> = defer(() => this.decoded$).pipe(pluck('sub'));
|
||||
userID$: Observable<string> = defer(() => this.decoded$).pipe(map(({ sub }) => sub));
|
||||
|
||||
private decoded$ = from(this.keycloakService.getToken()).pipe(
|
||||
map((token) => jwt_decode<JwtPayload>(token)),
|
||||
|
@ -1,43 +1,27 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Member, Organization, RoleId } from '@vality/swag-organizations';
|
||||
import { combineLatest, defer, Observable, of, ReplaySubject } from 'rxjs';
|
||||
import { catchError, map, pluck, shareReplay, switchMap } from 'rxjs/operators';
|
||||
import { combineLatest, defer, Observable, ReplaySubject } from 'rxjs';
|
||||
import { map, pluck, shareReplay, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { MembersService } from '@dsh/api/organizations';
|
||||
import { ErrorService, KeycloakTokenInfoService } from '@dsh/app/shared';
|
||||
import { ContextService } from '@dsh/app/shared';
|
||||
import { Initializable } from '@dsh/app/shared/types';
|
||||
import { SHARE_REPLAY_CONF } from '@dsh/operators';
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationManagementService implements Initializable {
|
||||
currentMember$: Observable<Member> = defer(() =>
|
||||
combineLatest([this.organization$, this.keycloakTokenInfoService.partyID$])
|
||||
).pipe(
|
||||
switchMap(([{ id: orgId }, userId]) =>
|
||||
this.membersService.getOrgMember({ orgId, userId }).pipe(
|
||||
catchError((error) => {
|
||||
if (!(error instanceof HttpErrorResponse && error.status === 404)) {
|
||||
this.errorService.error(error);
|
||||
}
|
||||
return of<Member>({ id: userId, userEmail: '', roles: [] });
|
||||
})
|
||||
)
|
||||
),
|
||||
shareReplay(SHARE_REPLAY_CONF)
|
||||
);
|
||||
members$: Observable<Member[]> = defer(() => this.organization$).pipe(
|
||||
switchMap(({ id }) => this.membersService.listOrgMembers({ orgId: id })),
|
||||
pluck('result'),
|
||||
shareReplay(SHARE_REPLAY_CONF)
|
||||
);
|
||||
isOrganizationOwner$: Observable<boolean> = defer(() =>
|
||||
combineLatest([this.organization$, this.keycloakTokenInfoService.partyID$])
|
||||
combineLatest([this.organization$, this.contextService.organization$.pipe(pluck('party'))])
|
||||
).pipe(
|
||||
map(([{ owner }, id]) => owner === id),
|
||||
shareReplay(SHARE_REPLAY_CONF)
|
||||
);
|
||||
isOrganizationAdmin$: Observable<boolean> = this.currentMember$.pipe(
|
||||
isOrganizationAdmin$: Observable<boolean> = this.contextService.member$.pipe(
|
||||
map((member) => member.roles.findIndex((r) => r.roleId === RoleId.Administrator) !== -1),
|
||||
shareReplay(SHARE_REPLAY_CONF)
|
||||
);
|
||||
@ -50,11 +34,7 @@ export class OrganizationManagementService implements Initializable {
|
||||
|
||||
private organization$ = new ReplaySubject<Organization>();
|
||||
|
||||
constructor(
|
||||
private membersService: MembersService,
|
||||
private keycloakTokenInfoService: KeycloakTokenInfoService,
|
||||
private errorService: ErrorService
|
||||
) {}
|
||||
constructor(private membersService: MembersService, private contextService: ContextService) {}
|
||||
|
||||
init(organization: Organization) {
|
||||
this.organization$.next(organization);
|
||||
|
@ -1,3 +1,3 @@
|
||||
export * from './section-links.module';
|
||||
export * from './section-links.service';
|
||||
export * from './model';
|
||||
export * from './types';
|
||||
|
@ -4,27 +4,41 @@ import { combineLatest, Observable } from 'rxjs';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
|
||||
import { WalletsService } from '@dsh/api/wallet';
|
||||
import { RoleAccessName, RoleAccessService } from '@dsh/app/auth';
|
||||
|
||||
import { SectionLink } from './model';
|
||||
import { createLinks } from './utils';
|
||||
import { SectionLink } from './types';
|
||||
|
||||
@Injectable()
|
||||
export class SectionsLinksService {
|
||||
sectionLinks$: Observable<SectionLink[]> = combineLatest([
|
||||
this.walletsService.hasWallets$,
|
||||
this.roleAccessService.isAccessAllowed([RoleAccessName.Wallets]),
|
||||
this.roleAccessService.isAccessAllowed([RoleAccessName.Claims]),
|
||||
this.transloco.selectTranslation('services'),
|
||||
]).pipe(
|
||||
map(([hasWallets]) => createLinks(this.getLabels(), hasWallets)),
|
||||
map(([hasWallets, allowWallets, allowClaims]) =>
|
||||
[
|
||||
{
|
||||
label: this.transloco.translate('sectionsLinks.links.payments', null, 'services'),
|
||||
path: `/payment-section`,
|
||||
},
|
||||
hasWallets &&
|
||||
allowWallets && {
|
||||
label: this.transloco.translate('sectionsLinks.links.wallets', null, 'services'),
|
||||
path: '/wallet-section',
|
||||
},
|
||||
allowClaims && {
|
||||
label: this.transloco.translate('sectionsLinks.links.claims', null, 'services'),
|
||||
path: '/claim-section',
|
||||
},
|
||||
].filter(Boolean)
|
||||
),
|
||||
first()
|
||||
);
|
||||
|
||||
constructor(private walletsService: WalletsService, private transloco: TranslocoService) {}
|
||||
|
||||
getLabels() {
|
||||
return {
|
||||
claims: this.transloco.translate('sectionsLinks.links.claims', null, 'services'),
|
||||
payments: this.transloco.translate('sectionsLinks.links.payments', null, 'services'),
|
||||
wallets: this.transloco.translate('sectionsLinks.links.wallets', null, 'services'),
|
||||
};
|
||||
}
|
||||
constructor(
|
||||
private walletsService: WalletsService,
|
||||
private transloco: TranslocoService,
|
||||
private roleAccessService: RoleAccessService
|
||||
) {}
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
import { SectionLink } from '../model';
|
||||
|
||||
export const createLinks = (
|
||||
{ claims, payments, wallets }: Record<'claims' | 'payments' | 'wallets', string>,
|
||||
hasWallets: boolean
|
||||
): SectionLink[] =>
|
||||
[
|
||||
{ label: payments, path: `/payment-section` },
|
||||
hasWallets && { label: wallets, path: '/wallet-section' },
|
||||
{ label: claims, path: '/claim-section' },
|
||||
].filter(Boolean);
|
@ -1 +0,0 @@
|
||||
export * from './create-links';
|
8
src/environments/environment.dev.ts
Normal file
8
src/environments/environment.dev.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { environment as prodEnvironment } from './environment.prod';
|
||||
import { Environment } from './types/environment';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const environment: Environment = {
|
||||
...prodEnvironment,
|
||||
production: false,
|
||||
};
|
@ -1,4 +1,8 @@
|
||||
import { Environment } from './types/environment';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const environment = {
|
||||
export const environment: Environment = {
|
||||
production: true,
|
||||
appConfigPath: '/appConfig.json',
|
||||
authConfigPath: '/authConfig.json',
|
||||
};
|
||||
|
9
src/environments/environment.stage.ts
Normal file
9
src/environments/environment.stage.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { environment as devEnvironment } from './environment.dev';
|
||||
import { Environment } from './types/environment';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const environment: Environment = {
|
||||
...devEnvironment,
|
||||
appConfigPath: '/appConfig.stage.json',
|
||||
authConfigPath: '/authConfig.stage.json',
|
||||
};
|
@ -1,11 +1,12 @@
|
||||
import { environment as devEnvironment } from './environment.dev';
|
||||
import { Environment } from './types/environment';
|
||||
|
||||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const environment = {
|
||||
production: false,
|
||||
};
|
||||
export const environment: Environment = devEnvironment;
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
|
5
src/environments/types/environment.ts
Normal file
5
src/environments/types/environment.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface Environment {
|
||||
production: boolean;
|
||||
appConfigPath: string;
|
||||
authConfigPath: string;
|
||||
}
|
@ -29,7 +29,8 @@
|
||||
"@dsh/app/sections/tokens": ["src/app/sections/tokens.ts"],
|
||||
"@dsh/type-utils": ["src/type-utils/index.ts"],
|
||||
"@dsh/utils": ["src/utils/index.ts"],
|
||||
"@dsh/operators": ["src/app/custom-operators/index.ts"]
|
||||
"@dsh/operators": ["src/app/custom-operators/index.ts"],
|
||||
"@dsh/app/*": ["src/app/*"]
|
||||
}
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
|
Loading…
Reference in New Issue
Block a user