Add zenodo DOI access token expiration check up and show proper pop up when the user has either no or expired token

This commit is contained in:
George Kalampokis 2020-04-06 18:16:05 +03:00
parent 07da2278cb
commit bdcbc5553b
11 changed files with 198 additions and 1 deletions

View File

@ -1,6 +1,8 @@
package eu.eudat.controllers; package eu.eudat.controllers;
import eu.eudat.exceptions.security.ExpiredTokenException;
import eu.eudat.exceptions.security.NonValidTokenException;
import eu.eudat.exceptions.security.NullEmailException; import eu.eudat.exceptions.security.NullEmailException;
import eu.eudat.logic.managers.UserManager; import eu.eudat.logic.managers.UserManager;
import eu.eudat.logic.proxy.config.configloaders.ConfigLoader; import eu.eudat.logic.proxy.config.configloaders.ConfigLoader;
@ -40,6 +42,7 @@ import org.springframework.social.oauth1.OAuthToken;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.transaction.Transactional; import javax.transaction.Transactional;
import java.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
@ -150,6 +153,16 @@ public class Login {
return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem<Principal>().payload(this.nonVerifiedUserAuthenticationService.Touch(principal.getToken())).status(ApiMessageCode.NO_MESSAGE)); return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem<Principal>().payload(this.nonVerifiedUserAuthenticationService.Touch(principal.getToken())).status(ApiMessageCode.NO_MESSAGE));
} }
@RequestMapping(method = RequestMethod.GET, value = {"/hasDOIToken"}, consumes = "application/json", produces = "application/json")
public @ResponseBody
ResponseEntity<ResponseItem<Boolean>> hasDOIToken(Principal principal) throws NullEmailException {
try {
return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem<Boolean>().payload(this.userManager.isDOITokenValid(principal)).status(ApiMessageCode.NO_MESSAGE));
} catch (NonValidTokenException | ExpiredTokenException | IOException e) {
return ResponseEntity.status(460).body(new ResponseItem<Boolean>().payload(false).status(ApiMessageCode.ERROR_MESSAGE).message(e.getMessage()));
}
}
@Transactional @Transactional
@RequestMapping(method = RequestMethod.POST, value = {"/logout"}, consumes = "application/json", produces = "application/json") @RequestMapping(method = RequestMethod.POST, value = {"/logout"}, consumes = "application/json", produces = "application/json")
public @ResponseBody public @ResponseBody

View File

@ -0,0 +1,8 @@
package eu.eudat.exceptions.security;
public class ExpiredTokenException extends Exception {
public ExpiredTokenException(String message) {
super(message);
}
}

View File

@ -7,6 +7,8 @@ import eu.eudat.data.entities.DMP;
import eu.eudat.data.entities.UserInfo; import eu.eudat.data.entities.UserInfo;
import eu.eudat.data.entities.UserRole; import eu.eudat.data.entities.UserRole;
import eu.eudat.data.query.items.table.userinfo.UserInfoTableRequestItem; import eu.eudat.data.query.items.table.userinfo.UserInfoTableRequestItem;
import eu.eudat.exceptions.security.ExpiredTokenException;
import eu.eudat.exceptions.security.NonValidTokenException;
import eu.eudat.exceptions.security.NullEmailException; import eu.eudat.exceptions.security.NullEmailException;
import eu.eudat.exceptions.security.UnauthorisedException; import eu.eudat.exceptions.security.UnauthorisedException;
import eu.eudat.logic.builders.entity.UserRoleBuilder; import eu.eudat.logic.builders.entity.UserRoleBuilder;
@ -29,6 +31,7 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import java.io.IOException; import java.io.IOException;
import java.time.Instant;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -112,4 +115,16 @@ public class UserManager {
dataTableData.setTotalCount((long) colaborators.size()); dataTableData.setTotalCount((long) colaborators.size());
return dataTableData; return dataTableData;
} }
public Boolean isDOITokenValid(Principal principal) throws NonValidTokenException, ExpiredTokenException, IOException {
if (principal.getZenodoToken() != null && !principal.getZenodoToken().isEmpty()) {
if (Instant.now().isBefore(principal.getZenodoDuration())) {
return true;
}
Map<String, Object> settings = Collections.singletonMap("zenodoToken", "");
this.updateSettings(settings, principal);
throw new ExpiredTokenException("Zenodo Token is expired");
}
throw new NonValidTokenException("This account has no Zenodo Token");
}
} }

View File

@ -159,4 +159,13 @@ export class AuthService extends BaseService {
}) })
); );
} }
public hasDOIToken(): Observable<any> {
this.actionUrl = this.configurationService.server + 'auth/';
const url = this.actionUrl + '/hasDOIToken';
const principal = this.current();
let headers = this.headers;
headers = headers.set('AuthToken', principal.token);
return this.http.get(url, { headers: headers });
}
} }

View File

@ -30,6 +30,7 @@ import { DmpWizardDatasetListingComponent } from '@app/ui/dmp/wizard/listing/dmp
import { CommonFormsModule } from '@common/forms/common-forms.module'; import { CommonFormsModule } from '@common/forms/common-forms.module';
import { FormValidationErrorsDialogModule } from '@common/forms/form-validation-errors-dialog/form-validation-errors-dialog.module'; import { FormValidationErrorsDialogModule } from '@common/forms/form-validation-errors-dialog/form-validation-errors-dialog.module';
import { CommonUiModule } from '@common/ui/common-ui.module'; import { CommonUiModule } from '@common/ui/common-ui.module';
import { MultipleChoiceDialogModule } from '@common/modules/multiple-choice-dialog/multiple-choice-dialog.module';
@NgModule({ @NgModule({
imports: [ imports: [
@ -42,7 +43,8 @@ import { CommonUiModule } from '@common/ui/common-ui.module';
AutoCompleteModule, AutoCompleteModule,
DmpRoutingModule, DmpRoutingModule,
DmpOverviewModule, DmpOverviewModule,
FormValidationErrorsDialogModule FormValidationErrorsDialogModule,
MultipleChoiceDialogModule
], ],
declarations: [ declarations: [
DmpListingComponent, DmpListingComponent,

View File

@ -22,6 +22,8 @@ import { Observable, of as observableOf } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { Role } from "@app/core/common/enum/role"; import { Role } from "@app/core/common/enum/role";
import { DmpInvitationDialogComponent } from '../invitation/dmp-invitation.component'; import { DmpInvitationDialogComponent } from '../invitation/dmp-invitation.component';
import { MultipleChoiceDialogModule } from '@common/modules/multiple-choice-dialog/multiple-choice-dialog.module';
import { MultipleChoiceDialogComponent } from '@common/modules/multiple-choice-dialog/multiple-choice-dialog.component';
@Component({ @Component({
selector: 'app-dmp-overview', selector: 'app-dmp-overview',
@ -322,6 +324,14 @@ export class DmpOverviewComponent extends BaseComponent implements OnInit {
} }
getDoi(dmp: DmpOverviewModel) { getDoi(dmp: DmpOverviewModel) {
this.authentication.hasDOIToken().subscribe(response => {
this.showConfirmationDOIDialog(dmp);
}, error => {
this.showErrorConfirmationDOIDialog(error.error.message, dmp);
});
}
showConfirmationDOIDialog(dmp: DmpOverviewModel) {
const dialogRef = this.dialog.open(ConfirmationDialogComponent, { const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
maxWidth: '600px', maxWidth: '600px',
restoreFocus: false, restoreFocus: false,
@ -346,6 +356,28 @@ export class DmpOverviewComponent extends BaseComponent implements OnInit {
}); });
} }
showErrorConfirmationDOIDialog(message: string, dmp: DmpOverviewModel) {
const dialogRef = this.dialog.open(MultipleChoiceDialogComponent, {
maxWidth: '600px',
restoreFocus: false,
data: {
message: message,
titles: [ this.language.instant('DMP-OVERVIEW.MULTIPLE-DIALOG.ZENODO-LOGIN'), this.language.instant('DMP-OVERVIEW.MULTIPLE-DIALOG.USE-DEFAULT')]
}
});
dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => {
switch (result) {
case 0:
this.authentication.logout();
this.router.navigate(['/login/external/zenodo']);
break;
case 1:
this.showConfirmationDOIDialog(dmp);
break;
}
});
}
onDOICallbackSuccess(): void { onDOICallbackSuccess(): void {
this.uiNotificationService.snackBarNotification(this.language.instant('DMP-EDITOR.SNACK-BAR.SUCCESSFUL-DOI'), SnackBarNotificationLevel.Success); this.uiNotificationService.snackBarNotification(this.language.instant('DMP-EDITOR.SNACK-BAR.SUCCESSFUL-DOI'), SnackBarNotificationLevel.Success);
} }

View File

@ -525,6 +525,10 @@
"ERROR": { "ERROR": {
"DELETED-DMP": "The requested DMP is deleted", "DELETED-DMP": "The requested DMP is deleted",
"FORBIDEN-DMP": "You are not allowed to access this DMP" "FORBIDEN-DMP": "You are not allowed to access this DMP"
},
"MULTIPLE-DIALOG": {
"ZENODO-LOGIN": "Login with Zenodo",
"USE-DEFAULT": "Use Default Token"
} }
}, },
"DATASET-LISTING": { "DATASET-LISTING": {

View File

@ -0,0 +1,30 @@
<div class="confirmation-dialog">
<div *ngIf="data.icon" class="row d-flex flex-row">
<div class="col-auto close-btn justify-content-start">
<mat-icon color="warn">{{ data.icon }}</mat-icon>
</div>
<div *ngIf="data.warning" class="col justify-content-center warn-text">{{ data.warning }}</div>
<div class="col-auto close-btn justify-content-end" (click)="close()">
<mat-icon>close</mat-icon>
</div>
</div>
<div class="row d-flex flex-row">
<div class="col message pb-4 pl-3">{{ data.message }}</div>
<div *ngIf="!data.icon" class="col-auto close-btn justify-content-end" (click)="close()">
<mat-icon>close</mat-icon>
</div>
</div>
<div *ngIf="data.privacyPolicyNames" class="row d-flex flex-col">
<div class="col-12 privacy-policy-names">{{ data.privacyPolicyNames }}</div>
<mat-checkbox [(ngModel)]="agreePrivacyPolicyNames" required class="checkbox-privacy">
{{'GENERAL.CONFIRMATION-DIALOG.ACTIONS.POLICY-AGREE' | translate}}
</mat-checkbox>
<div *ngIf="!agreePrivacyPolicyNames" class="required-policy">{{'GENERAL.CONFIRMATION-DIALOG.ACTIONS.REQUIRED' | translate}}</div>
</div>
<div class="row">
<div class="col"></div>
<span *ngFor="let title of data.titles; let i = index">
<div class="col-auto"><button mat-raised-button type="button" (click)="apply(i)" class="confirm">{{ title }}</button></div>
</span>
</div>
</div>

View File

@ -0,0 +1,44 @@
.confirmation-dialog {
.confirmation {
padding-bottom: 20px;
}
.privacy-policy-names {
font-weight: 700;
padding: 1em;
}
.close-btn {
margin-left: auto;
cursor: pointer;
}
.warn-text {
color: #f44336;
}
.cancel {
background-color: #aaaaaa;
color: #ffffff;
}
.confirm {
background-color: #2cba6c;
color: #ffffff;
}
.delete {
background-color: #ba2c2c;
color: #ffffff;
}
.checkbox-privacy {
padding: 0em 1em;
}
.required-policy {
padding: 0em 1.2em 1em;
font-size: smaller;
color: #f44336;
}
}

View File

@ -0,0 +1,25 @@
import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
@Component({
selector: 'app-multiple-choice-dialog',
templateUrl: './multiple-choice-dialog.component.html',
styleUrls: ['./multiple-choice-dialog.component.scss']
})
export class MultipleChoiceDialogComponent {
agreePrivacyPolicyNames = false;
constructor(
public dialogRef: MatDialogRef<MultipleChoiceDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any
) {
}
close() {
this.dialogRef.close(false);
}
apply(i: number) {
this.dialogRef.close(i);
}
}

View File

@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MultipleChoiceDialogComponent } from './multiple-choice-dialog.component';
import { CommonUiModule } from '@common/ui/common-ui.module';
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [CommonUiModule, FormsModule],
declarations: [MultipleChoiceDialogComponent],
exports: [MultipleChoiceDialogComponent],
entryComponents: [MultipleChoiceDialogComponent]
})
export class MultipleChoiceDialogModule { }