diff --git a/package-lock.json b/package-lock.json index dd9cd98f..073f6e17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9015,8 +9015,8 @@ } }, "fistful-proto": { - "version": "git+ssh://git@github.com/rbkmoney/fistful-proto.git#7b3f125aa7cbc069f740f598295bb56de713c39f", - "from": "git+ssh://git@github.com/rbkmoney/fistful-proto.git#7b3f125aa7cbc069f740f598295bb56de713c39f" + "version": "git+ssh://git@github.com/rbkmoney/fistful-proto.git#e340259cdd3add024f0139e21f0a2453312ef901", + "from": "git+ssh://git@github.com/rbkmoney/fistful-proto.git#e340259cdd3add024f0139e21f0a2453312ef901" }, "flake-idgen": { "version": "1.1.0", diff --git a/package.json b/package.json index c0dcf697..0e490d91 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "damsel": "git+ssh://git@github.com/rbkmoney/damsel.git#8851c242d2953cc52397af3d916b52b164ffe4c0", "deanonimus-proto": "github:rbkmoney/deanonimus-proto#b9fab4fd1c7690186efdc5974d113c82bd5765e9", "file-storage-proto": "git+ssh://git@github.com:rbkmoney/file-storage-proto.git#281e1ca4cea9bf32229a6c389f0dcf5d49c05a0b", - "fistful-proto": "git+ssh://git@github.com/rbkmoney/fistful-proto.git#7b3f125aa7cbc069f740f598295bb56de713c39f", + "fistful-proto": "git+ssh://git@github.com/rbkmoney/fistful-proto.git#e340259cdd3add024f0139e21f0a2453312ef901", "humanize-duration": "~3.21.0", "jsonc-parser": "~2.0.2", "keycloak-angular": "^8.0.1", diff --git a/src/app/api/.gitkeep b/src/app/api/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/app/api/fistful/index.ts b/src/app/api/fistful/index.ts new file mode 100644 index 00000000..3c5958cf --- /dev/null +++ b/src/app/api/fistful/index.ts @@ -0,0 +1 @@ +export * from './wallet'; diff --git a/src/app/api/fistful/wallet/index.ts b/src/app/api/fistful/wallet/index.ts new file mode 100644 index 00000000..cc79cb7b --- /dev/null +++ b/src/app/api/fistful/wallet/index.ts @@ -0,0 +1,2 @@ +export * from './wallet.module'; +export * from './management.service'; diff --git a/src/app/api/fistful/wallet/management.service.ts b/src/app/api/fistful/wallet/management.service.ts new file mode 100644 index 00000000..cd677ea9 --- /dev/null +++ b/src/app/api/fistful/wallet/management.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { KeycloakTokenInfoService } from '@cc/app/shared/services'; + +import { ThriftConnector } from '../../thrift-connector'; +import { WalletState, EventRange as EventRangeModel } from '../gen-model/wallet'; +import * as Management from './gen-nodejs/Management'; +import { EventRange } from './gen-nodejs/base_types'; + +@Injectable() +export class ManagementService extends ThriftConnector { + constructor(protected keycloakTokenInfoService: KeycloakTokenInfoService) { + super(keycloakTokenInfoService, Management, '/v1/wallet'); + } + + get(walletID: string, range: EventRangeModel = new EventRange()): Observable { + return this.callThriftServiceMethod('Get', walletID, range); + } +} diff --git a/src/app/api/fistful/wallet/wallet.module.ts b/src/app/api/fistful/wallet/wallet.module.ts new file mode 100644 index 00000000..91cc35e3 --- /dev/null +++ b/src/app/api/fistful/wallet/wallet.module.ts @@ -0,0 +1,8 @@ +import { NgModule } from '@angular/core'; + +import { ManagementService } from './management.service'; + +@NgModule({ + providers: [ManagementService], +}) +export class WalletModule {} diff --git a/src/app/api/index.ts b/src/app/api/index.ts new file mode 100644 index 00000000..f36f59da --- /dev/null +++ b/src/app/api/index.ts @@ -0,0 +1 @@ +export * from './fistful'; diff --git a/src/app/api/thrift-connector/index.ts b/src/app/api/thrift-connector/index.ts new file mode 100644 index 00000000..741bbb5f --- /dev/null +++ b/src/app/api/thrift-connector/index.ts @@ -0,0 +1 @@ +export * from './thrift-connector'; diff --git a/src/app/api/thrift-connector/thrift-connector.ts b/src/app/api/thrift-connector/thrift-connector.ts new file mode 100644 index 00000000..2bb16c6d --- /dev/null +++ b/src/app/api/thrift-connector/thrift-connector.ts @@ -0,0 +1,44 @@ +import { Observable } from 'rxjs'; +import { switchMap, shareReplay, map, first } from 'rxjs/operators'; + +import { KeycloakTokenInfoService } from '@cc/app/shared/services'; + +import { + connectToThriftService, + prepareThriftServiceMethod, + toConnectOptions, + ThriftService, + ThriftServiceConnection, +} from './utils'; + +export class ThriftConnector { + private connection$: Observable; + + constructor( + protected keycloakTokenInfoService: KeycloakTokenInfoService, + protected service: ThriftService, + protected endpoint: string + ) { + this.connection$ = this.keycloakTokenInfoService.decoded$.pipe( + map((token) => toConnectOptions(token)), + switchMap((connectOptions) => + connectToThriftService(endpoint, service, connectOptions) + ), + shareReplay({ + bufferSize: 1, + refCount: true, + }) + ); + } + + protected callThriftServiceMethod( + serviceMethodName: string, + ...args: P + ): Observable { + return this.connection$.pipe( + first(), + map((connection) => prepareThriftServiceMethod(connection, serviceMethodName)), + switchMap((fn) => fn(...args)) + ); + } +} diff --git a/src/app/api/thrift-connector/utils/connect-to-thrift-service.ts b/src/app/api/thrift-connector/utils/connect-to-thrift-service.ts new file mode 100644 index 00000000..044b7f99 --- /dev/null +++ b/src/app/api/thrift-connector/utils/connect-to-thrift-service.ts @@ -0,0 +1,28 @@ +import { Observable } from 'rxjs'; +import connectClient from 'woody_js'; +import { ConnectOptions } from 'woody_js/src/connect-options'; + +import { ThriftService, ThriftServiceConnection } from './types'; + +export const connectToThriftService = ( + endpoint: string, + service: ThriftService, + connectionOptions: ConnectOptions, + hostname: string = location.hostname, + port: string = location.port +): Observable => + new Observable((observer) => { + const connection = connectClient( + hostname, + port, + endpoint, + service, + connectionOptions, + (err) => { + observer.error(err); + observer.complete(); + } + ); + observer.next(connection); + observer.complete(); + }); diff --git a/src/app/api/thrift-connector/utils/index.ts b/src/app/api/thrift-connector/utils/index.ts new file mode 100644 index 00000000..588612c5 --- /dev/null +++ b/src/app/api/thrift-connector/utils/index.ts @@ -0,0 +1,4 @@ +export * from './connect-to-thrift-service'; +export * from './prepare-thrift-service-method'; +export * from './to-connect-options'; +export * from './types'; diff --git a/src/app/api/thrift-connector/utils/prepare-thrift-service-method.ts b/src/app/api/thrift-connector/utils/prepare-thrift-service-method.ts new file mode 100644 index 00000000..17647d03 --- /dev/null +++ b/src/app/api/thrift-connector/utils/prepare-thrift-service-method.ts @@ -0,0 +1,20 @@ +import { Observable } from 'rxjs'; +import isNil from 'lodash-es/isNil'; + +import { ThriftServiceConnection, ThriftServiceMethod } from './types'; + +export const prepareThriftServiceMethod = ( + connection: ThriftServiceConnection, + serviceMethodName: string +): ThriftServiceMethod => (...args): Observable => + new Observable((observer) => { + const serviceMethod = connection[serviceMethodName]; + if (isNil(serviceMethod)) { + observer.error(`Service method: "${serviceMethodName}" is not found in thrift client`); + observer.complete(); + } + serviceMethod.bind(connection)(...args, (err, result) => { + err ? observer.error(err) : observer.next(result); + observer.complete(); + }); + }); diff --git a/src/app/api/thrift-connector/utils/to-connect-options.ts b/src/app/api/thrift-connector/utils/to-connect-options.ts new file mode 100644 index 00000000..538eafd2 --- /dev/null +++ b/src/app/api/thrift-connector/utils/to-connect-options.ts @@ -0,0 +1,32 @@ +import { ConnectOptions } from 'woody_js/src/connect-options'; + +import { KeycloakToken } from '@cc/app/shared/services'; + +const toDepricatedHeaders = (email: string, username: string, partyID: string, realm: string) => ({ + 'x-rbk-meta-user-identity.email': email, + 'x-rbk-meta-user-identity.realm': realm, + 'x-rbk-meta-user-identity.username': username, + 'x-rbk-meta-user-identity.id': partyID, +}); + +const toHeaders = (email: string, username: string, partyID: string, realm: string) => ({ + 'woody.meta.user-identity.email': email, + 'woody.meta.user-identity.realm': realm, + 'woody.meta.user-identity.username': username, + 'woody.meta.user-identity.id': partyID, +}); + +export const toConnectOptions = ( + { email, name, sub }: KeycloakToken, + deprecatedHeaders = false, + realm = 'internal' +): ConnectOptions => ({ + headers: { + ...toHeaders(email, name, sub, realm), + ...(deprecatedHeaders ? toDepricatedHeaders(email, name, sub, realm) : undefined), + }, + deadlineConfig: { + amount: 3, + unitOfTime: 'm', + }, +}); diff --git a/src/app/api/thrift-connector/utils/types.ts b/src/app/api/thrift-connector/utils/types.ts new file mode 100644 index 00000000..89a6961b --- /dev/null +++ b/src/app/api/thrift-connector/utils/types.ts @@ -0,0 +1,5 @@ +import { Observable } from 'rxjs'; + +export type ThriftService = any; +export type ThriftServiceConnection = any; +export type ThriftServiceMethod = (...args) => Observable; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6be91f7f..f94b82c5 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -16,6 +16,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import * as moment from 'moment'; import 'moment/locale/ru'; +import { KeycloakTokenInfoModule } from '@cc/app/shared/services'; + import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { ClaimMgtModule } from './claim-mgt/claim-mgt.module'; @@ -78,6 +80,7 @@ moment.locale('en'); SearchClaimsModule, OperationsModule, DomainConfigModule, + KeycloakTokenInfoModule, // It is important that NotFoundModule module should be last NotFoundModule, ], diff --git a/src/app/shared/components/wallet-info/services/receive-wallet/receive-wallet.service.ts b/src/app/shared/components/wallet-info/services/receive-wallet/receive-wallet.service.ts index f84323ed..049fcce2 100644 --- a/src/app/shared/components/wallet-info/services/receive-wallet/receive-wallet.service.ts +++ b/src/app/shared/components/wallet-info/services/receive-wallet/receive-wallet.service.ts @@ -1,30 +1,35 @@ import { Injectable } from '@angular/core'; -import { merge, NEVER, ReplaySubject } from 'rxjs'; -import { catchError, switchMap, shareReplay } from 'rxjs/operators'; -import { progress } from '@rbkmoney/partial-fetcher/dist/progress'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { BehaviorSubject, NEVER, ReplaySubject, Subject } from 'rxjs'; +import { catchError, switchMap, shareReplay, tap } from 'rxjs/operators'; -import { WalletManagementService } from '../../../../../thrift-services/fistful/wallet-management.service'; +import { ManagementService as WalletManagementService } from '@cc/app/api/fistful'; +@UntilDestroy() @Injectable() export class ReceiveWalletService { private receiveWallet$ = new ReplaySubject(); - private error$ = new ReplaySubject(); + private error$ = new Subject(); + private loading$ = new BehaviorSubject(false); wallet$ = this.receiveWallet$.pipe( + tap(() => this.loading$.next(true)), switchMap((id) => - this.walletManagementService.getWallet(id).pipe( + this.walletManagementService.get(id).pipe( catchError((e) => { - console.log(e); + console.error(e); + this.loading$.next(false); this.error$.next(true); return NEVER; }) ) ), + tap(() => this.loading$.next(false)), + untilDestroyed(this), shareReplay(1) ); - isLoading$ = progress(this.receiveWallet$, merge(this.wallet$, this.error$)); - + isLoading$ = this.loading$.asObservable(); hasError$ = this.error$.asObservable(); constructor(private walletManagementService: WalletManagementService) {} diff --git a/src/app/shared/components/wallet-info/types/receive-wallet-params.ts b/src/app/shared/components/wallet-info/types/receive-wallet-params.ts deleted file mode 100644 index db7b460b..00000000 --- a/src/app/shared/components/wallet-info/types/receive-wallet-params.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ReceiveWalletParams { - destinationID: string; - identityID: string; -} diff --git a/src/app/shared/components/wallet-info/wallet-info.component.html b/src/app/shared/components/wallet-info/wallet-info.component.html index ec628b43..c04f20dd 100644 --- a/src/app/shared/components/wallet-info/wallet-info.component.html +++ b/src/app/shared/components/wallet-info/wallet-info.component.html @@ -1,7 +1,7 @@ - {{ walletID }} - {{ wallet.name }} + {{ wallet.id }} - {{ wallet.name }} Loading... - An error occurred while destination receiving + An error occurred while wallet receiving diff --git a/src/app/shared/components/wallet-info/wallet-info.module.ts b/src/app/shared/components/wallet-info/wallet-info.module.ts index 1e00b45f..8454efa8 100644 --- a/src/app/shared/components/wallet-info/wallet-info.module.ts +++ b/src/app/shared/components/wallet-info/wallet-info.module.ts @@ -1,7 +1,8 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { FistfulModule } from '../../../thrift-services/fistful/fistful.module'; +import { WalletModule } from '@cc/app/api/fistful/wallet'; + import { WalletInfoComponent } from './wallet-info.component'; const DECLARATIONS = [WalletInfoComponent]; @@ -9,6 +10,6 @@ const DECLARATIONS = [WalletInfoComponent]; @NgModule({ declarations: DECLARATIONS, exports: DECLARATIONS, - imports: [CommonModule, FistfulModule], + imports: [CommonModule, WalletModule], }) export class WalletInfoModule {} diff --git a/src/app/shared/services/index.ts b/src/app/shared/services/index.ts index 361f6aa2..00644ec9 100644 --- a/src/app/shared/services/index.ts +++ b/src/app/shared/services/index.ts @@ -1,3 +1,4 @@ export * from './query-params-store'; export * from './app-auth-guard'; export * from './fetch-parties.service'; +export * from './keycloak-token-info'; diff --git a/src/app/shared/services/keycloak-token-info/index.ts b/src/app/shared/services/keycloak-token-info/index.ts new file mode 100644 index 00000000..0e1036b5 --- /dev/null +++ b/src/app/shared/services/keycloak-token-info/index.ts @@ -0,0 +1,3 @@ +export * from './keycloak-token-info.module'; +export * from './keycloak-token-info.service'; +export * from './types'; diff --git a/src/app/shared/services/keycloak-token-info/keycloak-token-info.module.ts b/src/app/shared/services/keycloak-token-info/keycloak-token-info.module.ts new file mode 100644 index 00000000..a6b96d5b --- /dev/null +++ b/src/app/shared/services/keycloak-token-info/keycloak-token-info.module.ts @@ -0,0 +1,8 @@ +import { NgModule } from '@angular/core'; + +import { KeycloakTokenInfoService } from './keycloak-token-info.service'; + +@NgModule({ + providers: [KeycloakTokenInfoService], +}) +export class KeycloakTokenInfoModule {} diff --git a/src/app/shared/services/keycloak-token-info/keycloak-token-info.service.ts b/src/app/shared/services/keycloak-token-info/keycloak-token-info.service.ts new file mode 100644 index 00000000..60c92e30 --- /dev/null +++ b/src/app/shared/services/keycloak-token-info/keycloak-token-info.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import jwt_decode from 'jwt-decode'; +import { KeycloakService } from 'keycloak-angular'; +import { from, Observable } from 'rxjs'; +import { map, shareReplay } from 'rxjs/operators'; + +import { KeycloakToken } from './types/keycloak-token'; + +@UntilDestroy() +@Injectable() +export class KeycloakTokenInfoService { + decoded$: Observable = from(this.keycloakService.getToken()).pipe( + map((token) => jwt_decode(token)), + untilDestroyed(this), + shareReplay(1) + ); + + constructor(private keycloakService: KeycloakService) {} +} diff --git a/src/app/shared/services/keycloak-token-info/types/index.ts b/src/app/shared/services/keycloak-token-info/types/index.ts new file mode 100644 index 00000000..41670c91 --- /dev/null +++ b/src/app/shared/services/keycloak-token-info/types/index.ts @@ -0,0 +1 @@ +export * from './keycloak-token'; diff --git a/src/app/shared/services/keycloak-token-info/types/keycloak-token.ts b/src/app/shared/services/keycloak-token-info/types/keycloak-token.ts new file mode 100644 index 00000000..1b7d26f8 --- /dev/null +++ b/src/app/shared/services/keycloak-token-info/types/keycloak-token.ts @@ -0,0 +1,24 @@ +export interface KeycloakToken { + acr: string; + 'allowed-origins': string[]; + aud: string; + auth_time: number; + azp: string; + email: string; + exp: number; + family_name: string; + given_name: string; + iat: number; + iss: string; + jti: string; + name: string; + nbf: number; + nonce: string; + preferred_username: string; + realm_access: object; + resource_access: object; + scope: string; + session_state: string; + sub: string; + typ: string; +} diff --git a/src/app/thrift-services/fistful/fistful.module.ts b/src/app/thrift-services/fistful/fistful.module.ts index 1c10f30e..e5a9124b 100644 --- a/src/app/thrift-services/fistful/fistful.module.ts +++ b/src/app/thrift-services/fistful/fistful.module.ts @@ -2,15 +2,9 @@ import { NgModule } from '@angular/core'; import { FistfulAdminService } from './fistful-admin.service'; import { RepairerService } from './repairer.service'; -import { WalletManagementService } from './wallet-management.service'; import { RevertManagementService } from './revert-management.service'; @NgModule({ - providers: [ - RepairerService, - FistfulAdminService, - WalletManagementService, - RevertManagementService, - ], + providers: [RepairerService, FistfulAdminService, RevertManagementService], }) export class FistfulModule {} diff --git a/src/app/thrift-services/fistful/wallet-management.service.ts b/src/app/thrift-services/fistful/wallet-management.service.ts deleted file mode 100644 index 4bdbace3..00000000 --- a/src/app/thrift-services/fistful/wallet-management.service.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Injectable, NgZone } from '@angular/core'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; - -import { KeycloakTokenInfoService } from '../../keycloak-token-info.service'; -import { ThriftService } from '../services/thrift/thrift-service'; -import * as WalletManagement from './gen-nodejs/Management'; -import { EventRange, WalletState } from './gen-model/wallet'; -import { EventRange as ApiEventRange } from './gen-nodejs/base_types'; - -@Injectable() -export class WalletManagementService extends ThriftService { - constructor(keycloakTokenInfoService: KeycloakTokenInfoService, zone: NgZone) { - super(zone, keycloakTokenInfoService, '/v1/wallet', WalletManagement); - } - - // @TODO thrift have many Get methods inside different Management services, that's why method returns DepositState with WalletState values - getWallet(id: string, range: EventRange = new ApiEventRange()): Observable { - return this.toObservableAction('Get')(id, range).pipe( - map( - (depositState) => - ({ id: depositState.source_id, name: depositState.id } as WalletState) - ) - ); - } -} diff --git a/tools/compile-thrift.ts b/tools/compile-thrift.ts index bc32ad48..5d57fa52 100644 --- a/tools/compile-thrift.ts +++ b/tools/compile-thrift.ts @@ -116,8 +116,12 @@ function toPathConfig( }; } -async function clear({ model, meta, services }: PathsConfig) { - await del([model.outputFolder, ...services.map((s) => s.outputFolder), meta.outputFile]); +async function clear({ model, meta, services }: PathsConfig, outputServiceDirName = 'gen-nodejs') { + await del([ + model.outputFolder, + ...services.map((s) => path.join(s.outputFolder, outputServiceDirName)), + meta.outputFile, + ]); } function prepareOutputDirs({ services, outputNamespacePath }: PathsConfig) { diff --git a/tsconfig.json b/tsconfig.json index b570761c..8ccfd5e5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,8 @@ "paths": { "@cc/components/*": ["src/components/*"], "@cc/utils/*": ["src/utils/*"], - "@cc/app/shared/*": ["src/app/shared/*"] + "@cc/app/shared/*": ["src/app/shared/*"], + "@cc/app/api/*": ["src/app/api/*"] } }, "angularCompilerOptions": {