From 7d070a339cab73c4940007aaaffc0a8a3a7d0b9a Mon Sep 17 00:00:00 2001 From: gkolokythas Date: Wed, 23 Oct 2019 13:38:27 +0300 Subject: [PATCH] Implements OpenAIRE login provider. (Issue #200) --- .../main/java/eu/eudat/controllers/Login.java | 13 +++- .../OpenAIRE/OpenAIRECustomProvider.java | 10 +++ .../OpenAIRE/OpenAIRECustomProviderImpl.java | 63 ++++++++++++++++++ .../OpenAIRE/OpenAIREUser.java | 37 ++++++++++ .../validators/TokenValidatorFactoryImpl.java | 13 +++- .../openaire/OpenAIRETokenValidator.java | 52 +++++++++++++++ .../openaire/helpers/OpenAIRERequest.java | 12 ++++ .../helpers/OpenAIREResponseToken.java | 20 ++++++ .../resources/application-devel.properties | 7 ++ .../application-production.properties | 7 ++ .../resources/application-staging.properties | 7 ++ .../src/app/core/common/enum/auth-provider.ts | 3 +- .../src/app/ui/auth/login/img/openaire.png | Bin 0 -> 11910 bytes .../app/ui/auth/login/img/openaire_small.png | Bin 0 -> 2883 bytes .../app/ui/auth/login/login.component.html | 4 ++ .../app/ui/auth/login/login.component.scss | 15 +++++ .../src/app/ui/auth/login/login.component.ts | 9 +++ .../src/app/ui/auth/login/login.module.ts | 4 +- .../src/app/ui/auth/login/login.routing.ts | 4 +- .../openaire-login.component.html | 0 .../openaire-login.component.ts | 61 +++++++++++++++++ dmp-frontend/src/environments/environment.ts | 8 ++- 22 files changed, 341 insertions(+), 8 deletions(-) create mode 100644 dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/OpenAIRE/OpenAIRECustomProvider.java create mode 100644 dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/OpenAIRE/OpenAIRECustomProviderImpl.java create mode 100644 dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/OpenAIRE/OpenAIREUser.java create mode 100644 dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/openaire/OpenAIRETokenValidator.java create mode 100644 dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/openaire/helpers/OpenAIRERequest.java create mode 100644 dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/openaire/helpers/OpenAIREResponseToken.java create mode 100644 dmp-frontend/src/app/ui/auth/login/img/openaire.png create mode 100644 dmp-frontend/src/app/ui/auth/login/img/openaire_small.png create mode 100644 dmp-frontend/src/app/ui/auth/login/openaire-login/openaire-login.component.html create mode 100644 dmp-frontend/src/app/ui/auth/login/openaire-login/openaire-login.component.ts diff --git a/dmp-backend/web/src/main/java/eu/eudat/controllers/Login.java b/dmp-backend/web/src/main/java/eu/eudat/controllers/Login.java index 15acf15ae..53950a5a2 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/controllers/Login.java +++ b/dmp-backend/web/src/main/java/eu/eudat/controllers/Login.java @@ -10,6 +10,9 @@ import eu.eudat.logic.security.validators.b2access.helpers.B2AccessResponseToken import eu.eudat.logic.security.validators.linkedin.LinkedInTokenValidator; import eu.eudat.logic.security.validators.linkedin.helpers.LinkedInRequest; import eu.eudat.logic.security.validators.linkedin.helpers.LinkedInResponseToken; +import eu.eudat.logic.security.validators.openaire.OpenAIRETokenValidator; +import eu.eudat.logic.security.validators.openaire.helpers.OpenAIRERequest; +import eu.eudat.logic.security.validators.openaire.helpers.OpenAIREResponseToken; import eu.eudat.logic.security.validators.orcid.ORCIDTokenValidator; import eu.eudat.logic.security.validators.orcid.helpers.ORCIDRequest; import eu.eudat.logic.security.validators.orcid.helpers.ORCIDResponseToken; @@ -41,6 +44,7 @@ public class Login { private B2AccessTokenValidator b2AccessTokenValidator; private ORCIDTokenValidator orcidTokenValidator; private LinkedInTokenValidator linkedInTokenValidator; + private OpenAIRETokenValidator openAIRETokenValidator; private Logger logger; @@ -48,13 +52,14 @@ public class Login { @Autowired public Login(CustomAuthenticationProvider customAuthenticationProvider, AuthenticationService nonVerifiedUserAuthenticationService, TwitterTokenValidator twitterTokenValidator, LinkedInTokenValidator linkedInTokenValidator, B2AccessTokenValidator b2AccessTokenValidator, - ORCIDTokenValidator orcidTokenValidator, UserManager userManager, Logger logger) { + ORCIDTokenValidator orcidTokenValidator, OpenAIRETokenValidator openAIRETokenValidator, UserManager userManager, Logger logger) { this.customAuthenticationProvider = customAuthenticationProvider; this.nonVerifiedUserAuthenticationService = nonVerifiedUserAuthenticationService; this.twitterTokenValidator = twitterTokenValidator; this.linkedInTokenValidator = linkedInTokenValidator; this.b2AccessTokenValidator = b2AccessTokenValidator; this.orcidTokenValidator = orcidTokenValidator; + this.openAIRETokenValidator = openAIRETokenValidator; this.logger = logger; this.userManager = userManager; } @@ -99,6 +104,12 @@ public class Login { return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().payload(this.orcidTokenValidator.getAccessToken(orcidRequest)).status(ApiMessageCode.NO_MESSAGE)); } + @RequestMapping(method = RequestMethod.POST, value = {"/openAireRequestToken"}, produces = "application/json", consumes = "application/json") + public @ResponseBody + ResponseEntity> openAIRERequestToken(@RequestBody OpenAIRERequest openAIRERequest) { + return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().payload(this.openAIRETokenValidator.getAccessToken(openAIRERequest)).status(ApiMessageCode.NO_MESSAGE)); + } + @RequestMapping(method = RequestMethod.POST, value = {"/me"}, consumes = "application/json", produces = "application/json") public @ResponseBody ResponseEntity> authMe(Principal principal) throws NullEmailException { diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/OpenAIRE/OpenAIRECustomProvider.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/OpenAIRE/OpenAIRECustomProvider.java new file mode 100644 index 000000000..3b7855e08 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/OpenAIRE/OpenAIRECustomProvider.java @@ -0,0 +1,10 @@ +package eu.eudat.logic.security.customproviders.OpenAIRE; + +import eu.eudat.logic.security.validators.openaire.helpers.OpenAIREResponseToken; + +public interface OpenAIRECustomProvider { + + OpenAIREResponseToken getAccessToken(String code, String redirectUri, String clientId, String clientSecret); + + OpenAIREUser getUser(String accessToken); +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/OpenAIRE/OpenAIRECustomProviderImpl.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/OpenAIRE/OpenAIRECustomProviderImpl.java new file mode 100644 index 000000000..3d1d7e276 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/OpenAIRE/OpenAIRECustomProviderImpl.java @@ -0,0 +1,63 @@ +package eu.eudat.logic.security.customproviders.OpenAIRE; + +import eu.eudat.logic.security.validators.openaire.helpers.OpenAIREResponseToken; +import eu.eudat.logic.services.operations.authentication.AuthenticationService; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +@Component("openAIRECustomProvider") +public class OpenAIRECustomProviderImpl implements OpenAIRECustomProvider { + + private Environment environment; + + public OpenAIRECustomProviderImpl(Environment environment) { + this.environment = environment; + } + + public OpenAIREUser getUser(String accessToken) { + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = this.createBearerAuthHeaders(accessToken); + HttpEntity entity = new HttpEntity<>(headers); + + Map values = restTemplate.exchange(this.environment.getProperty("openaire.login.userinfo_endpoint"), HttpMethod.GET, entity, Map.class).getBody(); + return new OpenAIREUser().getOpenAIREUser(values); + } + + public OpenAIREResponseToken getAccessToken(String code, String redirectUri, String clientId, String clientSecret) { + RestTemplate template = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap map = new LinkedMultiValueMap(); + + map.add("grant_type", "authorization_code"); + map.add("code", code); + map.add("redirect_uri", redirectUri); + map.add("client_id", clientId); + map.add("client_secret", clientSecret); + HttpEntity> request = new HttpEntity>(map, headers); + + Map values = template.postForObject(this.environment.getProperty("openaire.login.access_token_url"), request, Map.class); + OpenAIREResponseToken openAIREResponseToken = new OpenAIREResponseToken(); + openAIREResponseToken.setAccessToken((String) values.get("access_token")); + openAIREResponseToken.setExpiresIn((Integer) values.get("expires_in")); + + return openAIREResponseToken; + } + + private HttpHeaders createBearerAuthHeaders(String accessToken) { + return new HttpHeaders() {{ + String authHeader = "Bearer " + new String(accessToken); + set("Authorization", authHeader); + }}; + } +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/OpenAIRE/OpenAIREUser.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/OpenAIRE/OpenAIREUser.java new file mode 100644 index 000000000..e20b65132 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/OpenAIRE/OpenAIREUser.java @@ -0,0 +1,37 @@ +package eu.eudat.logic.security.customproviders.OpenAIRE; + +import java.util.Map; + +public class OpenAIREUser { + private String id; + private String name; + private String email; + + public String getId() { + return id; + } + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + public void setEmail(String email) { + this.email = email; + } + + public OpenAIREUser getOpenAIREUser(Object data) { + this.id = (String) ((Map) data).get("sub"); + this.name = (String) ((Map) data).get("name"); + this.email = (String) ((Map) data).get("email"); + return this; + } +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/TokenValidatorFactoryImpl.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/TokenValidatorFactoryImpl.java index ed17db2e0..b4c5f6b3f 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/TokenValidatorFactoryImpl.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/TokenValidatorFactoryImpl.java @@ -2,12 +2,13 @@ package eu.eudat.logic.security.validators; import eu.eudat.logic.security.customproviders.B2Access.B2AccessCustomProvider; import eu.eudat.logic.security.customproviders.LinkedIn.LinkedInCustomProvider; -import eu.eudat.logic.security.customproviders.LinkedIn.LinkedInCustomProviderImpl; import eu.eudat.logic.security.customproviders.ORCID.ORCIDCustomProvider; +import eu.eudat.logic.security.customproviders.OpenAIRE.OpenAIRECustomProvider; import eu.eudat.logic.security.validators.b2access.B2AccessTokenValidator; import eu.eudat.logic.security.validators.facebook.FacebookTokenValidator; import eu.eudat.logic.security.validators.google.GoogleTokenValidator; import eu.eudat.logic.security.validators.linkedin.LinkedInTokenValidator; +import eu.eudat.logic.security.validators.openaire.OpenAIRETokenValidator; import eu.eudat.logic.security.validators.orcid.ORCIDTokenValidator; import eu.eudat.logic.security.validators.twitter.TwitterTokenValidator; import eu.eudat.logic.services.ApiContext; @@ -20,7 +21,7 @@ import org.springframework.stereotype.Service; @Service("tokenValidatorFactory") public class TokenValidatorFactoryImpl implements TokenValidatorFactory { public enum LoginProvider { - GOOGLE(1), FACEBOOK(2), TWITTER(3), LINKEDIN(4), NATIVELOGIN(5), B2_ACCESS(6), ORCID(7); + GOOGLE(1), FACEBOOK(2), TWITTER(3), LINKEDIN(4), NATIVELOGIN(5), B2_ACCESS(6), ORCID(7), OPENAIRE(8); private int value; @@ -48,6 +49,8 @@ public class TokenValidatorFactoryImpl implements TokenValidatorFactory { return B2_ACCESS; case 7: return ORCID; + case 8: + return OPENAIRE; default: throw new RuntimeException("Unsupported LoginProvider"); } @@ -60,18 +63,20 @@ public class TokenValidatorFactoryImpl implements TokenValidatorFactory { private B2AccessCustomProvider b2AccessCustomProvider; private ORCIDCustomProvider orcidCustomProvider; private LinkedInCustomProvider linkedInCustomProvider; + private OpenAIRECustomProvider openAIRECustomProvider; @Autowired public TokenValidatorFactoryImpl( ApiContext apiContext, Environment environment, AuthenticationService nonVerifiedUserAuthenticationService, B2AccessCustomProvider b2AccessCustomProvider, - ORCIDCustomProvider orcidCustomProvider, LinkedInCustomProvider linkedInCustomProvider) { + ORCIDCustomProvider orcidCustomProvider, LinkedInCustomProvider linkedInCustomProvider, OpenAIRECustomProvider openAIRECustomProvider) { this.apiContext = apiContext; this.environment = environment; this.nonVerifiedUserAuthenticationService = nonVerifiedUserAuthenticationService; this.b2AccessCustomProvider = b2AccessCustomProvider; this.orcidCustomProvider = orcidCustomProvider; this.linkedInCustomProvider = linkedInCustomProvider; + this.openAIRECustomProvider = openAIRECustomProvider; } public TokenValidator getProvider(LoginProvider provider) { @@ -88,6 +93,8 @@ public class TokenValidatorFactoryImpl implements TokenValidatorFactory { return new B2AccessTokenValidator(this.environment, this.nonVerifiedUserAuthenticationService, this.b2AccessCustomProvider); case ORCID: return new ORCIDTokenValidator(this.environment, this.nonVerifiedUserAuthenticationService, this.orcidCustomProvider); + case OPENAIRE: + return new OpenAIRETokenValidator(this.environment, this.nonVerifiedUserAuthenticationService, this.openAIRECustomProvider); default: throw new RuntimeException("Login Provider Not Implemented"); } diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/openaire/OpenAIRETokenValidator.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/openaire/OpenAIRETokenValidator.java new file mode 100644 index 000000000..74981af98 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/openaire/OpenAIRETokenValidator.java @@ -0,0 +1,52 @@ +package eu.eudat.logic.security.validators.openaire; + +import eu.eudat.exceptions.security.NonValidTokenException; +import eu.eudat.exceptions.security.NullEmailException; +import eu.eudat.logic.security.customproviders.OpenAIRE.OpenAIRECustomProvider; +import eu.eudat.logic.security.customproviders.OpenAIRE.OpenAIREUser; +import eu.eudat.logic.security.validators.TokenValidator; +import eu.eudat.logic.security.validators.openaire.helpers.OpenAIRERequest; +import eu.eudat.logic.security.validators.openaire.helpers.OpenAIREResponseToken; +import eu.eudat.logic.services.operations.authentication.AuthenticationService; +import eu.eudat.models.data.login.LoginInfo; +import eu.eudat.models.data.loginprovider.LoginProviderUser; +import eu.eudat.models.data.security.Principal; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +@Component("openAIRETokenValidator") +public class OpenAIRETokenValidator implements TokenValidator { + + private Environment environment; + private AuthenticationService nonVerifiedUserAuthenticationService; + private OpenAIRECustomProvider openAIRECustomProvider; + + public OpenAIRETokenValidator(Environment environment, AuthenticationService nonVerifiedUserAuthenticationService, OpenAIRECustomProvider openAIRECustomProvider) { + this.environment = environment; + this.nonVerifiedUserAuthenticationService = nonVerifiedUserAuthenticationService; + this.openAIRECustomProvider = openAIRECustomProvider; + } + + public OpenAIREResponseToken getAccessToken(OpenAIRERequest openAIRERequest) { + return this.openAIRECustomProvider.getAccessToken( + openAIRERequest.getCode(), this.environment.getProperty("openaire.login.redirect_uri"), + this.environment.getProperty("openaire.login.client_id"), this.environment.getProperty("openaire.login.client_secret") + ); + } + + @Override + public Principal validateToken(LoginInfo credentials) throws NonValidTokenException, IOException, GeneralSecurityException, NullEmailException { + OpenAIREUser openAIREUser = this.openAIRECustomProvider.getUser(credentials.getTicket()); + LoginProviderUser user = new LoginProviderUser(); + user.setId(openAIREUser.getId()); + user.setEmail(openAIREUser.getEmail()); + user.setName(openAIREUser.getName()); + user.setProvider(credentials.getProvider()); + user.setSecret(credentials.getTicket()); + + return this.nonVerifiedUserAuthenticationService.Touch(user); + } +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/openaire/helpers/OpenAIRERequest.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/openaire/helpers/OpenAIRERequest.java new file mode 100644 index 000000000..928ef2b55 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/openaire/helpers/OpenAIRERequest.java @@ -0,0 +1,12 @@ +package eu.eudat.logic.security.validators.openaire.helpers; + +public class OpenAIRERequest { + private String code; + + public String getCode() { + return code; + } + public void setCode(String code) { + this.code = code; + } +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/openaire/helpers/OpenAIREResponseToken.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/openaire/helpers/OpenAIREResponseToken.java new file mode 100644 index 000000000..372a3482e --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/openaire/helpers/OpenAIREResponseToken.java @@ -0,0 +1,20 @@ +package eu.eudat.logic.security.validators.openaire.helpers; + +public class OpenAIREResponseToken { + private String accessToken; + private Integer expiresIn; + + public String getAccessToken() { + return accessToken; + } + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public Integer getExpiresIn() { + return expiresIn; + } + public void setExpiresIn(Integer expiresIn) { + this.expiresIn = expiresIn; + } +} diff --git a/dmp-backend/web/src/main/resources/application-devel.properties b/dmp-backend/web/src/main/resources/application-devel.properties index f05d23d04..ed4bfe83e 100644 --- a/dmp-backend/web/src/main/resources/application-devel.properties +++ b/dmp-backend/web/src/main/resources/application-devel.properties @@ -52,6 +52,13 @@ orcid.login.client_secret=f6ddc717-f49e-4bce-b302-2e479b226a24 orcid.login.access_token_url=https://orcid.org/oauth/token orcid.login.redirect_uri=http://localhost:4200/login/external/orcid +#############OPENAIRE CONFIGURATIONS######### +openaire.login.client_id= +openaire.login.client_secret= +openaire.login.access_token_url= +openaire.login.redirect_uri= +openaire.login.userinfo_endpoint= + #############CONFIRMATION EMAIL CONFIGURATIONS######### conf_email.expiration_time_seconds=14400 conf_email.subject=OpenDMP email confirmation diff --git a/dmp-backend/web/src/main/resources/application-production.properties b/dmp-backend/web/src/main/resources/application-production.properties index cd4b738b6..28aa72581 100644 --- a/dmp-backend/web/src/main/resources/application-production.properties +++ b/dmp-backend/web/src/main/resources/application-production.properties @@ -68,6 +68,13 @@ orcid.login.client_secret= orcid.login.access_token_url=https://orcid.org/oauth/token orcid.login.redirect_uri=https://opendmp.eu/login/external/orcid +#############OPENAIRE CONFIGURATIONS######### +openaire.login.client_id= +openaire.login.client_secret= +openaire.login.access_token_url= +openaire.login.redirect_uri= +openaire.login.userinfo_endpoint= + #############SPRING DATASOURCE CONFIGURATIONS######### spring.datasource.maxIdle: 10 spring.datasource.max-active: 70 diff --git a/dmp-backend/web/src/main/resources/application-staging.properties b/dmp-backend/web/src/main/resources/application-staging.properties index 3cdae2ccc..8ab713dfd 100644 --- a/dmp-backend/web/src/main/resources/application-staging.properties +++ b/dmp-backend/web/src/main/resources/application-staging.properties @@ -74,6 +74,13 @@ orcid.login.client_secret= orcid.login.access_token_url=https://orcid.org/oauth/token orcid.login.redirect_uri=https://opendmp.eu/login/external/orcid +#############OPENAIRE CONFIGURATIONS######### +openaire.login.client_id= +openaire.login.client_secret= +openaire.login.access_token_url= +openaire.login.redirect_uri= +openaire.login.userinfo_endpoint= + #############ZENODO CONFIGURATIONS######### zenodo.url=https://sandbox.zenodo.org/api/ zenodo.access_token= diff --git a/dmp-frontend/src/app/core/common/enum/auth-provider.ts b/dmp-frontend/src/app/core/common/enum/auth-provider.ts index 460ce2b50..3dcc29b9b 100644 --- a/dmp-frontend/src/app/core/common/enum/auth-provider.ts +++ b/dmp-frontend/src/app/core/common/enum/auth-provider.ts @@ -5,5 +5,6 @@ export enum AuthProvider { LinkedIn = 4, //NativeLogin=5, B2Access = 6, - ORCID = 7 + ORCID = 7, + OpenAire = 8 } diff --git a/dmp-frontend/src/app/ui/auth/login/img/openaire.png b/dmp-frontend/src/app/ui/auth/login/img/openaire.png new file mode 100644 index 0000000000000000000000000000000000000000..473bb3f59fe49bd9085c30ba6c0cb18540887e94 GIT binary patch literal 11910 zcmcI~^;eWp+w~1Ns30NT4FZw^(jna?&47e7NJ@7I!qD9%prmv+f=CGnNOz}&nu1|yct9p#2yYE#tRVY6_H{H;!WOM#XD|r0qPJ~(_f?o6Z%IWLhQ?J|$ubjsH)6Jnk zn!~Mi`b%!_D{Av=iZD16YpyUnN_&oX^ zwzbK1?2m`mJrj%?!prV6xlQ$zP?0n4a8qaHPtf;wMrg{7TXnP}s2GnX+~OpTl)%Hi zC~^1{QY+S9zi(n)cmV!{z@J)K%y1|1oLeVU$X&=m35^ZB+e-KM{>JR*eK!AY9uzSo z40Q=RM8WdDLR~^7SQ5B2Lj?nq)T;IGn|9nbF^WF5@4B{v!xjD=PNU0wh!RMtI&aF| z^0iG`kyHqefTYh^m4L%uVepGa$zfifrP1rG=RGW3=ph1;w}&|iQO77<4yyB#>79bO zbDymOcR7R{BC_-DhX|nmg8Pr7tQ8=W!TtP)Z0HZrVd%~X@B@U)xa9D2yeR(ZwWun& z=@VwI2>Jt+T+9Yj^+r~f=0AI2>S7*V7Ie`x6fT>9xA!VJbb8>Z$RV~@_t<^B{EKUbf+_a)n%gKL=99B{0c1)37vpxzY}BcL$!+DqEjiL z&?me3n?l|-;MUv82@k$ldKI{QeZ@HM0Ky-e%?$Un2;6;hg<-!Sv!~wI^dk|smI7Kp zgI?u3_3lgZU_%qXhdvDS#S-yyBV9g`$hhI z#3xzJ*O#*Gw(H4izj(`Y)|1}c8*Dte0J}y(p<}8=k4Ym{WqS}ShsXkJqtb#!<$D(Q z6}mk{n*I!q{c>8*y-7br`v2t=$G$rgzX`DOzZ+)^yn1zamcVb$51vP{u zD{oMPV1yEb31ADp>ioomN;Tge8~lvq9I!&)rnt=qQYqu(V=#Gqw7u1aey-KAN3R*g zQYpjSIE&fUBBX6dJYinV2yfcZR;x=F?Znei4p=UwvBjDYgR zrA+dL_LSIJ*WR~wns?EEeqzm#ik-iYtuj<>DC?I?<>;OD+K-}l4V%c-6SGeIu0y@1 zBdbw2A{DZZ4>^@gHt3DKH8OHC(mf+@B0|6WhLzvx5>hs#Cop`ve8fjhopHNDCvta# zJSoGe5T<>u%Utj92tA}tC6FYw0S7DQPD(g3YV?fyDy?X}`eu>ql-h^=ajL=Y-486T zON$}2X@@JBA#0K$EkDu z4J_U23}4eEr=DG=x5Xl++@YO}%)7sid*f$L51lxa99qlVj@dsvx`~Pvetvhq^X9?r zSzFQ6Oh$4DAO23afCO_I0kpynR&$gRF{gphlG?2eit1T+ zk)5@I(!WiU?rA2EAxqusMhe5PRv*n;N0cxzQ^?$Z(FYDpZm(2{O`=hSh;f#U=g!}K zI;~d^xI!+c@7=FeZ+UcgGDcjEXlGkBhdaz?62tmsGxXqY#6~d~x8~4^7?ed|hC;8w z5=D~gR^3br-3CnTry8<|5J$;0bvqb+RJdSR@(Dm$@>yO$ioabydxwfF%)O&>fmw;r zHPLao#;B;05|Cs*fF-tO%EMJ_%eQfdyaG_6fiS(iRTAhi!_9UCE@p(2jGJ4~s+NrT z-R4E-9VdU&qMeTpozSEzxdbKw2{|}jWH!=00m-(nP@UxgkATI@a??KMxOw7wamynL zo#as>8)!!b0vX{Ei3(dEZ|x3){&7a8n(0{P*vX@NA>`DgwN7kIO@l*RGm#8 zP}rp|t-*WoedW)^HwpvUOc_XC26iVfZeRMiowm zAyaH6%Y!{^Cm(t&3Q!tz#K}!D(RslJ%{thND+zZ!frCx$r^w-_k1KcZ+V?{kbC>fi zR6T8mZ|Bf6^19R|5K6w1!?*3xN`*2hXClIae7;e*n7j^XHq`4v2X$B~{gSP(@Citw zz&m8)*5W_sfblNcDOkz&XE02gMZ`L*2sub4I==>Zy8o3Q1M!7nZ30PvoxmA>+f9*~ zMb@Dm0W+WrBwO{d%C^P&>)6Rt8OO#U@KTg?Ytd2YS#@+?mtF)(s(?!ior7D|=J{^= zxRv1Oyr#{T>C#OLKCCO&2|VQO>-ym}fn^h~Tj83mF{aktv$47$GU(R3~hi7xBE zpY~>;ru6V0tRJir-lAluO!=dBJIY<9UlL9gzFr3xE{!a7#rSqsOBgeZPvD* z%eQykEI(AZQ}nc5HM<{eScb+rAI6@vHw@%up~8w^0QgNjHT^y4X1O64xh~uTO&a~? zt_;s==(UW&H$5a#Hz8x7wWKZ*xUL@jwA#@A5a-==*zZEFxj zESi2sD#%l@cT7bUYkB*16ihhcq&*?lSthF?4l*-?Mj2cy1x0$_y#I{3i#+f3_ZnX?Zy z`?G!1H$YgneGz5)29X9b+{3M%yj^Y{eZ(??>Lz>Je2VWQUCMqC>5qV;z7MIAuktc;z0G5t<0{I> z!L06TP3nL15Iyba<$X!=BkN7spJM%IJ>!1-i!(ODvYQe?pU|{N?z22O4McrO|Ju$QGc14k%@l?W*(0t+E1b~hzO=)w9`*9@ zEv+<WXKTf>XSew;l!5a7bEDh*kF! z6&LzT^14w0+x^i~9sE_FSZcs6IG~>vR+N*+cV~~Tx1(!)u-`q8$!|(S6HOzNkI)wn zGsSsW4OK4guk3VWV(-qV-CZnRjX5#=9iO=VuuLJ2-T8Ocxk-za{pM~h_wtN0{n$`Y zPB;YhB{^@3oLmJ}ExusOF_X*%*V!gM^yj5Ze5czly4It2MxLNxAQ-KTWSM%H+b%wx92O zj$2_8Fdu|!p5tG3|Fxjw^d5J-ov9#abnhnnS;QPDAc+ipY|%K{a;c~Dp_?;$+Pp)Z z_H%>;BuG6!w!te&HZHqgXAn5>fXJF9mHF@I}gY>Qje8OqdS%g;iuPb*f_ZMYmVsO;4D+#PmY1E z2GmKO!V;+KTcj17E**f&Ju`TX(8q8EgL88m2_p?&0#f|EE&(*1wFUuJ3$Qz_ncuZLBez)hT(B)hpM@k^lrPO9k_YuW}BR~8_&xr z-$(6f{o_^nrCZd+TyF7g7xGvDNFNM%0XY)U^Bpk!T@8$NcY^m#zX^&GiE%WnMW)Ze z5ihh()Fw!DwUMzbk(OsH36vp#j%uD7|Gx0e>Tf#4#!#lj)lKhrw-^KTqmdag|Cpig zKq;xASQ{jYU!cDn0L}Xp+eQeCTcaeQq2ot@X0agVr{nBo3l9na+zKmvdZ0<4c7GBA zYjJrJ7S!{-1TYwJCDVn5qE5#HV(4>p&-ZXJ*NH2j?jG{M<^iz~_+&?B)GAnxf z_P5=SW&V_W1`b@p>*XTuHzk3nlhC*xjZ`(tZznnqQ;}i}@K$(`Z`j01E!*~pI$0~d zM34;>gXH*JZY5svOPlY`ggLEFXtCV^&^Vlrw*EL3zx1b(sQt!dXrQM39D_fB1!suSgTQ{E{GA5s^%h=>~2nlU-9x1tic?(qol7>TyD zCoF2U&66^gVNL3yYOJh(sJnkg@tIH|!hT;wk3i6&*4;W-@Qs=|g)F1Yq~+xAzuWynU0?*O_p%C}#2F=()UNqM&ZnBwoI zexE?a-{a4k+^LFZi#lM=Ri8+IydYSEOF)82%cpqpvQ)zWD;Kv`921u4SZ6-zbi;Rc z5#3(YDSI<9$F+&-b&QMIP4WmP*}YL%n$c+9TsI7;B{jg(25Wit|0vGog5=v**nmuC zjUw92W_+`Oy}@{S)t>_kEV16QEjNp|5apCM531G~S^nOhA`K_gp7I?Y-A9MUl*bTe z2VtnpTm%nciI*d%YdNz?{}yMvy5j+c z4Wi-`u9=%)OA(84>zsnL6c|;iNXmaqATwXiYQP0#@ZnEdR_ z0o*1vI)JH-XCyLvavdukJP0Dm&o2`cPBb_gdK*gx;#PC1ij+rD{!>qtwK=J1+} zo5Ksmq_x!AAJ7$w$O^5sLqO+Vl=Icx2O0!0a#3D^TWpBeH6RuF;n_9s`{kXX2s$Xs z@T@HwT2YPA=iJ@sOupttt~yU_{5UaU<~Rp6jfb7?*|CH--jBmH(fhsBRq%rrzeVE8J1w}x;ef7!4MieNLiYZ+q0?J zn`NaxW+KGn(q|)uOORG9sZm$Kpj$laB4-e0p7^r&!Z}OVbq;p}qe+r{>e zd&*f~c0-X%*gF$hz>5X&5_wi@3tzki$;2L7 zU~Tvgec;UE<h@B4mZI6ZDK$H+yyO}`FPw~R7hZ{dX#P{8sk=% z3%~K~MUV|WNJ)?*_qF(_i~S@-V3zvRiKc?bnUZWy-)zir?ErvDMd;$oN6d~TcJRRQ z(Zh&QB&J8e6*|oYt3spm=5Pqouq8BrK~}I>E9qn}%pLGOayGyfLCmS4j8MtAi*kF=ZNTd zjZgQ*KYea9fC6#8cJdkN>5U3UWv|CssGH#4%SYeprg!FB(?4mRD@AToo`DaY_E)|lBxZ?;)gt33^Nqk4O>VI<}w z2bNbUaNM$YpJpeE+=m9_OT3aP46D@}H{}g$`2CLF0~?ygMo0`3I)3NXNCLR9q38@S zs6`oxbDun6E59GO4kW@F&>l&Hoy!=|D~13<ac>|0Zhozjia$GClrjo4?}{7WBqW zxjX_W+A1ofVPY4qMzg+l9WAiJpsr%{N8_RyT3RQa%zdbbz8%lZ@aQoszJAQYTTSTI?!}Z@yNlqHnKvpQr$b~G{`;b(qY>i9`1+Ow-%*q1rPlfxNWg9 zUhm|Ais=AiiC6^_L6Uj|Otlc`^P-vg@2IqlEV88`XJZlm5-CBArWJpYo7E~r8w!wR z=RlsNX(Jcg?}l+8lko1|mb^u9jZ{$^7s;G$O_yKC6*WcZs_(ommXfI?8G+o|U;LVpWd@iWT&`YAvN1v<&3l+Z?AY4P2`pp{TPD5Q&0 z0`w9bz9=Ll=Ufm%o+DIc899r_4=Tu(JrP5^^XG5nLZx zaP~2>|5N0-m-fgpO@X$;0y>_VyJQ6HhzF?{H~sEGIB64p7-r`Ev?IZ9O!fCzng#aE z;f~~ABC{BVTVLHTWYh$t9e$tl5)44nM0&gx;5v?A zA&>3NlzjWsdbXL6P8?Fl0g?^$KGKbXFFtG(NX&lFF*8);@L#l{*WunbhsSS9{3hcp z0M&>Z@-6IvTtSs&$R_cR2B3#3UN zX1{QKABOyAkKxE`T!T=ri&~(yD=DiUdKq^TH_(65z4<_z!pZ)d)C!Z2Owb^9xVHmPJ=t{ z+lL-ck<~Um(@m9+EC7k&+sshH2#D1oa6BHDp;b|APXS0irbAhCu_q&=7%0*tH#6J* zx^qnRyjEp?53H==KO{5eOgZ0XO;nBGglW=n?}DD`#cFG|6I^zBfl*;{!t0Cp53Mc; z&|i33@%Ql+Ft6WUbWZqOdWQ`(aBS<>2?GZTgN2fdL6`Y#-b%xP?IEJT`i=I8xb;ca zmyav4PFbn89}FU3uT#$2>`$C@C5OcnUH$uvHC?JFt^~=q7F#^bcRD~c@!i<$IKE=X zYwZdqC!l6qDwt@=wAKb+oL1su{%GqcyPty8G0@e;@}#XmdH7oA+XqpKRiCpW&!QDE ze%dkJk!%}$g9iNWnl2Ki9$*MI8TCJ?4%B&3!5`)PUiK5J8PC+IF))g*Hag|X5Uy2I z4YboW1k5j|+BC4fQw(`kEcBNu0!T{0tqpRC{S;XokT?fuleARKJBW=m0j>Kd}>U3QY^xBn(IMzu#phM9C?X z1@VCf*#H28wh6?0OQM&~B_jh(8SdfrN(iNS`H+u%pUa;}37^+ks*d`Q^c}#P7f7^6 z_IqlXI!yrdFhWgaQi>6?qBNG`;-Wlc`nTD@9HO6EvG@}8q15M0P>ZiB);Y&QbP!B5 zoZfIFvcN&Huc$d_Ba)w`(lL`o?dT^ zeuJjOi>A|-b->YO$W(ej!(vp?0tGZFRVX-yTSEX$Duqnb7=rO5Ce>8OG|+i4tT&>g zUjOzlP0Gvah%5yiWr&l}H|CKFh2dw8>(4qlb?*@qtxrsZKO;~UOUA7Yc=ZDQt=hPA z7Oa?n#~4h+h3BR+s(a_x~$I5;)G z^T_IKhP}V&KA}u)-ASFIlRNOGs`L|p8-xT_WVvfwitNQXzb@Ap1~y_qH-;lf8jVw-%S9PueY%0$Q~A95*UQowH_Yu+82&eOu|>F{=DgE-N|gj|GuEPPO>G|J#K* zO+>ds`X7k|u@isJ8lczhg?A^QW}LW+1X|CHIW0@YN|tr=9b0^aQk4M>$J131^yPG5 zP8W`Ex+GWF_z7yc6La@m#Ov*kZh)Ct(%<(Fq-{S)v2 zdJ?9$!f>6hF9@`iRB}Tvc(8XQsWIdZ^ZP>&C)Rqt_S3O6dL|yE8Ob1&47Fvhga+jd z*5h|kpY#{yvdzP>{S;))M3V(nSXWY?l}tS)T?k@LM*7lmP;NY|XttBFI)%JiCGs3WYvbipAbGP?B^1^&0tJbr}(+ z5Ua{d(tHQ}4-;6AF!(3Zi}*~`Tzyncme z7F08T@`3Co{#uGotI0w&O^-L zWcX%5-pi@Bix@bWp1%1#0CPh{?z9ucCs`tN&RH6uo1tH1LHfFda^GtJ66mPHb$CR1 zTEtoZcxZbns%zBM80P{zb&DS_RU2vL#yPOEnl0-Z<;H4HhV6+1e>419ZN})wb(w{M^q93=k#J0`9;ik3lJJoqgszvk_Cl zbIGOeJFO&2;!rW&;+8m>9=~my4JRf|IZL|fe<-m@nr-2v3;r&h;7BKQc;Lz%n$Y;gYEFbqFZL zGn?LFXsLZYFu?W}G;JMNLn_&?c?)>LUt)< zu8Tr8r((CrwNf3gl;Vl6@hh>SjSR{*voPmYuoPz$9t}Nqi!Gl{Ne7Q|nFPO4@cPa=Na5Ww3G3{*24O`! zy_+2qt)*_jBq`(TaqC*s(t{On5pVpFHvaf$u?7LG#*Ti*ezzIM$-Qw24|-z~LPtL6 zIu3n~@kJx|!?pXgSRGkY{iunKM7+Y8?P)iDI z5*KqlH5Y218@Fv(%sG+JrDe+ss^3(N6nhA7(zDAgL2bFNQ(4U6QUWPGjE(B@0cPVD zAu=xCjV8?s*t)RY@q`U93cbsdKWwah6rJWBO=x}EhkDG^cc2i$>mf#<9P@#PUaN3K zNu{u_+|)*B$lI1<5AcIS$?{e0oYn9o#xHqbD(wiEf#r~@XLnUv~)U>S~xrn1- zr8;=2-CQ~50Pk+>Lc*6`X*h^E{Jvn@&IZ_4s$Y8819r8j@MZmw&x{A^U@XZFWqcTr zY&}H|ooh?w!7on$F68;?Obtw#D@BBxX^rerkx|?Q6w zDDA=|bJjnqKUF;4M5)RAG49>Fv|J$z zdTfT`r{ZBJIox3$r`@Cyz(#9{qo#@_4s=mVNM?(Ng!gWWl9}Pw!Nw1#LgJGK;1X^s zyN4RStK($OJQ=9T;R_}vysmoEXWuMDS=Dh}_V(7|pLYCE!V#FpE(^Oel4M(vFX^`Z zuwmAu`1Uq9oyL+KOv2OH5^>)qNz|3CB)G_iIozW`)Aa<%4gp5uSX~x8EVZx5v8l}Z ztfxsw*>T&VXNLMjMqj5km&EK`$mDs7o&ehfu9jfo{s=2(nFvU_8GI*#^#p#9-MGo+Y=kvz6fS#nAPo1LWD8r=KKt_mt0)HWj~l)1Oy`g$pRq zA?+NOzCAk8XG`?jig?$}lJBWaTI>{mxMgr1Q>hJpQA92D;a+{PY^d9+o)KLQ2;FVr z)f>girx!GWr61;;88ZR;YyF|*A1u2TIGMWE z_bF4avpyNdDh!A#ORlS1BUJ?a^3(T2lk|Kh(UPm8fAm+sS75YNK#)&FfVU}$>g6Z= zbaLoC&Rmj6UiRd#c9^l@SDmWaJ$t{b8PKcGZCLTm`9f$sM|8t9lj;FF8XgSIC%j%U z{;p0BxrO1IecfRnSNIpsvw*uH%q!qu@#u zkd36Qxg5`cuYM60Jm|Q?(eWO|r?i}SuClHPzrT z$egJ|wTM}2(tF`&`!_2Sf2DNWGS(uoT`ceY`2lj(sG4F9tc*w&pce>BNYOykeRzEr zC%0;4wR!N_U=0?A@Ci3Z{Jl}E<0*jpzM;ns&x6KiHQnFWB>-)k{?Ct&IYk-_DuaH! zt%WZw>mN1XPkxX5;SC(nVFvQObFgIvlc_3oUON5u*?_!NK#4~@qSB(syS#R)Wt{ID zG}&%apyy-}|GNI7B;f)+QY)sr={za@$G8H%UY-1hBsE@l%WeJEOgkv(DStFC*!-YB zu{&&=FIbHE^Y`!$O?2jyY*NADnjB#=YbCB4+&I=^S?NDZ^p^=$!%|VeICKg7^^nZyjn79;#}OUxV$D4Q5_-h}%_hLc;4O(29J;{N2W*gQ_yyCE$2m z+Idb!+OhQ)fvmm){ybY8GV77KI&kk6z#d(IfsSCJfPCb$DNb*kQvQmBSE^qsR;n8S zNp^WH4rk$bdT{uSP_gx;%Xn458$X0nC+#w?A!x9SEi05ZEQ<9PEo%ix)rv*@{z=MY zI3wdyQ1FR!8z6*hGEJLBdD}D=rZSIu6fl9!sEbqO9|Y5Y?IQnAo^iB0aaxLPVg${h zZ0iH%W{d+^3i7$dHvhd{f;mYO`Q@c#J{qziZ#vnjfd|1qd*2oovb)N|wSixUxhu5o zHb!vo1@65U))PcZ4EJ7SfE`W$r*`%Koe};up4R8h(bgX=&Q#XQq<6re>z-qoqb^s3?`7xaC&s zBAWQ!K4q?bs4HS9TS`EPNQA9Bpb{a>=lwWm|4?vd038P0_SuK`dFGk9=lp)Z^S<}o zdp>?=2>wSfI>{6XAdXUuP#MAiP@Y36NALxJH9WvKU`8M*cyD}i`Gd`A_n!6O+&LM-$&)2x06ZZ{ z>IeW`+)RppJ9(m@F}4qP_g(&!5j9qdZO3XF#1(j?A6Xs}>nV>`@4@WsAJMb>x_QTr z68BbBZwi9PmL8x_@5Im<({}Dq zRr<)?Y&HJ=Alz*c?Q{f>^zZ>8z;FCGw9^rc?WYH&z?b0aK!Rs1!I?=78xJwchqh zm8$cUSF5~(eR?N`na$E!##oR`a00U+@ViBk-`}s_{W)pr(`x`AcKS|ZnN>SN+keQx z#^SA?9Do`d#fDF+8>?2P4Keu780u$jtkNAV?!lR{JH=C{O82^p<9U*$(eSZ_MX!|t zK>qi)>vrs%UJc;T$o{6E7!c|nlQ9Oy8gGy1kn5>9pcMw0?Ci3|l(GRXBzW%q1uqR7 zUMH?8XaxMZvGR;;3l9C$(v(}HK24!T0*4Jr(aW;B)VYih@>T!-_b*r+7envnuNS#3 z9Kkp4AG^YQg2J*uzG zsx0~;l`%l6se%39SF8&b4Dnwa7vs@W;SR5RFzv57p>Prhz#RMhzy|MAbMVE=5bKkj ze%V%?mv6q2pAXCB%TN_nzjx2M1|zMf@LHh5p#-`W6?l7>opCEhp>tVvwavXPkKwgI zUf|q)TP)5AE&~b25wCfA?P#?~?k{{qO7aPB!_^}t`2+_b!nw?5lbb!b)WK_kFE1MY z69DJAE@yIO-xLe z1MhB|;~&4LkJWfA8~j=;mWUD%B|J1YF)zDOr@0l1+L4t zn~e}igpfR)PGbT9yIl@pj72eq#(LPwab$K$=^J}mgIBAj4f{kTjxyIeG8lu=D>4Mt z27~t5@A6k1Z7oi%nl}6{Z^hAo0N<`WPqsJLm*>g$&Yc6hwx&4X9wvT!_go((N$MO` zrB71I8qI%l94X~_?uaPzALM_(GVfmS_^+HzjCpcfkjZqTONS1=Mh2kjXG?WRh=0l9 zBlDWIIc`H6{tr?A4n7#QVUW!xjVFWzsVc4Dd2WAEaeU6bCh$yhd8Eb8W)JM5UG?U~ z4)yEWQ5#Ilc@E!vwmjtc#LX2`cyfD>NExV2q9sOO{_xHhJDMkNsfZ-NB=^_~WjO)> zL`EcRtf{dt5d`jtEUN{Az&$8Q>gmXcgruV4_*M5(pj~G1448VZ%<(ON`pKO#Y$Ob6 z0FK|V(o_Hq(iYch)VxS0+uG+H4&^1I(P!wn+*j%|Vo|*k zM_4S5&m$ue&K4ENe{wIx{b5siFN=ds0Z^@|Sm-R!9qn}F@WHrFPXR#ssTyJTY4cl@ zVR3P}Jgn|P96vyRpH3u|bv^_{5*TB1M3GN-76$-%1uKqs?Y!oYqR`o$A6T6!2>d)> zU-5v&;+V-81f5Qkkyo%H7XSu6^mb@fm31~_%&5}|e=jIpbrb*|>i3qP+3c9*a421M zI^pd67l-a%`10`j+>JwK^9fL+6UjoG#CA?dDUUj^xYJ$d!x=;Ul#v@u7j9kFpX>XG zoHS#opG!VRkY%+On#lL7D*ITc6V}=7O1LbmiM6%%SdJqf5JI|{&5pA@yRRFp)e6?i zO7mHbhTl#pF4%1HjvkR~SG+KHaN5??%u|dpPOld~v09~#sasBt1~3@_7N%YIyI3wi z&k<}NKfrhQM_J}IW;;s+FuE}=HktK*`knHF06-ZyyUZQ~P^H(BOpAlX0Kl#+^N)`w z-aIzs2&yQIt;BnW(?yKq8aP*qlcM>HCK(Zx%zZ@PFXey&z4>~T1hWp=x~kmtD) zNm6$x3e6#eY^0Pe+>v&62&Jq?hYr4v78I^r<>RB7s3>&8poiZH0)WzM@=E~pJcrMB zWtl^H4o4}&s7aeE8k`0LfJz})B^F?nSb$w-4>Q1E(2;Q;EeNhZ?iD!f0gwhDjR9$3 zs1X3?6yS5-PwRau#;vT#x4Go0Kj zZrrd30=Sm*&9Z9%V6(|r0B{u*wL$Uwey+(Wb;JRHtm0DuP;RBK%yv2-fXjgn02s97 zOx-&;I>|IaRTISAGOU;})UVRnkJITi36!!?-Mg(_c`Idyl;jiK%-CHC zs!9iFH2lUo2CZv=H-N}D)wBZoXp;%F|bn4cM`YqR)OZLbA`2N)~b)V_)UX2n8 z3Rj-#-fiu3lB9f)y5;2T@UYdTTT?S57-OO+^5gTrUs)I)wz@9>FoyZvx+aWdjD@Ny z4b$tzIfM|&Y<4`eH8t~O=Lgml)z&&jaU4nKd2(J=Z=Yi7bON>86#$?L#dwaOEGkq? z+W2f>{h;&oJLRQTiJezeJVOA9v75PN>UnKrAypX@RlMgThlP;yoI z$oWPP0+Rp$7cRbbXvm=CBh}T`VOFag?CUF<+PBwd{UdXc%PxG{^#0>Ac7NMfQK=Zz z(f@eX>9}eDm^XJ%&r_K{JZ7`W0l~q3n@?viDIE0hCcV`vjjQwhaQkyV&6~R?X4b5M zMW+Z^N&v}O902@nX{X#tn=6J3Jn~e^&b9O5t^k0KNJjf;h|8llDcZ2@P>!YKfgnw$ zsY?0UDzQ(A+xE{rmC_`_!&dhV4_nPrl23SjM(}IFqYbZ4r>O*3mMHS(drhbfZD>Oq h+R%nJw4n`k_zwgyj|v)U3%mdT002ovPDHLkV1oTKdmaD) literal 0 HcmV?d00001 diff --git a/dmp-frontend/src/app/ui/auth/login/login.component.html b/dmp-frontend/src/app/ui/auth/login/login.component.html index da8ce40e0..5b3511141 100644 --- a/dmp-frontend/src/app/ui/auth/login/login.component.html +++ b/dmp-frontend/src/app/ui/auth/login/login.component.html @@ -28,6 +28,10 @@ + diff --git a/dmp-frontend/src/app/ui/auth/login/login.component.scss b/dmp-frontend/src/app/ui/auth/login/login.component.scss index 0ec524671..af54ca616 100644 --- a/dmp-frontend/src/app/ui/auth/login/login.component.scss +++ b/dmp-frontend/src/app/ui/auth/login/login.component.scss @@ -191,6 +191,16 @@ span.orcidIconMedium { margin-right: 2em; } +span.openaireIcon { + background: url(img/openaire_small.png) no-repeat; + background-position: center; + float: right; + width: 100px; + height: 56px; + margin-left: 2em; + margin-right: 2em; +} + .b2access-button { margin-top: 10px; width: fit-content; @@ -201,6 +211,11 @@ span.orcidIconMedium { width: fit-content; } +.openaire-button { + margin-top: 10px; + width: fit-content; +} + .login-logo { background: url(img/open-dmp.png) no-repeat; width: 273px; diff --git a/dmp-frontend/src/app/ui/auth/login/login.component.ts b/dmp-frontend/src/app/ui/auth/login/login.component.ts index c15aced02..6c329587e 100644 --- a/dmp-frontend/src/app/ui/auth/login/login.component.ts +++ b/dmp-frontend/src/app/ui/auth/login/login.component.ts @@ -58,6 +58,10 @@ export class LoginComponent extends BaseComponent implements OnInit, AfterViewIn this.router.navigate(['/login/external/orcid']); } + public openaireLogin() { + this.router.navigate(['/login/openaire']); + } + public hasFacebookOauth(): boolean { return this.hasProvider(AuthProvider.Facebook); } @@ -82,6 +86,10 @@ export class LoginComponent extends BaseComponent implements OnInit, AfterViewIn return this.hasProvider(AuthProvider.ORCID); } + public hasOpenAireOauth(): boolean { + return this.hasProvider(AuthProvider.OpenAire); + } + public initProviders() { if (this.hasProvider(AuthProvider.Google)) { this.initializeGoogleOauth(); } if (this.hasProvider(AuthProvider.Facebook)) { this.initializeFacebookOauth(); } @@ -102,6 +110,7 @@ export class LoginComponent extends BaseComponent implements OnInit, AfterViewIn case AuthProvider.Twitter: return this.hasAllRequiredFieldsConfigured(environment.loginProviders.twitterConfiguration); case AuthProvider.B2Access: return this.hasAllRequiredFieldsConfigured(environment.loginProviders.b2accessConfiguration); case AuthProvider.ORCID: return this.hasAllRequiredFieldsConfigured(environment.loginProviders.orcidConfiguration); + case AuthProvider.OpenAire: return this.hasAllRequiredFieldsConfigured(environment.loginProviders.openAireConfiguration); default: throw new Error('Unsupported Provider Type'); } } diff --git a/dmp-frontend/src/app/ui/auth/login/login.module.ts b/dmp-frontend/src/app/ui/auth/login/login.module.ts index de4b22391..8eaa9ba92 100644 --- a/dmp-frontend/src/app/ui/auth/login/login.module.ts +++ b/dmp-frontend/src/app/ui/auth/login/login.module.ts @@ -9,6 +9,7 @@ import { LoginService } from './utilities/login.service'; import { B2AccessLoginComponent } from './b2access/b2access-login.component'; import { OrcidLoginComponent } from './orcid-login/orcid-login.component'; import { EmailConfirmation } from './email-confirmation/email-confirmation.component'; +import { OpenAireLoginComponent } from "./openaire-login/openaire-login.component"; @NgModule({ imports: [ @@ -22,7 +23,8 @@ import { EmailConfirmation } from './email-confirmation/email-confirmation.compo TwitterLoginComponent, B2AccessLoginComponent, OrcidLoginComponent, - EmailConfirmation + EmailConfirmation, + OpenAireLoginComponent ], providers: [LoginService] }) diff --git a/dmp-frontend/src/app/ui/auth/login/login.routing.ts b/dmp-frontend/src/app/ui/auth/login/login.routing.ts index 88c7fc0b4..e1bec1f77 100644 --- a/dmp-frontend/src/app/ui/auth/login/login.routing.ts +++ b/dmp-frontend/src/app/ui/auth/login/login.routing.ts @@ -6,6 +6,7 @@ import { LinkedInLoginComponent } from './linkedin-login/linkedin-login.componen import { LoginComponent } from './login.component'; import { OrcidLoginComponent } from './orcid-login/orcid-login.component'; import { TwitterLoginComponent } from './twitter-login/twitter-login.component'; +import { OpenAireLoginComponent } from "./openaire-login/openaire-login.component"; const routes: Routes = [ { path: '', component: LoginComponent }, @@ -14,7 +15,8 @@ const routes: Routes = [ { path: 'external/orcid', component: OrcidLoginComponent }, { path: 'external/b2access', component: B2AccessLoginComponent }, { path: 'confirmation/:token', component: EmailConfirmation }, - { path: 'confirmation', component: EmailConfirmation } + { path: 'confirmation', component: EmailConfirmation }, + { path: 'openaire', component: OpenAireLoginComponent} ]; @NgModule({ diff --git a/dmp-frontend/src/app/ui/auth/login/openaire-login/openaire-login.component.html b/dmp-frontend/src/app/ui/auth/login/openaire-login/openaire-login.component.html new file mode 100644 index 000000000..e69de29bb diff --git a/dmp-frontend/src/app/ui/auth/login/openaire-login/openaire-login.component.ts b/dmp-frontend/src/app/ui/auth/login/openaire-login/openaire-login.component.ts new file mode 100644 index 000000000..14754d688 --- /dev/null +++ b/dmp-frontend/src/app/ui/auth/login/openaire-login/openaire-login.component.ts @@ -0,0 +1,61 @@ +import { Component, OnInit } from "@angular/core"; +import { BaseComponent } from "../../../../core/common/base/base.component"; +import { ActivatedRoute, Router, Params } from "@angular/router"; +import { LoginService } from "../utilities/login.service"; +import { AuthService } from "../../../../core/services/auth/auth.service"; +import { HttpClient } from "@angular/common/http"; +import { takeUntil } from "rxjs/operators"; +import { environment } from "../../../../../environments/environment"; +import { AuthProvider } from "../../../../core/common/enum/auth-provider"; + +@Component({ + selector: 'app-openaire-login', + templateUrl: './openaire-login.component.html', +}) +export class OpenAireLoginComponent extends BaseComponent implements OnInit { + private returnUrl: string; + + constructor( + private route: ActivatedRoute, + private loginService: LoginService, + private authService: AuthService, + private router: Router, + private httpClient: HttpClient + ) { + super(); + } + + ngOnInit(): void { + this.route.queryParams + .pipe(takeUntil(this._destroyed)) + .subscribe((params: Params) => { + const returnUrlFromParams = params['returnUrl']; + if (returnUrlFromParams) { this.returnUrl = returnUrlFromParams; } + if (!params['code']) { this.openaireAuthorize(); } else { this.openaireLoginUser(params['code'], params['state']) } + }) + } + + public openaireAuthorize() { + window.location.href = environment.loginProviders.openAireConfiguration.oauthUrl + + '?response_type=code&client_id=' + environment.loginProviders.openAireConfiguration.clientId + + '&redirect_uri=' + environment.loginProviders.openAireConfiguration.redirectUri + + '&state=' + environment.loginProviders.openAireConfiguration.state + + '&scope=openid profile email'; + } + + public openaireLoginUser(code: string, state: string) { + if (state !== environment.loginProviders.openAireConfiguration.state) { + this.router.navigate(['/login']) + } + this.httpClient.post(environment.Server + 'auth/openAireRequestToken', { code: code, provider: AuthProvider.OpenAire }) + .pipe(takeUntil(this._destroyed)) + .subscribe((data: any) => { + this.authService.login({ ticket: data.payload.accessToken, provider: AuthProvider.OpenAire, data: null }) + .pipe(takeUntil(this._destroyed)) + .subscribe( + res => this.loginService.onLogInSuccess(res, this.returnUrl), + error => this.loginService.onLogInError(error) + ); + }); + } +} diff --git a/dmp-frontend/src/environments/environment.ts b/dmp-frontend/src/environments/environment.ts index ba3d6d89d..1e079ce43 100644 --- a/dmp-frontend/src/environments/environment.ts +++ b/dmp-frontend/src/environments/environment.ts @@ -8,7 +8,7 @@ export const environment = { }, defaultCulture: 'en-US', loginProviders: { - enabled: [1, 2, 3, 4, 5, 6, 7], + enabled: [1, 2, 3, 4, 5, 6, 7, 8], facebookConfiguration: { clientId: '' }, googleConfiguration: { clientId: '' }, linkedInConfiguration: { @@ -31,6 +31,12 @@ export const environment = { clientId: 'APP-766DI5LP8T75FC4R', oauthUrl: 'https://orcid.org/oauth/authorize', redirectUri: 'http://localhost:4200/login/external/orcid' + }, + openAireConfiguration: { + clientId: '', + oauthUrl: '', + redirectUri: '', + state: '987654321' } }, logging: {