From 4ef7b3345912c1aeeb1bac294613a59d775b0c8f Mon Sep 17 00:00:00 2001 From: Darin Krauss Date: Sun, 10 Mar 2024 21:24:59 -0700 Subject: [PATCH 1/7] [BACK-2916/BACK-2913] Separate personal and clinical registration - Separate personal and clinical registration forms - Add TOS/PP agreement to clinical registration form - Add alternate registration link at bottom of registration page - Add RegistrationRoleDiscoveryAuthenticator to discover role from query parameter - Add RegistrationRoleFormAction to set role discovered by RegistrationRoleDiscoveryAuthenticator - Add RegistrationTermsFormAction to set terms if clinical role - Add TidepoolLoginFormsProvider to display separate clinical or personal registration forms - Add RoleBean to capture common role-related functionality - Add RegistrationsRealmResourceProvider to allow restarting registration - Rename "Sign in" to "Log In" - Various form styling updates - Add Tidepool+ logo for display if clinical role - Update role selection from content - Minor updates to fix warnings - https://tidepool.atlassian.net/browse/BACK-2916 - https://tidepool.atlassian.net/browse/BACK-2913 --- .dockerignore | 4 + .gitignore | 6 +- Dockerfile | 4 +- .../ConditionUserInContextFactory.java | 3 - .../RedirectToRegistrationPage.java | 10 +- .../RedirectToRegistrationPageFactory.java | 3 - ...egistrationRoleDiscoveryAuthenticator.java | 58 ++++++++ ...tionRoleDiscoveryAuthenticatorFactory.java | 87 ++++++++++++ .../RegistrationRoleFormAction.java | 59 ++++++++ .../RegistrationRoleFormActionFactory.java | 86 ++++++++++++ .../RegistrationTermsFormAction.java | 93 +++++++++++++ .../RegistrationTermsFormActionFactory.java | 86 ++++++++++++ .../ResetUserInContextFactory.java | 3 - .../login/TidepoolLoginFormsProvider.java | 42 ++++++ .../TidepoolLoginFormsProviderFactory.java | 17 +++ .../keycloak/extensions/model/RoleBean.java | 98 ++++++++++++++ .../RegistrationsRealmResourceProvider.java | 120 +++++++++++++++++ ...strationsRealmResourceProviderFactory.java | 34 +++++ .../TidepoolAdminResourceProvider.java | 1 - .../services/messages/Messages.java | 7 + ...ycloak.authentication.AuthenticatorFactory | 3 +- ....keycloak.authentication.FormActionFactory | 2 + ...ices.resource.RealmResourceProviderFactory | 1 + tidepool-theme/login/login-username.ftl | 4 +- tidepool-theme/login/login.ftl | 8 +- .../login/messages/messages_en.properties | 28 +++- tidepool-theme/login/register-clinical.ftl | 127 ++++++++++++++++++ .../{register.ftl => register-personal.ftl} | 21 +-- tidepool-theme/login/resources/css/styles.css | 25 +++- .../resources/img/tidepool-logo-880x96.png | Bin 23024 -> 0 bytes .../resources/img/tidepool-logo-890x96.png | Bin 0 -> 13054 bytes .../img/tidepool-plus-logo-985x96.png | Bin 0 -> 13998 bytes tidepool-theme/login/template.ftl | 6 +- tidepool-theme/login/user_role_prompt.ftl | 10 +- 34 files changed, 1000 insertions(+), 56 deletions(-) create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleDiscoveryAuthenticator.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleDiscoveryAuthenticatorFactory.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleFormAction.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleFormActionFactory.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationTermsFormAction.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationTermsFormActionFactory.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/login/TidepoolLoginFormsProvider.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/login/TidepoolLoginFormsProviderFactory.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/model/RoleBean.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/resource/RegistrationsRealmResourceProvider.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/resource/RegistrationsRealmResourceProviderFactory.java create mode 100644 admin/src/main/java/org/tidepool/keycloak/extensions/services/messages/Messages.java create mode 100644 admin/src/main/resources/META-INF/services/org.keycloak.authentication.FormActionFactory create mode 100644 tidepool-theme/login/register-clinical.ftl rename tidepool-theme/login/{register.ftl => register-personal.ftl} (84%) delete mode 100644 tidepool-theme/login/resources/img/tidepool-logo-880x96.png create mode 100644 tidepool-theme/login/resources/img/tidepool-logo-890x96.png create mode 100644 tidepool-theme/login/resources/img/tidepool-plus-logo-985x96.png diff --git a/.dockerignore b/.dockerignore index 313cb2a..7b3e578 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,7 @@ Dockerfile .git/ .gitignore + +# Environment +.env +.envrc diff --git a/.gitignore b/.gitignore index 296e1bc..890a293 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,8 @@ target/ # Original theme -themes/base \ No newline at end of file +themes/base + +# Environment +.env +.envrc diff --git a/Dockerfile b/Dockerfile index bb65355..3c7a540 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,9 +7,9 @@ RUN unset MAVEN_CONFIG && \ ./mvnw versions:set -DnewVersion=LATEST && \ ./mvnw install && \ ./mvnw clean compile package && \ - wget -O keycloak-rest-provider.jar https://github.com/toddkazakov/keycloak-user-migration/releases/download/v2.0/keycloak-rest-provider.jar && \ + wget -O keycloak-rest-provider.jar https://github.com/daniel-frak/keycloak-user-migration/releases/download/1.0.0/keycloak-rest-provider-1.0.0.jar && \ wget -O keycloak-metrics-spi.jar https://github.com/aerogear/keycloak-metrics-spi/releases/download/3.0.0/keycloak-metrics-spi-3.0.0.jar && \ - wget -O keycloak-home-idp-discovery.jar https://github.com/toddkazakov/keycloak-home-idp-discovery/releases/download/v21.3.2/keycloak-home-idp-discovery.jar + wget -O keycloak-home-idp-discovery.jar https://github.com/tidepool-org/keycloak-home-idp-discovery/releases/download/v21.3.2/keycloak-home-idp-discovery.jar FROM alpine:3.15 as release diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/ConditionUserInContextFactory.java b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/ConditionUserInContextFactory.java index c684eb8..2cace99 100755 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/ConditionUserInContextFactory.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/ConditionUserInContextFactory.java @@ -22,8 +22,6 @@ public final class ConditionUserInContextFactory implements AuthenticatorFactory public static final String CONF_NEGATE = "negate"; - private Config.Scope config; - @Override public String getDisplayType() { return "Condition - User in Context"; @@ -72,7 +70,6 @@ public Authenticator create(KeycloakSession session) { @Override public void init(Config.Scope config) { - this.config = config; } @Override diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RedirectToRegistrationPage.java b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RedirectToRegistrationPage.java index 8d2aa37..5be3295 100755 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RedirectToRegistrationPage.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RedirectToRegistrationPage.java @@ -1,6 +1,5 @@ package org.tidepool.keycloak.extensions.authenticator; -import org.jboss.logging.Logger; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.Authenticator; import org.keycloak.models.*; @@ -9,30 +8,25 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; import java.net.URI; final class RedirectToRegistrationPage implements Authenticator { - private static final Logger LOG = Logger.getLogger(RedirectToRegistrationPage.class); - RedirectToRegistrationPage() { } @Override public void authenticate(AuthenticationFlowContext context) { - URI baseURI = prepareBaseUriBuilder(context).build(); + URI baseURI = prepareBaseUriBuilder(context).build(); URI register = Urls.realmRegisterPage(baseURI, context.getRealm().getName()); context.forceChallenge(Response.seeOther(register).build()); } private UriBuilder prepareBaseUriBuilder(AuthenticationFlowContext context) { - UriInfo uriInfo = context.getUriInfo(); + UriBuilder uriBuilder = context.getUriInfo().getBaseUriBuilder(); ClientModel client = context.getSession().getContext().getClient(); AuthenticationSessionModel authSession = context.getAuthenticationSession(); - String requestURI = uriInfo.getBaseUri().getPath(); - UriBuilder uriBuilder = UriBuilder.fromUri(requestURI); uriBuilder.replaceQuery(null); if (client != null) { diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RedirectToRegistrationPageFactory.java b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RedirectToRegistrationPageFactory.java index fcfc66b..8564eac 100755 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RedirectToRegistrationPageFactory.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RedirectToRegistrationPageFactory.java @@ -21,8 +21,6 @@ public final class RedirectToRegistrationPageFactory implements AuthenticatorFac private static final String PROVIDER_ID = "redirect-to-registration-page"; - private Config.Scope config; - @Override public String getDisplayType() { return "Redirect to Registration Page"; @@ -65,7 +63,6 @@ public Authenticator create(KeycloakSession session) { @Override public void init(Config.Scope config) { - this.config = config; } @Override diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleDiscoveryAuthenticator.java b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleDiscoveryAuthenticator.java new file mode 100644 index 0000000..73dabbf --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleDiscoveryAuthenticator.java @@ -0,0 +1,58 @@ +package org.tidepool.keycloak.extensions.authenticator; + +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.Authenticator; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.protocol.AuthorizationEndpointBase; +import org.tidepool.keycloak.extensions.model.RoleBean; +import org.tidepool.keycloak.extensions.roles.UserRolePromptRequiredAction; + +final class RegistrationRoleDiscoveryAuthenticator implements Authenticator { + + @Override + public void close() { + } + + @Override + public void authenticate(AuthenticationFlowContext context) { + + // Reset APP_INITIATED_FLOW so restart redirects to login, not registration + context.getAuthenticationSession().setClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW, null); + + String role = context.getUriInfo().getQueryParameters().getFirst(RoleBean.PARAMETER_ROLE); + if (RoleBean.ROLES_SET.contains(role)) { + context.getAuthenticationSession().setAuthNote(RoleBean.AUTH_NOTE_ROLE, role); + context.success(); + } else { + context.challenge(context.form().createForm(UserRolePromptRequiredAction.ROLES_FORM_FTL)); + } + } + + @Override + public void action(AuthenticationFlowContext context) { + String role = context.getHttpRequest().getDecodedFormParameters().getFirst(RoleBean.PARAMETER_ROLE); + if (RoleBean.ROLES_SET.contains(role)) { + context.getAuthenticationSession().setAuthNote(RoleBean.AUTH_NOTE_ROLE, role); + context.success(); + } else { + context.failure(AuthenticationFlowError.INTERNAL_ERROR); + } + } + + @Override + public boolean requiresUser() { + return false; + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + return true; + } + + @Override + public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { + } +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleDiscoveryAuthenticatorFactory.java b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleDiscoveryAuthenticatorFactory.java new file mode 100644 index 0000000..422f397 --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleDiscoveryAuthenticatorFactory.java @@ -0,0 +1,87 @@ +package org.tidepool.keycloak.extensions.authenticator; + +import java.util.List; +import java.util.Map; + +import org.keycloak.Config.Scope; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel.Requirement; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ServerInfoAwareProviderFactory; + +public final class RegistrationRoleDiscoveryAuthenticatorFactory + implements AuthenticatorFactory, ServerInfoAwareProviderFactory { + + private static final String ID = "tidepool-registration-role-discovery"; + + private static final Requirement[] REQUIREMENT_CHOICES = { Requirement.REQUIRED, Requirement.DISABLED }; + + @Override + public Authenticator create(KeycloakSession session) { + return new RegistrationRoleDiscoveryAuthenticator(); + } + + @Override + public void init(Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return ID; + } + + @Override + public String getDisplayType() { + return "Tidepool Registration Role Discovery"; + } + + @Override + public String getReferenceCategory() { + return "registration-role-discovery"; + } + + @Override + public boolean isConfigurable() { + return false; + } + + @Override + public Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public boolean isUserSetupAllowed() { + return false; + } + + @Override + public String getHelpText() { + return "Ensures a role is provided upon registration via query parameter or displayed form and sets the associated auth note"; + } + + @Override + public List getConfigProperties() { + return null; + } + + @Override + public Map getOperationalInfo() { + String version = getClass().getPackage().getImplementationVersion(); + if (version == null) { + version = "dev-snapshot"; + } + return Map.of("Version", version); + } +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleFormAction.java b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleFormAction.java new file mode 100644 index 0000000..36bf2e5 --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleFormAction.java @@ -0,0 +1,59 @@ +package org.tidepool.keycloak.extensions.authenticator; + +import org.keycloak.authentication.FormAction; +import org.keycloak.authentication.FormContext; +import org.keycloak.authentication.ValidationContext; +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.tidepool.keycloak.extensions.model.RoleBean; + +final class RegistrationRoleFormAction implements FormAction { + + private static final String EVENT_DETAIL_ROLE = "role"; + + @Override + public void close() { + } + + @Override + public void buildPage(FormContext context, LoginFormsProvider form) { + } + + @Override + public void validate(ValidationContext context) { + String role = context.getAuthenticationSession().getAuthNote(RoleBean.AUTH_NOTE_ROLE); + if (RoleBean.ROLES_SET.contains(role)) { + context.getEvent().detail(EVENT_DETAIL_ROLE, role); + } + + context.success(); + } + + @Override + public void success(FormContext context) { + String role = context.getAuthenticationSession().getAuthNote(RoleBean.AUTH_NOTE_ROLE); + if (RoleBean.ROLES_SET.contains(role)) { + RoleModel roleModel = context.getRealm().getRole(role); + if (roleModel != null) { + context.getUser().grantRole(roleModel); + } + } + } + + @Override + public boolean requiresUser() { + return false; + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + return true; + } + + @Override + public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { + } +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleFormActionFactory.java b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleFormActionFactory.java new file mode 100644 index 0000000..20582b6 --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationRoleFormActionFactory.java @@ -0,0 +1,86 @@ +package org.tidepool.keycloak.extensions.authenticator; + +import java.util.List; +import java.util.Map; + +import org.keycloak.Config.Scope; +import org.keycloak.authentication.FormAction; +import org.keycloak.authentication.FormActionFactory; +import org.keycloak.models.AuthenticationExecutionModel.Requirement; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ServerInfoAwareProviderFactory; + +public final class RegistrationRoleFormActionFactory implements FormActionFactory, ServerInfoAwareProviderFactory { + + private static final String ID = "tidepool-registration-role"; + + private static final Requirement[] REQUIREMENT_CHOICES = { Requirement.REQUIRED, Requirement.DISABLED }; + + @Override + public FormAction create(KeycloakSession session) { + return new RegistrationRoleFormAction(); + } + + @Override + public void init(Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return ID; + } + + @Override + public String getDisplayType() { + return "Tidepool Registration Role"; + } + + @Override + public String getReferenceCategory() { + return "registration-role"; + } + + @Override + public boolean isConfigurable() { + return false; + } + + @Override + public Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public boolean isUserSetupAllowed() { + return false; + } + + @Override + public String getHelpText() { + return "Applies the role based upon the associated auth note set by the tidepool-registration-role-discovery authenticator"; + } + + @Override + public List getConfigProperties() { + return null; + } + + @Override + public Map getOperationalInfo() { + String version = getClass().getPackage().getImplementationVersion(); + if (version == null) { + version = "dev-snapshot"; + } + return Map.of("Version", version); + } +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationTermsFormAction.java b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationTermsFormAction.java new file mode 100644 index 0000000..ae218f9 --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationTermsFormAction.java @@ -0,0 +1,93 @@ +package org.tidepool.keycloak.extensions.authenticator; + +import java.util.ArrayList; +import java.util.List; + +import javax.ws.rs.core.MultivaluedMap; + +import org.keycloak.authentication.FormAction; +import org.keycloak.authentication.FormContext; +import org.keycloak.authentication.ValidationContext; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.FormMessage; +import org.tidepool.keycloak.extensions.model.RoleBean; +import org.tidepool.keycloak.extensions.services.messages.Messages; + +final class RegistrationTermsFormAction implements FormAction { + + private static final String FORM_TERMS = "terms"; + + private static final String FORM_TERMS_ON = "on"; + + private static final String USER_ATTRIBUTE_TERMS_AND_CONDITIONS = "terms_and_conditions"; + + @Override + public void close() { + } + + @Override + public void buildPage(FormContext context, LoginFormsProvider form) { + } + + @Override + public void validate(ValidationContext context) { + + // TEMPORARY: Currently only for Clinical registration which includes TOS/PP + // agreement on clinical registration form. Remove once TOS/PP agreement + // included on personal registration form. + if (!RoleBean.hasClinicalRoleFromAuthenticationSession(context.getAuthenticationSession())) { + context.success(); + return; + } + + context.getEvent().detail(Details.REGISTER_METHOD, "form"); + + MultivaluedMap formParameters = context.getHttpRequest().getDecodedFormParameters(); + List errors = new ArrayList<>(); + + if (!FORM_TERMS_ON.equalsIgnoreCase(formParameters.getFirst(FORM_TERMS))) { + errors.add(new FormMessage(FORM_TERMS, Messages.TERMS_NOT_ACCEPTED)); + } + + if (errors.size() > 0) { + formParameters.remove(FORM_TERMS); + context.error(Errors.INVALID_REGISTRATION); + context.validationError(formParameters, errors); + } else { + context.success(); + } + } + + @Override + public void success(FormContext context) { + + // TEMPORARY: Currently only for Clinical registration which includes TOS/PP + // agreement on clinical registration form. Remove once TOS/PP agreement + // included on personal registration form. + if (!RoleBean.hasClinicalRoleFromRealmUser(context.getRealm(), context.getUser())) { + return; + } + + Long secondsSinceEpoch = java.time.Instant.now().getEpochSecond(); + context.getUser().setAttribute(USER_ATTRIBUTE_TERMS_AND_CONDITIONS, List.of(String.valueOf(secondsSinceEpoch))); + } + + @Override + public boolean requiresUser() { + return false; + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + return true; + } + + @Override + public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { + } +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationTermsFormActionFactory.java b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationTermsFormActionFactory.java new file mode 100644 index 0000000..73b3c2a --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationTermsFormActionFactory.java @@ -0,0 +1,86 @@ +package org.tidepool.keycloak.extensions.authenticator; + +import java.util.List; +import java.util.Map; + +import org.keycloak.Config.Scope; +import org.keycloak.authentication.FormAction; +import org.keycloak.authentication.FormActionFactory; +import org.keycloak.models.AuthenticationExecutionModel.Requirement; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ServerInfoAwareProviderFactory; + +public final class RegistrationTermsFormActionFactory implements FormActionFactory, ServerInfoAwareProviderFactory { + + private static final String ID = "tidepool-registration-terms"; + + private static final Requirement[] REQUIREMENT_CHOICES = { Requirement.REQUIRED, Requirement.DISABLED }; + + @Override + public FormAction create(KeycloakSession session) { + return new RegistrationTermsFormAction(); + } + + @Override + public void init(Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return ID; + } + + @Override + public String getDisplayType() { + return "Tidepool Registration Terms"; + } + + @Override + public String getReferenceCategory() { + return "registration-terms"; + } + + @Override + public boolean isConfigurable() { + return false; + } + + @Override + public Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public boolean isUserSetupAllowed() { + return false; + } + + @Override + public String getHelpText() { + return "Determines whether the terms have been accepted in the form and sets the terms_and_conditions attribute on the user"; + } + + @Override + public List getConfigProperties() { + return null; + } + + @Override + public Map getOperationalInfo() { + String version = getClass().getPackage().getImplementationVersion(); + if (version == null) { + version = "dev-snapshot"; + } + return Map.of("Version", version); + } +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/ResetUserInContextFactory.java b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/ResetUserInContextFactory.java index 1a6fe71..188fc0f 100755 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/ResetUserInContextFactory.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/ResetUserInContextFactory.java @@ -21,8 +21,6 @@ public final class ResetUserInContextFactory implements AuthenticatorFactory, Se private static final String PROVIDER_ID = "reset-user-in-context"; - private Config.Scope config; - @Override public String getDisplayType() { return "Reset User in Context"; @@ -65,7 +63,6 @@ public Authenticator create(KeycloakSession session) { @Override public void init(Config.Scope config) { - this.config = config; } @Override diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/login/TidepoolLoginFormsProvider.java b/admin/src/main/java/org/tidepool/keycloak/extensions/login/TidepoolLoginFormsProvider.java new file mode 100644 index 0000000..f67725f --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/login/TidepoolLoginFormsProvider.java @@ -0,0 +1,42 @@ +package org.tidepool.keycloak.extensions.login; + +import java.net.URI; +import java.util.Locale; + +import javax.ws.rs.core.Response; + +import org.keycloak.forms.login.freemarker.FreeMarkerLoginFormsProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.theme.Theme; +import org.tidepool.keycloak.extensions.model.RoleBean; + +public class TidepoolLoginFormsProvider extends FreeMarkerLoginFormsProvider { + + private static final String FORM_ATTRIBUTE_ROLE = "role"; + + private static final String REGISTER_FORM = "register.ftl"; + private static final String REGISTER_FORM_CLINICAL = "register-clinical.ftl"; + private static final String REGISTER_FORM_PERSONAL = "register-personal.ftl"; + + public TidepoolLoginFormsProvider(KeycloakSession session) { + super(session); + } + + @Override + protected Response processTemplate(Theme theme, String templateName, Locale locale) { + URI baseUri = super.prepareBaseUriBuilder(false).build(); + + RoleBean roleBean = new RoleBean(realm, baseUri, context, authenticationSession); + attributes.put(FORM_ATTRIBUTE_ROLE, roleBean); + + if (templateName == REGISTER_FORM) { + if (roleBean.hasClinicalRole()) { + templateName = REGISTER_FORM_CLINICAL; + } else { + templateName = REGISTER_FORM_PERSONAL; + } + } + + return super.processTemplate(theme, templateName, locale); + } +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/login/TidepoolLoginFormsProviderFactory.java b/admin/src/main/java/org/tidepool/keycloak/extensions/login/TidepoolLoginFormsProviderFactory.java new file mode 100644 index 0000000..5823ec0 --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/login/TidepoolLoginFormsProviderFactory.java @@ -0,0 +1,17 @@ +package org.tidepool.keycloak.extensions.login; + +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.forms.login.LoginFormsProviderFactory; +import org.keycloak.forms.login.freemarker.FreeMarkerLoginFormsProviderFactory; +import org.keycloak.models.KeycloakSession; + +import com.google.auto.service.AutoService; + +@AutoService(LoginFormsProviderFactory.class) +public class TidepoolLoginFormsProviderFactory extends FreeMarkerLoginFormsProviderFactory { + + @Override + public LoginFormsProvider create(KeycloakSession session) { + return new TidepoolLoginFormsProvider(session); + } +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/model/RoleBean.java b/admin/src/main/java/org/tidepool/keycloak/extensions/model/RoleBean.java new file mode 100644 index 0000000..fab7fc4 --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/model/RoleBean.java @@ -0,0 +1,98 @@ +package org.tidepool.keycloak.extensions.model; + +import java.net.URI; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.services.Urls; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.tidepool.keycloak.extensions.resource.RegistrationsRealmResourceProvider; + +public class RoleBean { + + public static final String ROLE_CLINICIAN = "clinician"; + public static final String ROLE_CLINIC_DEPRECATED = "clinic"; + public static final String ROLE_PATIENT = "patient"; + + public static final List ROLES_LIST = Arrays.asList(ROLE_CLINICIAN, ROLE_PATIENT); + public static final Set ROLES_SET = new HashSet<>(ROLES_LIST); + + public static final List ROLES_CLINICAL_LIST = Arrays.asList(ROLE_CLINICIAN, ROLE_CLINIC_DEPRECATED); + public static final Set ROLES_CLINICAL_SET = new HashSet<>(ROLES_CLINICAL_LIST); + + public static final String PARAMETER_ROLE = "role"; + + public static final String AUTH_NOTE_ROLE = "role"; + + private final RealmModel realm; + private final URI baseUri; + private final AuthenticationFlowContext context; + private final AuthenticationSessionModel authenticationSession; + + public RoleBean(RealmModel realm, URI baseUri, AuthenticationFlowContext context, + AuthenticationSessionModel authenticationSession) { + this.realm = realm; + this.baseUri = baseUri; + this.context = context; + this.authenticationSession = authenticationSession; + } + + public boolean hasClinicalRole() { + if (hasClinicalRoleFromAuthenticationSession(authenticationSession)) { + return true; + } + if (context != null && hasClinicalRoleFromRealmUser(context.getRealm(), context.getUser())) { + return true; + } + return false; + } + + public static boolean hasClinicalRoleFromAuthenticationSession(AuthenticationSessionModel authenticationSession) { + if (authenticationSession != null) { + if (ROLES_CLINICAL_SET.contains(authenticationSession.getAuthNote(AUTH_NOTE_ROLE))) { + return true; + } + } + return false; + } + + public static boolean hasClinicalRoleFromRealmUser(RealmModel realm, UserModel user) { + if (realm != null && user != null) { + for (String clinicalRole : ROLES_CLINICAL_SET) { + RoleModel clinicalRoleModel = realm.getRole(clinicalRole); + if (user.hasRole(clinicalRoleModel)) { + return true; + } + } + } + return false; + } + + public URI getRegistrationUriForClinicianRole() { + return getRegistrationUriForRole(RoleBean.ROLE_CLINICIAN); + } + + public URI getRegistrationUriForPatientRole() { + return getRegistrationUriForRole(RoleBean.ROLE_PATIENT); + } + + private URI getRegistrationUriForRole(String role) { + return Urls + .realmBase(baseUri) + .path(RegistrationsRealmResourceProvider.class, "registrations") + .path(RegistrationsRealmResourceProvider.class, "restart") + .queryParam(PARAMETER_ROLE, role) + .build(getRealmName()); + + } + + private String getRealmName() { + return realm != null ? realm.getName() : null; + } +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/resource/RegistrationsRealmResourceProvider.java b/admin/src/main/java/org/tidepool/keycloak/extensions/resource/RegistrationsRealmResourceProvider.java new file mode 100644 index 0000000..846a63a --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/resource/RegistrationsRealmResourceProvider.java @@ -0,0 +1,120 @@ +package org.tidepool.keycloak.extensions.resource; + +import java.net.URI; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + +import org.keycloak.authentication.AuthenticationProcessor; +import org.keycloak.common.ClientConnection; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.http.HttpRequest; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakContext; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.AuthorizationEndpointBase; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.AuthenticationSessionManager; +import org.keycloak.services.resource.RealmResourceProvider; +import org.keycloak.services.resources.LoginActionsService; +import org.keycloak.services.resources.SessionCodeChecks; +import org.keycloak.services.util.AuthenticationFlowURLHelper; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.tidepool.keycloak.extensions.model.RoleBean; + +public class RegistrationsRealmResourceProvider implements RealmResourceProvider { + + private static final String PATH_REGISTRATIONS = "{realm}/" + RegistrationsRealmResourceProviderFactory.ID; + private static final String PATH_RESTART = "restart"; + + private KeycloakSession session; + + public RegistrationsRealmResourceProvider(KeycloakSession session) { + this.session = session; + } + + @Override + public void close() { + } + + @Override + public Object getResource() { + return this; + } + + @Path(PATH_REGISTRATIONS) + public RegistrationsRealmResourceProvider registrations( + final @PathParam("realm") String name) { + return this; + } + + /** + * Restart authentication with registration. Allows specification of role + * (clinical or personal) to initiate registration. + * + * Mimics LoginActionsService.restartSession, but restarts with registration + * flow. + * + * @param authSessionId + * @param clientId + * @param tabId + * @param role + * @return + */ + @GET + @Path(PATH_RESTART) + public Response restart( + final @QueryParam(LoginActionsService.AUTH_SESSION_ID) String authSessionId, + final @QueryParam(Constants.CLIENT_ID) String clientId, + final @QueryParam(Constants.TAB_ID) String tabId, + final @QueryParam(RoleBean.PARAMETER_ROLE) String role) { + + KeycloakContext context = session.getContext(); + RealmModel realm = context.getRealm(); + HttpRequest request = context.getHttpRequest(); + ClientConnection clientConnection = context.getConnection(); + EventBuilder event = new EventBuilder(realm, session, clientConnection); + + event.event(EventType.RESTART_AUTHENTICATION); + + SessionCodeChecks checks = new SessionCodeChecks(realm, context.getUri(), request, + clientConnection, session, event, authSessionId, null, null, clientId, tabId, null); + + AuthenticationSessionModel authenticationSession = checks.initialVerifyAuthSession(); + if (authenticationSession == null) { + return checks.getResponse(); + } + + String flowPath = authenticationSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW); + if (flowPath == null) { + flowPath = LoginActionsService.REGISTRATION_PATH; + } + + UserSessionModel userSession = new AuthenticationSessionManager(session).getUserSession(authenticationSession); + if (userSession != null) { + AuthenticationManager.backchannelLogout(session, userSession, false); + } + + AuthenticationProcessor.resetFlow(authenticationSession, LoginActionsService.REGISTRATION_PATH); + + URI redirectUri = getLastExecutionUrl(flowPath, null, authenticationSession.getClient().getClientId(), tabId); + + if (role != null) { + redirectUri = UriBuilder.fromUri(redirectUri).queryParam(RoleBean.PARAMETER_ROLE, role).build(); + } + + return Response.status(Response.Status.FOUND).location(redirectUri).build(); + } + + private URI getLastExecutionUrl(String flowPath, String executionId, String clientId, String tabId) { + return new AuthenticationFlowURLHelper(session, session.getContext().getRealm(), session.getContext().getUri()) + .getLastExecutionUrl(flowPath, executionId, clientId, tabId); + } +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/resource/RegistrationsRealmResourceProviderFactory.java b/admin/src/main/java/org/tidepool/keycloak/extensions/resource/RegistrationsRealmResourceProviderFactory.java new file mode 100644 index 0000000..dca1d10 --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/resource/RegistrationsRealmResourceProviderFactory.java @@ -0,0 +1,34 @@ +package org.tidepool.keycloak.extensions.resource; + +import org.keycloak.Config.Scope; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.services.resource.RealmResourceProvider; +import org.keycloak.services.resource.RealmResourceProviderFactory; + +public class RegistrationsRealmResourceProviderFactory implements RealmResourceProviderFactory { + + public static final String ID = "registrations"; + + @Override + public RealmResourceProvider create(KeycloakSession session) { + return new RegistrationsRealmResourceProvider(session); + } + + @Override + public void init(Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return ID; + } +} diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/resource/TidepoolAdminResourceProvider.java b/admin/src/main/java/org/tidepool/keycloak/extensions/resource/TidepoolAdminResourceProvider.java index ef08a44..be1c948 100644 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/resource/TidepoolAdminResourceProvider.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/resource/TidepoolAdminResourceProvider.java @@ -1,6 +1,5 @@ package org.tidepool.keycloak.extensions.resource; -import org.jboss.logging.Logger; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.models.*; import org.keycloak.services.resource.RealmResourceProvider; diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/services/messages/Messages.java b/admin/src/main/java/org/tidepool/keycloak/extensions/services/messages/Messages.java new file mode 100644 index 0000000..d462769 --- /dev/null +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/services/messages/Messages.java @@ -0,0 +1,7 @@ +package org.tidepool.keycloak.extensions.services.messages; + +public class Messages extends org.keycloak.services.messages.Messages { + + public static final String TERMS_NOT_ACCEPTED = "termsNotAcceptedMessage"; + +} diff --git a/admin/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/admin/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory index cdeacd8..4488404 100755 --- a/admin/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory +++ b/admin/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -1,3 +1,4 @@ org.tidepool.keycloak.extensions.authenticator.ConditionUserInContextFactory org.tidepool.keycloak.extensions.authenticator.RedirectToRegistrationPageFactory -org.tidepool.keycloak.extensions.authenticator.ResetUserInContextFactory \ No newline at end of file +org.tidepool.keycloak.extensions.authenticator.RegistrationRoleDiscoveryAuthenticatorFactory +org.tidepool.keycloak.extensions.authenticator.ResetUserInContextFactory diff --git a/admin/src/main/resources/META-INF/services/org.keycloak.authentication.FormActionFactory b/admin/src/main/resources/META-INF/services/org.keycloak.authentication.FormActionFactory new file mode 100644 index 0000000..b0cde15 --- /dev/null +++ b/admin/src/main/resources/META-INF/services/org.keycloak.authentication.FormActionFactory @@ -0,0 +1,2 @@ +org.tidepool.keycloak.extensions.authenticator.RegistrationRoleFormActionFactory +org.tidepool.keycloak.extensions.authenticator.RegistrationTermsFormActionFactory diff --git a/admin/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory b/admin/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory index 76a824b..f44a238 100644 --- a/admin/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory +++ b/admin/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory @@ -1 +1,2 @@ +org.tidepool.keycloak.extensions.resource.RegistrationsRealmResourceProviderFactory org.tidepool.keycloak.extensions.resource.TidepoolAdminResourceProviderFactory diff --git a/tidepool-theme/login/login-username.ftl b/tidepool-theme/login/login-username.ftl index a9a955a..b729ef7 100644 --- a/tidepool-theme/login/login-username.ftl +++ b/tidepool-theme/login/login-username.ftl @@ -22,9 +22,9 @@ /> <#if messagesPerField.existsError('username')> - +
${kcSanitize(messagesPerField.get('username'))?no_esc} - +
diff --git a/tidepool-theme/login/login.ftl b/tidepool-theme/login/login.ftl index a10973c..afdc1f2 100644 --- a/tidepool-theme/login/login.ftl +++ b/tidepool-theme/login/login.ftl @@ -17,9 +17,9 @@ /> <#if messagesPerField.existsError('username','password')> - +
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc} - +
@@ -34,9 +34,9 @@ /> <#if usernameHidden?? && messagesPerField.existsError('username','password')> - +
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc} - +
diff --git a/tidepool-theme/login/messages/messages_en.properties b/tidepool-theme/login/messages/messages_en.properties index bb67f44..1e0a8e6 100644 --- a/tidepool-theme/login/messages/messages_en.properties +++ b/tidepool-theme/login/messages/messages_en.properties @@ -1,18 +1,25 @@ doRegister=Sign up -doLogIn=Sign in +doLogIn=Log In doForgotPassword=Forgot your password? -registerTitle=Create Tidepool Account +registerTitleClinical=Create Your Clinical Tidepool Account +registerTitlePersonal=Create Your Personal Tidepool Account doCreateAccount=Create Account continue=Continue keepingYourDataSecure=Keeping your data private and secure is important to us! next=Next -letsGetStarted=Sign in to Tidepool +letsGetStarted=Log in to Tidepool enterYourEmail=Enter your email address enterYourUsername=Enter your username enterYourUsernameOrEmail=Enter your username or email address notYou=Not you? noAccount=Don''t have an account? alreadyHaveAnAccount=Already have an account? +needPersonalAccount=Need to manage data for yourself or someone you care for? +needClinicalAccount=Need an account for your work at a clinic? +createAccountPrefix=Create a +createAccountPersonal=Personal Account +createAccountClinical=Clinical Account +createAccountSuffix=instead. saml.post-form.title=Redirecting, please wait... saml.post-form.message=Your browser will automatically redirect you to a login screen. @@ -23,4 +30,17 @@ emailLinkIdpResendVerificationCode=Resend Verification Link emailLinkIdpConfirm=Confirm emailLinkIdpConfirmEmailMessage=Confirm your email address to connect your {0} account with your current {1} account. -emailBoundToIdp=This account is SSO enabled. Please sign in. \ No newline at end of file +emailBoundToIdp=This account is SSO enabled. Please log in. + +termsNotAcceptedMessage=Please accept the Tidepool Applications Terms of Use and Privacy Policy. + +doSignIn=Log In +loginAccountTitle=Log in to your account +loginTitle=Log in to {0} +password-help-text=Log in by entering your password. +auth-username-password-form-help-text=Log in by entering your username and password. +webauthn-doAuthenticate=Log in with Security Key + +home-idp-discovery-help-text=Log in via your home identity provider which will be automatically determined based on your provided email address. + +unknownEmailMessage=This email doesn''t belong to an account yet. diff --git a/tidepool-theme/login/register-clinical.ftl b/tidepool-theme/login/register-clinical.ftl new file mode 100644 index 0000000..4bddf0f --- /dev/null +++ b/tidepool-theme/login/register-clinical.ftl @@ -0,0 +1,127 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm', 'terms') displayInfo=true; section> + <#if section = "header"> +
+ ${msg("registerTitleClinical")} +
+ <#elseif section = "form"> +
+ + + +
+
+ +
+
+ + + <#if messagesPerField.existsError('email')> +
+ ${kcSanitize(messagesPerField.get('email'))?no_esc} +
+ +
+
+ + <#if !realm.registrationEmailAsUsername> +
+
+ +
+
+ + + <#if messagesPerField.existsError('username')> +
+ ${kcSanitize(messagesPerField.get('username'))?no_esc} +
+ +
+
+ + + <#if passwordRequired??> +
+
+ +
+
+ + + <#if messagesPerField.existsError('password')> +
+ ${kcSanitize(messagesPerField.get('password'))?no_esc} +
+ +
+
+ +
+
+ +
+
+ + + <#if messagesPerField.existsError('password-confirm')> +
+ ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc} +
+ +
+
+ + +
+
+
+
+ + +
+ <#if messagesPerField.existsError('terms')> + + ${kcSanitize(messagesPerField.get('terms'))?no_esc} + + +
+
+
+ + <#if recaptchaRequired??> +
+
+
+
+
+ + +
+
+ +
+
+
+ <#elseif section = "info" > +
+ ${msg("alreadyHaveAnAccount")} ${msg("doLogIn")} +
+
+ ${msg("needPersonalAccount")} ${msg("createAccountPrefix")} ${msg("createAccountPersonal")} ${msg("createAccountSuffix")} +
+ + \ No newline at end of file diff --git a/tidepool-theme/login/register.ftl b/tidepool-theme/login/register-personal.ftl similarity index 84% rename from tidepool-theme/login/register.ftl rename to tidepool-theme/login/register-personal.ftl index 2d7e1ad..a5d5510 100644 --- a/tidepool-theme/login/register.ftl +++ b/tidepool-theme/login/register-personal.ftl @@ -2,7 +2,7 @@ <@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm') displayInfo=true; section> <#if section = "header">
- ${msg("registerTitle")} + ${msg("registerTitlePersonal")}
<#elseif section = "form">
@@ -20,9 +20,9 @@ /> <#if messagesPerField.existsError('email')> - +
${kcSanitize(messagesPerField.get('email'))?no_esc} - +
@@ -39,9 +39,9 @@ /> <#if messagesPerField.existsError('username')> - +
${kcSanitize(messagesPerField.get('username'))?no_esc} - +
@@ -59,9 +59,9 @@ /> <#if messagesPerField.existsError('password')> - +
${kcSanitize(messagesPerField.get('password'))?no_esc} - +
@@ -78,9 +78,9 @@ /> <#if messagesPerField.existsError('password-confirm')> - +
${kcSanitize(messagesPerField.get('password-confirm'))?no_esc} - +
@@ -104,5 +104,8 @@
${msg("alreadyHaveAnAccount")} ${msg("doLogIn")}
+
+ ${msg("needClinicalAccount")} ${msg("createAccountPrefix")} ${msg("createAccountClinical")} ${msg("createAccountSuffix")} +
\ No newline at end of file diff --git a/tidepool-theme/login/resources/css/styles.css b/tidepool-theme/login/resources/css/styles.css index c852869..2892bd5 100644 --- a/tidepool-theme/login/resources/css/styles.css +++ b/tidepool-theme/login/resources/css/styles.css @@ -168,6 +168,7 @@ a.tp-btn-primary:hover, .tp-btn-primary:hover:enabled { .tp-form-error { color: #de514b; font-size: 16px; + margin-top: 5px; } .tp-alert { @@ -201,6 +202,7 @@ a.tp-btn-primary:hover, .tp-btn-primary:hover:enabled { font-size: 16px; font-weight: 500; text-align: center; + margin-bottom: 5px; } .login-pf-page #kc-registration i { @@ -285,7 +287,7 @@ a.tp-btn-primary:hover, .tp-btn-primary:hover:enabled { } .login-pf-page form { - max-width: 320px; + max-width: 360px; margin-left: auto; margin-right: auto; } @@ -296,13 +298,14 @@ a.tp-btn-primary:hover, .tp-btn-primary:hover:enabled { } .login-pf-page #kc-form, .login-pf-page #kc-register-form { - max-width: 320px; + max-width: 360px; } @media (min-width: 580px) { .login-pf-page #kc-form, .login-pf-page #kc-register-form { - padding-left: 90px; - padding-right: 90px; + max-width: 360px; + padding-left: 70px; + padding-right: 70px; } } @@ -337,7 +340,7 @@ a.tp-btn-primary:hover, .tp-btn-primary:hover:enabled { } .login-pf-page .form-group { - max-width: 320px; + max-width: 360px; margin: 15px auto 0; display: flow-root; } @@ -565,7 +568,7 @@ svg { .card-input-description { font-size: 18px; - color: #8c8c8c; + color: #6d6d6d; margin-bottom: 10px; } @@ -600,7 +603,15 @@ svg { } .clinician-terms-wrapper { - text-align: center; + margin-top: 10px; +} + +.clinician-terms-wrapper input { + vertical-align: top; +} + +.clinician-terms-wrapper label { + max-width: 95%; } .mail-icon { diff --git a/tidepool-theme/login/resources/img/tidepool-logo-880x96.png b/tidepool-theme/login/resources/img/tidepool-logo-880x96.png deleted file mode 100644 index 08b3860b5190eadd679f1f1d23a07954c478a065..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23024 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�#K{|_3nTYkN^KB392O`m|CPaCQ`+jq{m*s z2_)J5eu?j_c-v4iP|Gd{@(@0p+C9MU2MZsr{SSz(pLA*9I&N}-$H_$5){Au}m5!vm zx_Iplg`uVr8|vtp+7&QHj~k!q0?@NUW=A)_+iUS&=*cPY5iRH*H7)?hZ;-L%cYhU1 zvoFc5-elRnUEG$Bt~n+!Cf@N5ZWb#ZfQZWJ`J3QoOTEZt!X~29@8>MT2?~a(@z9+f zmCS2Oc^ijuHG>`bc0>0)%t>B{lb^lt>V|E>oe8B-guAiMSET2U-Z3q~T5*Y>Vw4s+Ie8 zX?m@Ac5>Zi>EXsW5Tl0$H$I~s^<;?dFEB{H@RoVq52n|~z=tPDNap96e7{~=G6D&s zygj z4b)SUYIxbL8qkfz{Mn780i%cfU+6~)VuHWrjDMzHk=_0EwhTpgv6RCNjQ(K$p1I2m{1^Y5 zB{PSS&zNN*;oBb-k!_0L^62MoMAbjF`R8dFsQ&nk^-*gizxwx&cVk)g7rlgf{l!;T zsiUEz*V+{L`&8awI#wl_e`OG4&&93mV~P{g23&c5UW(>o`s`q3Na{E0U>xw#}9jpb2T zy1n+xKgs9bnIOLL+f`RJXrC8mO3Pp1_QQwTdLhHv69G0k6g>G$EJ_5q-Ez1!#ubwl z7o0pU5#Dn9vkRk{d~Fmx5xys1A&!RI)chAzF54a%d&<`(82dEyYk} z9W1Ikg&p{m#$Q`R!3#AP)mf*DR!?HB@aTWh5~clJ8ehX<yR z+VY!Mfl?|&uyatwZEmaaS+!l{XasTH#JtSqKAWOeF@zEv=V>#vdmpv_fL~^3gqq8m zjES=oR9O*DwbOr;S-KweuCw{}qg(dwQ0^auVQ|nr?{Dgw@AMaxl{n7`qWi#KV}urU z4coBHe56r6GUe1~Ll*kTVD!5rEhyu0%2=a%bZU^#>fwqiM#7))-~ERCI^2>6adD03 zt)QTC%V1@y6X5^N71+a-T3H;x61kzRjML%RmszM68@j@q?-AQr-26%=+(7n}ftW_^ z=V+}qrl5$p8SN+-$QZS-nC1mYUCb{x5Ym{=L9_2X9wxQOZTjXfagKErSmF^NK(OxM zxtJeiO%S=YF1fljzFUIShpW(e?NukMW2N~nx;PP@)zXKchmnGwt>AxCROyhWEtNX? zsM8*Y0!9TRtoil>-_YwDz6GLof4`{~-ir%aEKm-^tXnJNr1D153z2r{g^>De5>rCFS^^ z+##cbiV^RAeW0nlI5KB!tOLbv7U<#Si_s`PHahUx_r#O{-#C#xt%qZe464VOjcEzi zP?*>OH>?lqxnN6+9UGn9p-XGU){=oylPG3B`?Wmo0vMl}oY(d|&g+34rjwF+hL>ra zsFBoy38sa`voQ2)+p;b7Y~a~IFuHWl*N+89Uw4%Ji+Xyp-2XO$V}6T{WJu+KzjfCP z&{t+MQU6EoUQ9-EAorXe&VFe3$A8S)1Ve0$GZffqk5hwtB4Qg8<9U~}{q}CnlAJ6) z>F*zESXJn{ZIEw%q4AIaHpa6R(6>}UEUV%;c{~Rd&60gh7~n=hINl!;WZnGYc77?YC^F<$L(w2m>Yc`!Gbh(hu}F5kd`_Souspge z26(r(0}*$;fEUlqLH?sE6`ukJo{UZw<#vQQ_gZ#sVV>KO0Q8RC5J=5=qCYQa zK>MgirOTS0gkRt4D~O&ea$x{vV>w>Y`+yUf#ak22m{!)>^Rbqd{}+L9-OWy9AI&C( zXor<;ssr8t1BE9G#Ba_1vj}q*4HeAzC*C0>xWYJ224Y#)eLirnheJiVi-PQwLSNMW z#?N92=iy16eU2he>DO!W0HB@+6jHEfYhIwZF6HtrM3rE>`~7vXjendX>t4IJ(-by< zGUSAjJx*N=uvJvm#CP-EQzB{S@P@{cCX`<~qXm7GEy?TkU+kp12iHvXZRy!;JBeir zI|XJknpelL>M#r^rT(v?O5+z6us+8@>|#nviFQAQ0DESEVYMWmaogFkmt`)rOxSLQg@0P>KY`(=RImSGt$0H-rXNZSp5*Xwn+>vzQ zLTS(ZR9TTF$lt(i4N9Iuu!Yi?_aG8~0+QJY3^>yF)jQe%HlYov7wS%u2HAW>fY@o{ zsCRWAQz%1=tc@HWon8M3@fiX5Rc2*MKI?<9t_g9Ka>{ZwohS415t}|fdBAOCJhdEx zvx?dcUoh(pM;O>R3^i$h@8qBvvVF;*RZfUZ^Ca9*S677{%WDZJHC&d$DWP%y@jbOO zOZ8(1CW7eGL^tjcBzo%ZYD2SV9)zr z$dkMlUd+7OQY~t&KyP23laDE7U|IkfGoLXI>A|)hQZI1M5{HK+p}AS$%egzg9$%sj z1|WEcA6xv?8j%3X8ra`HA->zse_g3=c4KFInj%fyBFlVw5iCLjMZm>z(G(A*nzTx? z2Lo=m#ZzUhVJiR_ZG%*46_}N*lQ@QS`rZLIumX!>qy4EQI(B54w&s5v9@aOC*=XYV z5s~~nXQUu@oxRX4HgL9BDez%PTVa`{>_N*|Ize54= z8z74lJSbeldpq0HZ0l*^<}C7J_{=$ao$-$SEQuo!AmRPTb3qA3a0?d#Vz^UJgZM#0 z|K_POwh;LrT>*K3S^0bVXrbpNa7Vk67aR&H=itQm&^F2#MvjJ5No@tuHmZImlYcPa z5N9@DvagtKLLwO3fdZ}NMJ`vL;#?a!)nfh6ducS)&39&Fpow8dfHcS{q@27s7lFoo z?;bqYv7z}oo@jX)iuYsQ-nD4-&U_(U4I4M`bu^J(vjX}2+Fi=c|CTyF95C~%2z|Fy zgpE4|nX*lnG5_J%9_TPOa`-OUNruuF#)IbfH;h_WWFW^ ztW|a>w2-}RI;V>1JLN03hotJV#?ve@#j$nDaAS?Dbe`0jgfQKsNy=4cCR%2#6xXAU ze9M-WHg;L(1>PYB%ppE!ogk=Nbq1@6!{Q>vF<3YP>>#Aw(1cMenZF(<(A)w8akcA; zvx6S~SZ8>J$Hk~BL5Vu!9jvT@7y_QyA7%y;ArV*dHwtsdmx|Iltwlbob_R{8>`E5q zB}a-zA^K4-`wZg=0k7utpKyAFXv8{ihnt(?NmF5wY;SQFrys6w7N1g#rY=vs!tqt0tOQ6d3?!QW*>?&XTA!Qrm?iW!0--`8yf-Ii* zur_pQIQ2o-av1dSHmgUTh3BSkU%#p!nS7soaL5>A+RDHV^-pTqTiJTnDo9Ip(*Z|s zHgrlQz;cg2#zMrW)HKkh;S!bQW}+lTfm^I{G{vC5DuX5ZCt!>$PBTfzftKJ*5WZ$; z-|DF#73NpXDo{M!H-eHYrbI3I6`El|mTZ$WAuUA^*UYy8bAToIPoib7MeRI*Keo*D zQQ>~@Eas){#~8smd6hxUg4{R$Dyw%!j|u}CxJhJA$STGs%~4sEVDc?J4Za0sgg!*r z6JdTpr+Fg`n2*fqo?l0<nN?AW#-D&8)pPqIjOYwC zIXnke?F&__$N(I$zDp{9G*_b_U$HJ#7;XBr`KC9)=6OxIjS0GjrMtr9_=Iwc$K$T^^t$kDMgPjszV9(Pu?-_LPE3e5bK};*FjGepm*AlAZ#Fu`<>B|N{@VNe4JfjFfxGe7D%RcVo zu#hba7urcOA;TN8Hp7><9KV?H9M+<}Z6rysEmz)_!uWD5cnL6k>oKN*f~r4~6L6kX z6*xcL%H5U`wjw{Ay!T8E$IrBR7w)+m6oukZKNt;d_5Xv?>3sxSwn5WxC5gehljDQU z>_e%W4UM{w#fR3vJ+OXY`YA1MsrCeU7_a&{ZIf{`i&OUR_muY(?&LajMT=sH3Y*zc z&_Q7~ecjU2*PyW1u*BodGdh1orP&UwTqFeRNxAQJ215<@KJRpqkI5H9Kzu-5il1(a zR8Ekj`9>08eJ@j(L&JFPd9jzFzpYZ12yfgS>O^%~Eyg2fvPr&&1CC@6K3lO~j$ORO zfmQM9nu7y1zHaCei(x_aFUQ(@ximl}SmbM`xbuVGeg#h7B7SfC2s)0d4~Ktt?8N`O z=niO0v>!v=lcP5}w~LyJMSL<`CM!oRcx@7vUc<0u-z+^lJ0C7JDhxK5P(9Gm}{+7e>CXtVw zN6-j+?iQh{0i7ppi3^uB#NnmWbipTL(K8|jCpOZtUu$pE+5X?cTbchx!3g#rktkmN zspk_YnnpJylQhW0xRYeKxFL|6CB(@2Z_f3BQ}|Yt@;;HN-{_I$-OMy}Tg|Yio`#(E zEx)2#{=8B8tk+9i2*Kf)L9MM1A{ktEOGl^S{lmy*0WW^N%|@ke0IAKsP{_giyCNsp zM~*RR0wkOKJ41RZxIWXt9aws(C z?_ew$4N6kK6L1Mu*SnBN|BoAU@yq*7hd%j5*UQ&W*He=Gy{pua27C+=S5!Ii@jcRm zl?DWA9;lvbpHHRtVbcg<4L%o;tlaw#iLn0>3K|d}v>UJvTzBI-Xa~J=q7`p2^g*3O zoQc~FHA_xOivF&TBz}sH%#^y4N9dP_mozR*f-Edawz`N7n5Wv8t{8mkF+M$#B=a0N7#R6TOp}(cT0fx3_tmIje~^PJ-Wkgy0T8;q0hwPZIF7Q zV{Dun0ebF9^n4}qMttxM7cV)dH#Lx}idu$xBIK;g0mmEq0T=B#v^db~!U6YGhtQ0kL0+dTs4-xU)%oc7JI4 zmcDMAJ4w>J6iFsUDhv!MNPLL+bzP)^cw+x56v65`y{&kK?&0)Wiavf4kU{E`?% zEGAXi@Q4G7|F&SzbC?vhBY#BePwM&)nEt>`L6+%*8n8hZueWa*{AF{wk^ZQyfDYUF z@Qr*(^}Yl9L)ZU+KQ(x;FAOPK2s!(GO0O=yPhTZ;3a zFF1sPlf~Bx4wdCJJZzrp(`ScVX@ET?r5cVNDD%`n9ueP-lxGd&<6aStV$~lCl7pV~ zXE$I;dOo%`FhIl7@)YbQ$rugPAHt~rRI9#kNqB4Ut{%&h48UfEN6^T^MlJ#Bce)%q z4a-`qki>fP8aB~6N)<{yk^nOd0oIjTRs361^ZA$JMR-$>=41wqNVM$j3X2z_C+2A9 zJg+D8ANr%6ZvKibSr?Fmlg@@7(0_4CTCu2htmzFYs|`%KM_IckUYfS$h~}P}QE;Wo zbU6Tyc;!=Sd9$(8nC{s7!kBwhH7aC5H~5y5SAI3xO+La8H4bRN$Wp{#(*I3KqzE~f z8hkIXHnH`TIjucht&RJb6u5~TSc}(z-I0r{8kh3ECrdgPnxaAF1Oe8{z1O>bKg;w> z$Ik#kAxl%Q+p=B@Plgc{MO>sXDnW{2`j73Umox!VLEA3pu}~UB`Zi>Oyaz1l8Wdr$ zYALqopkG#`ObN{qCj!pLFG0)|wUN#>*>fMt(*HTtFfjp}+s5qH`p-_NT^IoFsM3Vx zL@}d9H8=WJD|tjt|J0;S!roj`wG~sU+$q>BH$^qO+N#y22IDqO7S9El&jnwtczzj4 z7Fk?oR7*lI%4104GRoKtL?%t`!(pnrce@)Ro90U?&Viw%7M(@{IqJ8tEPC~chlP19 zjC9Pa;lgvx!aC0}I4l-DV$m$-nt55g|1zpe)V;iF8=L#;eM%1w0tLcg0!|!$%8K0` zzGmll0shs2qKfOl%kK<-Zhk;`#ad@s-JnJM09xZTR|LA8fv|_)FEfb!nyJ454Oa`Z zxFCnysB}t|Qs^`D_u=ph2ncTWfBypb7GIE1hZn^+!&VnSEyO!zdWu(FHaEK zZrz7sp<7x$uuOBljFCdq>sLMGSiJc9Zy*Xt7qSzZ$+J|tNwc>0N=5gSVN8DbUtaSl zPV!wVA#dCANw~C*(9Xu6$)7c7=#DU+6E^!{`hNik8TaNp_tX|;C65@^eiF4|P6~ZM zggbCpu=dBdw~M5)Gi^rd$id!Ue=cFV{wR#LQ&7g;@Y@A455$?P)3v(V^_i*J7uZ>c zK(bAk#_mE{wZ?d3)J1L$#9M|%7Xpqj;5g)blpCXG{n2Tf)6gMqS`OIrhOnpOdK@ue zzq{e4vGQIb&}Inm(OkGVJ_k;7CSsJgg#!#2H>+KWdeZvx0^1!fKiR{E!9@8K8>EkH z1wg;33FMYmq_<_#lFGvSu`YBlgln;v7In>5RiziA-2allA-~R+3P_x;QJd>`=g zG!hqaT{Vp?bczuNhb3_^-X4b$+N!hqunpi5ocktMg&4gUQ55bN1CC?9zq33AODcP) zih7o!DC1Oxp4q)qd=GS{u9q`!QS@dyOqG&~<@5WW+Bpdw`@wiO$HJq>=(xD}qe?55 z4_Q~XY@OZIK|^tiu91&nydBXB*_f)3EAsB9m95}m+oY&dzFJ?o{A6B^imHxOsdAo6 zRgTBf**+LhvgKYz+kzf139BtgJ) z0=d{T6`LaD6%zT< zNCYGTAt3-~B_lB?C3In*+&M_#cms|rU9@>=DSo(-(&F-XxO3s?xu~v>6f9~!KgrA5 zRt6lrOs4A;GU;TDus(jpi7`c-OG_?&ZYnMJi;|B~jDR%YqPTFSY7zmAKGMn4$OQ%K z$aFMf{lkF>jJJ*PE;7dBkt2QKz*F*&2uK84i2x)0L^&`jo<=keEA#_Wa#OvGT>>lP zB>Z(HjQ)(y_(!x`=T~qXQoR)(F+~$fSu=XHT`-#BVZD7m!ly+4iZ(gul3y!w<9(Sq zcLsgm3azAx9t5NT7d>SuHIWD$K;Y&_S%<1!e>?!z2iYapb8_Gli#Xt&rHd^wzxcB zZWlQS4(wZ;e@O0<|Efao%kjywdxke+nd<;2Cu8~iZukJ0T*i} zF7=IC1mMNWRi=@lyz$@t8@UJ_n$BTXb{`H09W{F<{UOYJB|C|LL_i`CG6FDp_!7b- z5e8VDP$EJfZd#n^O~SA7-TFjDzE(s95T0sH;E(Z1qwYyk`g`fzcP) zvWAnAK|8C+%V?925s^D6EhP)FmElsXhKZUzjzM*n1P2gsv;oHtag?4X50s_6u?Yg) zyV;jt&i29Ch2WU5m;%?`&$`#ukPNJ&KLI(Ny+tF#4;XD>&KaY8d$IBDd^j!npO}=< zK@BGW%8C^Pf1a>x5a&xrM%b&$LM}ASdaoOf3!7+QQQ1wk1wtN;#;1WC3Ix}_BcXXs2n3dhNKnL>-zE{CiN zWl1adFv;NY8EtcdfWTHrU(4xFE+01zZpq{oaRhiDp>S?-{Hn4=HQ^E_n2m#a$XORZ zs1u_pH$duPm`0CBr?M86_e%UeR$5+YdZ4niG_{Yk6Q;5cA&pSu80R8KJm;a??W0B8 zN`eNq52@3pBbQt>%spZ7_!yz3_`qIGOzQLKnl*V|Q3)`krfkv49~DCWh*`B1r0<5` zF5uY}`!{-{K{#BLXNdGWa4_h|SUX#0c;E2}+v+QBr;Lun-jriu6RR-F%Yge*5=Ul~ zZY~r)J|QzX`!5(Oy$0nl2=7T8_*J7G`&d!v!^P!=ABmPvR%*@wpQca4`*t?ojjnh% zeCTkjL!J2ixG6)P675{3_sLGevGccKjnWv%(ieZ+>E;&jTTDsZ!^IU#wus7TXZnfL z*lWP;Vi6hfE%e8?P$ZH@(zY!EeDGo)t1rfP+SgJ}lzvuPUT~_FK{oI{Y#*tA7&AYY z;$3k!<&SUU2cjr{YdDjwX@tprZol04P1V}>;MLoNtXUM~daNZ19*1XI87$-PQ8lT*2X*pw;NN6fD=I>QZ3C1}Ev+b+XC>c88q_iTyt=a* zpTqHs_wMEF{$zYK8gCP)vem$TkRVPA8rnX=L?~1*2EQwCj_(I~GwEv4~Cn(v7eE`Gb-bKgq`?ZuDabb_B$66VvBPNb8z&Djii)R#JS^7-G4ktV`#u%@H`YeKLyY9fCLSJR6{*^ zH4Ln|#xw_GF#Gl=bm*5K3<-XR@2+0y*10=ofc6Hbj(&>FU ziZWt-dBG|(Iv$5vw>bT`s%5B8&5bRE-oL)Q@a|yzcs%NtQ&85QTA{T{1D#&z2-ph&j81>yQeDH) z!TrQgGR(Xs#3`p(G2ZwLJO*)=m>X|AC#K>%oRg87bD3ejO(Pi1Z2D;BzTrFHwXdHQ z+E}3BD*@uDG8DSLVWcnKgHqMyI>U(B7LF=n(s|M1!Eh&Qh2n0#own`>iCkPDC3SeZrFgLt;@c&0z-Og4$cweM>$x# zkjGuT{SQh_iS@(fO_;%W+oGuCkMn2J`=KMc2l8qo1bVPqG9T{(st@A}1Fg@Xy)`V!}7l@2lK2E4W$*DKKSyKb9CR zShQ#n`poi@2(%3X^}03{oGp8L#Cth{FP|x`SpL3|MDQYcEk)M@=>RJLvx}P#V50V#l7kX?!@&>V>Jee}MKaBYM0? z_aV-5-3|IH(184-cuRgsAfEeJ-DL>x87MAL82EMd`Fwn&kjp7AiGb}8D5)rX7eevB zW*0iY&7RF3u(IMxv-p;AtjCsGogWzeX3MyPArN6Bz9Hd@y5hD7CyIdtrCXPM0yX(x zvkRU(n?0L7Fioti(TR@p68%!}e8`M1Os(x96m>I}cwgR%OC*z?y_5ddEJ8lC6#{2Y zx`*(UH32=pX7*EoTm29hj|~0ea4}#f5i?rl({-*doFt04rhc#bD7hiV2M^ zqG~I*3`XClikT29^D}X7OzqhlXOQ+eHS% zs`19Lh4Ca@rr2O?Sf;!7c!y&YKlrG8lY&Ksesor^+@qTW9k}HKS}3P84+L1e(KKVd zz)x6bbU+PtwkVP$lL*)r0q*?`b#<1gfLMBcavEQjRMl9_&3Wc{!BN1p=YDY-$B@}*8!84`l`wRo_ zNWUcQ@mP+E-~u!h=U@ZxruN9tzIg;+|L!k4%FM7#b)p-_`SWS|jQwqWnm#w)*Hv!n z$wz-1pRN9)0Wpz-2F%Nxw>VDb{A8>|z&;3gcW>t_E38k~HHvOwuo-*xcEYpV(~U{2 z{3?XrVueo9goXfLRUn!YMtIPP1+=MO<0TSuSOYn5RykRus^oa2$5K6cwKoEIANm^R z$;{f)z^TwjID|Vy3KZ*Onn~LnC`Dr!>uq}j4ttD3I#U4^UP$R0p^?7_5kLi0fAGoL zot{AItWi3X3I?1`C=(q;{5X{mAdna2I4eA7OoLX>+d;l6&;kwP`3;GR5UNI>_s}N8Q z=?pJ#;^%AOh@)**GIn=Ad@5#rzPCb)dU{?-W#NCV&`O%n5CF#FvzyFXS*!8nTGW@8 zmOp}SxywaPg5fr9j>p>j5t_#KO@&!YOIWqB28QihNPPqd+nIC@*al-ImtkIJI^vAN z8rvIiZ~wUh6;%lPkAuCJ zYejhq)1!P~xZOuo(r4Zbde{8h=^kMoQJMe^sCbz&<1%Gb4OZR(kffSQ*>?dV$d~1d z(Xk}k1I3wzp<`(#hj1MW!$&xcnS7CoLwSqUZG>o@x{!)U$t&}19|ZV;&8Vv_IlyQt z5e|WRr|GlNJwKByAyGLMGjX38(QpHjDs=iqQ-)13rjapWL4XmHatNFQ&gHUH)_K$D zkE)As@2Ecp=F~9N`?=yGXUv;Lv-3RkDK9Tf=;WibA5$|jTyJ$_yowtZprd0X25f~Ru z9{dI3elm&=6pmTD!iHs50U}WsVgQf`y?5vyRH4yX8h~9x5F<*!&DuuvJRDx$I8M=U z3Ds5je*XK&|IvNXpt#h*907PXe9oMrc`V+IQjxPhzuu7lOaD_kpUyod;+y^tzSCd8 z5p@s!1>gBXqR`WD=HkqT)gH~Wl22hq00U;Tg;-Lu8#5})tfqlcwqET;(lKy49_{eE z_$$WyvKGHDqZ2*~hTG7&9=gIRzi6gI3-7Toh1PA$hs#=w#-g&|nU z_QxU;5^^s&lctgX0RQ2^^2f_xjhUU}VH|7-M(gzW-#w&m{t`|q23XMrAKNUrxQ0hU z^9u*3&j-bo=2X^Vk>?(C4)}Wd1!zP*wU#Qjsc^>6 zyoP8#pmx!ep(jU6#}bF_zJWat7X;ItH|$I$L+5*_@XSmw-ZI{P2=I0ldj&7 zUj@1=VGh=UJiu zCylqZEjU*%azAo-t!;A&z@3tXYAOlZPs2?6Q~2id18`^-1e$#+VON+LZ#==eiskFk zR^NtO%##GUD4D^HH%tMTj;}vU`Rs?l)gIQ@ ztMNk)$r*l8{6+d1%LA^OJA<}3d&i$P+MZA7o717&*D8f%s&k}i5vJ}KygL>O8+>cXFh7@ zWAp_^*dTuz@T*3HaA$FO{*$~T>K`BInIA}AQ?1Rz`!qUGJotRzrplH40Np_GP7b@) z!+O?fBoB34OWk3pW2Ez@xijgDPS#P%9vT9CR&3qM-T#O0;%0C-ypap)03Y_Sy@vsf zTY?R`G^UZi_D2A-D-$rgayKMB>Y%jPk4VW1m!jUxm#!TQmv?Q6z@(|{LNs3gz~7N= znGag{P1t5|2l}su+adh(#~`M%Fz>TlbV6qZIw}b5<)^BuU|=WzNB^`&&zULNXW*EE z1!&Xz_|u?0--emrtBNZM7B|MV=$JG{*!6edQDeJQ*Y3fAcx)JC?2f-*pw!B)hs3?P z^1H=BXF}|%ms8~8hQ*|N$T@Ixo{b7E6(81ay!c-*g}fHad#X4t>8aVHFjtv_kFPgA zwp#p^P#0ZjbbzvzGH)>CrjcW*iyQ&0tKB}b8;5%C>RwNFdpy*#L1-l}Nz)R6%#@KQ zV9(*-mqjag!|0%ABR&NU2|SC^ZxqHB7F8_Y8YoS$Ff1h+2-(g@=k!>-f7qf!eqcmh zTw0$0d9bujkDoN1{Rw3~3Z3;nFnjmmeoefKy!MQTTA879y82Rn69jmNZPA=n{RvZg z<9LwrzKKe4{)YTIlO!_A!3YdU&N8<=Yy_zaIm_z7JbT0cC@CU zebadx9L7s&*lSnOnpI;1`iwr(>#ZM$&FTFq(G#(!ybK4*F28v4uw|jFRLRU7dx*Ac y&jsigoC-~QpbgrI_Gp#c?RsO~mZi-+bpAijpI%Wkr0q-q00007gNbc5ZsW$bZL_g$Hnue}8{4+;^u24{`(@UAnRU(|XFt#L z+k3*56eN-0@!`S1z>uV+#8kk*AU#0e^BgElA-DE+Zhk=`zMpH|ud|RisJvc_k&b{C*nJ2lAj}?`WQWJMx;Bmh>HU3Iy z^zdFmDWWamCk{q8It8mw4OWS^-Q4c`c?sjj_Ig_J>u8$pD-f-hFYx?Gs1a)K59zo__mgBI=S7Z_P zu6Mt?5;;TwG7K$!@nFX#r$T(!zy^46e?+o}1|b|`W4muKkoa!A+0N6lYz}2fMv$mq zH1>{362Cg$uz&c3em39gf-mk&oC}W+>#}9mv_n(1q+sG@W5)mw$1OBh+AB*o8K0(Tc$V}c0i zUMsU__xmQ2e?e$pI$9N=%Kh-Yd1F?9m|y|`17F`p#yeyCUbg4cP6ZNo4-Om0Tst9> zydWD5jc2?$`e7e71P zxSKrXFd7bVY)E@OY7S|U0r~pDg0K8`1N-T3pPavQPIo(sXGvI3v>9o>K(~xcb;^x6 zY(L#fx?G^mUu;O2YSmEWkcYxgl_g^w5b!zW9W->&mylZkRace<@jV(2vDLL`)QFHb z5CX#8ag1zTOt$gvRNX`eZa%j6`(?L!fSV0{kxKrJ{gHmOJw@7Bszs@Mcx;lj*LANu zz36*TIi|Z7u&!9=@53xx?uAbV$!*A1kE}Pk7+@2=-Wkz3umb5?Zczr`Ye{Z&GgM6v z3!>aK@HRTlSf+-g`A=$vH;vL$`F&ga;%ntoj8@#bE(`&mAq6+!bqzhx@N`Ttdo0yu znG?QVlgKDRmP2-XU_^^T{!1^T5EG_kiPV=ZHP;{Dh=U}dTl36&KOUf6u6XHS6R1{L z&qu@u>4>(dV8!FNb}N*JyMh~WHp8Moz-6-*M@Ju<$9_M|s6gMR3H+{9n#y=xB1`rC0hGj_!M&^CePqX_y`N)`B{tB?pW=?F?`d_a;VB-{K*K+ z?$)l;>zP7S##L#fD8^~Nc~W&824{J?KG{g_GxpT*|a&c=*9J#vV*n;t)O3%bvm#>l=e zJl3CUAvsSq3v*Uj@$Vg04C=iY$~UH_)mEZ&07xUQbB$puJ?`eKxwBUK3>SHIiGb)U zPax+v(+j>&WWMFS_c71kkBl=O$kl+~h^<$|n5g|#4I;#jY?t-SLZrU7!b0CX-s$eR zlY1cD;odv2)$PA_TNZ;qAQum*21-S4AtgI*i?bFZW{Uy&(j@oFeux1Jn&Vx`he_Nc z)a>1G9)>)pW_)gWtG(U?Yqa^N!pZyLwhe1G2Rny%whg!dQ&cVBA4vw&OB8R$drK2M zk}%m~moz6?FMeD1)neP1#d}?+9QCIG+&_8x3fVlx22w5e^4LeU8Om-F(q@d-Qy2n) z2<#e}kV8>P`2wAg+ijcSY!Ag%LHM z$3{?dyj!y{2hHf`?Q#}3dHI!FH zv51mJJHQo@j;Y^(w&c~0Nw7BVBTI>_z&c}x%-0bC^j4Axe^;ofW|@U^Z}+q2Kb}Jz zM6Q@mrh3p|lM2a(r&}?rK4#hmMv4VDqVY2;lzu^e9CcrK0;A7-{StJLBM1FspG(~- zhKd-kX8Rq@a0ngjp4^fc*D(w!olM2f~|h&$8?vXKURJQGJA_d7R# zNAt@M%Trltw%|P^2yfRhhSYdUlDFEA*a{bdDZj`+^+H^~>UN*{ectyGm1c}Ma(*<3H%JDS}IK=oMAymQjU_0jEu>`F)wLcs16+-cDe0}YQ zq>IDIh70$l72w=@Kd}J@gDw?Imme>0GcljCJt3C9T?_{BN})+gUY85Mn`=cYg^^R! zEf#w)`aa$ljftO`ncffM6|tt68q3Xv11Y^~vg?^d1ire{zr7o~E%9Kif8D8~=lY?| zRS3@E|ClZ{?HY@?{Npn7yNzpGpDIluHAD>XxJnn-$%aAm_+o$cpe+ByMj>dwkS zhc-c%_JA|d!y4`>x)^rgLT#8DHTj{ZHEYZ|#D@bvJ=EanVq2_8xSviu0Yd{(=PTNA7!C(lgl0Zud3fN-*$}6K_K@1D;rT z7rQ?ayIhso2;*TqV$B0` zJ{UNoaqW^$a^^SFc?BuYtl^m4$F!=*GL%MYq}n9pgDuWrVn1 zaFZuz#Z7$SvBr9n6>#2u&^k6(X&6~ILdpc?RQ9>m+Xi4*>V3W2FG%)@2l zlZ7V=d^SU5E0?=-bBHC0zUb^bHC`Reyl6& zswVA}9elXt%w9D8%^^%wH&T4qb>=0PTR=oBsiy5oIK94p@Z44Ce&28xE7zX9%LFK?sjBO!!@e0?&e=~H^8;jRqt)~WqJKrfF}c9Gc(nTben?tCcP8dKk7+2 z=h~ofy##d<&LuQQL2Z)AWtFwUmt=Goz-JU@Y=J8VT}}%jp^F%xyL6Qgjod2GG@T?` z!3vCF#a`<+L;wh~Md{rdH$@!ok$i@Am#SK(4jOMqx|lhpdT{o_W+T1u&WFRvJySWC z!n`&4tD?-R2a{uM9kUu*SLvuSM9ya}LH`{TMfw5D1*+#qh1ImL zZI4IBZy6cGWroiK>Zv4hk(E2&Z+Rshn#mCwP}fw+6|cpS_{B%zROAk8d7S%O4K)NR zw&`SS;$|21t|-OgFp#_Vh4xSG~zbkb>I~ zDP*@6dERamW|-LY@%%Jin5 zj|4M@F4$pF+PalElz^G%O`r65omzMq!(M!$vo7} zkX^>@%yd^dkN6jobG!jy-!+=vgA#yKjE$MIobChtF*50}7f9nDSeYZg`7 zmk-Yw%KB)XIAMymbsN{i{?9DGtb2>L;RbXR&buT{LU-8FY4Hd6hCCrb5!89t&*JRT z=A7uHe3Sr@_tD#K3cB~NcLzh-Te_ZM(B>pFO=`pVxiom9LoaCu!odZkBqN)wESsVw zaY!X$a29?;va}NTwm}s*IUoMNM}?pj5PGZHn-ib$ypPm_Iac{S$z6PP-_1-6>Uf-8 z1n6h@S0Xp8=q3v%Aq4tQ>$7dGg7{<1{CMklV!E}(On zvnH~6fR*NzxH%dejIlL7SUdh)&Do{7FELU#HO0m^cs_^OLw$&=2Bo+Cr-u09IaX`h zJO=Z4S#uw*e1Maj-Jvx~ot1GKziSc9P_Z|ckg|%xQqSsU;lW4##P(8W7@}{e#NO6R z?|j-H4*HSx?-#*;j^Ja-);rsgq;R*pI_NTCTr}-5FFhC`-)$%pmRKboK$px1NIzVT zC%gQ_Huke)eqqcmK(@B8*J!;vz;PNzt<1dRTa=}t||m=%DU!E#Z%NqXE(&Od|xLT5ud^LU0?3KaH;yHe)7l zkJFqt2MzilD@guYBU>Fht~r^o)`Z6E!u`0ZXz^HJI!5b#>!=+}EV02_%m;22=?JAF zw=}~3{)$qskN}P6tvx@jni+3~MK7Q%BKB2tZm6;OmZNoVMN^bIpulvF5i*PT?7i*2 zkgn+ZmUhe@)|#Vm&(18ofDAc4(0GpXA%Joq@g*Q!gmB{X>PEQmjN}VpZ)vTgHQLTi zWc-p}rO*d$C6UFjZwQsE%p73SMG?_j07;-#UvU zj)^ma2faPzSH)=Z>)$g|>61%PGC)#>%Q3jvMFXQY!o65Zrs~YEn~g*^+8|xH&nggprSBlY~C)$#*V{Sc`UD_Q)NS$$G83S;cD9Fqu;Pz_Bm9ItMdW3 zOBg1)!>)?SpIRz)R4%H(9tp2xB?Rk^JA3K4{%oJXc{NTLb?hkN74Q3GKvYbm#2qGe zdetbK-{y`#)Og$%P<(Mi2V3_`nvR})=8`UU*qEbCauNwz>cMbXGp54-8j=+qa-}i* z*z~q`z)5A3EFaW4A%2tE-*3p!Iv(-a?Vq5nnbr-Pjhfjf%_gxkVg@@~;E06J30h~Q z0m{{F(ur8*Y1;U8MXEtJtT#=)0Wl(h4dUt>;Zvrf5FRa;8q%)Dim~LL%3>5CPZKcV zybU*)RZvP?h?w2J!7H8VSRa7z5#b;M%Q_cNAEQ2Si%OspEGkXJn$9TuQY=v;DOZ8? zO<{)J_B>;CV@Mv1E2zt2q%^zv+i-MgSo$ zO&o?QYVwPjEy;+^awj)ix}d2I(Uf+S zxP1VuE5*Y(Bm|WQ`DYJ{@OtxcZIy8fHZSzE*H$E^1DLop9>abY^wf7}fkRIv35jX{ z!Il}oO3D{J$@CBst`eK85r$Sm<|=j{DgWKN)zzC^5m+3(Zr^aR`uKF$;WDvOTOZ+Q z?%S_XCkteZabh|AkzdeW_ypMk{yn+2g^0H(3@+qvq+bTyxy;dd*P-WcZ^;&KHVVH> z(^hi4ctpk95wZEv%y#oM*Sdj9(AZgPaJg5`biT~>ED+T~Yc1uGi_3eC3&QoO66!JG zFYhilfGTAQl&{dHvuxF(`Cl@cfJil3HpLKrs}qI;BHz@J7*jDb`P zaV<=9b?i&mWOL2r%ShmRPC}vBDn|qNOot79@Y)C^AJyRUY5WlF4I#Mg`Nz8fG`d1_ zD09{0x}dC57QZXyJwKyjV53Y=DvbQJf?|8g#j;Dz_?sNp4dJz#sG|MBKSv0j5SW5` z3M^}AJ;|e*3c;B?RUV4tu;I@H?Al;bK|OmJ2aLS_o|=-_c3$@_EyRnltK_3aUmWq& z48|mv*!b?~tAn|p9l`N=QIHlqEFE@%Cep!nOxQXm%5lm4HPltFmQ1oY)NuY9qS2(A zCRH#M@Vc#vcL+E9AV)sz(?NKgXvVRZIw{ssN?cs&P@CM2%%(l$jX(6vqAaR-ErR)> z*AZmPoUVBYZEOxQXwQJ+bAIn@yU6G~+4pSVTnb7}$^FVQ4Fx@7=Nxw(Q_f}#nz%Gi zzLXP0TaYal&k?WVnOC-AdptF!&KK8d1I2toRV(7PP*!qtldY-jv=N`VGtd#E9%CZ# zR(nkjAP%o#J;IYO{!wBV!A`F5omp;^K{$dtVaE29V)&QSMl?V;vW{jWXeK?v%Xc0@ zq1^H$yYfshj`H? zP7M=T4cVLIAc62pp_V;-_uso=EY98*q2j0|r1F{NO)~j2p=fWS*j{A=FcIHetXy;Zrmi4q*O5j2_6K51 z+$B}iGvm(Qb}N!af?dBrI4aI=dW;f1KZ88^tk`PRW2!Z(pe`Pja;Hr4L7g>H7e+;h z48>(NzPoq)pYr9uDXa}5@b*+++4je1>)^7quR2`5CK1)5QlH76P3faS zrbxqjIf`agXJ4Glb-&UY1&3o5%o|_`J6p@8I(6_LQ?5no#5M>TG6p}GI|2)%^h}V6 z*zD(qMe0x^UMgV6eIkN}k`;j23{ImThni;|Tcm-)Sjqu>jw@VR{?MiF*%w&a8ulX) z?C^@C*_FlzGOcKp;YRyl=Cm)oZ==OJQnE=Ykr}GUP|1_Y&^BpFZVLC z#eG~XGtl3yb#wXY+1fP@+?-<<>ZTz&wJobfveJ_edexfehY1HMB2JEvLaaM@*yJ`< zi}PdbuKYC~rGmr;TG*d;927QIuE8r?=Pgz-wM!%aarot;Mq2$ewmT7S2acMVPP5Ek z>!|uQ9{U-evGEIEO{W;c$h4uqEHAguPc0kasc)B#-n4r`S1`g(6EQ4ZtMNE+?f0+l!(=oDtMX)OA(7xM%#jo{{H@PCRWaiu z7~CRt#P>0keF_(jBLU$$c~)nuNHib{vHEii>Jan_m^3c$mvJ?;`bIS zgGYy#YPCWKzWt}EZS{I2e!X?kCj5Qct1B*0I8s=a-qDJtW&Y)X5afeU5e;KB^avcO#y3FpW8?f15_zncKEL$gM0(Mlmc)uQBYK^kCZ2-swf~h+dNq&lYq^sF;^lfJs1>E zSTsCCNkYoA$drPAIHZl2rpces{uR1JX*Z9Uf7Oeogd8hG*MO~!=Y)@xLr7TyOs1P| z(-G{U{;w*t>_7rPZQcX*#F^Y&0)z!DTvSg%1{sV65EWl>?@^lNzp zUlUpLmtrtyr6e>K&V@>U>Yx^AT56QCR(oSbI-yek1g8^k-$>LIpKWV+?s$*Zf`L$) z$+kf4BDC=+5kS|^AViqdjv{u^o?Vq&eGa%^64NH_aMRSpA66Ij<$A(7|6^}b6 zz6W~x6B(qv%BAJ6y4GLv(J3@77HM&C>tR^FzZ}u7lw^qStcWH~ujb`ad~q4^0qc@0 z&r6|HF**uH@*z*g_}b$7$fYi`oFNGV2WSL)caT%!%}^g_Hiv9>lk%x*4- z$8_Of@pjU68SB%Qe$?xV*{nig&e^A(Qv3Yn~7_*&ZWSIQmC3u$(h2Ks% z)fV?7nAJl=5Bgrwbop|WWhp-Wu{TUsaYw2)s96a^!9k!a9WB_D<+CUIv z;mh`Spfr}~y(D=_2xNb5#rL8X{iuTFNKavWKMtLWI`6~ueiqbWVm#_;M#VtXuK|_kupN5po4Y~LaBkbWd>vtqB~9TR ziClijCvtQf>0QSW)TU!s$4>!YK1pg4m7Syg15jbbSt&bc*eCxMWVsN^W&(B$^A#8$ z{iYw7bKJ~k<0CcQvd!r{8MjiLJwr=(d+0`$6DpWdoF4M8XIa&>$(uaNf6PPe9I}Cy zXHWvEB2B26Etp`RhO2nQQi~)zdQ0ecNd{SZro7=##N+zOu_0xiYN!zyC2Dq2AX*~| z0cr%G%rBqH;}1pQKDRhoH2WJBd9 z+hK`^#}*prONx53K2QKLlJ3x_>7LKnMYW*^`z9CI+ydGZOp!rlNc=j)kb#t_ppy~w z3lwywKLTJ#Nn`DvcRSBH`tjb{I=TqhwE1mnfd7zEB z@(!B@ouZ@hu%18low`f1v7dkVuUfo0TG7e)cXz0d+amtF5Z%Rps0ou9b{obmpP&Ov zoChg1bTE}R^*aP*@&N1iGk2+0#U*QJkZT*Fg)O2W_-|(cIGf^ELHyTs^yqXE`IwQR zb(l&}YClvFKn)4M!pqF-jf&Ld0Q0eo!lskrJ$=_6snPXLQrHKV&tkRTNyelZpOH1l zIR0|dA54)7sF){;*`Jj6%2gS9N|Xwa>Y^*lTl_k0+I>`v#O_h<25WF~wTVT)a;k^_bA?VNf$ z`7jns>YA4e=~{3Pb@KYXEy zrVi^2-Vbkm(d9z zuxOI`BiI1h0N+MYsul`b4}KKGBF@Vh=Tk2ZrN}U!-yo*6N8pK4A=Il<*5I#&O?<>D<<&!YC_BVGu80&zJ6JtXmV;}ZT3a!b!< zZn(T6P zi2Q+fa309I5N)s#Y!K5unKf3)gKF5j9GE8V3nB7gpE4Qsu$%AbmT`v51{{*v%u95n z5&4K&)43IaH&p(>o~t~H6$I(c1{{1xPNfYpo5kqLx#C<$n(@M~&WZd^_7CjI{{)(> z*uFapqu#ByODV*ya6?v#Ra+XQQ6ePf4K9INzeGi*4=2#)fQEYb;Qg`@3>)3}yvzXHY#gkv9mEX_kz0}qYVZU(&YSikYyq=i+z4#S9Jx0~gYH0*2 zs7$I{ez7%wu*>rgM!sa;m6=YX*M0Y_q#uVyKNQTcnK?kCIJv5&C0gp6Z~WHiz2*;U z797mfg~bLh|8%nF4`{z<( z)qfZ(HpeFNreact|47wpfh1}GI;I^q(7b=I_m#P|`*{A>=r_a_!qyK>T)HL%j;Ei! zJiYC=8ajSn>i~CT7s0q%Xc;Me9Bs&)q0K0M-p(-}2{3bGg@xZW1LYQ@(WNaA`Gqm7 zb>-Qs0_1CdKT?&AJ;epja@~lxJXZIHE~2h^Ka0XQ#y>tKN$8SS5VXgRX1`qL zxNfp9B7pz&7@MR~!lbkhiyv0lO78kn*Q@c5ARtaV1KP$67fWCd$wZ}u@kMFyxQYZq zlOY0LXCu*60=SJnS3=HJ#c$kzCvrkxvw&lEQsO5E_98W^@>J9?Ch1s~*FBub#SwfK zTeQ1Wa?oWXk4@%`j+zV?l{&}EMex9 zS5;1kx(g-8Fxcx183T>dl(#bztCQ`YfjE%!!#lL{nT99H0_P1|ip}H<@8MX18AXC8 z?eUAzQB*R7IVCp|%%X*!#h6L9R92p=*fNcpGP>+6j^Z`447-zhz?hxKVpmtznM{7* z*YB|NWrZ~6JzM2d+ft;j_7!p9Z9^Li_)x_WU^pvJX^9L_EB(X1pAY&ca*US94y_IB>Y1~6^g8sQu^=E|6tC=`bcV-rh?rjHy0t2HQj6cb zceh5XfzFsT>c5?=*#!G_pHZOxAf%pTbfv(`~g4U+b^lyJZj~R97R>F zTkiELEbZ>Ep&-jo!?SaM^L<-T{k+d!`!csDQnDoX`6D8qA{MSsg^<-$D#E2_8HqoG zYC|A1Q6`TtJmVm2jhM{)QIMI~;TTJu;}(TfILv=I$6z50oHy4wJ-Xvr8;cWz z^1lV}sO8>EubRgpuMvr_$9HDHKsh0(S7L_a6UB>6)JP+&=&SMcTFP$~%2&7%6GbVD zkkH z*vxHrD~&Iqkuf~B`(6!JXV!z8#@|e;g|i?Knp7y>19E$F+UJk*%hj90dZAP?kKU2& z>ghQdbN7R%gN(W*>*9KzcNfQ|=%~{oSaSoxm$PrBJ*0!Pag) zcB3JdZW7!?)>#>6NACD5AbEbK>IG}f&uAATp5FpyX(vW8!)L|NR&V;JCTc%FbjNfp zy6Q_SP!Q#8v%P7Z1rhLyvg!mpb-zO;9wk0Dz|?=_m%8VLo#PPygz#0uP=*v zV6{Hn?k@c$31w#))cCdWOXs_uve6xS`d^;@WaO-NHeXL3{(1hC7xLyG5|GqNDXOQB zjkRPU?YwHaAYIyZ_EEfuhqygoeUXaV5!>C}y-hjfg(=E2KQ7!X05%M9%KyRzB-14a zZ~<<4s4It3%Oon_kCJqv9Q!`52M8l9G~v_ysXHshS7^3`rpMKaf!59pvl3v=ebR9LYP` zl+MyisUhz9u$CCmt#FbVTKV*$fwY#f^e_G3;X0o3d_)*(az*_>;6=Bc*k4km=Co(c zXuvh*{>q3^Ydvw^baIrrt*n-doql<1_xjQZhGrfO7s^FH*d;4z%aqX{MC33PjJi~} z^@9AUNYJ1J0HN+<2)j4EBA)t2H|bkSDO$dkYcc{fSjq0jfn0?a%swZ2o)P&)n>Fkv zhSd|Ed%Z2&>3PM6?>)XK-jtb2Cd}-TUQF*HYFN zXA@G1=%Tri>ag?pERAqQ&b`-xfpQVLn~+)b@CHobVH<;Z_&`}kOc*hAjvq7BH;@_Z z34$8~4fef?t65nF2^TxLeSM`Ls$7{^f4wm^Z_-iVt3=Ea4p5w0{R`)z>Ht!;Ey#9z?|Z-9A8z`wJ|$s_I;?L>Wil5ZvHxPf3N-mBba4(ZSG% z(jzAi(U@AtiL9jDU6ev(GM&MC&k5PVPbZO9V{7It_9*dvK0p1SNFAuLLWl#Ni92SH z${;1MfFM4o(UKsZyD4$iM-fGZb@@r*%#QfC%frFJD#d zIrkn-u79=H@5G;nh zZ_{C0IwRn-xmWVxKZlOJ5yPW>LR=ZzDP@@NbW=uo z31%B4W1%Z7d5dAyr9lI3T{#ffahXOI}z|w?}bbJ>faO}>Ou#Pn1|Q71Px+;i=<^`!s@bP>u_T) zS{r3am`Ag4-9`KuUf!F_s_gLXk#AwiD(wejS}fae;kQhD_gw>vljI{omkL(UTEIM%Z#M7cj2#)?T0Y;0*H3q66g_AeI3_gWC*&Z~W_*-7UV2kC*L$Lur!vZ^K!smZECI;>z_J+9V`bw4c2Yc8^TW&wv zJ@Ku=Z217`yqT^KoO1#&vGc*DtEx;Ofw7GZ&9;NlwEdLNiV%CMx`I^zUsFQSh+t6* zJGL?l|Fnz1(73I5<{<@b-G0uXBtDjXM8j1oInQ+<*byP}trkZs%tzn1XN(cu%a_Gj zVjM9RmQ~;>ZIcwroRP&kItuLMt-;eh7B^g3NGcN0YIR8jwUQLMndRaI(m|fN5OiiL zuAgB!&3jju@^V7Eai8;V@7w*CO#x=rb)@ITR?_psTb(UqnG6uK6!%Y>WC?~$!i+ZR zeJmGf?tq4gPM0_0+x+RU%H1g)WU!c;Z>=egW6aAu=&3w0l_W(Di<}iSt3lqPvfFwD zv*SwlQ`e}x``B6vDG+DtyfjQ8fVd3YC>>mAICF!O+aIyvsnT_uZL2*4%$0 Zqybvj6uq2lNMJHs}|7@_#fMiu=M}{ literal 0 HcmV?d00001 diff --git a/tidepool-theme/login/resources/img/tidepool-plus-logo-985x96.png b/tidepool-theme/login/resources/img/tidepool-plus-logo-985x96.png new file mode 100644 index 0000000000000000000000000000000000000000..51aaa8a0b08ef003126ea18ef14391aaf32e23df GIT binary patch literal 13998 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 literal 0 HcmV?d00001 diff --git a/tidepool-theme/login/template.ftl b/tidepool-theme/login/template.ftl index d3fb385..169b1f7 100644 --- a/tidepool-theme/login/template.ftl +++ b/tidepool-theme/login/template.ftl @@ -67,7 +67,11 @@ <#nested "pre-header"> - + <#if role?? && role.hasClinicalRole()> + + <#else> + + <#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())> <#if displayRequiredFields> diff --git a/tidepool-theme/login/user_role_prompt.ftl b/tidepool-theme/login/user_role_prompt.ftl index 4368481..502ff5a 100644 --- a/tidepool-theme/login/user_role_prompt.ftl +++ b/tidepool-theme/login/user_role_prompt.ftl @@ -1,10 +1,6 @@ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true; section> - <#if section = "header"> -<#-- --> -<#-- --> -<#-- --> - <#elseif section = "form"> + <#if section = "form">

Which kind of account do you need?

@@ -24,11 +20,11 @@
-
Clinician Account
+
Clinical Account
- You are a doctor, a clinic or other healthcare provider that wants to use Tidepool to help people in your care. + You are a doctor, nurse, or other clinical or administrative staff who wants to use Tidepool at your clinic.
From 012a4d0e417c807c3d40813d3929e2f87705dd71 Mon Sep 17 00:00:00 2001 From: Darin Krauss Date: Thu, 28 Mar 2024 12:23:28 -0700 Subject: [PATCH 2/7] Update Dockerfile - Use latest keycloak-home-idp-discovery - Use newer Docker base images --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3c7a540..e28861d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM maven:3.8.4-jdk-11 as build +FROM maven:3.8.6-jdk-11 as build COPY . /build WORKDIR /build @@ -9,9 +9,9 @@ RUN unset MAVEN_CONFIG && \ ./mvnw clean compile package && \ wget -O keycloak-rest-provider.jar https://github.com/daniel-frak/keycloak-user-migration/releases/download/1.0.0/keycloak-rest-provider-1.0.0.jar && \ wget -O keycloak-metrics-spi.jar https://github.com/aerogear/keycloak-metrics-spi/releases/download/3.0.0/keycloak-metrics-spi-3.0.0.jar && \ - wget -O keycloak-home-idp-discovery.jar https://github.com/tidepool-org/keycloak-home-idp-discovery/releases/download/v21.3.2/keycloak-home-idp-discovery.jar + wget -O keycloak-home-idp-discovery.jar https://github.com/tidepool-org/keycloak-home-idp-discovery/releases/download/v21.4.0-alpha.1/keycloak-home-idp-discovery.jar -FROM alpine:3.15 as release +FROM alpine:latest as release COPY --from=build /build/admin/target/*.jar /release/extensions/ COPY --from=build /build/*.jar /release/extensions/ From 904b139d26198315048fca71f5b5ad256a55d794 Mon Sep 17 00:00:00 2001 From: Darin Krauss Date: Fri, 29 Mar 2024 17:44:54 -0700 Subject: [PATCH 3/7] Specifically build linux/amd64 Docker image --- Makefile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 2edb51b..ae880cf 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,4 @@ build-artifacts: # Builds the docker image build: build-artifacts - docker build -t tidepool/keycloak-extensions:$(image_tag) . - - + docker build --platform linux/amd64 --tag tidepool/keycloak-extensions:$(image_tag) . From 8ba973ffb7a84f40442783dd4bc78a663c77766c Mon Sep 17 00:00:00 2001 From: Darin Krauss Date: Sat, 6 Apr 2024 09:50:17 -0700 Subject: [PATCH 4/7] [BACK-2974] Always enable Create Account button for clinical registration --- tidepool-theme/login/register-clinical.ftl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tidepool-theme/login/register-clinical.ftl b/tidepool-theme/login/register-clinical.ftl index 4bddf0f..b17ef36 100644 --- a/tidepool-theme/login/register-clinical.ftl +++ b/tidepool-theme/login/register-clinical.ftl @@ -90,8 +90,8 @@
- - + +
<#if messagesPerField.existsError('terms')> @@ -112,7 +112,7 @@
- +
From b6847ad945c7ce01f984944186d32e1d9239df81 Mon Sep 17 00:00:00 2001 From: Darin Krauss Date: Tue, 23 Apr 2024 02:16:25 -0700 Subject: [PATCH 5/7] Rename clinical to clinician --- .../RegistrationTermsFormAction.java | 12 +++++----- .../login/TidepoolLoginFormsProvider.java | 6 ++--- .../keycloak/extensions/model/RoleBean.java | 22 +++++++++---------- .../RegistrationsRealmResourceProvider.java | 2 +- .../login/messages/messages_en.properties | 6 ++--- ...er-clinical.ftl => register-clinician.ftl} | 2 +- tidepool-theme/login/register-personal.ftl | 2 +- tidepool-theme/login/template.ftl | 2 +- tidepool-theme/login/user_role_prompt.ftl | 4 ++-- 9 files changed, 29 insertions(+), 29 deletions(-) rename tidepool-theme/login/{register-clinical.ftl => register-clinician.ftl} (99%) diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationTermsFormAction.java b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationTermsFormAction.java index ae218f9..a461507 100644 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationTermsFormAction.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/authenticator/RegistrationTermsFormAction.java @@ -37,10 +37,10 @@ public void buildPage(FormContext context, LoginFormsProvider form) { @Override public void validate(ValidationContext context) { - // TEMPORARY: Currently only for Clinical registration which includes TOS/PP - // agreement on clinical registration form. Remove once TOS/PP agreement + // TEMPORARY: Currently only for Clinician registration which includes TOS/PP + // agreement on clinician registration form. Remove once TOS/PP agreement // included on personal registration form. - if (!RoleBean.hasClinicalRoleFromAuthenticationSession(context.getAuthenticationSession())) { + if (!RoleBean.hasClinicianRoleFromAuthenticationSession(context.getAuthenticationSession())) { context.success(); return; } @@ -66,10 +66,10 @@ public void validate(ValidationContext context) { @Override public void success(FormContext context) { - // TEMPORARY: Currently only for Clinical registration which includes TOS/PP - // agreement on clinical registration form. Remove once TOS/PP agreement + // TEMPORARY: Currently only for Clinician registration which includes TOS/PP + // agreement on clinician registration form. Remove once TOS/PP agreement // included on personal registration form. - if (!RoleBean.hasClinicalRoleFromRealmUser(context.getRealm(), context.getUser())) { + if (!RoleBean.hasClinicianRoleFromRealmUser(context.getRealm(), context.getUser())) { return; } diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/login/TidepoolLoginFormsProvider.java b/admin/src/main/java/org/tidepool/keycloak/extensions/login/TidepoolLoginFormsProvider.java index f67725f..7387207 100644 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/login/TidepoolLoginFormsProvider.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/login/TidepoolLoginFormsProvider.java @@ -15,7 +15,7 @@ public class TidepoolLoginFormsProvider extends FreeMarkerLoginFormsProvider { private static final String FORM_ATTRIBUTE_ROLE = "role"; private static final String REGISTER_FORM = "register.ftl"; - private static final String REGISTER_FORM_CLINICAL = "register-clinical.ftl"; + private static final String REGISTER_FORM_CLINICIAN = "register-clinician.ftl"; private static final String REGISTER_FORM_PERSONAL = "register-personal.ftl"; public TidepoolLoginFormsProvider(KeycloakSession session) { @@ -30,8 +30,8 @@ protected Response processTemplate(Theme theme, String templateName, Locale loca attributes.put(FORM_ATTRIBUTE_ROLE, roleBean); if (templateName == REGISTER_FORM) { - if (roleBean.hasClinicalRole()) { - templateName = REGISTER_FORM_CLINICAL; + if (roleBean.hasClinicianRole()) { + templateName = REGISTER_FORM_CLINICIAN; } else { templateName = REGISTER_FORM_PERSONAL; } diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/model/RoleBean.java b/admin/src/main/java/org/tidepool/keycloak/extensions/model/RoleBean.java index fab7fc4..c1e781a 100644 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/model/RoleBean.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/model/RoleBean.java @@ -23,8 +23,8 @@ public class RoleBean { public static final List ROLES_LIST = Arrays.asList(ROLE_CLINICIAN, ROLE_PATIENT); public static final Set ROLES_SET = new HashSet<>(ROLES_LIST); - public static final List ROLES_CLINICAL_LIST = Arrays.asList(ROLE_CLINICIAN, ROLE_CLINIC_DEPRECATED); - public static final Set ROLES_CLINICAL_SET = new HashSet<>(ROLES_CLINICAL_LIST); + public static final List ROLES_CLINICIAN_LIST = Arrays.asList(ROLE_CLINICIAN, ROLE_CLINIC_DEPRECATED); + public static final Set ROLES_CLINICIAN_SET = new HashSet<>(ROLES_CLINICIAN_LIST); public static final String PARAMETER_ROLE = "role"; @@ -43,30 +43,30 @@ public RoleBean(RealmModel realm, URI baseUri, AuthenticationFlowContext context this.authenticationSession = authenticationSession; } - public boolean hasClinicalRole() { - if (hasClinicalRoleFromAuthenticationSession(authenticationSession)) { + public boolean hasClinicianRole() { + if (hasClinicianRoleFromAuthenticationSession(authenticationSession)) { return true; } - if (context != null && hasClinicalRoleFromRealmUser(context.getRealm(), context.getUser())) { + if (context != null && hasClinicianRoleFromRealmUser(context.getRealm(), context.getUser())) { return true; } return false; } - public static boolean hasClinicalRoleFromAuthenticationSession(AuthenticationSessionModel authenticationSession) { + public static boolean hasClinicianRoleFromAuthenticationSession(AuthenticationSessionModel authenticationSession) { if (authenticationSession != null) { - if (ROLES_CLINICAL_SET.contains(authenticationSession.getAuthNote(AUTH_NOTE_ROLE))) { + if (ROLES_CLINICIAN_SET.contains(authenticationSession.getAuthNote(AUTH_NOTE_ROLE))) { return true; } } return false; } - public static boolean hasClinicalRoleFromRealmUser(RealmModel realm, UserModel user) { + public static boolean hasClinicianRoleFromRealmUser(RealmModel realm, UserModel user) { if (realm != null && user != null) { - for (String clinicalRole : ROLES_CLINICAL_SET) { - RoleModel clinicalRoleModel = realm.getRole(clinicalRole); - if (user.hasRole(clinicalRoleModel)) { + for (String clinicianRole : ROLES_CLINICIAN_SET) { + RoleModel clinicianRoleModel = realm.getRole(clinicianRole); + if (user.hasRole(clinicianRoleModel)) { return true; } } diff --git a/admin/src/main/java/org/tidepool/keycloak/extensions/resource/RegistrationsRealmResourceProvider.java b/admin/src/main/java/org/tidepool/keycloak/extensions/resource/RegistrationsRealmResourceProvider.java index 846a63a..b4cd1f9 100644 --- a/admin/src/main/java/org/tidepool/keycloak/extensions/resource/RegistrationsRealmResourceProvider.java +++ b/admin/src/main/java/org/tidepool/keycloak/extensions/resource/RegistrationsRealmResourceProvider.java @@ -57,7 +57,7 @@ public RegistrationsRealmResourceProvider registrations( /** * Restart authentication with registration. Allows specification of role - * (clinical or personal) to initiate registration. + * (clinician or personal) to initiate registration. * * Mimics LoginActionsService.restartSession, but restarts with registration * flow. diff --git a/tidepool-theme/login/messages/messages_en.properties b/tidepool-theme/login/messages/messages_en.properties index 1e0a8e6..9c75b48 100644 --- a/tidepool-theme/login/messages/messages_en.properties +++ b/tidepool-theme/login/messages/messages_en.properties @@ -1,7 +1,7 @@ doRegister=Sign up doLogIn=Log In doForgotPassword=Forgot your password? -registerTitleClinical=Create Your Clinical Tidepool Account +registerTitleClinician=Create Your Clinician Tidepool Account registerTitlePersonal=Create Your Personal Tidepool Account doCreateAccount=Create Account continue=Continue @@ -15,10 +15,10 @@ notYou=Not you? noAccount=Don''t have an account? alreadyHaveAnAccount=Already have an account? needPersonalAccount=Need to manage data for yourself or someone you care for? -needClinicalAccount=Need an account for your work at a clinic? +needClinicianAccount=Need an account for your work at a clinic? createAccountPrefix=Create a createAccountPersonal=Personal Account -createAccountClinical=Clinical Account +createAccountClinician=Clinician Account createAccountSuffix=instead. saml.post-form.title=Redirecting, please wait... diff --git a/tidepool-theme/login/register-clinical.ftl b/tidepool-theme/login/register-clinician.ftl similarity index 99% rename from tidepool-theme/login/register-clinical.ftl rename to tidepool-theme/login/register-clinician.ftl index b17ef36..96a6f21 100644 --- a/tidepool-theme/login/register-clinical.ftl +++ b/tidepool-theme/login/register-clinician.ftl @@ -2,7 +2,7 @@ <@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm', 'terms') displayInfo=true; section> <#if section = "header">
- ${msg("registerTitleClinical")} + ${msg("registerTitleClinician")}
<#elseif section = "form">
diff --git a/tidepool-theme/login/register-personal.ftl b/tidepool-theme/login/register-personal.ftl index a5d5510..86f4b80 100644 --- a/tidepool-theme/login/register-personal.ftl +++ b/tidepool-theme/login/register-personal.ftl @@ -105,7 +105,7 @@ ${msg("alreadyHaveAnAccount")} ${msg("doLogIn")}
- ${msg("needClinicalAccount")} ${msg("createAccountPrefix")} ${msg("createAccountClinical")} ${msg("createAccountSuffix")} + ${msg("needClinicianAccount")} ${msg("createAccountPrefix")} ${msg("createAccountClinician")} ${msg("createAccountSuffix")}
\ No newline at end of file diff --git a/tidepool-theme/login/template.ftl b/tidepool-theme/login/template.ftl index 169b1f7..7cabe1e 100644 --- a/tidepool-theme/login/template.ftl +++ b/tidepool-theme/login/template.ftl @@ -67,7 +67,7 @@ <#nested "pre-header"> - <#if role?? && role.hasClinicalRole()> + <#if role?? && role.hasClinicianRole()> <#else> diff --git a/tidepool-theme/login/user_role_prompt.ftl b/tidepool-theme/login/user_role_prompt.ftl index 502ff5a..70990d1 100644 --- a/tidepool-theme/login/user_role_prompt.ftl +++ b/tidepool-theme/login/user_role_prompt.ftl @@ -20,11 +20,11 @@
-
Clinical Account
+
Clinician Account
- You are a doctor, nurse, or other clinical or administrative staff who wants to use Tidepool at your clinic. + You are a doctor, nurse, or other clinician or administrative staff who wants to use Tidepool at your clinic.
From 357f9777501aa0f6e96182203753c85b4020e846 Mon Sep 17 00:00:00 2001 From: Darin Krauss Date: Tue, 23 Apr 2024 15:18:20 -0700 Subject: [PATCH 6/7] Fix typo --- tidepool-theme/login/user_role_prompt.ftl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidepool-theme/login/user_role_prompt.ftl b/tidepool-theme/login/user_role_prompt.ftl index 70990d1..d85b004 100644 --- a/tidepool-theme/login/user_role_prompt.ftl +++ b/tidepool-theme/login/user_role_prompt.ftl @@ -24,7 +24,7 @@
- You are a doctor, nurse, or other clinician or administrative staff who wants to use Tidepool at your clinic. + You are a doctor, nurse, or other clinical or administrative staff who wants to use Tidepool at your clinic.
From 553f93bb64360e650630a96b11a047fed9855dbb Mon Sep 17 00:00:00 2001 From: Darin Krauss Date: Tue, 23 Apr 2024 16:08:03 -0700 Subject: [PATCH 7/7] Update to latest keycloak-home-idp-discovery --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e28861d..36a6726 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN unset MAVEN_CONFIG && \ ./mvnw clean compile package && \ wget -O keycloak-rest-provider.jar https://github.com/daniel-frak/keycloak-user-migration/releases/download/1.0.0/keycloak-rest-provider-1.0.0.jar && \ wget -O keycloak-metrics-spi.jar https://github.com/aerogear/keycloak-metrics-spi/releases/download/3.0.0/keycloak-metrics-spi-3.0.0.jar && \ - wget -O keycloak-home-idp-discovery.jar https://github.com/tidepool-org/keycloak-home-idp-discovery/releases/download/v21.4.0-alpha.1/keycloak-home-idp-discovery.jar + wget -O keycloak-home-idp-discovery.jar https://github.com/tidepool-org/keycloak-home-idp-discovery/releases/download/v21.4.0/keycloak-home-idp-discovery.jar FROM alpine:latest as release