All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.imunity.vaadin.registration.StandaloneRegistrationView Maven / Gradle / Ivy

/*
 * Copyright (c) 2021 Bixbit - Krzysztof Benedyczak. All rights reserved.
 * See LICENCE.txt file for licensing information.
 */
package io.imunity.vaadin.registration;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.page.Page;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.WrappedSession;
import io.imunity.vaadin.elements.LinkButton;
import io.imunity.vaadin.elements.NotificationPresenter;
import io.imunity.vaadin.elements.UnityViewComponent;
import io.imunity.vaadin.endpoint.common.RemoteRedirectedAuthnResponseProcessingFilter;
import io.imunity.vaadin.endpoint.common.RemoteRedirectedAuthnResponseProcessingFilter.PostAuthenticationDecissionWithContext;
import io.imunity.vaadin.endpoint.common.forms.RegCodeException;
import io.imunity.vaadin.endpoint.common.forms.VaadinLogoImageLoader;
import io.imunity.vaadin.endpoint.common.forms.components.WorkflowCompletedComponent;
import io.imunity.vaadin.endpoint.common.layout.WrappedLayout;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import pl.edu.icm.unity.base.authn.AuthenticationOptionKey;
import pl.edu.icm.unity.base.exceptions.EngineException;
import pl.edu.icm.unity.base.exceptions.IdentityExistsException;
import pl.edu.icm.unity.base.exceptions.WrongArgumentException;
import pl.edu.icm.unity.base.message.MessageSource;
import pl.edu.icm.unity.base.registration.*;
import pl.edu.icm.unity.base.registration.RegistrationContext.TriggeringMode;
import pl.edu.icm.unity.base.registration.RegistrationWrapUpConfig.TriggeringState;
import pl.edu.icm.unity.base.utils.Log;
import pl.edu.icm.unity.engine.api.RegistrationsManagement;
import pl.edu.icm.unity.engine.api.authn.IdPLoginController;
import pl.edu.icm.unity.engine.api.authn.InteractiveAuthenticationProcessor.PostAuthenticationStepDecision;
import pl.edu.icm.unity.engine.api.authn.remote.RemotelyAuthenticatedPrincipal;
import pl.edu.icm.unity.engine.api.config.UnityServerConfiguration;
import pl.edu.icm.unity.engine.api.finalization.WorkflowFinalizationConfiguration;
import pl.edu.icm.unity.engine.api.registration.PostFillingHandler;

import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static pl.edu.icm.unity.engine.api.endpoint.SharedEndpointManagement.REGISTRATION_PATH;

@Route(value = REGISTRATION_PATH + ":" + StandaloneRegistrationView.FORM_PARAM, layout = WrappedLayout.class)
class StandaloneRegistrationView extends UnityViewComponent implements BeforeEnterObserver
{
	private static final Logger log = Log.getLogger(Log.U_SERVER_WEB, StandaloneRegistrationView.class);

	public static final String FORM_PARAM = "form";
	public static final String REG_CODE_PARAM = "regcode";
	private final RegistrationsManagement regMan;
	private final MessageSource msg;
	private final UnityServerConfiguration cfg;
	private final IdPLoginController idpLoginController;
	private final RequestEditorCreator editorCreator;
	private final AutoLoginAfterSignUpProcessor autoLoginProcessor;
	private final NotificationPresenter notificationPresenter;
	private final VaadinLogoImageLoader logoImageLoader;
	private RegistrationForm form;
	private String registrationCode;
	private PostFillingHandler postFillHandler;
	private final VerticalLayout main;
	private Runnable customCancelHandler;
	private Runnable completedRegistrationHandler;
	private Runnable gotoSignInRedirector;
	private Map> parameters;

	@Autowired
	StandaloneRegistrationView(MessageSource msg,
	                                  @Qualifier("insecure") RegistrationsManagement regMan,
	                                  UnityServerConfiguration cfg,
	                                  IdPLoginController idpLoginController,
	                                  RequestEditorCreator editorCreator,
	                                  AutoLoginAfterSignUpProcessor autoLogin, VaadinLogoImageLoader logoImageLoader,
	                                  NotificationPresenter notificationPresenter)
	{
		this.msg = msg;
		this.regMan = regMan;
		this.cfg = cfg;
		this.idpLoginController = idpLoginController;
		this.editorCreator = editorCreator;
		this.autoLoginProcessor = autoLogin;
		this.logoImageLoader = logoImageLoader;
		this.notificationPresenter = notificationPresenter;

		main = new VerticalLayout();
		main.addClassName("u-standalone-public-form");
		main.setWidthFull();
		main.getStyle().set("gap", "0");
		getContent().add(main);
	}

	@Override
	public String getPageTitle()
	{
		return Optional.ofNullable(form)
				.flatMap(form -> Optional.ofNullable(form.getPageTitle()))
				.map(title -> title.getValue(msg))
				.orElse("");
	}

	void init(RegistrationForm form, TriggeringMode mode, Runnable customCancelHandler, Runnable completedRegistrationHandler,
	          Runnable gotoSignInRedirector)
	{
		this.form = form;
		String pageTitle = form.getPageTitle() == null ? null : form.getPageTitle().getValue(msg);
		this.postFillHandler = new PostFillingHandler(form.getName(), form.getWrapUpConfig(), msg,
				pageTitle, form.getLayoutSettings().getLogoURL(), true);
		enter(mode, customCancelHandler, completedRegistrationHandler, gotoSignInRedirector);
	}

	@Override
	public void beforeEnter(BeforeEnterEvent event)
	{
		form = event.getRouteParameters().get(FORM_PARAM)
				.map(this::getForm)
				.orElse(null);
		parameters = event.getLocation().getQueryParameters().getParameters();
		if(form == null)
		{
			notificationPresenter.showError(msg.getMessage("RegistrationErrorName.title"), msg.getMessage("RegistrationErrorName.description"));
			return;
		}

		String pageTitle = form.getPageTitle().getValue(msg);
		postFillHandler = new PostFillingHandler(form.getName(), form.getWrapUpConfig(), msg,
				pageTitle, form.getLayoutSettings().getLogoURL(), true);

		registrationCode = event.getLocation().getQueryParameters()
				.getParameters()
				.getOrDefault(REG_CODE_PARAM, List.of())
				.stream().findFirst().orElse(null);

		enter(TriggeringMode.manualStandalone, null, null, null);
	}

	private void enter(TriggeringMode mode, Runnable customCancelHandler, Runnable completedRegistrationHandler,
			Runnable gotoSignInRedirector)
	{
		this.customCancelHandler = customCancelHandler;
		this.completedRegistrationHandler = completedRegistrationHandler;
		this.gotoSignInRedirector = gotoSignInRedirector;
		selectInitialView(mode);
	}
	
	private void selectInitialView(TriggeringMode mode) 
	{
		WrappedSession session = VaadinSession.getCurrent().getSession();
		PostAuthenticationDecissionWithContext postAuthnStepDecision = (PostAuthenticationDecissionWithContext) session
				.getAttribute(RemoteRedirectedAuthnResponseProcessingFilter.DECISION_SESSION_ATTRIBUTE);
		if (postAuthnStepDecision != null)
		{
			session.removeAttribute(RemoteRedirectedAuthnResponseProcessingFilter.DECISION_SESSION_ATTRIBUTE);
			if (!postAuthnStepDecision.triggeringContext.isRegistrationTriggered())
				log.error("Got to standalone registration view with not-registration triggered "
						+ "remote authn results, {}", postAuthnStepDecision.triggeringContext);
			processRemoteAuthnResult(postAuthnStepDecision, mode);
		} else
		{
			showFirstStage(RemotelyAuthenticatedPrincipal.getLocalContext(), mode);
		}
	}

	private void processRemoteAuthnResult(PostAuthenticationDecissionWithContext postAuthnStepDecisionWithContext, 
			TriggeringMode mode)
	{
		log.debug("Remote authentication result found in session, triggering its processing");
		PostAuthenticationStepDecision postAuthnStepDecision = postAuthnStepDecisionWithContext.decision;
		switch (postAuthnStepDecision.getDecision())
		{
			case COMPLETED -> onUserExists();
			case ERROR -> onAuthnError(postAuthnStepDecision.getErrorDetail().error.resovle(msg), mode);
			case GO_TO_2ND_FACTOR ->
					throw new IllegalStateException("2nd factor authN makes no sense upon registration");
			case UNKNOWN_REMOTE_USER -> showSecondStage(postAuthnStepDecision.getUnknownRemoteUserDetail().unknownRemotePrincipal.remotePrincipal,
					TriggeringMode.afterRemoteLoginFromRegistrationForm, false,
					postAuthnStepDecisionWithContext.triggeringContext.invitationCode,
					postAuthnStepDecisionWithContext.triggeringContext.authenticationOptionKey);
			default -> throw new IllegalStateException("Unsupported post-authn decission for registration view: "
					+ postAuthnStepDecision.getDecision());
		}
	}
	
	
	private void showFirstStage(RemotelyAuthenticatedPrincipal context, TriggeringMode mode)
	{
		main.removeAll();
		
		editorCreator.init(form, true, context, registrationCode, null, parameters);
		editorCreator.createFirstStage(new EditorCreatedCallback(mode), this::onLocalSignupClickHandler);
	}

	private void showSecondStage(RemotelyAuthenticatedPrincipal context, TriggeringMode mode, 
			boolean withCredentials, String presetRegistrationCode, AuthenticationOptionKey authnOptionKey)
	{
		main.removeAll();

		editorCreator.init(form, true, context, presetRegistrationCode, authnOptionKey, parameters);
		editorCreator.createSecondStage(new EditorCreatedCallback(mode), withCredentials);
	}
	
	private void editorCreated(RegistrationRequestEditor editor, TriggeringMode mode)
	{
		if (isAutoSubmitPossible(editor, mode))
		{
			onSubmit(editor, mode);
		} else
		{
			showEditorContent(editor, mode);
			editor.performAutomaticRemoteSignupIfNeeded();
		}
	}
	
	private void showEditorContent(RegistrationRequestEditor editor, TriggeringMode mode)
	{
		SignUpTopHeaderComponent header = new SignUpTopHeaderComponent(cfg, msg, getGoToSignInRedirector(editor));
		main.add(header);

		main.add(editor);
		editor.setWidthFull();
		main.setAlignItems(FlexComponent.Alignment.CENTER);
		
		Button okButton = null;
		Component cancelButton = null;
		if (editor.isSubmissionPossible())
		{
			okButton = createOKButton(editor, mode);
			okButton.setWidthFull();
			
			if (form.getLayoutSettings().isShowCancel())
			{
				cancelButton = createCancelButton();
			}
		} else if (isStanaloneModeFromAuthNScreen() && form.getLayoutSettings().isShowCancel())
		{
			cancelButton = createCancelButton();
		}

		HorizontalLayout formButtons;
		if (okButton != null)
		{
			formButtons = new HorizontalLayout();
			formButtons.setWidth(editor.formWidth(), editor.formWidthUnit());
			formButtons.add(okButton);
			formButtons.setMargin(false);
			main.add(formButtons);
		}

		if (cancelButton != null)
		{
			main.add(cancelButton);
		}
	}
	
	private Button createOKButton(RegistrationRequestEditor editor, TriggeringMode mode)
	{
		Button button = new Button(
				msg.getMessage("RegistrationRequestEditorDialog.submitRequest"),
				event -> onSubmit(editor, mode));
		button.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
		if(form.getLayoutSettings().isCompactInputs())
			button.getStyle().set("margin-top", "-1.5em");
		return button;
	}

	private Component createCancelButton()
	{
		return new LinkButton(msg.getMessage("cancel"), event -> onCancel());
	}
	
	private Optional getGoToSignInRedirector(RegistrationRequestEditor editor)
	{
		if (!form.isShowSignInLink() || editor.getStage() != RegistrationRequestEditor.Stage.FIRST)
			return Optional.empty();

		if (gotoSignInRedirector != null)
			return Optional.of(gotoSignInRedirector);
		
		if (Strings.isEmpty(form.getSignInLink()))
			return Optional.empty();
		
		Runnable signinRedirector = () -> 
		{
			if (completedRegistrationHandler != null)
				completedRegistrationHandler.run();
			UI.getCurrent().getPage().open(form.getSignInLink(), null);
		};
		return Optional.of(signinRedirector);
	}
	
	private boolean isAutoSubmitPossible(RegistrationRequestEditor editor, TriggeringMode mode)
	{
		return mode == TriggeringMode.afterRemoteLoginFromRegistrationForm
				&& !editor.isUserInteractionRequired();
	}

	private void handleError(Exception e, RegCodeException.ErrorCause cause)
	{
		log.warn("Registration error", e);
		WorkflowFinalizationConfiguration finalScreenConfig = postFillHandler
				.getFinalRegistrationConfigurationOnError(cause.getTriggerState());
		gotoFinalStep(finalScreenConfig);
	}
	
	private void onUserExists()
	{
		log.debug("External authentication resulted in existing user, aborting registration");
		WorkflowFinalizationConfiguration finalScreenConfig = 
				postFillHandler.getFinalRegistrationConfigurationOnError(TriggeringState.PRESET_USER_EXISTS);
		gotoFinalStep(finalScreenConfig);
	}

	private void onAuthnError(String authenticatorError, TriggeringMode mode)
	{
		log.info("External authentication failed, aborting: {}", authenticatorError);
		notificationPresenter.showError(authenticatorError, "");
		showFirstStage(RemotelyAuthenticatedPrincipal.getLocalContext(), mode);
	}
	
	private void onLocalSignupClickHandler(String presetInvitationCode)
	{
		showSecondStage(RemotelyAuthenticatedPrincipal.getLocalContext(), TriggeringMode.manualStandalone,
				true, presetInvitationCode, null);
	}
	
	private void onCancel()
	{
		if (isCustomCancelHandlerEnabled())
		{
			customCancelHandler.run();
		}
		else
		{
			WorkflowFinalizationConfiguration finalScreenConfig = postFillHandler
					.getFinalRegistrationConfigurationOnError(TriggeringState.CANCELLED);
			gotoFinalStep(finalScreenConfig);
		}
	}
	
	private boolean isCustomCancelHandlerEnabled()
	{
		return isStanaloneModeFromAuthNScreen()
				&& !postFillHandler.hasConfiguredFinalizationFor(TriggeringState.CANCELLED);
	}
	
	private boolean isStanaloneModeFromAuthNScreen()
	{
		return customCancelHandler != null;
	}

	private void onSubmit(RegistrationRequestEditor editor, TriggeringMode mode)
	{
		RegistrationContext context = new RegistrationContext(idpLoginController.isLoginInProgress(), mode);
		RegistrationRequest request = editor.getRequestWithStandardErrorHandling(isWithCredentials(mode))
				.orElse(null);
		if (request == null)
			return;
		try
		{
			String requestId = regMan.submitRegistrationRequest(request, context);
			RegistrationRequestState requestState = getRequestStatus(requestId);
			
			autoLoginProcessor.signInIfPossible(editor, requestState);
			
			RegistrationRequestStatus effectiveStateForFinalization = requestState == null 
					? RegistrationRequestStatus.rejected 
					: requestState.getStatus();
			WorkflowFinalizationConfiguration finalScreenConfig = 
					postFillHandler.getFinalRegistrationConfigurationPostSubmit(requestId,
							effectiveStateForFinalization);
			gotoFinalStep(finalScreenConfig);
		} catch (IdentityExistsException e)
		{
			WorkflowFinalizationConfiguration finalScreenConfig = 
					postFillHandler.getFinalRegistrationConfigurationOnError(TriggeringState.PRESET_USER_EXISTS);
			gotoFinalStep(finalScreenConfig);
			
		} catch (WrongArgumentException e)
		{
			SubmissionErrorHandler.handleFormSubmissionError(e, msg, editor, notificationPresenter);
		} catch (Exception e)
		{
			log.warn("Registration request submision failed", e);
			WorkflowFinalizationConfiguration finalScreenConfig = 
					postFillHandler.getFinalRegistrationConfigurationOnError(TriggeringState.GENERAL_ERROR);
			gotoFinalStep(finalScreenConfig);
		}
	}

	private boolean isWithCredentials(TriggeringMode mode)
	{
		return mode != TriggeringMode.afterRemoteLoginFromRegistrationForm;
	}

	private void gotoFinalStep(WorkflowFinalizationConfiguration config)
	{
		log.debug("Registration is finalized, status: {}", config);
		if (completedRegistrationHandler != null)
			completedRegistrationHandler.run();
		if (config.autoRedirect)
			redirect(UI.getCurrent().getPage(), config.redirectURL, idpLoginController);
		else
			showFinalScreen(config);
	}
	
	private void showFinalScreen(WorkflowFinalizationConfiguration config)
	{
		VerticalLayout wrapper = new VerticalLayout();
		wrapper.setSpacing(false);
		wrapper.setMargin(false);
		wrapper.setSizeFull();

		Image logo = logoImageLoader.loadImageFromUri(config.logoURL).orElse(null);
		WorkflowCompletedComponent finalScreen = new WorkflowCompletedComponent(config, logo);
		wrapper.add(finalScreen);
		finalScreen.setFontSize("2em");
		wrapper.setAlignItems(FlexComponent.Alignment.CENTER);
		getContent().removeAll();
		getContent().add(wrapper);
	}

	private static void redirect(Page page, String redirectUrl, IdPLoginController loginController)
	{
		loginController.breakLogin();
		page.open(redirectUrl, null);
	}
	
	private RegistrationRequestState getRequestStatus(String requestId) 
	{
		try
		{
			return regMan.getRegistrationRequest(requestId);
		} catch (EngineException e)
		{
			log.error("Shouldn't happen: can't get request status, assuming rejected", e);
		}
		return null;
	}

	private RegistrationForm getForm(String name)
	{
		name = URLDecoder.decode(name, StandardCharsets.UTF_8);
		try
		{
			List forms = regMan.getForms();
			for (RegistrationForm regForm: forms)
				if (regForm.isPubliclyAvailable() && regForm.getName().equals(name))
					return regForm;
		} catch (EngineException e)
		{
			log.error("Can't load registration forms", e);
		}
		return null;
	}

	private class EditorCreatedCallback implements RequestEditorCreator.RequestEditorCreatedCallback
	{
		private final TriggeringMode mode;
		
		public EditorCreatedCallback(TriggeringMode mode)
		{
			this.mode = mode;
		}

		@Override
		public void onCreationError(Exception e, RegCodeException.ErrorCause cause)
		{
			handleError(e, cause);
		}
		
		@Override
		public void onCreated(RegistrationRequestEditor editor)
		{
			editorCreated(editor, mode);
		}

		@Override
		public void onCancel()
		{
			//nop
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy