developers/src/app/apis/apis.component.ts

462 lines
24 KiB
TypeScript

import {ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild} from "@angular/core";
import {FormArray, FormBuilder, FormGroup, Validators} from "@angular/forms";
import {Subscription} from "rxjs";
import {NotificationHandler} from "../openaireLibrary/utils/notification-handler";
import {API, ApisService} from "../services/apis.service";
import {AlertModal} from "../openaireLibrary/utils/modal/alert";
import {StringUtils} from "../openaireLibrary/utils/string-utils.class";
import {UserManagementService} from "../openaireLibrary/services/user-management.service";
declare var copy;
@Component({
selector: `apis`,
template: `
<div page-content>
<div header class="uk-section-small uk-container uk-container-small uk-flex uk-flex-right@m uk-flex-center">
<button (click)="openEditModal()" class="uk-button uk-button-primary uk-flex uk-flex-middle">
<icon name="add" [flex]="true"></icon>
<span class="uk-margin-xsmall-left">New Service</span>
</button>
</div>
<div inner class="uk-section-small uk-container uk-container-small">
<div *ngIf="loading" class="uk-height-large uk-position-relative">
<loading class="uk-position-center"></loading>
</div>
<div *ngIf="!loading">
<div class="uk-alert-primary uk-alert uk-margin-top-remove uk-flex uk-flex-middle">
<icon name="info" [flex]="true"></icon>
<span class="uk-margin-small-left">You can register up to 5 services.
For more information please read the <a href="https://graph.openaire.eu/docs/apis/authentication" target="_blank">OpenAIRE API Authentication documentation</a>.</span>
</div>
<div *ngIf="apis.length === 0"
class="uk-margin-large-top uk-card uk-card-default uk-height-small uk-position-relative">
<div class="uk-position-center uk-text-bold">You have not registered any service yet!</div>
</div>
<table *ngIf="apis.length > 0" class="uk-table uk-table-divider uk-margin-medium-top">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Client ID</th>
<th>Creation Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let api of apis; let i=index">
<td><img [src]="getLogoUrl(api)" [alt]="api.service.name + ' logo'"/></td>
<td>{{api.service.name}}</td>
<td>{{api.service.clientId}}</td>
<td>{{api.service.creationDate | date: 'dd-MM-YYYY HH:mm'}}</td>
<td>
<span class="uk-flex uk-flex-middle">
<icon *ngIf="api.details" name="edit" class="clickable" customClass="uk-text-primary"
(click)="openEditModal(i)"></icon>
<icon name="delete" class="clickable uk-margin-xsmall-left"
customClass="uk-text-danger" (click)="openDeleteModal(i)"></icon>
<icon *ngIf="!api.invalid && api.details" name="info" class="clickable uk-margin-xsmall-left"
customClass="uk-text-secondary" (click)="openInfoModal(i)"></icon>
<icon *ngIf="api.invalid" name="warning" class="clickable uk-margin-xsmall-left"
customClass="uk-text-warning"
uk-tooltip="Please click edit to fill the missing information and preview your details."></icon>
<icon *ngIf="!api.details" name="warning" class="clickable uk-margin-xsmall-left"
customClass="uk-text-warning"
uk-tooltip="This service doesn't exist in our database. Please contact administrator."></icon>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<modal-alert #editModal (alertOutput)="save()" [large]="true"
[okDisabled]="form?.disabled || form?.invalid || form?.pristine">
<div *ngIf="form" class="uk-grid" uk-grid>
<div class="uk-width-1-2@m uk-width-1-1" input placeholder="Service Name"
[formInput]="form.get('name')"></div>
<div class="uk-width-1-2@m uk-width-1-1" input type="URL" placeholder="Service URL"
[formInput]="form.get('url')"></div>
<div class="uk-width-1-1" input type="textarea" placeholder="Service Description"
[formInput]="form.get('description')">
</div>
<div class="uk-width-1-1" input type="chips" placeholder="Contact Emails" [visibleChips]="3"
[separators]="[',']" [formInput]="contacts" [validators]="Validators.email" [addExtraChips]="true" [hint]="'Add a contact email for your service'">
<div note>Separate groups with commas</div>
</div>
<div class="uk-width-1-1" input type="chips" placeholder="Target groups"
[addExtraChips]="true" [separators]="[',']" [visibleChips]="groups.length"
[formInput]="target" [options]="groups" [showOptionsOnEmpty]="false" [hint]="'Add a target group of your service'">
<div note>Separate groups with commas</div>
</div>
<div class="uk-width-1-2@m uk-width-1-1" input type="logoURL" placeholder="Logo URL"
[formInput]="form.get('logoURL')">
</div>
<div class="uk-width-1-2@m uk-width-1-1" input type="select" placeholder="Frequency of use (statistical purposes)"
[formInput]="form.get('frequency')" [options]="frequency">
</div>
<div class="uk-width-1-1">
<label class="uk-text-bold">Security Level</label>
<div id="security-hint" class="uk-margin">Register your service to get a client id and a client
secret. Use the client id and secret to make your requests.
<a href="https://graph.openaire.eu/docs/apis/authentication/#basic-service-authentication-and-registration" target="_blank">Read more...</a>
</div>
<div class="uk-flex uk-flex-middle">
<div class="uk-margin-small-right">
<label><input type="radio" name="securityLevel" [disabled]="index !== -1"
class="uk-radio" (ngModelChange)="securityChanged($event)"
[ngModel]="advanced" [value]="false"><span
class="uk-margin-small-left" [class.uk-text-muted]="index !== -1">Basic</span></label>
</div>
<div>
<label><input type="radio" name="securityLevel" [disabled]="index !== -1"
class="uk-radio" (ngModelChange)="securityChanged($event)"
[ngModel]="advanced" [value]="true"><span
class="uk-margin-small-left" [class.uk-text-muted]="index !== -1">Advanced</span></label>
</div>
</div>
</div>
<div id="advanced" class="uk-width-1-1" *ngIf="advanced">
<div class="uk-width-1-1">
<div class="uk-flex uk-flex-between">
<label class="uk-text-bold">Public Key</label>
<div class="uk-flex uk-flex-middle">
<div class="uk-margin-small-right">
<label><input type="radio" name="keyType" class="uk-radio"
(ngModelChange)="keyTypeChanged($event)" [ngModel]="byValue"
[value]="true"><span
class="uk-margin-small-left">By Value</span></label>
</div>
<div>
<label><input type="radio" name="keyType" class="uk-radio"
(ngModelChange)="keyTypeChanged($event)" [ngModel]="byValue"
[value]="false"><span
class="uk-margin-small-left">By URI</span></label>
</div>
</div>
</div>
</div>
<div *ngIf="byValue" class="uk-width-1-1" input type="textarea" [formInput]="form.get('value')"
rows="5" placeholder="Key Value" [hint]="hint"></div>
<div *ngIf="!byValue" class="uk-width-1-1" type="logoURL" input [formInput]="form.get('uri')"
placeholder="Key URI"></div>
</div>
</div>
</modal-alert>
<modal-alert #deleteModal [overflowBody]="false" (alertOutput)="confirm()"></modal-alert>
<modal-alert #infoModal [large]="false">
<div *ngIf="api?.details">
<ul class="uk-list uk-list-divider">
<li>
<div class="uk-flex">
<div class="uk-text-bold uk-width-small uk-margin-xsmall-right">Service Name</div>
<div class="uk-width-expand uk-text-break">{{api.details.clientName}}</div>
</div>
</li>
<li>
<div class="uk-flex">
<div class="uk-text-bold uk-width-small uk-margin-xsmall-right">
<div class="uk-text-bold uk-flex uk-flex-middle">
<span class="uk-margin-xsmall-right">Client Id</span>
<a class="uk-link uk-link-reset" (click)="copyToClipboard('client-id')"><icon name="content_copy" customClass="uk-text-secondary" [flex]="true"></icon></a>
</div>
</div>
<div id="client-id" class="uk-width-expand uk-text-break">{{api.details.clientId}}</div>
</div>
</li>
<li>
<div class="uk-flex">
<div class="uk-text-bold uk-width-small uk-margin-xsmall-right">Scope</div>
<div class="uk-width-expand uk-text-break">openid</div>
</div>
</li>
<li>
<div class="uk-flex">
<div class="uk-text-bold uk-width-small uk-margin-xsmall-right">Grant type</div>
<div class="uk-width-expand uk-text-break">{{api.details.grantTypes.join(', ')}}</div>
</div>
</li>
<li *ngIf="api.service.contacts?.length > 0">
<div class="uk-flex">
<div class="uk-text-bold uk-width-small uk-margin-xsmall-right">Contacts</div>
<div class="uk-width-expand uk-text-break">{{api.service.contacts.join(', ')}}</div>
</div>
</li>
<li *ngIf="!api.service.contacts?.length > 0 && api.details.contacts?.length > 0">
<div class="uk-flex">
<div class="uk-text-bold uk-width-small uk-margin-xsmall-right">Contacts</div>
<div class="uk-width-expand uk-text-break">{{api.details.contacts.join(', ')}}</div>
</div>
</li>
<ng-template [ngIf]="!api.service.keyType" [ngIfElse]="key">
<div class="uk-flex">
<div class="uk-text-bold uk-width-small uk-margin-xsmall-right">
<div class="uk-text-bold uk-flex uk-flex-middle">
<span class="uk-margin-xsmall-right">Client Secret</span>
<a class="uk-link uk-link-reset" (click)="copyToClipboard('client-secret')"><icon name="content_copy" customClass="uk-text-secondary" [flex]="true"></icon></a>
</div>
</div>
<div id="client-secret" class="uk-width-expand uk-text-break">{{api.details.clientSecret}}</div>
</div>
<li>
<div class="uk-flex">
<div class="uk-text-bold uk-width-small uk-margin-xsmall-right">Authentication method</div>
<div class="uk-width-expand uk-text-break">Client Secret Basic</div>
</div>
</li>
</ng-template>
<ng-template #key>
<li>
<div class="uk-flex">
<div class="uk-text-bold uk-width-small uk-margin-xsmall-right">Authentication method</div>
<div class="uk-width-expand uk-text-break">Asymmetrically-signed JWT assertion</div>
</div>
</li>
<li>
<div class="uk-flex">
<div class="uk-text-bold uk-width-small uk-margin-xsmall-right">Token Endpoint Authentication Signing Algorithm</div>
<div class="uk-width-expand uk-text-break">RSASSA using SHA-256 hash algorithm</div>
</div>
</li>
<div class="uk-flex">
<div class="uk-text-bold uk-width-small uk-margin-xsmall-right">
<div class="uk-text-bold uk-flex uk-flex-middle">
<span class="uk-margin-xsmall-right">Public Key</span>
<a class="uk-link uk-link-reset" (click)="copyToClipboard('public-key')"><icon name="content_copy" customClass="uk-text-secondary" [flex]="true"></icon></a>
</div>
</div>
<div *ngIf="api.details.jwksUri" id="public-key" class="uk-width-expand uk-text-break">{{api.details.jwksUri}}</div>
<div *ngIf="api.details.jwks" id="public-key" class="uk-width-expand uk-text-break">
<pre><code class="uk-overflow-auto">{{api.details.jwks.keys[0] | json}}</code></pre>
</div>
</div>
</ng-template>
<li>
<div class="uk-flex">
<div class="uk-text-bold uk-width-small uk-margin-xsmall-right">Creation Date</div>
<div class="uk-width-expand uk-text-break">{{api.service.creationDate | date: 'dd-MM-YYYY HH:mm'}}</div>
</div>
</li>
</ul>
</div>
</modal-alert>
`,
styleUrls: ['apis.component.less']
})
export class ApisComponent implements OnInit, OnDestroy {
form: FormGroup;
loading: boolean = true;
subscriptions: any[] = [];
apis: API[] = [];
api: API;
index: number = -1;
advanced: boolean = false;
byValue: boolean = true;
hint = '{"kty": ..., "e": ... , "use": ... , "kid": ..., "alg": ... , "n": ...}';
frequency: string[] = ['Daily', 'Weekly', 'Monthly'];
groups: string[] = ['Commerce', 'Education', 'Health', 'Non profit', 'Public Sector'];
@ViewChild("editModal") editModal: AlertModal;
@ViewChild("deleteModal") deleteModal: AlertModal;
@ViewChild("infoModal") infoModal: AlertModal;
constructor(private fb: FormBuilder,
private apisService: ApisService,
private userManagementService: UserManagementService,
private cdr: ChangeDetectorRef) {
}
ngOnInit() {
this.loading = true;
this.subscriptions.push(this.apisService.getMyServices().subscribe(apis => {
this.apis = apis;
this.loading = false;
}, error => {
this.apis = [];
this.loading = false;
}));
}
ngOnDestroy() {
this.subscriptions.forEach(subscription => {
if(subscription instanceof Subscription) {
subscription.unsubscribe();
}
})
}
get target():FormArray {
if(this.form) {
return <FormArray>this.form.get('target');
} else {
return this.fb.array([]);
}
}
get contacts():FormArray {
if(this.form) {
return <FormArray>this.form.get('contacts');
} else {
return this.fb.array([]);
}
}
openEditModal(index: number = -1) {
let api = new API();
this.index = index;
this.editModal.okButtonLeft = false;
if(index !== -1) {
api = this.apis[index];
this.editModal.alertTitle = "Edit Service";
this.editModal.okButtonText = "Update";
} else {
this.editModal.alertTitle = "Create Service";
this.editModal.okButtonText = "Create";
api.service.contacts = [this.userManagementService.user.email];
}
this.form = this.fb.group({
name: this.fb.control(api.service.name, Validators.required),
keyType: this.fb.control(api.service.keyType),
description: this.fb.control(api.service.description, Validators.required),
contacts: this.fb.array([], Validators.required),
frequency: this.fb.control(api.service.frequency),
target: this.fb.array([], Validators.required),
logoURL: this.fb.control(api.details.logoUri, [StringUtils.urlValidator()]),
url: this.fb.control(api.service.url, [Validators.required, StringUtils.urlValidator()]),
uri: this.fb.control(api.details.jwksUri, [StringUtils.urlValidator()]),
value: this.fb.control(null, [StringUtils.jsonValidator('The format should be {"kty": ..., "e": ... , "use": ... , "kid": ..., "alg": ... , "n": ...}')])
});
if(api.service.contacts) {
api.service.contacts.forEach(email => {
this.contacts.push(this.fb.control(email));
});
}
if(api.service.target) {
api.service.target.forEach(target => {
this.target.push(this.fb.control(target));
});
}
if(api.details?.jwks?.keys) {
this.form.get('value').setValue(JSON.stringify(api.details.jwks.keys[0]));
}
if(api.invalid) {
this.form.markAllAsTouched();
this.cdr.detectChanges();
}
this.advanced = !!this.form.get('keyType').getRawValue();
this.byValue = this.advanced && this.form.get('keyType').getRawValue() === 'value';
this.formChanges();
this.editModal.open();
}
openDeleteModal(index: number) {
this.index = index;
let api = this.apis[this.index];
this.deleteModal.alertTitle = 'Delete Service';
this.deleteModal.alertMessage = true;
this.deleteModal.message = 'You are going to delete ' + api.service.name + '. Are you sure you want to proceed?';
this.deleteModal.okButtonText = 'Yes';
this.deleteModal.cancelButtonText = 'No';
this.deleteModal.open();
}
openInfoModal(index: number) {
this.api = this.apis[index];
this.infoModal.okButton = false;
this.infoModal.cancelButtonText = 'Close';
this.infoModal.alertTitle = 'Details for ' + this.api.details.clientName;
this.infoModal.open();
}
save() {
this.loading = true;
if(this.index === -1) {
this.subscriptions.push(this.apisService.create(this.form.getRawValue()).subscribe(api => {
let index = this.apis.push(api) - 1;
NotificationHandler.rise('Your service has been created successfully.');
this.openInfoModal(index);
this.loading = false;
}, error => {
console.error(error);
NotificationHandler.rise('An error has occurred. Please try again later.', 'danger');
this.loading = false;
}));
} else {
this.subscriptions.push(this.apisService.save(this.apis[this.index].service.id, this.form.getRawValue()).subscribe(api => {
this.apis[this.index] = api;
NotificationHandler.rise('Your service has been saved successfully.');
this.openInfoModal(this.index);
this.loading = false;
}, error => {
console.error(error);
NotificationHandler.rise('An error has occurred. Please try again later.', 'danger');
this.loading = false;
}));
}
}
confirm() {
this.loading = true;
this.subscriptions.push(this.apisService.delete(this.apis[this.index].service.id).subscribe(() => {
this.apis.splice(this.index, 1);
NotificationHandler.rise('Your service has been created deleted.');
this.loading = false;
}, error => {
console.error(error);
NotificationHandler.rise('An error has occurred. Please try again later.', 'danger');
this.loading = false;
}));
}
securityChanged(event) {
this.advanced = event;
if(this.advanced) {
this.keyTypeChanged(true);
document.getElementById('advanced').scrollIntoView({behavior: 'smooth'});
} else {
this.form.get('keyType').setValue(null);
}
}
keyTypeChanged(event) {
this.byValue = event;
if(this.byValue) {
this.form.get('keyType').setValue('value');
} else {
this.form.get('keyType').setValue('uri');
}
}
formChanges() {
this.subscriptions.push(this.form.get('keyType').valueChanges.subscribe(value => {
if(value === 'value') {
this.form.get('value').addValidators(Validators.required);
this.form.get('uri').removeValidators(Validators.required);
} else if(value === 'uri') {
this.form.get('value').removeValidators(Validators.required);
this.form.get('uri').addValidators(Validators.required);
} else {
this.form.get('value').removeValidators(Validators.required);
this.form.get('uri').removeValidators(Validators.required);
}
this.form.get('value').updateValueAndValidity();
this.form.get('uri').updateValueAndValidity();
this.cdr.detectChanges();
}));
}
public getLogoUrl(api: API): string {
return api?.details?.logoUri?api.details.logoUri:"assets/common-assets/placeholder.png";
}
copyToClipboard(id: string) {
if(copy.exec(id)) {
NotificationHandler.rise('Copied to clipboard!');
} else {
NotificationHandler.rise('Unable to copy to clipboard!', 'danger');
}
}
protected readonly Validators = Validators;
}