diff --git a/.idea/prettier.xml b/.idea/prettier.xml index f6adb1d5..e84eb411 100644 --- a/.idea/prettier.xml +++ b/.idea/prettier.xml @@ -1,6 +1,7 @@ + diff --git a/.vscode/settings.json b/.vscode/settings.json index 4e8f14fc..2c8981b9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,4 @@ { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll.tslint": true - }, - "javascript.preferences.importModuleSpecifier": "relative", - "typescript.preferences.importModuleSpecifier": "relative", "typescript.tsdk": "node_modules/typescript/lib", "cSpell.words": [ "CAPI", @@ -46,6 +40,5 @@ "инвойса" ], "cSpell.language": "en,ru", - "prettier.prettierPath": "node_modules/prettier", - "tasksStatusbar.taskLabelFilter": "Start", + "prettier.prettierPath": "node_modules/prettier" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 98a5acf3..00000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "start", - "problemMatcher": [], - "label": "Start dev" - }, - { - "type": "npm", - "script": "stage", - "problemMatcher": [], - "label": "Start stage" - } - ] -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1c718e2a..1b1a070e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,9 @@ "@sentry/angular": "7.7.0", "@sentry/integrations": "7.7.0", "@sentry/tracing": "7.7.0", + "@vality/ng-core": "^0.3.0", "@vality/swag-anapi-v2": "2.0.1-38f360b.0", + "@vality/swag-api-keys": "^1.0.0", "@vality/swag-claim-management": "0.1.1-bfc2e6c.0", "@vality/swag-organizations": "1.0.1-cd6cc10.0", "@vality/swag-payments": "0.1.1-01da4bb.0", @@ -6048,6 +6050,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@vality/ng-core": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-0.3.0.tgz", + "integrity": "sha512-uezgCzRGWTfe6PDnAqbFQ0nqCv+8auIGTd1tej8hV+h0fTOahAJybTrpYpzvegYdVJ1IwVAUqMAiLKo5RCtKhQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": ">=15.0.0", + "@angular/common": ">=15.0.0", + "@angular/core": ">=15.0.0", + "@angular/material": ">=15.0.0", + "coerce-property": ">=0.3.2", + "lodash-es": "^4.0.0", + "utility-types": ">=3.0.0" + } + }, "node_modules/@vality/swag-anapi-v2": { "version": "2.0.1-38f360b.0", "resolved": "https://registry.npmjs.org/@vality/swag-anapi-v2/-/swag-anapi-v2-2.0.1-38f360b.0.tgz", @@ -6060,6 +6079,18 @@ "@angular/core": ">=14.0.0" } }, + "node_modules/@vality/swag-api-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@vality/swag-api-keys/-/swag-api-keys-1.0.0.tgz", + "integrity": "sha512-UjPzFVLy3dccgW14MnNlVUfRDIIiZGlYhtgCxBOFoPIyf954BsVeUOm/f4gpZXwzDhU6ge60HAUzTCxlu5/Sew==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=14.0.0", + "@angular/core": ">=14.0.0" + } + }, "node_modules/@vality/swag-claim-management": { "version": "0.1.1-bfc2e6c.0", "license": "Apache-2.0", @@ -22478,6 +22509,14 @@ } } }, + "@vality/ng-core": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-0.3.0.tgz", + "integrity": "sha512-uezgCzRGWTfe6PDnAqbFQ0nqCv+8auIGTd1tej8hV+h0fTOahAJybTrpYpzvegYdVJ1IwVAUqMAiLKo5RCtKhQ==", + "requires": { + "tslib": "^2.3.0" + } + }, "@vality/swag-anapi-v2": { "version": "2.0.1-38f360b.0", "resolved": "https://registry.npmjs.org/@vality/swag-anapi-v2/-/swag-anapi-v2-2.0.1-38f360b.0.tgz", @@ -22486,6 +22525,14 @@ "tslib": "^2.3.0" } }, + "@vality/swag-api-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@vality/swag-api-keys/-/swag-api-keys-1.0.0.tgz", + "integrity": "sha512-UjPzFVLy3dccgW14MnNlVUfRDIIiZGlYhtgCxBOFoPIyf954BsVeUOm/f4gpZXwzDhU6ge60HAUzTCxlu5/Sew==", + "requires": { + "tslib": "^2.3.0" + } + }, "@vality/swag-claim-management": { "version": "0.1.1-bfc2e6c.0", "requires": { diff --git a/package.json b/package.json index 1066371a..7a4dfbd9 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,9 @@ "@sentry/angular": "7.7.0", "@sentry/integrations": "7.7.0", "@sentry/tracing": "7.7.0", + "@vality/ng-core": "^0.3.0", "@vality/swag-anapi-v2": "2.0.1-38f360b.0", + "@vality/swag-api-keys": "^1.0.0", "@vality/swag-claim-management": "0.1.1-bfc2e6c.0", "@vality/swag-organizations": "1.0.1-cd6cc10.0", "@vality/swag-payments": "0.1.1-01da4bb.0", diff --git a/src/app/api/api-keys/api-keys.module.ts b/src/app/api/api-keys/api-keys.module.ts new file mode 100644 index 00000000..5a2b52d4 --- /dev/null +++ b/src/app/api/api-keys/api-keys.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { Configuration } from '@vality/swag-api-keys'; + +import { ConfigService } from '../../config'; + +@NgModule({ + providers: [ + { + provide: Configuration, + deps: [ConfigService], + useFactory: (configService: ConfigService) => + new Configuration({ basePath: `${configService.apiEndpoint}/apikeys/v1` }), + }, + ], +}) +export class ApiKeysModule {} diff --git a/src/app/api/api-keys/api-keys.service.ts b/src/app/api/api-keys/api-keys.service.ts new file mode 100644 index 00000000..a8ea2218 --- /dev/null +++ b/src/app/api/api-keys/api-keys.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@angular/core'; +import { ApiKeysService as ApiService } from '@vality/swag-api-keys'; + +import { createApi } from '../utils'; +import { PartyIdExtension } from '../utils/extensions'; + +@Injectable({ + providedIn: 'root', +}) +export class ApiKeysService extends createApi(ApiService, [PartyIdExtension]) {} diff --git a/src/app/api/api-keys/index.ts b/src/app/api/api-keys/index.ts new file mode 100644 index 00000000..f91b9a01 --- /dev/null +++ b/src/app/api/api-keys/index.ts @@ -0,0 +1,2 @@ +export * from './api-keys.service'; +export * from './api-keys.module'; diff --git a/src/app/api/utils/extensions/party-id-extension.ts b/src/app/api/utils/extensions/party-id-extension.ts index f17a29cc..a2225e72 100644 --- a/src/app/api/utils/extensions/party-id-extension.ts +++ b/src/app/api/utils/extensions/party-id-extension.ts @@ -14,7 +14,7 @@ export class PartyIdExtension implements ApiExtension { selector() { return this.contextOrganizationService.organization$.pipe( first(), - map(({ party }) => ({ partyID: party })) + map(({ party }) => ({ partyID: party, partyId: party })) ); } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 03119996..8e583ec2 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -27,6 +27,7 @@ import { QUERY_PARAMS_SERIALIZERS } from '@dsh/app/shared/services/query-params/ import { createDateRangeWithPresetSerializer } from '@dsh/components/date-range-filter'; import { SpinnerModule } from '@dsh/components/indicators'; +import { ApiKeysModule } from './api/api-keys'; import { OrganizationsModule } from './api/organizations'; import { AppComponent } from './app.component'; import { AuthModule, KeycloakAngularModule, KeycloakService } from './auth'; @@ -66,6 +67,7 @@ import { ENV, environment } from '../environments'; QuestionaryAggrProxyModule, WalletModule, SpinnerModule, + ApiKeysModule, ], providers: [ LanguageService, diff --git a/src/app/sections/payment-section/integrations/api-keys/api-keys-expanded-id-manager.service.ts b/src/app/sections/payment-section/integrations/api-keys/api-keys-expanded-id-manager.service.ts new file mode 100644 index 00000000..702d8ba6 --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/api-keys-expanded-id-manager.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ApiKey } from '@vality/swag-api-keys'; +import { Observable } from 'rxjs'; + +import { ExpandedIdManager } from '@dsh/app/shared/services'; + +import { FetchApiKeysService } from './fetch-api-keys.service'; + +@Injectable() +export class ApiKeysExpandedIdManager extends ExpandedIdManager { + constructor( + protected route: ActivatedRoute, + protected router: Router, + private fetchApiKeysService: FetchApiKeysService + ) { + super(route, router); + } + + protected get dataSet$(): Observable { + return this.fetchApiKeysService.apiKeys$; + } +} diff --git a/src/app/sections/payment-section/integrations/api-keys/api-keys-list/api-keys-list.component.html b/src/app/sections/payment-section/integrations/api-keys/api-keys-list/api-keys-list.component.html new file mode 100644 index 00000000..76d13102 --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/api-keys-list/api-keys-list.component.html @@ -0,0 +1,63 @@ +
+ + + {{ t('name') | uppercase }} + {{ t('createdAt') | uppercase }} + {{ t('status') | uppercase }} + + + + + + {{ apiKey.name }} + + {{ + apiKey.createdAt | date: 'dd MMMM yyyy, HH:mm' + }} + {{ apiKey.status }} + + + + +
+
{{ t('details') }} #{{ apiKey.id }}
+
{{ apiKey.createdAt | date: 'dd MMMM yyyy, HH:mm' }}
+
+
+
+
+ +
+ {{ apiKey.name }} +
+
+ +
{{ apiKey.status }}
+
+ +
{{ apiKey.createdAt | date: 'dd MMMM yyyy, HH:mm' }}
+
+
+ +
+
+ +
+
+
+
+
+
+
+
diff --git a/src/app/sections/payment-section/integrations/api-keys/api-keys-list/api-keys-list.component.ts b/src/app/sections/payment-section/integrations/api-keys/api-keys-list/api-keys-list.component.ts new file mode 100644 index 00000000..afc834df --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/api-keys-list/api-keys-list.component.ts @@ -0,0 +1,25 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { UntilDestroy } from '@ngneat/until-destroy'; +import { DialogService } from '@vality/ng-core'; +import { ApiKey } from '@vality/swag-api-keys'; + +import { ApiKeyDeleteDialogComponent } from './components/api-key-delete-dialog/api-key-delete-dialog.component'; + +@UntilDestroy() +@Component({ + selector: 'dsh-api-keys-list', + templateUrl: 'api-keys-list.component.html', +}) +export class ApiKeysListComponent { + @Input() apiKeys: ApiKey[]; + @Input() expandedId: number; + @Input() lastUpdated: string; + @Output() expandedIdChange = new EventEmitter(); + @Output() refreshData = new EventEmitter(); + + constructor(private dialogService: DialogService) {} + + delete(apiKey: ApiKey) { + this.dialogService.open(ApiKeyDeleteDialogComponent, { apiKeyId: apiKey.id }); + } +} diff --git a/src/app/sections/payment-section/integrations/api-keys/api-keys-list/api-keys-list.module.ts b/src/app/sections/payment-section/integrations/api-keys/api-keys-list/api-keys-list.module.ts new file mode 100644 index 00000000..dcfd40c6 --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/api-keys-list/api-keys-list.module.ts @@ -0,0 +1,32 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { TranslocoModule } from '@ngneat/transloco'; +import { DialogModule } from '@vality/ng-core'; + +import { ButtonModule } from '@dsh/components/buttons'; +import { IndicatorsModule } from '@dsh/components/indicators'; +import { LayoutModule } from '@dsh/components/layout'; + +import { ApiKeysListComponent } from './api-keys-list.component'; +import { ApiKeyDeleteDialogComponent } from './components/api-key-delete-dialog/api-key-delete-dialog.component'; + +@NgModule({ + imports: [ + TranslocoModule, + MatSnackBarModule, + LayoutModule, + FlexLayoutModule, + CommonModule, + IndicatorsModule, + MatDividerModule, + ButtonModule, + ApiKeyDeleteDialogComponent, + DialogModule, + ], + declarations: [ApiKeysListComponent], + exports: [ApiKeysListComponent], +}) +export class ApiKeysListModule {} diff --git a/src/app/sections/payment-section/integrations/api-keys/api-keys-list/components/api-key-delete-dialog/api-key-delete-dialog.component.html b/src/app/sections/payment-section/integrations/api-keys/api-keys-list/components/api-key-delete-dialog/api-key-delete-dialog.component.html new file mode 100644 index 00000000..7d39ace2 --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/api-keys-list/components/api-key-delete-dialog/api-key-delete-dialog.component.html @@ -0,0 +1,14 @@ + +
+ {{ t('desc') }} +
+ + + +
diff --git a/src/app/sections/payment-section/integrations/api-keys/api-keys-list/components/api-key-delete-dialog/api-key-delete-dialog.component.ts b/src/app/sections/payment-section/integrations/api-keys/api-keys-list/components/api-key-delete-dialog/api-key-delete-dialog.component.ts new file mode 100644 index 00000000..5dce041e --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/api-keys-list/components/api-key-delete-dialog/api-key-delete-dialog.component.ts @@ -0,0 +1,49 @@ +import { Component, Injector } from '@angular/core'; +import { FlexModule } from '@angular/flex-layout'; +import { TranslocoModule } from '@ngneat/transloco'; +import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy'; +import { DialogSuperclass } from '@vality/ng-core'; +import { RequestRevokeApiKeyRequestParams } from '@vality/swag-api-keys'; + +import { ApiKeysService } from '@dsh/api/api-keys'; +import { BaseDialogModule } from '@dsh/app/shared/components/dialog/base-dialog'; +import { ErrorService, NotificationService } from '@dsh/app/shared/services'; +import { ButtonModule } from '@dsh/components/buttons'; +import { SpinnerModule } from '@dsh/components/indicators'; + +@UntilDestroy() +@Component({ + selector: 'dsh-api-key-delete-dialog', + standalone: true, + templateUrl: './api-key-delete-dialog.component.html', + styles: [], + imports: [BaseDialogModule, SpinnerModule, FlexModule, ButtonModule, TranslocoModule], +}) +export class ApiKeyDeleteDialogComponent extends DialogSuperclass< + ApiKeyDeleteDialogComponent, + Pick +> { + constructor( + injector: Injector, + private apiKeysService: ApiKeysService, + private errorService: ErrorService, + private notificationService: NotificationService + ) { + super(injector); + } + + confirm() { + this.apiKeysService + .requestRevokeApiKey(this.dialogData) + .pipe(untilDestroyed(this)) + .subscribe({ + next: () => { + this.notificationService.success(); + this.closeWithSuccess(); + }, + error: (err) => { + this.errorService.error(err); + }, + }); + } +} diff --git a/src/app/sections/payment-section/integrations/api-keys/api-keys-list/index.ts b/src/app/sections/payment-section/integrations/api-keys/api-keys-list/index.ts new file mode 100644 index 00000000..f13f4d1b --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/api-keys-list/index.ts @@ -0,0 +1,2 @@ +export * from './webhook-list.module'; +export * from './webhook-list.component'; diff --git a/src/app/sections/payment-section/integrations/api-keys/api-keys-routing.module.ts b/src/app/sections/payment-section/integrations/api-keys/api-keys-routing.module.ts new file mode 100644 index 00000000..d03bc962 --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/api-keys-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { ApiKeysComponent } from './api-keys.component'; +import { ApiKeyRevokeComponent } from './components/api-key-revoke/api-key-revoke.component'; + +const ROUTES: Routes = [ + { + path: '', + component: ApiKeysComponent, + }, + { + path: ':apiKeyId/revoke/:apiKeyRevokeToken', + component: ApiKeyRevokeComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(ROUTES)], +}) +export class ApiKeysRoutingModule {} diff --git a/src/app/sections/payment-section/integrations/api-keys/api-keys.component.html b/src/app/sections/payment-section/integrations/api-keys/api-keys.component.html new file mode 100644 index 00000000..19a84cac --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/api-keys.component.html @@ -0,0 +1,36 @@ +
+
+
+
+ {{ t('showInactive') }} +
+ +
+ +
+ + +
+ +
+
+ + + + +
diff --git a/src/app/sections/payment-section/integrations/api-keys/api-keys.component.scss b/src/app/sections/payment-section/integrations/api-keys/api-keys.component.scss new file mode 100644 index 00000000..658f7ee0 --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/api-keys.component.scss @@ -0,0 +1,3 @@ +:host { + width: 100%; +} diff --git a/src/app/sections/payment-section/integrations/api-keys/api-keys.component.ts b/src/app/sections/payment-section/integrations/api-keys/api-keys.component.ts new file mode 100644 index 00000000..4f87e1f3 --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/api-keys.component.ts @@ -0,0 +1,55 @@ +import { Component } from '@angular/core'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { DialogService } from '@vality/ng-core'; +import { ApiKeyStatus, ListApiKeysRequestParams } from '@vality/swag-api-keys'; + +import { ApiKeyCreateDialogComponent } from '@dsh/app/sections/payment-section/integrations/api-keys/components/api-key-create-dialog/api-key-create-dialog.component'; + +import { ApiKeysExpandedIdManager } from './api-keys-expanded-id-manager.service'; +import { FetchApiKeysService } from './fetch-api-keys.service'; +import { QueryParamsService } from '../../../../shared'; + +@UntilDestroy() +@Component({ + templateUrl: 'api-keys.component.html', + styleUrls: ['api-keys.component.scss'], + providers: [ApiKeysExpandedIdManager, FetchApiKeysService], +}) +export class ApiKeysComponent { + showInactive = this.qp.params.showInactive; + apiKeys$ = this.fetchApiKeysService.apiKeys$; + isLoading$ = this.fetchApiKeysService.isLoading$; + expandedId$ = this.apiKeysExpandedIdManager.expandedId$; + lastUpdated$ = this.fetchApiKeysService.lastUpdated$; + + constructor( + private qp: QueryParamsService<{ showInactive: boolean }>, + private apiKeysExpandedIdManager: ApiKeysExpandedIdManager, + private fetchApiKeysService: FetchApiKeysService, + private dialogService: DialogService + ) {} + + update(params: Omit = {}) { + this.fetchApiKeysService.update(Object.assign(params, !this.showInactive && { status: ApiKeyStatus.Active })); + } + + create() { + this.dialogService + .open(ApiKeyCreateDialogComponent) + .afterClosed() + .pipe(untilDestroyed(this)) + .subscribe(() => { + this.update(); + }); + } + + toggle() { + this.showInactive = !this.showInactive; + void this.qp.set({ showInactive: this.showInactive }); + this.update(); + } + + expandedIdChange(id: number) { + this.apiKeysExpandedIdManager.expandedIdChange(id); + } +} diff --git a/src/app/sections/payment-section/integrations/api-keys/api-keys.module.ts b/src/app/sections/payment-section/integrations/api-keys/api-keys.module.ts new file mode 100644 index 00000000..b567ddea --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/api-keys.module.ts @@ -0,0 +1,38 @@ +import { ClipboardModule } from '@angular/cdk/clipboard'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FlexModule } from '@angular/flex-layout'; +import { MatInputModule } from '@angular/material/input'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { TranslocoModule } from '@ngneat/transloco'; + +import { ButtonModule } from '@dsh/components/buttons'; +import { EmptySearchResultModule } from '@dsh/components/empty-search-result'; +import { SpinnerModule } from '@dsh/components/indicators'; +import { CardModule } from '@dsh/components/layout'; + +import { ApiKeysListModule } from './api-keys-list/api-keys-list.module'; +import { ApiKeysRoutingModule } from './api-keys-routing.module'; +import { ApiKeysComponent } from './api-keys.component'; +import { ApiKeyCreateDialogComponent } from './components/api-key-create-dialog/api-key-create-dialog.component'; +import { ApiKeyRevokeComponent } from './components/api-key-revoke/api-key-revoke.component'; + +@NgModule({ + declarations: [ApiKeysComponent, ApiKeyRevokeComponent], + imports: [ + ApiKeysRoutingModule, + FlexModule, + TranslocoModule, + CardModule, + MatInputModule, + CommonModule, + ButtonModule, + ClipboardModule, + EmptySearchResultModule, + SpinnerModule, + MatSlideToggleModule, + ApiKeysListModule, + ApiKeyCreateDialogComponent, + ], +}) +export class ApiKeysModule {} diff --git a/src/app/sections/payment-section/integrations/api-keys/components/api-key-create-dialog/api-key-create-dialog.component.html b/src/app/sections/payment-section/integrations/api-keys/components/api-key-create-dialog/api-key-create-dialog.component.html new file mode 100644 index 00000000..aef7b5b7 --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/components/api-key-create-dialog/api-key-create-dialog.component.html @@ -0,0 +1,33 @@ + +
+ + {{ t('apiKey') }} + + +
+ +
+ + {{ t('keyName') }} + + +
+ {{ t('desc') }} +
+
+
+ + + + + + +
diff --git a/src/app/sections/payment-section/integrations/api-keys/components/api-key-create-dialog/api-key-create-dialog.component.ts b/src/app/sections/payment-section/integrations/api-keys/components/api-key-create-dialog/api-key-create-dialog.component.ts new file mode 100644 index 00000000..14179d62 --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/components/api-key-create-dialog/api-key-create-dialog.component.ts @@ -0,0 +1,76 @@ +import { ClipboardModule, Clipboard } from '@angular/cdk/clipboard'; +import { CommonModule } from '@angular/common'; +import { Component, Injector } from '@angular/core'; +import { FlexModule } from '@angular/flex-layout'; +import { ReactiveFormsModule, NonNullableFormBuilder } from '@angular/forms'; +import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input'; +import { TranslocoModule, TranslocoService } from '@ngneat/transloco'; +import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy'; +import { DialogSuperclass } from '@vality/ng-core'; + +import { ApiKeysService } from '@dsh/api/api-keys'; +import { BaseDialogModule } from '@dsh/app/shared/components/dialog/base-dialog'; +import { ErrorService, NotificationService } from '@dsh/app/shared/services'; +import { ButtonModule } from '@dsh/components/buttons'; +import { SpinnerModule } from '@dsh/components/indicators'; + +@UntilDestroy() +@Component({ + selector: 'dsh-api-key-create-dialog', + standalone: true, + templateUrl: './api-key-create-dialog.component.html', + styles: [], + imports: [ + BaseDialogModule, + SpinnerModule, + FlexModule, + ButtonModule, + TranslocoModule, + MatInputModule, + ReactiveFormsModule, + CommonModule, + ClipboardModule, + ], +}) +export class ApiKeyCreateDialogComponent extends DialogSuperclass { + form = this.fb.group({ name: '' }); + apiKey: string; + + constructor( + injector: Injector, + private apiKeysService: ApiKeysService, + private errorService: ErrorService, + private notificationService: NotificationService, + private fb: NonNullableFormBuilder, + private clipboard: Clipboard, + private transloco: TranslocoService + ) { + super(injector); + } + + confirm() { + this.apiKeysService + .issueApiKey() + .pipe(untilDestroyed(this)) + .subscribe({ + next: (res) => { + this.apiKey = res.accessToken; + }, + error: (err) => { + this.errorService.error(err); + }, + }); + } + + copy() { + if (this.clipboard.copy(this.apiKey)) { + this.notificationService.success( + this.transloco.selectTranslate('apiKeys.copy.success', null, 'payment-section') + ); + } else { + this.notificationService.error( + this.transloco.selectTranslate('apiKeys.copy.error', null, 'payment-section') + ); + } + } +} diff --git a/src/app/sections/payment-section/integrations/api-keys/components/api-key-revoke/api-key-revoke.component.html b/src/app/sections/payment-section/integrations/api-keys/components/api-key-revoke/api-key-revoke.component.html new file mode 100644 index 00000000..95d49e2f --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/components/api-key-revoke/api-key-revoke.component.html @@ -0,0 +1 @@ +
diff --git a/src/app/sections/payment-section/integrations/api-keys/components/api-key-revoke/api-key-revoke.component.ts b/src/app/sections/payment-section/integrations/api-keys/components/api-key-revoke/api-key-revoke.component.ts new file mode 100644 index 00000000..23fcafe5 --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/components/api-key-revoke/api-key-revoke.component.ts @@ -0,0 +1,42 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslocoService } from '@ngneat/transloco'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; + +import { ApiKeysService } from '@dsh/api/api-keys'; +import { ErrorService, NotificationService } from '@dsh/app/shared/services'; + +@UntilDestroy() +@Component({ + selector: 'dsh-api-key-revoke', + templateUrl: './api-key-revoke.component.html', +}) +export class ApiKeyRevokeComponent implements OnInit { + constructor( + private route: ActivatedRoute, + private apiKeysService: ApiKeysService, + private router: Router, + private notificationService: NotificationService, + private errorService: ErrorService, + private translocoService: TranslocoService + ) {} + + ngOnInit(): void { + const { apiKeyRevokeToken, apiKeyId } = this.route.snapshot.params as Record; + this.apiKeysService + .revokeApiKey({ apiKeyRevokeToken, apiKeyId }) + .pipe(untilDestroyed(this)) + .subscribe({ + next: () => { + this.notificationService.success( + this.translocoService.selectTranslate('apiKeys.revoke.success', null, 'payment-section') + ); + void this.router.navigate(['../../..'], { relativeTo: this.route }); + }, + error: (err) => { + this.errorService.error(err); + void this.router.navigate(['../../..'], { relativeTo: this.route }); + }, + }); + } +} diff --git a/src/app/sections/payment-section/integrations/api-keys/fetch-api-keys.service.ts b/src/app/sections/payment-section/integrations/api-keys/fetch-api-keys.service.ts new file mode 100644 index 00000000..36733621 --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/fetch-api-keys.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; +import { ListApiKeysRequestParams } from '@vality/swag-api-keys'; +import { BehaviorSubject, Observable, defer, of } from 'rxjs'; +import { catchError, map, switchMap } from 'rxjs/operators'; + +import { ApiKeysService } from '@dsh/api/api-keys'; +import { ErrorService } from '@dsh/app/shared/services'; +import { mapToTimestamp, shareReplayRefCount } from '@dsh/operators'; +import { inProgressFrom, progressTo } from '@dsh/utils'; + +@Injectable() +export class FetchApiKeysService { + apiKeys$ = defer(() => this.fetchApiKeys$).pipe( + switchMap((p) => + this.apiKeysService.listApiKeys(p).pipe( + map((r) => r.results), + progressTo(() => this.progress$), + catchError((err) => { + this.errorService.error(err); + return of([]); + }) + ) + ), + shareReplayRefCount() + ); + isLoading$ = inProgressFrom(() => this.progress$, this.apiKeys$); + lastUpdated$: Observable = this.apiKeys$.pipe(mapToTimestamp, shareReplayRefCount()); + + private progress$ = new BehaviorSubject(0); + private fetchApiKeys$ = new BehaviorSubject>({}); + + constructor(private apiKeysService: ApiKeysService, private errorService: ErrorService) {} + + update(params: Omit = {}) { + this.fetchApiKeys$.next(params); + } +} diff --git a/src/app/sections/payment-section/integrations/api-keys/index.ts b/src/app/sections/payment-section/integrations/api-keys/index.ts new file mode 100644 index 00000000..60afa632 --- /dev/null +++ b/src/app/sections/payment-section/integrations/api-keys/index.ts @@ -0,0 +1 @@ +export * from './api-keys.module'; diff --git a/src/app/sections/payment-section/integrations/integrations-routing.module.ts b/src/app/sections/payment-section/integrations/integrations-routing.module.ts index c3932431..12a86731 100644 --- a/src/app/sections/payment-section/integrations/integrations-routing.module.ts +++ b/src/app/sections/payment-section/integrations/integrations-routing.module.ts @@ -4,6 +4,7 @@ import { RouterModule, Routes } from '@angular/router'; import { createPrivateRoute, RoleAccessName } from '@dsh/app/auth'; import { IntegrationsComponent } from './integrations.component'; +import { environment } from '../../../../environments'; const ROUTES: Routes = [ { @@ -26,8 +27,10 @@ const ROUTES: Routes = [ ), createPrivateRoute( { - path: 'api-key', - loadChildren: () => import('./api-key').then((m) => m.ApiKeyModule), + path: 'api-keys', + loadChildren: environment.stage + ? () => import('./api-keys').then((m) => m.ApiKeysModule) + : () => import('./api-key').then((m) => m.ApiKeyModule), }, [RoleAccessName.ApiKeys] ), diff --git a/src/app/sections/payment-section/integrations/integrations.component.ts b/src/app/sections/payment-section/integrations/integrations.component.ts index c2421496..4cd4a6b4 100644 --- a/src/app/sections/payment-section/integrations/integrations.component.ts +++ b/src/app/sections/payment-section/integrations/integrations.component.ts @@ -1,6 +1,8 @@ import { Component } from '@angular/core'; import { TranslocoService } from '@ngneat/transloco'; +import { environment } from '../../../../environments'; + @Component({ templateUrl: 'integrations.component.html', }) @@ -11,8 +13,10 @@ export class IntegrationsComponent { label$: this.transloco.selectTranslate('integrations.tabs.payment-link', null, 'payment-section'), }, { - path: 'api-key', - label$: this.transloco.selectTranslate('integrations.tabs.api-key', null, 'payment-section'), + path: 'api-keys', + label$: environment.stage + ? this.transloco.selectTranslate('integrations.tabs.api-keys', null, 'payment-section') + : this.transloco.selectTranslate('integrations.tabs.api-key', null, 'payment-section'), }, { path: 'webhooks', diff --git a/src/app/shared/services/notification/notification.service.ts b/src/app/shared/services/notification/notification.service.ts index cf221355..c447b966 100644 --- a/src/app/shared/services/notification/notification.service.ts +++ b/src/app/shared/services/notification/notification.service.ts @@ -6,6 +6,7 @@ import { LegacySimpleSnackBar as SimpleSnackBar, } from '@angular/material/legacy-snack-bar'; import { TranslocoService } from '@ngneat/transloco'; +import { Observable, first, isObservable, timeout } from 'rxjs'; const DEFAULT_DURATION_MS = 3000; @@ -14,21 +15,32 @@ export class NotificationService { constructor(private snackBar: MatSnackBar, private transloco: TranslocoService) {} success( - message: string = this.transloco.translate('notification.success', null, 'services') + message: string | Observable = this.transloco.translate('notification.success', null, 'services') ): MatSnackBarRef { return this.openSnackBar(message); } error( - message: string = this.transloco.translate('notification.error', null, 'services') + message: string | Observable = this.transloco.translate('notification.error', null, 'services') ): MatSnackBarRef { return this.openSnackBar(message); } - private openSnackBar(message: string, config: MatSnackBarConfig = {}): MatSnackBarRef { - return this.snackBar.open(message, this.transloco.translate('notification.ok', null, 'services'), { + private openSnackBar( + message: string | Observable, + config: MatSnackBarConfig = {} + ): MatSnackBarRef { + const okMessage = this.transloco.translate('notification.ok', null, 'services'); + const resConfig = { duration: DEFAULT_DURATION_MS, ...config, - }); + }; + if (isObservable(message)) { + message.pipe(first(), timeout(5000)).subscribe((m) => { + this.snackBar.open(m, okMessage, resConfig); + }); + return; + } + return this.snackBar.open(message, okMessage, resConfig); } } diff --git a/src/assets/i18n/payment-section/en.json b/src/assets/i18n/payment-section/en.json index 59cb9fcf..12500221 100644 --- a/src/assets/i18n/payment-section/en.json +++ b/src/assets/i18n/payment-section/en.json @@ -18,6 +18,38 @@ "description": "Your private key to access the", "title": "API key" }, + "apiKeys": { + "copy": { + "error": "An error occurred while copying", + "success": "Successfully copied" + }, + "create": "Release key", + "createDialog": { + "apiKey": "Private API key", + "confirm": "Release key", + "copy": "Copy", + "desc": "Private API key will be available only once, at the moment of its issuance. Therefore, it is necessary to copy the key and save it in a secure location after receiving it.", + "keyName": "Key name", + "title": "Release a new key" + }, + "deleteDialog": { + "confirm": "Request key revocation", + "desc": "You will receive an email with a confirmation link. Please follow the link to confirm the operation. After revoking the key, it will no longer be valid, and if you need access to the API in the future, you will have to create a new one.", + "title": "Key revocation request" + }, + "emptyResult": "No keys released", + "list": { + "createdAt": "Created at", + "delete": "Revoke key", + "details": "Key data", + "name": "Name", + "status": "Status" + }, + "revoke": { + "success": "Key successfully revoked" + }, + "showInactive": "Show inactive" + }, "balances": { "title": "Available amount" }, @@ -76,6 +108,7 @@ "integrations": { "tabs": { "api-key": "API KEY", + "api-keys": "API KEYS", "payment-link": "PAYMENT LINK", "webhooks": "WEBHOOKS" } @@ -146,8 +179,8 @@ "shops": "Shops" }, "table": { - "id": "ID", "amount": "Amount", + "id": "ID", "shop": "Shop", "status": "Status", "statusChanged": "Last modified" diff --git a/src/assets/i18n/payment-section/ru.json b/src/assets/i18n/payment-section/ru.json index 57d5de06..67eee9d6 100644 --- a/src/assets/i18n/payment-section/ru.json +++ b/src/assets/i18n/payment-section/ru.json @@ -18,6 +18,38 @@ "description": "Ваш приватный ключ для доступа к", "title": "API ключ" }, + "apiKeys": { + "copy": { + "error": "Ошибка копирования", + "success": "Успешно скопировано" + }, + "create": "Выпустить ключ", + "createDialog": { + "apiKey": "Приватный API ключ", + "confirm": "Выпустить ключ", + "copy": "Скопировать", + "desc": "Приватный API ключ будет доступен только один раз, в момент его выпуска. Поэтому необходимо скопировать ключ и сохранить его в надежном месте после получения.", + "keyName": "Название ключа", + "title": "Выпустить новый ключ" + }, + "deleteDialog": { + "confirm": "Запросить отзыв ключа", + "desc": "На вашу электронную почту будет отправлено письмо со ссылкой. Пожалуйста, перейдите по этой ссылке, чтобы подтвердить операцию. После отзыва ключ больше не будет действительным, и если вам потребуется доступ к API в будущем, вам придется создать новый ключ.", + "title": "Запрос на отзыв ключа" + }, + "emptyResult": "Не выпущено ни одного ключа", + "list": { + "createdAt": "Создан", + "delete": "Удалить", + "details": "Данные ключа", + "name": "Название", + "status": "Статус" + }, + "revoke": { + "success": "Ключ успешно отозван" + }, + "showInactive": "Показывать неактивные" + }, "balances": { "title": "Доступные средства" }, @@ -76,6 +108,7 @@ "integrations": { "tabs": { "api-key": "API КЛЮЧ", + "api-keys": "API КЛЮЧИ", "payment-link": "ПЛАТЕЖНАЯ ССЫЛКА", "webhooks": "WEBHOOKS" } @@ -146,8 +179,8 @@ "shops": "Магазины" }, "table": { - "id": "ID", "amount": "Сумма списания", + "id": "ID", "shop": "Магазин", "status": "Статус", "statusChanged": "Статус изменен" diff --git a/src/environments/default-environment.ts b/src/environments/default-environment.ts new file mode 100644 index 00000000..ab9b0f4c --- /dev/null +++ b/src/environments/default-environment.ts @@ -0,0 +1,9 @@ +import { Environment } from './types/environment'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const DEFAULT_ENVIRONMENT: Environment = { + production: false, + stage: window.location.host.split('.')[0] === 'stage', + appConfigPath: '/appConfig.json', + authConfigPath: '/authConfig.json', +}; diff --git a/src/environments/environment.dev.ts b/src/environments/environment.dev.ts deleted file mode 100644 index 9f2f815e..00000000 --- a/src/environments/environment.dev.ts +++ /dev/null @@ -1,8 +0,0 @@ -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, -}; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index fa3919fd..652b03e9 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,8 +1,8 @@ +import { DEFAULT_ENVIRONMENT } from './default-environment'; import { Environment } from './types/environment'; // eslint-disable-next-line @typescript-eslint/naming-convention export const environment: Environment = { + ...DEFAULT_ENVIRONMENT, production: true, - appConfigPath: '/appConfig.json', - authConfigPath: '/authConfig.json', }; diff --git a/src/environments/environment.stage.ts b/src/environments/environment.stage.ts index 069ee6a1..76539481 100644 --- a/src/environments/environment.stage.ts +++ b/src/environments/environment.stage.ts @@ -1,9 +1,10 @@ -import { environment as devEnvironment } from './environment.dev'; +import { DEFAULT_ENVIRONMENT } from './default-environment'; import { Environment } from './types/environment'; // eslint-disable-next-line @typescript-eslint/naming-convention export const environment: Environment = { - ...devEnvironment, + ...DEFAULT_ENVIRONMENT, appConfigPath: '/appConfig.stage.json', authConfigPath: '/authConfig.stage.json', + stage: true, }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index d6c07b1f..a01eb2ee 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,4 +1,4 @@ -import { environment as devEnvironment } from './environment.dev'; +import { DEFAULT_ENVIRONMENT } from './default-environment'; import { Environment } from './types/environment'; // This file can be replaced during build by using the `fileReplacements` array. @@ -6,7 +6,7 @@ import { Environment } from './types/environment'; // The list of file replacements can be found in `angular.json`. // eslint-disable-next-line @typescript-eslint/naming-convention -export const environment: Environment = devEnvironment; +export const environment: Environment = DEFAULT_ENVIRONMENT; /* * For easier debugging in development mode, you can import the following file diff --git a/src/environments/types/environment.ts b/src/environments/types/environment.ts index 0614e97a..569f5d8d 100644 --- a/src/environments/types/environment.ts +++ b/src/environments/types/environment.ts @@ -2,4 +2,5 @@ export interface Environment { production: boolean; appConfigPath: string; authConfigPath: string; + stage: boolean; } diff --git a/src/styles/dsh/styles/_dsh-dialog-pane.scss b/src/styles/dsh/styles/_dsh-dialog-pane.scss index 7e1c6356..12723840 100644 --- a/src/styles/dsh/styles/_dsh-dialog-pane.scss +++ b/src/styles/dsh/styles/_dsh-dialog-pane.scss @@ -6,7 +6,8 @@ width: 100%; height: 100%; - .mat-dialog-container { + .mat-dialog-container, + .mat-mdc-dialog-surface { border-radius: 0 !important; } } diff --git a/src/styles/mat/styles/_mat-dialog.scss b/src/styles/mat/styles/_mat-dialog.scss index c47cd9b9..e37f4219 100644 --- a/src/styles/mat/styles/_mat-dialog.scss +++ b/src/styles/mat/styles/_mat-dialog.scss @@ -1,5 +1,6 @@ @mixin mat-dialog-override() { - .mat-dialog-container { + .mat-dialog-container, + .mat-mdc-dialog-surface { box-shadow: none !important; padding: 24px !important; border-radius: 8px !important;