diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..910a2a5f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules\\typescript\\lib" +} diff --git a/src/app/admin/admin-routing.module.ts b/src/app/admin/admin-routing.module.ts index fffe9c8a..e5343681 100644 --- a/src/app/admin/admin-routing.module.ts +++ b/src/app/admin/admin-routing.module.ts @@ -12,6 +12,9 @@ import { UserDetailComponent } from './users/user-detail/user-detail.component'; import { UserEditComponent } from './users/user-edit/user-edit.component'; import { UserListComponent } from './users/user-list/user-list.component'; import { UsersComponent } from './users/users.component'; +import { ApiKeyComponent } from './api-key/api-key.component'; +import { ApiKeyListComponent } from './api-key/api-key-list/api-key-list.component'; +import { ApiKeyEditComponent } from './api-key/api-key-edit/api-key-edit.component'; const adminRoutes: Routes = [ @@ -44,6 +47,18 @@ const adminRoutes: Routes = [ }, ], }, + { + path: 'api-key', + component: ApiKeyComponent, + children: [ + { path: '', component: ApiKeyListComponent }, + { path: 'new-api-key', component: ApiKeyEditComponent }, + { + path: ':api-key-id/edit-api-key', + component: ApiKeyEditComponent, + }, + ], + }, ]; diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts index c7c52faf..93210070 100644 --- a/src/app/admin/admin.module.ts +++ b/src/app/admin/admin.module.ts @@ -28,6 +28,10 @@ import { UsersComponent } from './users/users.component'; import { MatSelectModule } from '@angular/material/select'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectSearchModule } from '@shared/components/mat-select-search/mat-select-search.module'; +import { ApiKeyComponent } from './api-key/api-key.component'; +import { ApiKeyListComponent } from './api-key/api-key-list/api-key-list.component'; +import { ApiKeyTableComponent } from './api-key/api-key-list/api-key-table/api-key-table.component'; +import { ApiKeyEditComponent } from './api-key/api-key-edit/api-key-edit.component'; @NgModule({ declarations: [ @@ -46,6 +50,10 @@ import { MatSelectSearchModule } from '@shared/components/mat-select-search/mat- OrganisationDetailComponent, OrganisationEditComponent, OrganisationListComponent, + ApiKeyComponent, + ApiKeyListComponent, + ApiKeyTableComponent, + ApiKeyEditComponent, ], imports: [ AdminRoutingModule, @@ -79,6 +87,10 @@ import { MatSelectSearchModule } from '@shared/components/mat-select-search/mat- OrganisationDetailComponent, OrganisationEditComponent, OrganisationListComponent, + ApiKeyComponent, + ApiKeyListComponent, + ApiKeyTableComponent, + ApiKeyEditComponent, ], }) export class AdminModule {} diff --git a/src/app/admin/api-key/api-key-edit/api-key-edit.component.html b/src/app/admin/api-key/api-key-edit/api-key-edit.component.html new file mode 100644 index 00000000..268d6972 --- /dev/null +++ b/src/app/admin/api-key/api-key-edit/api-key-edit.component.html @@ -0,0 +1,66 @@ + + +
+
+ +
+ +
+
+ * + +
+
+ +
+
+ * + + + {{ permission.name }} + + +
+
+ +
+ + +
+
diff --git a/src/app/admin/api-key/api-key-edit/api-key-edit.component.scss b/src/app/admin/api-key/api-key-edit/api-key-edit.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/admin/api-key/api-key-edit/api-key-edit.component.ts b/src/app/admin/api-key/api-key-edit/api-key-edit.component.ts new file mode 100644 index 00000000..015f7852 --- /dev/null +++ b/src/app/admin/api-key/api-key-edit/api-key-edit.component.ts @@ -0,0 +1,106 @@ +import { Location } from '@angular/common'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { PermissionResponse } from '@app/admin/permission/permission.model'; +import { PermissionService } from '@app/admin/permission/permission.service'; +import { TranslateService } from '@ngx-translate/core'; +import { ErrorMessageService } from '@shared/error-message.service'; +import { BackButton } from '@shared/models/back-button.model'; +import { SharedVariableService } from '@shared/shared-variable/shared-variable.service'; +import { ApiKeyRequest } from '../api-key.model'; +import { ApiKeyService } from '../api-key.service'; + +@Component({ + selector: 'app-api-key-edit', + templateUrl: './api-key-edit.component.html', + styleUrls: ['./api-key-edit.component.scss'], +}) +export class ApiKeyEditComponent implements OnInit { + apiKeyRequest = new ApiKeyRequest(); + public backButton: BackButton = { + label: '', + routerLink: ['admin', 'api-key'], + }; + public title = ''; + public submitButton = ''; + public errorMessage: string; + public errorMessages: string[]; + public errorFields: string[]; + public formFailedSubmit = false; + public permissions: PermissionResponse[] = []; + private organizationId: number; + + constructor( + private translate: TranslateService, + private route: ActivatedRoute, + private location: Location, + private apiKeyService: ApiKeyService, + private permissionService: PermissionService, + private errorMessageService: ErrorMessageService, + private sharedVariableService: SharedVariableService + ) { + translate.use('da'); + } + + ngOnInit(): void { + this.getPermissions(); + this.translate.use('da'); + this.translate + .get(['NAV.API-KEY', 'FORM.EDIT-API-KEY', 'API-KEY.EDIT.SAVE']) + .subscribe((translations) => { + this.backButton.label = translations['NAV.API-KEY']; + this.title = translations['FORM.EDIT-API-KEY']; + this.submitButton = translations['API-KEY.EDIT.SAVE']; + }); + + this.organizationId = this.sharedVariableService.getSelectedOrganisationId(); + } + + private getPermissions() { + this.permissionService + .getPermissions( + undefined, + undefined, + undefined, + undefined, + undefined, + this.organizationId + ) + .subscribe( + (permissions) => { + this.permissions = permissions.data.filter( + (x) => x.organization?.id === this.organizationId + ); + }, + (error: HttpErrorResponse) => { + this.showError(error); + } + ); + } + + onSubmit(): void { + this.create(); + } + + private create(): void { + this.apiKeyService.create(this.apiKeyRequest).subscribe( + () => this.routeBack(), + (err) => this.showError(err) + ); + } + + public compare(o1: any, o2: any): boolean { + return o1 === o2; + } + + private showError(err: HttpErrorResponse) { + const result = this.errorMessageService.handleErrorMessageWithFields(err); + this.errorFields = result.errorFields; + this.errorMessages = result.errorMessages; + } + + routeBack(): void { + this.location.back(); + } +} diff --git a/src/app/admin/api-key/api-key-list/api-key-list.component.html b/src/app/admin/api-key/api-key-list/api-key-list.component.html new file mode 100644 index 00000000..8ecf306e --- /dev/null +++ b/src/app/admin/api-key/api-key-list/api-key-list.component.html @@ -0,0 +1,17 @@ + + +
+
+
+
+ +
+
+
+
diff --git a/src/app/admin/api-key/api-key-list/api-key-list.component.scss b/src/app/admin/api-key/api-key-list/api-key-list.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/admin/api-key/api-key-list/api-key-list.component.ts b/src/app/admin/api-key/api-key-list/api-key-list.component.ts new file mode 100644 index 00000000..f422eac1 --- /dev/null +++ b/src/app/admin/api-key/api-key-list/api-key-list.component.ts @@ -0,0 +1,28 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Title } from '@angular/platform-browser'; +import { TranslateService } from '@ngx-translate/core'; +import { SharedVariableService } from '@shared/shared-variable/shared-variable.service'; + +@Component({ + selector: 'app-api-key-list', + templateUrl: './api-key-list.component.html', + styleUrls: ['./api-key-list.component.scss'], +}) +export class ApiKeyListComponent implements OnInit { + @Input() organisationId: number; + + constructor( + public translate: TranslateService, + private titleService: Title, + private globalService: SharedVariableService + ) { + translate.use('da'); + } + + ngOnInit(): void { + this.translate.get(['TITLE.API-KEY']).subscribe((translations) => { + this.titleService.setTitle(translations['TITLE.API-KEY']); + }); + this.organisationId = this.globalService.getSelectedOrganisationId(); + } +} diff --git a/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.html b/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.html new file mode 100644 index 00000000..e52194d0 --- /dev/null +++ b/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.html @@ -0,0 +1,72 @@ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{ 'API-KEY.NAME' | translate }} + + {{ element.name }} + + {{ 'API-KEY.PERMISSIONS' | translate }} + + + + {{ pm.name }} +
+
+
+ {{ 'NoUsersAdded' | translate }} +
+ {{ 'API-KEY.KEY' | translate }} + + {{ element.key }} + + {{ 'API-KEY.TABLE-ROW.DELETE' | translate }} + +
+ + +
diff --git a/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.scss b/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.ts b/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.ts new file mode 100644 index 00000000..208d9c8e --- /dev/null +++ b/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.ts @@ -0,0 +1,111 @@ +import { AfterViewInit, Component, Input, ViewChild } from '@angular/core'; +import { MatPaginator, PageEvent } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { Router } from '@angular/router'; +import { environment } from '@environments/environment'; +import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service'; +import { MeService } from '@shared/services/me.service'; +import { merge, Observable, of } from 'rxjs'; +import { catchError, map, startWith, switchMap } from 'rxjs/operators'; +import { ApiKeyGetManyResponse, ApiKeyResponse } from '../../api-key.model'; +import { ApiKeyService } from '../../api-key.service'; + +@Component({ + selector: 'app-api-key-table', + templateUrl: './api-key-table.component.html', + styleUrls: ['./api-key-table.component.scss'], +}) +export class ApiKeyTableComponent implements AfterViewInit { + @Input() organisationId: number; + displayedColumns: string[] = [ + 'name', + 'permissions', + 'key', + 'menu', + ]; + data: ApiKeyResponse[] = []; + isLoadingResults = true; + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + resultsLength = 0; + public pageSize = environment.tablePageSize; + + constructor( + private meService: MeService, + private router: Router, + private apiKeyService: ApiKeyService, + private deleteDialogService: DeleteDialogService + ) {} + + ngAfterViewInit(): void { + // If the user changes the sort order, reset back to the first page. + this.sort.sortChange.subscribe(() => (this.paginator.pageIndex = 0)); + + merge(this.sort.sortChange, this.paginator.page) + .pipe( + startWith({}), + switchMap(() => { + this.isLoadingResults = true; + return this.getApiKeysByOrganisationId( + this.sort.active, + this.sort.direction + ); + }), + map((data) => { + // Flip flag to show that loading has finished. + this.isLoadingResults = false; + this.resultsLength = data.count; + + return data.data; + }), + catchError(() => { + this.isLoadingResults = false; + return of([]); + }) + ) + .subscribe((data) => (this.data = data)); + } + + getApiKeysByOrganisationId( + orderByColumn: string, + orderByDirection: string + ): Observable { + return this.apiKeyService.getApiKeys( + this.paginator.pageSize, + this.paginator.pageIndex * this.paginator.pageSize, + orderByColumn, + orderByDirection, + null, + this.organisationId + ); + } + + canAccess(_element: ApiKeyResponse) { + return this.meService.hasAdminAccessInTargetOrganization( + this.organisationId + ); + } + + routeToPermissions(element: any) { + this.router.navigate(['admin/api-key', element.id]); + } + + deleteApiKey(id: number) { + this.deleteDialogService.showSimpleDialog().subscribe((response) => { + if (response) { + this.apiKeyService.delete(id).subscribe((response) => { + if (response.ok && response.body.affected > 0) { + this.refresh(); + } + }); + } + }); + } + + private refresh() { + const pageEvent = new PageEvent(); + pageEvent.pageIndex = this.paginator.pageIndex; + pageEvent.pageSize = this.paginator.pageSize; + this.paginator.page.emit(pageEvent); + } +} diff --git a/src/app/admin/api-key/api-key.component.ts b/src/app/admin/api-key/api-key.component.ts new file mode 100644 index 00000000..97c6b2fe --- /dev/null +++ b/src/app/admin/api-key/api-key.component.ts @@ -0,0 +1,11 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-api-key', + template: '', +}) +export class ApiKeyComponent implements OnInit { + constructor() {} + + ngOnInit(): void {} +} diff --git a/src/app/admin/api-key/api-key.model.ts b/src/app/admin/api-key/api-key.model.ts new file mode 100644 index 00000000..050a799b --- /dev/null +++ b/src/app/admin/api-key/api-key.model.ts @@ -0,0 +1,23 @@ +import { PermissionResponse } from '../permission/permission.model'; + +export class ApiKeyRequest { + id: number; + name: string; + permissions?: PermissionResponse[]; +} + +export interface ApiKeyResponse { + id: number; + name: string; + key: string; + permissions?: PermissionResponse[]; + createdBy: number; + updatedBy: number; + createdByName: string; + updatedByName: string; +} + +export interface ApiKeyGetManyResponse { + data: ApiKeyResponse[]; + count: number; +} diff --git a/src/app/admin/api-key/api-key.service.ts b/src/app/admin/api-key/api-key.service.ts new file mode 100644 index 00000000..91a6dc2c --- /dev/null +++ b/src/app/admin/api-key/api-key.service.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@angular/core'; +import { RestService } from '@shared/services/rest.service'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { UserMinimalService } from '../users/user-minimal.service'; +import { + ApiKeyGetManyResponse, + ApiKeyRequest, + ApiKeyResponse, +} from './api-key.model'; + +@Injectable({ + providedIn: 'root', +}) +export class ApiKeyService { + endpoint = 'api-key'; + constructor( + private restService: RestService, + private userMinimalService: UserMinimalService + ) {} + + create(body: ApiKeyRequest): Observable { + return this.restService.post(this.endpoint, body, { + observe: 'response', + }); + } + + get(id: number): Observable { + return this.restService.get(this.endpoint, {}, id).pipe( + map((response: ApiKeyResponse) => { + response.createdByName = this.userMinimalService.getUserNameFrom( + response.createdBy + ); + response.updatedByName = this.userMinimalService.getUserNameFrom( + response.updatedBy + ); + return response; + }) + ); + } + + getApiKeys( + limit: number = 1000, + offset: number = 0, + orderByColumn?: string, + orderByDirection?: string, + userId?: number, + organisationId?: number + ): Observable { + if (userId) { + return this.restService.get(this.endpoint, { + limit, + offset, + orderOn: orderByColumn, + sort: orderByDirection, + userId, + }); + } else if (organisationId) { + return this.restService.get(this.endpoint, { + limit, + offset, + orderOn: orderByColumn, + sort: orderByDirection, + organisationId, + }); + } else { + return this.restService.get(this.endpoint, { + limit, + offset, + orderOn: orderByColumn, + sort: orderByDirection, + }); + } + } + + delete(id: number) { + return this.restService.delete(this.endpoint, id); + } +} diff --git a/src/app/admin/organisation/organisation.model.ts b/src/app/admin/organisation/organisation.model.ts index d3961220..f07c0a52 100644 --- a/src/app/admin/organisation/organisation.model.ts +++ b/src/app/admin/organisation/organisation.model.ts @@ -1,5 +1,6 @@ import { Application } from '@applications/application.model'; import { PayloadDecoder } from '../../payload-decoder/payload-decoder.model'; +import { PermissionResponse } from '../permission/permission.model'; export class Organisation { id?: number; @@ -18,8 +19,7 @@ export interface OrganisationResponse { payloadDecoders: PayloadDecoder[]; applications: Application[]; - // TODO: This. - permissions: any[]; + permissions: PermissionResponse[]; } export interface OrganisationGetManyResponse { @@ -30,4 +30,4 @@ export interface OrganisationGetManyResponse { export interface OrganisationGetMinimalResponse { data: Organisation[]; count: number; -} \ No newline at end of file +} diff --git a/src/app/admin/permission/permission-list/permission-list.component.ts b/src/app/admin/permission/permission-list/permission-list.component.ts index cec1432d..7559e47a 100644 --- a/src/app/admin/permission/permission-list/permission-list.component.ts +++ b/src/app/admin/permission/permission-list/permission-list.component.ts @@ -49,7 +49,6 @@ export class PermissionListComponent implements OnInit, OnChanges { } deletePermission(id: number) { - console.log("list") this.permissionService.deletePermission(id).subscribe((response) => { if (response.ok && response.body.affected > 0) { this.getPermissions(); diff --git a/src/app/dashboard/dashboard.component.ts b/src/app/dashboard/dashboard.component.ts index 1c6916ae..a071e8ca 100644 --- a/src/app/dashboard/dashboard.component.ts +++ b/src/app/dashboard/dashboard.component.ts @@ -48,13 +48,13 @@ export class DashboardComponent implements OnInit { } else { const error = params['error']; if (error) { - if (error == "MESSAGE.KOMBIT-LOGIN-FAILED") { + if (error == "MESSAGE.KOMBIT-LOGIN-FAILED" || error == "MESSAGE.API-KEY-AUTH-FAILED") { this.router.navigate(['/not-authorized'], { state: { message: this.kombitError, code: 401 } }); } if (error == "MESSAGE.USER-INACTIVE") { this.router.navigate(['/not-authorized'], { state: { message: this.noAccess, code: 401 } }); } else { this.router.navigate(['/not-authorized'], { state: { message: this.unauthorizedMessage, code: 401 } }); - } + } } } await this.sharedVariableService.setUserInfo(); diff --git a/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html index 566b42df..f6a9a123 100644 --- a/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html +++ b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html @@ -40,4 +40,11 @@ - \ No newline at end of file + + diff --git a/src/app/navbar/organisation-dropdown/organisation-dropdown.component.ts b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.ts index 3d73055b..97364003 100644 --- a/src/app/navbar/organisation-dropdown/organisation-dropdown.component.ts +++ b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.ts @@ -3,7 +3,7 @@ import { Router } from '@angular/router'; import { Organisation } from '@app/admin/organisation/organisation.model'; import { PermissionType } from '@app/admin/permission/permission.model'; import { UserResponse } from '@app/admin/users/user.model'; -import { faExchangeAlt, faLayerGroup, faUsers, faIdBadge, faToolbox, faBurn } from '@fortawesome/free-solid-svg-icons'; +import { faExchangeAlt, faLayerGroup, faUsers, faIdBadge, faToolbox, faBurn, faKey } from '@fortawesome/free-solid-svg-icons'; import { TranslateService } from '@ngx-translate/core'; import { SharedVariableService } from '@shared/shared-variable/shared-variable.service'; @@ -24,6 +24,7 @@ export class OrganisationDropdownComponent implements OnInit { faIdBadge = faIdBadge; faToolbox = faToolbox; faBurn = faBurn; + faKey = faKey; constructor( diff --git a/src/assets/i18n/da.json b/src/assets/i18n/da.json index 1c086680..2381c4c3 100644 --- a/src/assets/i18n/da.json +++ b/src/assets/i18n/da.json @@ -35,7 +35,8 @@ "DEVICE-MODEL": "Device model", "BACK": "Tilbage", "LOGOUT": "Log ud", - "HELP": "Hjælp" + "HELP": "Hjælp", + "API-KEY": "API nøgler" }, "TOPBAR":{ "SEARCH": { @@ -165,7 +166,7 @@ "AUTHORIZATIONHEADER": "Authorization header", "NO-AUTHORIZATIONHEADER": "Ingen Authorization header angivet", "ADD-TO-OPENDATADK": "Send data til OpenDataDK", - "OPENDATA-DK": "OpenDataDK", + "OPENDATA-DK": "OpenDataDK", "NO-OPENDATA-DK": "Der er ikke oprettet nogen datadeling med Open Data DK endnu" }, "MULTICAST": { @@ -429,7 +430,8 @@ "EDIT-SIGFOX-GROUPS": "Redigér Sigfox grupper", "EDIT-SIGFOX-GROUP": "Redigér Sigfox gruppe", "EDIT-USERS": "Redigér bruger", - "EDIT-DEVICE-PROFILE": "Redigér device profil" + "EDIT-DEVICE-PROFILE": "Redigér device profil", + "EDIT-API-KEY": "Redigér API nøgle" }, "QUESTION": { "CREATE-IOT-DEVICE": "IoT-enhed", @@ -577,6 +579,9 @@ "SELECT-ENERGYLIMITATIONCLASS": "Vælg energibegrænsningsklassen (energyLimitationClass)", "SELECT-SUPPORTEDPROTOCOL": "Vælg understøttede protokoller (supportedProtocol)", "FIWARE-LINK-TEXT": "Denne data model er adopteret fra Fiware og følger ETSI standarden" + }, + "PERMISSION": { + "SELECT-PERMISSION": "Vælg brugergruppe" } }, "QUESTION-LORA-GATEWAY": { @@ -897,7 +902,8 @@ "MULTICAST": "OS2IoT - Multicast", "BULKIMPORT": "OS2IoT - Bulk import", "IOTDEVICE": "OS2IoT - IoT enhed", - "FRONTPAGE": "OS2IoT - Forside" + "FRONTPAGE": "OS2IoT - Forside", + "API-KEY": "OS2IoT - API nøgler" }, "PAGINATOR": { @@ -915,5 +921,25 @@ "Forbidden resource": "Du har ikke rettigheder til at foretage denne handling", "GENERIC_HTTP": "Generisk HTTP", "LORAWAN": "LoRaWAN", - "SIGFOX": "Sigfox" + "SIGFOX": "Sigfox", + "API-KEY": { + "NAME": "Navn", + "ORGANIZATION": "Organisation", + "PERMISSIONS": "Brugergrupper", + "KEY": "Nøgle", + "CREATE-NEW-API-KEY": "Opret ny nøgle", + "DETAIL": {}, + "EDIT": { + "NAME": "Indtast nøglens navn", + "NAME-PLACEHOLDER": "Indtast nøglens navn", + "CANCEL": "Annuller", + "SAVE": "Gem nøgle", + "CREATE-API-KEY": "Opret nøgle" + }, + "TABLE-ROW": { + "EDIT": "Redigér", + "DELETE": "Slet" + } + }, + "NoUsersAdded": "Ingen brugergrupper er tilføjet" } diff --git a/tslint.json b/tslint.json index 4f4dff57..b35a3d4e 100644 --- a/tslint.json +++ b/tslint.json @@ -129,7 +129,7 @@ "use-lifecycle-interface": true, "use-pipe-transform-interface": true, "variable-name": { - "options": ["ban-keywords", "check-format", "allow-pascal-case"] + "options": ["ban-keywords", "check-format", "allow-pascal-case", "allow-leading-underscore"] }, "whitespace": { "options": [