diff --git a/src/frontend/packages/core/src/app.routing.ts b/src/frontend/packages/core/src/app.routing.ts index f90388713f..a57cda750d 100644 --- a/src/frontend/packages/core/src/app.routing.ts +++ b/src/frontend/packages/core/src/app.routing.ts @@ -94,6 +94,7 @@ const appRoutes: Routes = [ }, { path: 'about', loadChildren: () => import('./features/about/about.module').then(m => m.AboutModule) }, { path: 'user-profile', loadChildren: () => import('./features/user-profile/user-profile.module').then(m => m.UserProfileModule) }, + { path: 'api-keys', loadChildren: () => import('./features/api-keys/api-keys.module').then(m => m.ApiKeysModule) }, { path: 'events', loadChildren: () => import('./features/event-page/event-page.module').then(m => m.EventPageModule) }, { path: 'errors/:endpointId', diff --git a/src/frontend/packages/core/src/features/api-keys/add-api-key-dialog/add-api-key-dialog.component.html b/src/frontend/packages/core/src/features/api-keys/add-api-key-dialog/add-api-key-dialog.component.html new file mode 100644 index 0000000000..bf044a85bf --- /dev/null +++ b/src/frontend/packages/core/src/features/api-keys/add-api-key-dialog/add-api-key-dialog.component.html @@ -0,0 +1,28 @@ +
+ + +
+
+
+

+ Create an API Key +

+
+
+
+
+ + + +
+ + + + + + + +
\ No newline at end of file diff --git a/src/frontend/packages/core/src/features/api-keys/add-api-key-dialog/add-api-key-dialog.component.scss b/src/frontend/packages/core/src/features/api-keys/add-api-key-dialog/add-api-key-dialog.component.scss new file mode 100644 index 0000000000..a235091e2d --- /dev/null +++ b/src/frontend/packages/core/src/features/api-keys/add-api-key-dialog/add-api-key-dialog.component.scss @@ -0,0 +1,28 @@ +.key-dialog { + &__loading { + left: 0; + position: absolute; + right: 0; + top: 0; + &-wrapper { + position: relative; + margin: 0 -24px; + transform: translateY(-24px); + } + } + + &__title { + display: flex; + h2 { + flex: 1; + } + } + + &__actions { + justify-content: flex-end; + } + + mat-form-field { + width: 100%; + } +} diff --git a/src/frontend/packages/core/src/features/api-keys/add-api-key-dialog/add-api-key-dialog.component.spec.ts b/src/frontend/packages/core/src/features/api-keys/add-api-key-dialog/add-api-key-dialog.component.spec.ts new file mode 100644 index 0000000000..e27047ec08 --- /dev/null +++ b/src/frontend/packages/core/src/features/api-keys/add-api-key-dialog/add-api-key-dialog.component.spec.ts @@ -0,0 +1,40 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogRef } from '@angular/material/dialog'; + +import { BaseTestModules } from '../../../../test-framework/core-test.helper'; +import { AddApiKeyDialogComponent } from './add-api-key-dialog.component'; + +describe('AddApiKeyDialogComponent', () => { + let component: AddApiKeyDialogComponent; + let fixture: ComponentFixture; + + const mockDialogRef = { + close: () => { } + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...BaseTestModules, + ], + declarations: [AddApiKeyDialogComponent], + providers: [ + { + provide: MatDialogRef, + useValue: mockDialogRef + } + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AddApiKeyDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/features/api-keys/add-api-key-dialog/add-api-key-dialog.component.ts b/src/frontend/packages/core/src/features/api-keys/add-api-key-dialog/add-api-key-dialog.component.ts new file mode 100644 index 0000000000..eea0b54086 --- /dev/null +++ b/src/frontend/packages/core/src/features/api-keys/add-api-key-dialog/add-api-key-dialog.component.ts @@ -0,0 +1,66 @@ +import { Component, OnDestroy } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { MatDialogRef } from '@angular/material/dialog'; +import { BehaviorSubject, Subscription } from 'rxjs'; +import { filter, first, map, pairwise, tap } from 'rxjs/operators'; + +import { ApiKey } from '../../../../../store/src/apiKey.types'; +import { entityCatalog } from '../../../../../store/src/entity-catalog/entity-catalog'; +import { RequestInfoState } from '../../../../../store/src/reducers/api-request-reducer/types'; +import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-catalog'; +import { NormalizedResponse } from '../../../../../store/src/types/api.types'; +import { safeUnsubscribe } from '../../../core/utils.service'; + +@Component({ + selector: 'app-add-api-key-dialog', + templateUrl: './add-api-key-dialog.component.html', + styleUrls: ['./add-api-key-dialog.component.scss'] +}) +export class AddApiKeyDialogComponent implements OnDestroy { + + private hasErrored = new BehaviorSubject(null); + public hasErrored$ = this.hasErrored.asObservable(); + private isBusy = new BehaviorSubject(false) + public isBusy$ = this.isBusy.asObservable(); + + private sub: Subscription; + + public formGroup: FormGroup; + + constructor( + private fb: FormBuilder, + public dialogRef: MatDialogRef, + ) { + this.formGroup = this.fb.group({ + comment: ['', Validators.required], + }); + } + + ngOnDestroy(): void { + safeUnsubscribe(this.sub); + } + + submit() { + this.sub = stratosEntityCatalog.apiKey.api.create(this.formGroup.controls.comment.value).pipe( + tap(() => { + this.isBusy.next(true); + this.hasErrored.next(null); + }), + pairwise(), + filter(([oldR, newR]) => oldR.creating && !newR.creating), + map(([, newR]) => newR), + tap(state => { + if (state.error) { + this.hasErrored.next(`Failed to create key: ${state.message}`); + this.isBusy.next(false); + } else { + const response: NormalizedResponse = state.response; + const entityKey = entityCatalog.getEntityKey(stratosEntityCatalog.apiKey.actions.create('')); + this.dialogRef.close(response.entities[entityKey][response.result[0]]) + } + }), + first() + ).subscribe() + } + +} diff --git a/src/frontend/packages/core/src/features/api-keys/api-keys-page/api-keys-page.component.html b/src/frontend/packages/core/src/features/api-keys/api-keys-page/api-keys-page.component.html new file mode 100644 index 0000000000..f1da38761b --- /dev/null +++ b/src/frontend/packages/core/src/features/api-keys/api-keys-page/api-keys-page.component.html @@ -0,0 +1,33 @@ + +

API Keys

+
+ +
+
+ +
+ + + + vpn_keyNew API Key + + +

Your API Key has been successfully created. Use the following information to connect to Stratos.

+

Please safely record these details, there is no later way to view them

+
    +
  • Secret: {{keyDetails.secret}}
  • +
+ +
+
+
+ + + \ No newline at end of file diff --git a/src/frontend/packages/core/src/features/api-keys/api-keys-page/api-keys-page.component.scss b/src/frontend/packages/core/src/features/api-keys/api-keys-page/api-keys-page.component.scss new file mode 100644 index 0000000000..291e1522e1 --- /dev/null +++ b/src/frontend/packages/core/src/features/api-keys/api-keys-page/api-keys-page.component.scss @@ -0,0 +1,15 @@ +.keys-page { + &__new { + mat-card { + margin-bottom: 24px; + mat-card-header { + align-items: center; + display: flex; + margin-bottom: 15px; + mat-icon { + margin-right: 5px; + } + } + } + } +} \ No newline at end of file diff --git a/src/frontend/packages/core/src/features/api-keys/api-keys-page/api-keys-page.component.spec.ts b/src/frontend/packages/core/src/features/api-keys/api-keys-page/api-keys-page.component.spec.ts new file mode 100644 index 0000000000..1c2318bba0 --- /dev/null +++ b/src/frontend/packages/core/src/features/api-keys/api-keys-page/api-keys-page.component.spec.ts @@ -0,0 +1,42 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogRef } from '@angular/material/dialog'; + +import { BaseTestModules } from '../../../../test-framework/core-test.helper'; +import { TabNavService } from '../../../tab-nav.service'; +import { ApiKeysPageComponent } from './api-keys-page.component'; + +describe('ApiKeysPageComponent', () => { + let component: ApiKeysPageComponent; + let fixture: ComponentFixture; + + const mockDialogRef = { + close: () => { } + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...BaseTestModules, + ], + declarations: [ApiKeysPageComponent], + providers: [ + { + provide: MatDialogRef, + useValue: mockDialogRef + }, + TabNavService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ApiKeysPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/features/api-keys/api-keys-page/api-keys-page.component.ts b/src/frontend/packages/core/src/features/api-keys/api-keys-page/api-keys-page.component.ts new file mode 100644 index 0000000000..0587de148a --- /dev/null +++ b/src/frontend/packages/core/src/features/api-keys/api-keys-page/api-keys-page.component.ts @@ -0,0 +1,59 @@ +import { Component } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Observable, Subject } from 'rxjs'; +import { first, map, startWith } from 'rxjs/operators'; + +import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-catalog'; +import { ApiKeyListConfigService } from '../../../shared/components/list/list-types/apiKeys/apiKey-list-config.service'; +import { ListConfig } from '../../../shared/components/list/list.component.types'; +import { AddApiKeyDialogComponent } from '../add-api-key-dialog/add-api-key-dialog.component'; + +@Component({ + selector: 'app-api-keys-page', + templateUrl: './api-keys-page.component.html', + styleUrls: ['./api-keys-page.component.scss'], + providers: [{ + provide: ListConfig, + useClass: ApiKeyListConfigService, + }] +}) +export class ApiKeysPageComponent { + + public keyDetails = new Subject(); + public keyDetails$ = this.keyDetails.asObservable(); + public hasKeys$: Observable; + + constructor( + private dialog: MatDialog, + ) { + this.hasKeys$ = stratosEntityCatalog.apiKey.store.getPaginationService().entities$.pipe( + map(entities => entities && !!entities.length), + startWith(null), + ) + } + + addApiKey() { + this.showDialog().pipe(first()).subscribe(key => { + this.keyDetails.next(key); + }) + } + + clearKeyDetails() { + this.keyDetails.next(); + } + + private showDialog(): Observable { + return this.dialog.open(AddApiKeyDialogComponent, { + disableClose: true, + }).afterClosed().pipe( + map(newApiKey => { + if (newApiKey && newApiKey.guid) { + stratosEntityCatalog.apiKey.api.getMultiple(); + return newApiKey; + } + return null; + }) + ); + } + +} diff --git a/src/frontend/packages/core/src/features/api-keys/api-keys.module.ts b/src/frontend/packages/core/src/features/api-keys/api-keys.module.ts new file mode 100644 index 0000000000..c2311b97d4 --- /dev/null +++ b/src/frontend/packages/core/src/features/api-keys/api-keys.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; + +import { CoreModule } from '../../core/core.module'; +import { SharedModule } from '../../shared/shared.module'; +import { ApiKeysPageComponent } from './api-keys-page/api-keys-page.component'; +import { ApiKeysRoutingModule } from './api-keys.routing'; +import { AddApiKeyDialogComponent } from './add-api-key-dialog/add-api-key-dialog.component'; + + +@NgModule({ + imports: [ + CoreModule, + SharedModule, + ApiKeysRoutingModule, + ], + declarations: [ + ApiKeysPageComponent, + AddApiKeyDialogComponent + ] +}) +export class ApiKeysModule { } + diff --git a/src/frontend/packages/core/src/features/api-keys/api-keys.routing.ts b/src/frontend/packages/core/src/features/api-keys/api-keys.routing.ts new file mode 100644 index 0000000000..395c58188b --- /dev/null +++ b/src/frontend/packages/core/src/features/api-keys/api-keys.routing.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { ApiKeysPageComponent } from './api-keys-page/api-keys-page.component'; + +const apiKeys: Routes = [ + { + path: '', + component: ApiKeysPageComponent + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(apiKeys), + ] +}) +export class ApiKeysRoutingModule { } diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html index c8e3f991c3..0775265f9d 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html +++ b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html @@ -5,7 +5,7 @@

Endpoints

[routerLink]="'/endpoints/new/'" matTooltip="Register Endpoint"> add - diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/apiKeys/apiKey-data-source.ts b/src/frontend/packages/core/src/shared/components/list/list-types/apiKeys/apiKey-data-source.ts new file mode 100644 index 0000000000..a57eee6721 --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/list/list-types/apiKeys/apiKey-data-source.ts @@ -0,0 +1,32 @@ +import { Store } from '@ngrx/store'; + +import { GetAllApiKeys } from '../../../../../../../store/src/actions/apiKey.actions'; +import { ApiKey } from '../../../../../../../store/src/apiKey.types'; +import { AppState } from '../../../../../../../store/src/app-state'; +import { ListDataSource } from '../../data-sources-controllers/list-data-source'; +import { IListConfig } from '../../list.component.types'; + +export class ApiKeyDataSource extends ListDataSource { + + constructor( + store: Store, + listConfig: IListConfig, + action: GetAllApiKeys, + ) { + super({ + store, + action, + schema: action.entity[0], + getRowUniqueId: (object) => action.entity[0].getId(object), + paginationKey: action.paginationKey, + isLocal: true, + transformEntities: [ + { + type: 'filter', + field: 'comment' + }, + ], + listConfig, + }); + } +} diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/apiKeys/apiKey-list-config.service.ts b/src/frontend/packages/core/src/shared/components/list/list-types/apiKeys/apiKey-list-config.service.ts new file mode 100644 index 0000000000..035a9067da --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/list/list-types/apiKeys/apiKey-list-config.service.ts @@ -0,0 +1,103 @@ +import { Injectable } from '@angular/core'; +import { SortDirection } from '@angular/material/sort'; +import { Store } from '@ngrx/store'; +import moment from 'moment'; + +import { ListView } from '../../../../../../../store/src/actions/list.actions'; +import { ApiKey } from '../../../../../../../store/src/apiKey.types'; +import { AppState } from '../../../../../../../store/src/app-state'; +import { stratosEntityCatalog } from '../../../../../../../store/src/stratos-entity-catalog'; +import { ConfirmationDialogConfig } from '../../../confirmation-dialog.config'; +import { ConfirmationDialogService } from '../../../confirmation-dialog.service'; +import { ITableColumn } from '../../list-table/table.types'; +import { IListAction, IListConfig, ListViewTypes } from '../../list.component.types'; +import { ApiKeyDataSource } from './apiKey-data-source'; + +@Injectable() +export class ApiKeyListConfigService implements IListConfig { + + private static comment = 'comment'; + private static lastUsedName = 'last_used'; + + private deleteAction: IListAction = { + action: (item: ApiKey) => { + const confirmation = new ConfirmationDialogConfig( + 'Delete Key', + `Are you sure?`, + 'Delete', + true + ); + this.confirmDialog.open( + confirmation, + () => stratosEntityCatalog.apiKey.api.delete(item.guid) + ); + }, + label: 'Delete', + description: 'Delete API Key', + } + private singleActions: IListAction[] = [this.deleteAction]; + + + public readonly columns: ITableColumn[] = [ + { + columnId: ApiKeyListConfigService.comment, + headerCell: () => 'Description', + cellDefinition: { + valuePath: 'comment' + }, + sort: { + type: 'sort', + orderKey: ApiKeyListConfigService.comment, + field: 'comment' + }, + cellFlex: '2' + }, + { + columnId: ApiKeyListConfigService.lastUsedName, + headerCell: () => 'Last Used', + cellDefinition: { + getValue: row => row.last_used ? moment(row.last_used).format('LLL') : null + }, + sort: { + type: 'sort', + orderKey: ApiKeyListConfigService.lastUsedName, + field: 'last_used' + }, + cellFlex: '1' + } + ]; + + isLocal = true; + dataSource: ApiKeyDataSource; + viewType = ListViewTypes.TABLE_ONLY; + defaultView = 'table' as ListView; + text = { + title: '', + filter: 'Filter API Keys' + }; + enableTextFilter = true; + + constructor( + store: Store, + private confirmDialog: ConfirmationDialogService, + ) { + const action = stratosEntityCatalog.apiKey.actions.getMultiple(); + action.initialParams = { + 'order-direction': 'desc' as SortDirection, + 'order-direction-field': 'comment' + }; + this.dataSource = new ApiKeyDataSource( + store, + this, + action + ); + } + + public getGlobalActions = () => []; + public getMultiActions = () => []; + public getSingleActions = () => this.singleActions; + public getColumns = () => this.columns; + public getDataSource = () => this.dataSource; + public getMultiFiltersConfigs = () => []; + +} diff --git a/src/frontend/packages/core/src/shared/components/list/list.component.ts b/src/frontend/packages/core/src/shared/components/list/list.component.ts index 318bd0dd7e..2bc6da4898 100644 --- a/src/frontend/packages/core/src/shared/components/list/list.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list.component.ts @@ -701,7 +701,7 @@ export class ListComponent implements OnInit, OnChanges, OnDestroy, AfterView map(requestInfo => ({ deleting: requestInfo.deleting.busy, error: requestInfo.deleting.error, - message: requestInfo.deleting.error ? `Sorry, deletion failed` : null + message: requestInfo.deleting.error ? requestInfo.deleting.message || `Sorry, deletion failed` : null })) ); }; diff --git a/src/frontend/packages/core/src/shared/components/no-content-message/no-content-message.component.html b/src/frontend/packages/core/src/shared/components/no-content-message/no-content-message.component.html index efe500ff02..18aefbc5f3 100644 --- a/src/frontend/packages/core/src/shared/components/no-content-message/no-content-message.component.html +++ b/src/frontend/packages/core/src/shared/components/no-content-message/no-content-message.component.html @@ -1,4 +1,4 @@ -
+
@@ -24,61 +25,67 @@
-
- -
-
- -
- +
+
+ +
+ - | - - - -
-

Recent Activity

- -
-
- - - -
-
- -
-
{{ username$ | async }}
-
+ | + + + +
+

Recent Activity

+ +
-
- - + + +
+
+ +
+
{{ username$ | async }}
+
+
+
+ + + +
+
+ -
- -
-
-
+ +
diff --git a/src/frontend/packages/core/src/shared/shared.module.ts b/src/frontend/packages/core/src/shared/shared.module.ts index e5915d0af6..e18dd04581 100644 --- a/src/frontend/packages/core/src/shared/shared.module.ts +++ b/src/frontend/packages/core/src/shared/shared.module.ts @@ -57,6 +57,7 @@ import { TableCellStatusDirective } from './components/list/list-table/table-cel import { listTableCells } from './components/list/list-table/table-cell/table-cell.component'; import { TableComponent } from './components/list/list-table/table.component'; import { listTableComponents } from './components/list/list-table/table.types'; +import { ApiKeyListConfigService } from './components/list/list-types/apiKeys/apiKey-list-config.service'; import { EndpointCardComponent } from './components/list/list-types/endpoint/endpoint-card/endpoint-card.component'; import { EndpointListHelper } from './components/list/list-types/endpoint/endpoint-list.helpers'; import { EndpointsListConfigService } from './components/list/list-types/endpoint/endpoints-list-config.service'; @@ -318,6 +319,7 @@ import { UserPermissionDirective } from './user-permission.directive'; ListConfig, EndpointListHelper, EndpointsListConfigService, + ApiKeyListConfigService, ConfirmationDialogService, InternalEventMonitorFactory, MetricsRangeSelectorService, diff --git a/src/frontend/packages/store/src/actions/apiKey.actions.ts b/src/frontend/packages/store/src/actions/apiKey.actions.ts new file mode 100644 index 0000000000..4b10e34bef --- /dev/null +++ b/src/frontend/packages/store/src/actions/apiKey.actions.ts @@ -0,0 +1,44 @@ +import { apiKeyEntityType, STRATOS_ENDPOINT_TYPE, stratosEntityFactory } from '../helpers/stratos-entity-factory'; +import { PaginatedAction, PaginationParam } from '../types/pagination.types'; +import { EntityRequestAction } from '../types/request.types'; + +export const API_KEY_ADD = '[API Key] Add API Key' +export const API_KEY_DELETE = '[API Key] Delete API Key' +export const API_KEY_GET_ALL = '[API Key] Get All API Key' + +abstract class BaseApiKeyAction implements EntityRequestAction { + entityType = apiKeyEntityType; + endpointType = STRATOS_ENDPOINT_TYPE; + entity = [stratosEntityFactory(apiKeyEntityType)] + constructor(public type: string) { } +} + +interface PaginationApiKeyAction extends PaginatedAction, EntityRequestAction { + flattenPagination: boolean; +} +interface SingleApiKeyAction extends EntityRequestAction { + guid: string; +} + +export class AddApiKey extends BaseApiKeyAction implements SingleApiKeyAction { + constructor(public comment: string) { + super(API_KEY_ADD); + } + guid = 'ADD' +} + +export class DeleteApiKey extends BaseApiKeyAction implements SingleApiKeyAction { + constructor(public guid: string) { + super(API_KEY_DELETE); + } +} + +export class GetAllApiKeys extends BaseApiKeyAction implements PaginationApiKeyAction { + constructor() { + super(API_KEY_GET_ALL); + this.paginationKey = 'CURRENT_USERS'; + } + flattenPagination = true; + paginationKey: string; + initialParams: PaginationParam +} \ No newline at end of file diff --git a/src/frontend/packages/store/src/apiKey.types.ts b/src/frontend/packages/store/src/apiKey.types.ts new file mode 100644 index 0000000000..35ac4736b7 --- /dev/null +++ b/src/frontend/packages/store/src/apiKey.types.ts @@ -0,0 +1,7 @@ +export interface ApiKey { + comment: string; + guid: string; + last_used: string; + secret: string; + user_guid: string; +} \ No newline at end of file diff --git a/src/frontend/packages/store/src/effects/apiKey.effects.ts b/src/frontend/packages/store/src/effects/apiKey.effects.ts new file mode 100644 index 0000000000..d4dc3a033b --- /dev/null +++ b/src/frontend/packages/store/src/effects/apiKey.effects.ts @@ -0,0 +1,132 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Actions, Effect, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { catchError, mergeMap, switchMap } from 'rxjs/operators'; + +import { + AddApiKey, + API_KEY_ADD, + API_KEY_DELETE, + API_KEY_GET_ALL, + DeleteApiKey, + GetAllApiKeys, +} from '../actions/apiKey.actions'; +import { ApiKey } from '../apiKey.types'; +import { InternalAppState } from '../app-state'; +import { BrowserStandardEncoder } from '../browser-encoder'; +import { entityCatalog } from '../entity-catalog/entity-catalog'; +import { proxyAPIVersion } from '../jetstream'; +import { NormalizedResponse } from '../types/api.types'; +import { StartRequestAction, WrapperRequestActionFailed, WrapperRequestActionSuccess } from '../types/request.types'; + +const apiKeyUrlPath = `/pp/${proxyAPIVersion}/api_keys`; + +@Injectable() +export class ApiKeyEffect { + + constructor( + private http: HttpClient, + private actions$: Actions, + private store: Store, + ) { + } + + @Effect() add = this.actions$.pipe( + ofType(API_KEY_ADD), + mergeMap(action => { + const actionType = 'create'; + this.store.dispatch(new StartRequestAction(action, actionType)) + + return this.http.post(apiKeyUrlPath, new HttpParams({ + encoder: new BrowserStandardEncoder(), + fromObject: { + comment: action.comment + } + })).pipe( + switchMap(newApiKey => { + const guid = action.entity[0].getId(newApiKey); + const entityKey = entityCatalog.getEntityKey(action); + const response: NormalizedResponse = { + entities: { + [entityKey]: { + [guid]: newApiKey + } + }, + result: [guid] + } + this.store.dispatch(new WrapperRequestActionSuccess(response, action, actionType)); + return []; + }), + catchError(err => { + this.store.dispatch(new WrapperRequestActionFailed(this.convertErrorToString(err), action, actionType)); + return []; + }) + ); + }) + ); + + @Effect() delete = this.actions$.pipe( + ofType(API_KEY_DELETE), + mergeMap(action => { + const actionType = 'delete'; + this.store.dispatch(new StartRequestAction(action, actionType)) + + return this.http.delete(apiKeyUrlPath, { + params: new HttpParams({ + encoder: new BrowserStandardEncoder(), + fromObject: { + guid: action.guid + } + }) + }).pipe( + switchMap(() => { + this.store.dispatch(new WrapperRequestActionSuccess(null, action, actionType)); + return []; + }), + catchError(err => { + this.store.dispatch(new WrapperRequestActionFailed(this.convertErrorToString(err), action, actionType)); + return []; + }) + ); + }) + ); + + @Effect() getAll = this.actions$.pipe( + ofType(API_KEY_GET_ALL), + mergeMap(action => { + const actionType = 'fetch'; + this.store.dispatch(new StartRequestAction(action, actionType)) + return this.http.get(apiKeyUrlPath).pipe( + switchMap((res: ApiKey[]) => { + const entityKey = entityCatalog.getEntityKey(action); + const response: NormalizedResponse = { + entities: { + [entityKey]: { + } + }, + result: [] + } + + res.forEach(apiKey => { + const guid = action.entity[0].getId(apiKey); + response.entities[entityKey][guid] = apiKey; + response.result.push(guid); + }); + + this.store.dispatch(new WrapperRequestActionSuccess(response, action, actionType)); + return []; + }), + catchError(err => { + this.store.dispatch(new WrapperRequestActionFailed(this.convertErrorToString(err), action, actionType)); + return []; + }) + ); + }) + ); + + private convertErrorToString(err: any): string { + // We should look into beefing this up / combining with generic error handling + return err && err.error ? err.error : 'Failed API Key action'; + } +} \ No newline at end of file diff --git a/src/frontend/packages/store/src/helpers/stratos-entity-factory.ts b/src/frontend/packages/store/src/helpers/stratos-entity-factory.ts index 1182c576a3..2c570b69e5 100644 --- a/src/frontend/packages/store/src/helpers/stratos-entity-factory.ts +++ b/src/frontend/packages/store/src/helpers/stratos-entity-factory.ts @@ -4,6 +4,7 @@ export const userFavouritesEntityType = 'userFavorites'; export const endpointEntityType = 'endpoint'; export const userProfileEntityType = 'userProfile'; export const systemInfoEntityType = 'systemInfo'; +export const apiKeyEntityType = 'apiKey'; export const metricEntityType = 'metrics'; @@ -31,6 +32,9 @@ entityCache[endpointEntityType] = EndpointSchema; const UserProfileInfoSchema = new StratosEntitySchema(userProfileEntityType, 'id'); entityCache[userProfileEntityType] = UserProfileInfoSchema; +const ApiKeySchema = new StratosEntitySchema(apiKeyEntityType, 'guid'); +entityCache[apiKeyEntityType] = ApiKeySchema; + export function stratosEntityFactory(key: string): EntitySchema { const entity = entityCache[key]; if (!entity) { diff --git a/src/frontend/packages/store/src/store.module.ts b/src/frontend/packages/store/src/store.module.ts index 856021f529..903f620b94 100644 --- a/src/frontend/packages/store/src/store.module.ts +++ b/src/frontend/packages/store/src/store.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'; import { EffectsModule } from '@ngrx/effects'; import { APIEffect } from './effects/api.effects'; +import { ApiKeyEffect } from './effects/apiKey.effects'; import { AuthEffect } from './effects/auth.effects'; import { DashboardEffect } from './effects/dashboard.effects'; import { EndpointApiError } from './effects/endpoint-api-errors.effects'; @@ -43,7 +44,8 @@ import { AppReducersModule } from './reducers.module'; UserProfileEffect, RecursiveDeleteEffect, UserFavoritesEffect, - PermissionsEffects + PermissionsEffects, + ApiKeyEffect ]) ] }) diff --git a/src/frontend/packages/store/src/stratos-action-builders.ts b/src/frontend/packages/store/src/stratos-action-builders.ts index c64a95cd01..5f8b83f174 100644 --- a/src/frontend/packages/store/src/stratos-action-builders.ts +++ b/src/frontend/packages/store/src/stratos-action-builders.ts @@ -1,3 +1,4 @@ +import { AddApiKey, DeleteApiKey, GetAllApiKeys } from './actions/apiKey.actions'; import { AuthParams, BaseEndpointAction, @@ -195,4 +196,21 @@ export const userProfileActionBuilder: UserProfileActionBuilder = { get: (userGuid: string) => new FetchUserProfileAction(userGuid), updateProfile: (profile: UserProfileInfo, password: string) => new UpdateUserProfileAction(profile, password), updatePassword: (guid: string, passwordChanges: UserProfilePasswordUpdate) => new UpdateUserPasswordAction(guid, passwordChanges) +} + +export interface ApiKeyActionBuilder extends OrchestratedActionBuilders { + create: ( + comment: string + ) => AddApiKey; + delete: ( + guid: string + ) => DeleteApiKey; + getMultiple: ( + + ) => GetAllApiKeys; +} +export const apiKeyActionBuilder: ApiKeyActionBuilder = { + create: (comment: string) => new AddApiKey(comment), + delete: (guid: string) => new DeleteApiKey(guid), + getMultiple: () => new GetAllApiKeys() } \ No newline at end of file diff --git a/src/frontend/packages/store/src/stratos-entity-catalog.ts b/src/frontend/packages/store/src/stratos-entity-catalog.ts index 9237a16978..5a58cc14aa 100644 --- a/src/frontend/packages/store/src/stratos-entity-catalog.ts +++ b/src/frontend/packages/store/src/stratos-entity-catalog.ts @@ -1,8 +1,10 @@ +import { ApiKey } from './apiKey.types'; import { StratosCatalogEndpointEntity, StratosCatalogEntity, } from './entity-catalog/entity-catalog-entity/entity-catalog-entity'; import { + ApiKeyActionBuilder, EndpointActionBuilder, SystemInfoActionBuilder, UserFavoriteActionBuilder, @@ -39,6 +41,12 @@ export class StratosEntityCatalog { > metricsEndpoint: StratosCatalogEndpointEntity; + + apiKey: StratosCatalogEntity< + undefined, + ApiKey, + ApiKeyActionBuilder + > } export const stratosEntityCatalog = new StratosEntityCatalog(); diff --git a/src/frontend/packages/store/src/stratos-entity-generator.ts b/src/frontend/packages/store/src/stratos-entity-generator.ts index cb8322ea25..c1ae7ffae0 100644 --- a/src/frontend/packages/store/src/stratos-entity-generator.ts +++ b/src/frontend/packages/store/src/stratos-entity-generator.ts @@ -1,3 +1,4 @@ +import { ApiKey } from './apiKey.types'; import { StratosBaseCatalogEntity, StratosCatalogEndpointEntity, @@ -5,16 +6,18 @@ import { } from './entity-catalog/entity-catalog-entity/entity-catalog-entity'; import { IStratosEntityDefinition } from './entity-catalog/entity-catalog.types'; import { - endpointEntityType, + apiKeyEntityType, STRATOS_ENDPOINT_TYPE, - stratosEntityFactory, systemInfoEntityType, userFavouritesEntityType, userProfileEntityType, } from './helpers/stratos-entity-factory'; +import { endpointEntityType, EndpointModel, stratosEntityFactory } from './public-api'; import { addOrUpdateUserFavoriteMetadataReducer, deleteUserFavoriteMetadataReducer } from './reducers/favorite.reducer'; import { systemEndpointsReducer } from './reducers/system-endpoints.reducer'; import { + ApiKeyActionBuilder, + apiKeyActionBuilder, EndpointActionBuilder, endpointActionBuilder, SystemInfoActionBuilder, @@ -25,7 +28,6 @@ import { userProfileActionBuilder, } from './stratos-action-builders'; import { stratosEntityCatalog } from './stratos-entity-catalog'; -import { EndpointModel } from './types/endpoint.types'; import { SystemInfo } from './types/system.types'; import { UserFavorite } from './types/user-favorites.types'; import { UserProfileInfo } from './types/user-profile.types'; @@ -45,7 +47,8 @@ export function generateStratosEntities(): StratosBaseCatalogEntity[] { generateSystemInfo(stratosType), generateUserFavorite(stratosType), generateUserProfile(stratosType), - generateMetricsEndpoint() + generateMetricsEndpoint(), + generateAPIKeys(stratosType) ] } @@ -151,3 +154,22 @@ function generateMetricsEndpoint() { ) return stratosEntityCatalog.metricsEndpoint; } + +function generateAPIKeys(stratosType) { + const definition: IStratosEntityDefinition = { + schema: stratosEntityFactory(apiKeyEntityType), + type: apiKeyEntityType, + endpoint: stratosType, + } + stratosEntityCatalog.apiKey = new StratosCatalogEntity< + undefined, + ApiKey, + ApiKeyActionBuilder + >( + definition, + { + actionBuilders: apiKeyActionBuilder + } + ) + return stratosEntityCatalog.apiKey; +}