diff --git a/src/main/java/pm/axe/internal/HasTabInit.java b/src/main/java/pm/axe/internal/HasTabInit.java new file mode 100644 index 000000000..b667713aa --- /dev/null +++ b/src/main/java/pm/axe/internal/HasTabInit.java @@ -0,0 +1,7 @@ +package pm.axe.internal; + +import pm.axe.db.models.User; + +public interface HasTabInit { + void tabInit(User user); +} diff --git a/src/main/java/pm/axe/ui/pages/user/ProfilePage.java b/src/main/java/pm/axe/ui/pages/user/ProfilePage.java deleted file mode 100644 index 247e14162..000000000 --- a/src/main/java/pm/axe/ui/pages/user/ProfilePage.java +++ /dev/null @@ -1,235 +0,0 @@ -package pm.axe.ui.pages.user; - -import com.vaadin.flow.component.AbstractField; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.checkbox.Checkbox; -import com.vaadin.flow.component.dependency.CssImport; -import com.vaadin.flow.component.details.Details; -import com.vaadin.flow.component.html.*; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.orderedlayout.FlexComponent; -import com.vaadin.flow.component.orderedlayout.FlexLayout; -import com.vaadin.flow.component.orderedlayout.HorizontalLayout; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.component.select.Select; -import com.vaadin.flow.component.textfield.EmailField; -import com.vaadin.flow.component.textfield.PasswordField; -import com.vaadin.flow.component.textfield.TextField; -import com.vaadin.flow.router.BeforeEnterEvent; -import com.vaadin.flow.router.BeforeEnterObserver; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.spring.annotation.SpringComponent; -import com.vaadin.flow.spring.annotation.UIScope; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import pm.axe.Endpoint; -import pm.axe.db.models.Account; -import pm.axe.db.models.User; -import pm.axe.db.models.UserSettings; -import pm.axe.services.user.AccountService; -import pm.axe.services.user.UserSettingsService; -import pm.axe.session.AxeSession; -import pm.axe.ui.MainView; -import pm.axe.ui.elements.PasswordGenerator; -import pm.axe.ui.layouts.AxeCompactLayout; -import pm.axe.users.AccountType; - -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -@SpringComponent -@UIScope -@RequiredArgsConstructor -@CssImport(value = "./css/profile_page.css") -@Route(value = Endpoint.UI.PROFILE_PAGE, layout = MainView.class) -@PageTitle("My Profile - Axe.pm") -public class ProfilePage extends AxeCompactLayout implements BeforeEnterObserver { - private final AccountService accountService; - private final UserSettingsService userSettingsService; - private boolean pageAlreadyInitialized = false; - private User user; - private List confirmedAccounts; - - private Checkbox tfaBox; - - @Override - public void beforeEnter(BeforeEnterEvent event) { - if (event.isRefreshEvent()) return; - boundUserIfAny(); - if (Objects.isNull(user)) { - event.forwardTo(LoginPage.class); - return; - } - confirmedAccounts = getConfirmedAccountsFor(user); - - if (!pageAlreadyInitialized) { - initPage(); - pageAlreadyInitialized = true; - } - } - - private void initPage() { - //title - H2 title = new H2("My Profile"); - title.setClassName("profile-title"); - - //username - TextField username = new TextField("Username"); - username.setValue(user.getUsername()); - username.setReadOnly(true); - Button editUsernameButton = new Button("Edit"); - Button saveUsernameButton = new Button("Save"); - FlexLayout usernameLayout = new FlexLayout(username, editUsernameButton); - usernameLayout.setAlignItems(FlexComponent.Alignment.BASELINE); - usernameLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.AROUND); - //TODO replace with methods and fix button replace login - see MyLinksPage - editUsernameButton.addClickListener(e -> { - username.setReadOnly(false); - usernameLayout.replace(editUsernameButton, saveUsernameButton); - }); - saveUsernameButton.addClickListener(e -> { - username.setReadOnly(true); - usernameLayout.replace(saveUsernameButton, editUsernameButton); - }); - - //email - EmailField emailField = new EmailField("E-mail"); - emailField.setValue(getCurrentEmail()); - emailField.setReadOnly(true); - - Button editEmailButton = new Button("Edit"); - Button saveEmailButton = new Button("Save"); - FlexLayout emailLayout = new FlexLayout(emailField, editEmailButton); - emailLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.AROUND); - emailLayout.setAlignItems(Alignment.BASELINE); - - //TODO replace with methods and fix button replace login - see MyLinksPage - editEmailButton.addClickListener(e -> { - emailField.setReadOnly(false); - emailLayout.replace(editEmailButton, saveEmailButton); - }); - saveEmailButton.addClickListener(e -> { - emailField.setReadOnly(true); - emailLayout.replace(saveEmailButton, editEmailButton); - }); - - //how Axe use email details - Details howEmailUsedDetails = new Details("What will be send to email?"); - Span span = new Span("Account Recovery"); //TODO replace with UL with usage - see RegForm for UL example - howEmailUsedDetails.setOpened(false); - howEmailUsedDetails.setContent(span); - - //TODO tg section - Hr firstSeparator = new Hr(); - //change password section - H4 changePasswordTitle = new H4("Change Password"); - PasswordField oldPasswordInput = new PasswordField("Old Password"); - PasswordField newPasswordInput = new PasswordField("New Password"); - PasswordGenerator passwordGenerator = PasswordGenerator.create(); - passwordGenerator.setOpened(false); - passwordGenerator.setCopyTarget(newPasswordInput); - Button updatePasswordButton = new Button("Update"); - VerticalLayout passwordLayout = new VerticalLayout(changePasswordTitle, - oldPasswordInput, newPasswordInput, passwordGenerator, - updatePasswordButton); - - //second separator - Hr secondSeparator = new Hr(); - - //tfa section - H5 tfaTitle = new H5("Two-Factor Authentication (2FA)"); - - tfaBox = new Checkbox("Protect my account with additional one time codes"); - tfaBox.setValue(userSettingsService.isTfaEnabled(user)); - tfaBox.setEnabled(!confirmedAccounts.isEmpty()); - tfaBox.addValueChangeListener(this::onTfaBoxChanged); - - Span noConfirmedAccountsSpan = getNoConfirmedAccountsSpan(); - TextField tfaField = new TextField(); - tfaField.setReadOnly(true); - - Label sendToLabel = new Label("Send to:"); - Select tfaChannelSelect = new Select<>(); - Button saveTfaChannelButton = new Button("Save"); - - HorizontalLayout tfaSelectLayout = new HorizontalLayout(sendToLabel, tfaChannelSelect, saveTfaChannelButton); - tfaSelectLayout.setAlignItems(Alignment.CENTER); - HorizontalLayout tfaFieldLayout = new HorizontalLayout(sendToLabel, tfaField); - tfaFieldLayout.setAlignItems(Alignment.CENTER); - - VerticalLayout tfaContent = new VerticalLayout(tfaBox); - tfaContent.setPadding(false); - - if (confirmedAccounts.isEmpty()) { - tfaContent.add(noConfirmedAccountsSpan); - } else if (confirmedAccounts.size() == 1) { - tfaField.setValue(getAccountTypeName(confirmedAccounts.get(0))); - tfaContent.add(tfaFieldLayout); - } else { - tfaChannelSelect.setItems(confirmedAccounts.stream().map(this::getAccountTypeName).toList()); - tfaContent.add(tfaSelectLayout); - } - - //TODO 3rd separator - //TODO Settings Tab/Zone - //TODO danger zone - - //overall layout - add(title, usernameLayout, emailLayout, howEmailUsedDetails, - firstSeparator, passwordLayout, - secondSeparator, tfaTitle, tfaContent); - } - - private void onTfaBoxChanged(AbstractField.ComponentValueChangeEvent valueChangeEvent) { - Optional userSettings = userSettingsService.getUserSettings(user); - if (userSettings.isPresent()) { - userSettings.get().setTfaEnabled(tfaBox.getValue()); - userSettingsService.updateUserSettings(userSettings.get()); - Notification.show("Saved"); - } else { - tfaBox.setEnabled(false); - Notification.show("Failed to save. System error."); - } - } - - private void boundUserIfAny() { - Optional axeSession = AxeSession.getCurrent(); - if (axeSession.isPresent()) { - if (axeSession.get().hasUser()) { - user = axeSession.get().getUser(); - } - } - } - - //TODO maybe Optional instead - private String getCurrentEmail() { - Optional emailAccount = accountService.getAccount(user, AccountType.EMAIL); - if (emailAccount.isPresent()) { - Optional plainTextEmail = accountService.decryptAccountName(emailAccount.get()); - if (plainTextEmail.isPresent() && StringUtils.isNotBlank(plainTextEmail.get())) { - return plainTextEmail.get(); - } else { - return ""; - } - } else { - return ""; - } - } - - private List getConfirmedAccountsFor(final User user) { - return accountService.getAllAccountsLinkedWithUser(user).stream().filter(Account::isConfirmed).toList(); - } - - private String getAccountTypeName(final Account account) { - return StringUtils.capitalize(account.getType().name()); - } - - private Span getNoConfirmedAccountsSpan() { - Span firstPart = new Span("In order to use 2FA, please "); - Anchor link = new Anchor(Endpoint.UI.CONFIRM_ACCOUNT_PAGE, "confirm account"); - Span lastPart = new Span(" ."); - return new Span(firstPart, link, lastPart); - } -} diff --git a/src/main/java/pm/axe/ui/pages/user/profile/ProfilePage.java b/src/main/java/pm/axe/ui/pages/user/profile/ProfilePage.java new file mode 100644 index 000000000..51f224038 --- /dev/null +++ b/src/main/java/pm/axe/ui/pages/user/profile/ProfilePage.java @@ -0,0 +1,121 @@ +package pm.axe.ui.pages.user.profile; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.dependency.CssImport; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.tabs.Tab; +import com.vaadin.flow.component.tabs.TabVariant; +import com.vaadin.flow.component.tabs.Tabs; +import com.vaadin.flow.component.tabs.TabsVariant; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import com.vaadin.flow.spring.annotation.UIScope; +import lombok.RequiredArgsConstructor; +import pm.axe.Endpoint; +import pm.axe.db.models.User; +import pm.axe.internal.HasTabInit; +import pm.axe.session.AxeSession; +import pm.axe.ui.MainView; +import pm.axe.ui.layouts.AxeCompactLayout; +import pm.axe.ui.pages.user.LoginPage; +import pm.axe.ui.pages.user.profile.tabs.DangerZoneTab; +import pm.axe.ui.pages.user.profile.tabs.ProfileTab; +import pm.axe.ui.pages.user.profile.tabs.SecurityTab; +import pm.axe.ui.pages.user.profile.tabs.SettingsTab; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@SpringComponent +@UIScope +@RequiredArgsConstructor +@CssImport(value = "./css/profile_page.css") +@Route(value = Endpoint.UI.PROFILE_PAGE, layout = MainView.class) +@PageTitle("My Profile - Axe.pm") +public class ProfilePage extends AxeCompactLayout implements BeforeEnterObserver { + private final Tab profileTab = new Tab(VaadinIcon.USER.create(), new Span("Profile")); + private final Tab securityTab = new Tab(VaadinIcon.SHIELD.create(), new Span("Security")); + private final Tab settingsTab = new Tab(VaadinIcon.COG.create(), new Span("Settings")); + private final Tab dangerZoneTab = new Tab(VaadinIcon.FIRE.create(), new Span("Danger Zone")); + private final ProfileTab profileTabContent; + private final SecurityTab securityTabContent; + private final SettingsTab settingsTabContent; + private final DangerZoneTab dangerZoneTabContent; + private boolean pageAlreadyInitialized = false; + private User user; + + private final Div content = new Div(); + private final Map contentMap = new LinkedHashMap<>(); + + @Override + public void beforeEnter(BeforeEnterEvent event) { + if (event.isRefreshEvent()) return; + boundUserIfAny(); + if (Objects.isNull(user)) { + event.forwardTo(LoginPage.class); + return; + } + + if (!pageAlreadyInitialized) { + initPage(); + pageAlreadyInitialized = true; + } + } + + private void initPage() { + removeAll(); + //content map + contentMap.clear(); + contentMap.put(profileTab, profileTabContent); + contentMap.put(securityTab, securityTabContent); + contentMap.put(settingsTab, settingsTabContent); + contentMap.put(dangerZoneTab, dangerZoneTabContent); + + //title + H2 title = new H2("My Profile"); + title.setClassName("profile-title"); + + //tabs + //set icon on top on text + contentMap.forEach((tab, content) -> tab.addThemeVariants(TabVariant.LUMO_ICON_ON_TOP)); + //init content + contentMap.forEach((tab, content) -> { + if (content instanceof HasTabInit) { + ((HasTabInit) content).tabInit(user); + } + }); + Tabs tabs = new Tabs(); + tabs.addSelectedChangeListener(event -> setContent(event.getSelectedTab())); + contentMap.forEach((tab, content) -> tabs.add(tab)); + tabs.addThemeVariants(TabsVariant.LUMO_EQUAL_WIDTH_TABS); + tabs.setWidthFull(); + + add(title, tabs, content); + } + + private void boundUserIfAny() { + Optional axeSession = AxeSession.getCurrent(); + if (axeSession.isPresent()) { + if (axeSession.get().hasUser()) { + user = axeSession.get().getUser(); + } + } + } + + private void setContent(final Tab tab) { + content.removeAll(); + if (tab == null) { + return; + } + Component tabContent = contentMap.get(tab); + content.add(tabContent); + } +} diff --git a/src/main/java/pm/axe/ui/pages/user/profile/tabs/DangerZoneTab.java b/src/main/java/pm/axe/ui/pages/user/profile/tabs/DangerZoneTab.java new file mode 100644 index 000000000..27112a59c --- /dev/null +++ b/src/main/java/pm/axe/ui/pages/user/profile/tabs/DangerZoneTab.java @@ -0,0 +1,20 @@ +package pm.axe.ui.pages.user.profile.tabs; + +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.spring.annotation.SpringComponent; +import com.vaadin.flow.spring.annotation.UIScope; +import lombok.RequiredArgsConstructor; +import pm.axe.db.models.User; +import pm.axe.internal.HasTabInit; + +@RequiredArgsConstructor +@SpringComponent +@UIScope +public class DangerZoneTab extends VerticalLayout implements HasTabInit { + + @Override + public void tabInit(final User user) { + add(new Span("This is Danger Zone")); + } +} diff --git a/src/main/java/pm/axe/ui/pages/user/profile/tabs/ProfileTab.java b/src/main/java/pm/axe/ui/pages/user/profile/tabs/ProfileTab.java new file mode 100644 index 000000000..f9110dc8b --- /dev/null +++ b/src/main/java/pm/axe/ui/pages/user/profile/tabs/ProfileTab.java @@ -0,0 +1,142 @@ +package pm.axe.ui.pages.user.profile.tabs; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.details.Details; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.FlexLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.EmailField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.spring.annotation.SpringComponent; +import com.vaadin.flow.spring.annotation.UIScope; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import pm.axe.db.models.Account; +import pm.axe.db.models.Token; +import pm.axe.db.models.User; +import pm.axe.internal.HasTabInit; +import pm.axe.result.OperationResult; +import pm.axe.services.user.AccountService; +import pm.axe.services.user.TokenService; +import pm.axe.ui.elements.TelegramSpan; +import pm.axe.users.AccountType; + +import java.util.Optional; + +@RequiredArgsConstructor +@SpringComponent +@UIScope +public class ProfileTab extends VerticalLayout implements HasTabInit { + private final AccountService accountService; + private final TokenService tokenService; + private User user; + + @Override + public void tabInit(final User user) { + this.user = user; + + //username + TextField username = new TextField("Username"); + username.setValue(user.getUsername()); + username.setReadOnly(true); + Button editUsernameButton = new Button("Edit"); + Button saveUsernameButton = new Button("Save"); + FlexLayout usernameLayout = new FlexLayout(username, editUsernameButton); + usernameLayout.setAlignItems(FlexComponent.Alignment.BASELINE); + usernameLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.AROUND); + //TODO replace with methods and fix button replace login - see MyLinksPage + editUsernameButton.addClickListener(e -> { + username.setReadOnly(false); + usernameLayout.replace(editUsernameButton, saveUsernameButton); + }); + saveUsernameButton.addClickListener(e -> { + username.setReadOnly(true); + usernameLayout.replace(saveUsernameButton, editUsernameButton); + }); + + //email + EmailField emailField = new EmailField("E-mail"); + emailField.setValue(getCurrentEmail()); + emailField.setReadOnly(true); + + Button editEmailButton = new Button("Edit"); + Button saveEmailButton = new Button("Save"); + FlexLayout emailLayout = new FlexLayout(emailField, editEmailButton); + emailLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.AROUND); + emailLayout.setAlignItems(Alignment.BASELINE); + + //TODO replace with methods and fix button replace login - see MyLinksPage + editEmailButton.addClickListener(e -> { + emailField.setReadOnly(false); + emailLayout.replace(editEmailButton, saveEmailButton); + }); + saveEmailButton.addClickListener(e -> { + emailField.setReadOnly(true); + emailLayout.replace(saveEmailButton, editEmailButton); + }); + + //how Axe use email details + Details howEmailUsedDetails = new Details("What will be sent to email?"); + Span span = new Span("Account Recovery"); //TODO replace with UL with usage - see RegForm for UL example + howEmailUsedDetails.setOpened(false); + howEmailUsedDetails.setContent(span); + + //telegram section + FlexLayout telegramLayout = new FlexLayout(); + Optional telegramAccount = getTelegramAccount(); + if (telegramAccount.isPresent()) { + TextField telegramField = new TextField("Telegram"); + telegramField.setPrefixComponent(VaadinIcon.AT.create()); + telegramField.setValue(telegramAccount.get().getAccountName()); + Button unlink = new Button("Unlink"); + telegramLayout.add(telegramField, unlink); + } else { + Optional tgToken = getTelegramToken(); + if (tgToken.isPresent()) { + TelegramSpan telegramSpan = TelegramSpan.create(tgToken.get()); + telegramLayout.add(telegramSpan); + } else { + telegramLayout.setVisible(false); + } + } + + add(usernameLayout, emailLayout, howEmailUsedDetails, telegramLayout); + } + + //TODO maybe Optional instead + private String getCurrentEmail() { + Optional emailAccount = accountService.getAccount(user, AccountType.EMAIL); + if (emailAccount.isPresent()) { + Optional plainTextEmail = accountService.decryptAccountName(emailAccount.get()); + if (plainTextEmail.isPresent() && StringUtils.isNotBlank(plainTextEmail.get())) { + return plainTextEmail.get(); + } else { + return ""; + } + } else { + return ""; + } + } + + private Optional getTelegramAccount() { + return accountService.getAccount(user, AccountType.TELEGRAM); + } + + private Optional getTelegramToken() { + Optional telegramToken = tokenService.getTelegramToken(user); + Token tgToken; + if (telegramToken.isPresent()) { + tgToken = telegramToken.get(); + } else { + OperationResult tokenCreateResult = tokenService.createTelegramConfirmationToken(user); + if (tokenCreateResult.ok()) { + tgToken = tokenCreateResult.getPayload(Token.class); + } else { + tgToken = null; + } + } + return Optional.ofNullable(tgToken); + } +} diff --git a/src/main/java/pm/axe/ui/pages/user/profile/tabs/SecurityTab.java b/src/main/java/pm/axe/ui/pages/user/profile/tabs/SecurityTab.java new file mode 100644 index 000000000..e77c55b72 --- /dev/null +++ b/src/main/java/pm/axe/ui/pages/user/profile/tabs/SecurityTab.java @@ -0,0 +1,126 @@ +package pm.axe.ui.pages.user.profile.tabs; + +import com.vaadin.flow.component.AbstractField; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.html.*; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.select.Select; +import com.vaadin.flow.component.textfield.PasswordField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.spring.annotation.SpringComponent; +import com.vaadin.flow.spring.annotation.UIScope; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import pm.axe.Endpoint; +import pm.axe.db.models.Account; +import pm.axe.db.models.User; +import pm.axe.db.models.UserSettings; +import pm.axe.internal.HasTabInit; +import pm.axe.services.user.AccountService; +import pm.axe.services.user.UserSettingsService; +import pm.axe.ui.elements.PasswordGenerator; + +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +@SpringComponent +@UIScope +public class SecurityTab extends VerticalLayout implements HasTabInit { + private final AccountService accountService; + private final UserSettingsService userSettingsService; + private Checkbox tfaBox; + + private User user; + + @Override + public void tabInit(final User user) { + this.user = user; + List confirmedAccounts = getConfirmedAccountsFor(user); + + //change password section + H4 changePasswordTitle = new H4("Change Password"); + PasswordField oldPasswordInput = new PasswordField("Old Password"); + PasswordField newPasswordInput = new PasswordField("New Password"); + PasswordGenerator passwordGenerator = PasswordGenerator.create(); + passwordGenerator.setOpened(false); + passwordGenerator.setCopyTarget(newPasswordInput); + Button updatePasswordButton = new Button("Update"); + VerticalLayout changePasswordLayout = new VerticalLayout(changePasswordTitle, + oldPasswordInput, newPasswordInput, passwordGenerator, + updatePasswordButton); + changePasswordLayout.setPadding(false); + changePasswordLayout.setSpacing(false); + + Hr separator = new Hr(); + + //tfa section + H4 tfaTitle = new H4("Two-Factor Authentication (2FA)"); + + tfaBox = new Checkbox("Protect my account with additional one time codes"); + tfaBox.setValue(userSettingsService.isTfaEnabled(user)); + tfaBox.setEnabled(!confirmedAccounts.isEmpty()); + tfaBox.addValueChangeListener(this::onTfaBoxChanged); + + Span noConfirmedAccountsSpan = getNoConfirmedAccountsSpan(); + TextField tfaField = new TextField(); + tfaField.setReadOnly(true); + + Label sendToLabel = new Label("Send to:"); + Select tfaChannelSelect = new Select<>(); + Button saveTfaChannelButton = new Button("Save"); + + HorizontalLayout tfaSelectLayout = new HorizontalLayout(sendToLabel, tfaChannelSelect, saveTfaChannelButton); + tfaSelectLayout.setAlignItems(Alignment.CENTER); + HorizontalLayout tfaFieldLayout = new HorizontalLayout(sendToLabel, tfaField); + tfaFieldLayout.setAlignItems(Alignment.CENTER); + + VerticalLayout tfaContent = new VerticalLayout(tfaBox); + tfaContent.setPadding(false); + + if (confirmedAccounts.isEmpty()) { + tfaContent.add(noConfirmedAccountsSpan); + } else if (confirmedAccounts.size() == 1) { + tfaField.setValue(getAccountTypeName(confirmedAccounts.get(0))); + tfaContent.add(tfaFieldLayout); + } else { + tfaChannelSelect.setItems(confirmedAccounts.stream().map(this::getAccountTypeName).toList()); + tfaContent.add(tfaSelectLayout); + } + + VerticalLayout tfaLayout = new VerticalLayout(tfaTitle, tfaBox, tfaContent); + tfaLayout.setPadding(false); + + add(changePasswordLayout, separator, tfaLayout); + } + + private void onTfaBoxChanged(AbstractField.ComponentValueChangeEvent valueChangeEvent) { + Optional userSettings = userSettingsService.getUserSettings(user); + if (userSettings.isPresent()) { + userSettings.get().setTfaEnabled(tfaBox.getValue()); + userSettingsService.updateUserSettings(userSettings.get()); + Notification.show("Saved"); + } else { + tfaBox.setEnabled(false); + Notification.show("Failed to save. System error."); + } + } + + private List getConfirmedAccountsFor(final User user) { + return accountService.getAllAccountsLinkedWithUser(user).stream().filter(Account::isConfirmed).toList(); + } + + private String getAccountTypeName(final Account account) { + return StringUtils.capitalize(account.getType().name()); + } + + private Span getNoConfirmedAccountsSpan() { + Span firstPart = new Span("In order to use 2FA, please "); + Anchor link = new Anchor(Endpoint.UI.CONFIRM_ACCOUNT_PAGE, "confirm account"); + Span lastPart = new Span(" ."); + return new Span(firstPart, link, lastPart); + } +} diff --git a/src/main/java/pm/axe/ui/pages/user/profile/tabs/SettingsTab.java b/src/main/java/pm/axe/ui/pages/user/profile/tabs/SettingsTab.java new file mode 100644 index 000000000..e27bf70f4 --- /dev/null +++ b/src/main/java/pm/axe/ui/pages/user/profile/tabs/SettingsTab.java @@ -0,0 +1,19 @@ +package pm.axe.ui.pages.user.profile.tabs; + +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.spring.annotation.SpringComponent; +import com.vaadin.flow.spring.annotation.UIScope; +import lombok.RequiredArgsConstructor; +import pm.axe.db.models.User; +import pm.axe.internal.HasTabInit; + +@RequiredArgsConstructor +@SpringComponent +@UIScope +public class SettingsTab extends VerticalLayout implements HasTabInit { + @Override + public void tabInit(final User user) { + add(new Span("This is Settings Tab")); + } +}