diff --git a/checkstyle.xml b/checkstyle.xml
index 18b066a50..85f193cdf 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -146,7 +146,13 @@
-
+
+
+
+
+
diff --git a/frontend/css/axe_form_styles.css b/frontend/css/axe_form_styles.css
new file mode 100644
index 000000000..0e132d0bd
--- /dev/null
+++ b/frontend/css/axe_form_styles.css
@@ -0,0 +1,24 @@
+.axe-form-title {
+ align-self: center;
+}
+
+.axe-form-subtitle {
+ padding-left: 1rem;
+}
+.space-after-submit-button {
+ width: 100%;
+}
+
+.vertically-compact {
+ padding-top: 1px;
+ padding-bottom: 1px;
+}
+
+.axe-fields > * {
+ min-width: 99%;
+}
+
+.space-after-fields {
+ padding-left: 1rem;
+ max-width: 99%;
+}
diff --git a/frontend/css/common_styles.css b/frontend/css/common_styles.css
index 1de36d8c9..26a712a62 100644
--- a/frontend/css/common_styles.css
+++ b/frontend/css/common_styles.css
@@ -209,3 +209,19 @@ vaadin-drawer-toggle {
vaadin-icon[icon="vaadin:sign-in"] {
padding-left: 0 !important;
}
+
+.green {
+ color: green;
+}
+
+.red {
+ color: red;
+}
+
+.fit-in-window {
+ flex-wrap: wrap;
+}
+
+.fit-in-window > * {
+ flex: 1 0 auto;
+}
diff --git a/frontend/css/danger_dialog.css b/frontend/css/danger_dialog.css
new file mode 100644
index 000000000..954967604
--- /dev/null
+++ b/frontend/css/danger_dialog.css
@@ -0,0 +1,20 @@
+html {
+ --axe-warning-zone-color: rgb(231 235 239);
+}
+
+html[theme="dark"] {
+ --axe-warning-zone-color: rgb(55 72 95);
+}
+
+.danger-dialog {
+ max-width: min(501px,97%);
+}
+
+.italic-text {
+ font-style: italic;
+}
+
+.danger-dialog-warning-zone {
+ background: var(--axe-warning-zone-color);
+}
+
diff --git a/frontend/css/forgot_password_page.css b/frontend/css/forgot_password_page.css
new file mode 100644
index 000000000..88ea3cab4
--- /dev/null
+++ b/frontend/css/forgot_password_page.css
@@ -0,0 +1,3 @@
+.result-span {
+ padding-left: 1rem;
+}
diff --git a/frontend/css/login_page.css b/frontend/css/login_page.css
new file mode 100644
index 000000000..b206f55bb
--- /dev/null
+++ b/frontend/css/login_page.css
@@ -0,0 +1,4 @@
+.forgot-password-section {
+ text-align: center;
+ width: 100%;
+}
diff --git a/frontend/css/profile_page.css b/frontend/css/profile_page.css
new file mode 100644
index 000000000..9495a5365
--- /dev/null
+++ b/frontend/css/profile_page.css
@@ -0,0 +1,28 @@
+.profile-title {
+ align-self: center;
+}
+
+.toggle-with-prefix-postfix {
+ margin-right: 0.3rem;
+ margin-left: 0.3rem;
+}
+
+.tab-content {
+ padding-left: 0!important;
+ padding-right: 0!important;
+}
+
+@media (max-width: 400px) {
+ .fit-in-section {
+ width: min-content;
+ }
+}
+
+.telegram-details {
+ display: block;
+ max-width: min(290px, 100%);
+}
+
+.telegram-details div {
+ margin: 0.5rem;
+}
diff --git a/frontend/css/registration_page.css b/frontend/css/registration_page.css
new file mode 100644
index 000000000..87200051b
--- /dev/null
+++ b/frontend/css/registration_page.css
@@ -0,0 +1,8 @@
+.input {
+ flex-shrink: 0.3;
+ width: 100%;
+}
+
+.info-button {
+ border-radius: 100%;
+}
diff --git a/src/main/java/pm/axe/Axe.java b/src/main/java/pm/axe/Axe.java
index 5ead552e9..9c59c6107 100644
--- a/src/main/java/pm/axe/Axe.java
+++ b/src/main/java/pm/axe/Axe.java
@@ -32,6 +32,7 @@ public static class C {
public static final int ONE_SECOND_IN_MILLIS = 1000;
public static final String TIME_DATE_FORMAT = "dd/MM/yyyy HH:mm:ss z";
public static final String MINUS = "-";
+ public static final String PLUS = "+";
}
public static class Defaults {
diff --git a/src/main/java/pm/axe/Endpoint.java b/src/main/java/pm/axe/Endpoint.java
index eab9b026c..ea0977b5c 100644
--- a/src/main/java/pm/axe/Endpoint.java
+++ b/src/main/java/pm/axe/Endpoint.java
@@ -86,6 +86,12 @@ public static class UI {
public static final String WELCOME_PAGE = "welcome";
public static final String REGISTRATION_FAILED_PAGE = "registrationFailed";
public static final String LOGIN_PAGE = "login";
+ public static final String REGISTRATION_PAGE = "register";
+ public static final String FORGOT_PASSWORD_PAGE = "forgot-password";
+ public static final String TOS_PAGE = "terms-of-service";
+ public static final String PROFILE_PAGE = "profile";
+ public static final String CONFIRM_ACCOUNT_PAGE = "confirm-account";
+ public static final String LOGIN_VERIFICATION_PAGE = "verify";
}
/**
diff --git a/src/main/java/pm/axe/api/user/PostUserRestController.java b/src/main/java/pm/axe/api/user/PostUserRestController.java
index c5f49bd3f..50f412644 100644
--- a/src/main/java/pm/axe/api/user/PostUserRestController.java
+++ b/src/main/java/pm/axe/api/user/PostUserRestController.java
@@ -103,7 +103,7 @@ public ResponseEntity> registerUser(final @RequestBody PostUserRequest request
}
} else {
log.info("{} There is no user-defined Username in Request. Generating custom Username", TAG);
- OperationResult usernameGenerationResult = usernameGenerator.generate();
+ OperationResult usernameGenerationResult = usernameGenerator.generateRandom();
if (usernameGenerationResult.ok()) {
username = usernameGenerationResult.getStringPayload();
} else {
diff --git a/src/main/java/pm/axe/core/IdentGenerator.java b/src/main/java/pm/axe/core/IdentGenerator.java
index 23527dea6..63733fe4f 100644
--- a/src/main/java/pm/axe/core/IdentGenerator.java
+++ b/src/main/java/pm/axe/core/IdentGenerator.java
@@ -34,9 +34,6 @@ public final class IdentGenerator {
public static final String VALID_IDENT_PATTERN = "^[a-zA-Z0-9]([._-](?![._-])|[a-zA-Z0-9]){0,"
+ IDENT_MAX_LENGTH_WITHOUT_FIRST_AND_LAST_CHARS + "}[a-zA-Z0-9]$";
- private static final int TOKEN_SUBSTRING_START_INDEX = 0;
- private static final int TOKEN_SUBSTRING_END_INDEX = 7;
-
private IdentGenerator() {
throw new UnsupportedOperationException("Utility class");
}
@@ -68,7 +65,9 @@ public static String generateTokenIdent(final Token token) {
private static String generateAccountConfirmationIdent(final Token token) {
String prefix = token.getTokenType().getIdentPrefix();
- String firstPartOfToken = token.getToken().substring(TOKEN_SUBSTRING_START_INDEX, TOKEN_SUBSTRING_END_INDEX);
- return String.join("", prefix, firstPartOfToken);
+ String randomNumber = RandomStringUtils.randomNumeric(1);
+ String randomChar = RandomStringUtils.randomAlphanumeric(1);
+ String randomNum = RandomStringUtils.randomNumeric(1);
+ return String.join("", prefix, randomNumber, randomChar, randomNum);
}
}
diff --git a/src/main/java/pm/axe/db/dao/AccountDao.java b/src/main/java/pm/axe/db/dao/AccountDao.java
index 4f900536c..d25408fde 100644
--- a/src/main/java/pm/axe/db/dao/AccountDao.java
+++ b/src/main/java/pm/axe/db/dao/AccountDao.java
@@ -21,6 +21,15 @@ public interface AccountDao extends CrudRepository {
*/
Optional findByUserAndType(User user, AccountType accountType);
+ /**
+ * Searching for User's Account by its {@link AccountType}.
+ *
+ * @param user account's owner
+ * @param accountType type of account
+ * @return true - if found or false - if not.
+ */
+ boolean existsByUserAndType(User user, AccountType accountType);
+
/**
* Lists all {@link Account}s of given {@link AccountType}.
*
diff --git a/src/main/java/pm/axe/db/dao/TokenDao.java b/src/main/java/pm/axe/db/dao/TokenDao.java
index bead824cc..f705469f7 100644
--- a/src/main/java/pm/axe/db/dao/TokenDao.java
+++ b/src/main/java/pm/axe/db/dao/TokenDao.java
@@ -3,6 +3,7 @@
import lombok.NonNull;
import org.springframework.scheduling.annotation.Async;
import pm.axe.db.dao.base.TimeAwareCrudDao;
+import pm.axe.db.models.Account;
import pm.axe.db.models.Token;
import pm.axe.db.models.User;
import pm.axe.users.TokenType;
@@ -62,6 +63,17 @@ public interface TokenDao extends TimeAwareCrudDao {
*/
Token findByTokenTypeAndUser(TokenType tokenType, User user);
+ /**
+ * Finds {@link TokenType#ACCOUNT_CONFIRMATION_TOKEN} by its {@link TokenType}, {@link User} and {@link Account}.
+ *
+ * @param tokenType normally {@link TokenType#ACCOUNT_CONFIRMATION_TOKEN}.
+ * @param user {@link Token}'s owner
+ * @param account {@link Account} to be confirmed
+ *
+ * @return {@link Optional} with found {@link Token} record or {@link Optional#empty()}.
+ */
+ Optional findByTokenTypeAndUserAndConfirmationFor(TokenType tokenType, User user, Account account);
+
/**
* Finds all {@link Token} records owned by {@link User}.
*
diff --git a/src/main/java/pm/axe/db/models/Account.java b/src/main/java/pm/axe/db/models/Account.java
index 05455ab78..a60c8f9f6 100644
--- a/src/main/java/pm/axe/db/models/Account.java
+++ b/src/main/java/pm/axe/db/models/Account.java
@@ -41,6 +41,21 @@ public static Account.Builder create(final AccountType accountType) {
return new Builder(accountType);
}
+ /**
+ * This method updates fields iof current {@link Account} record with values from another.
+ *
+ * @param anotherAccount another non-null {@link Account} record to copy values from.
+ * @throws IllegalArgumentException when anotherAccount is NULL.
+ */
+ public void copy(final Account anotherAccount) {
+ if (anotherAccount == null) throw new IllegalArgumentException("Another Account cannot be null");
+ this.setUser(anotherAccount.getUser());
+ this.setType(anotherAccount.getType());
+ this.setAccountName(anotherAccount.getAccountName());
+ this.setConfirmed(anotherAccount.isConfirmed());
+ this.setExtraInfo(anotherAccount.getExtraInfo());
+ }
+
private Account(final AccountType accountType, final User user) {
this.type = accountType;
this.user = user;
diff --git a/src/main/java/pm/axe/db/models/Token.java b/src/main/java/pm/axe/db/models/Token.java
index c976a4614..5a0221424 100644
--- a/src/main/java/pm/axe/db/models/Token.java
+++ b/src/main/java/pm/axe/db/models/Token.java
@@ -23,7 +23,7 @@ public class Token extends TimeModel {
/**
* Default length of Token (code).
*/
- private static final int CODE_TOKEN_LEN = 6;
+ public static final int CODE_TOKEN_LEN = 6;
@Column(name = "token", nullable = false, unique = true)
private String token;
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..f568737dc
--- /dev/null
+++ b/src/main/java/pm/axe/internal/HasTabInit.java
@@ -0,0 +1,16 @@
+package pm.axe.internal;
+
+import com.vaadin.flow.component.tabs.Tab;
+import pm.axe.db.models.User;
+
+/**
+ * This interface determines, that {@link Tab} has {@link #tabInit(User)} method.
+ */
+public interface HasTabInit {
+ /**
+ * Method, that should be called at initialization phase.
+ *
+ * @param user bound {@link User}. Usually stored within current session.
+ */
+ void tabInit(User user);
+}
diff --git a/src/main/java/pm/axe/mail/EmailConfirmationStatus.java b/src/main/java/pm/axe/mail/EmailConfirmationStatus.java
new file mode 100644
index 000000000..73d77914e
--- /dev/null
+++ b/src/main/java/pm/axe/mail/EmailConfirmationStatus.java
@@ -0,0 +1,19 @@
+package pm.axe.mail;
+
+import com.vaadin.flow.component.icon.VaadinIcon;
+import lombok.Getter;
+
+public enum EmailConfirmationStatus {
+ CONFIRMED(VaadinIcon.CHECK, "Email confirmed"),
+ PENDING(VaadinIcon.ELLIPSIS_CIRCLE, "Validation pending..."),
+ FAILED(VaadinIcon.FIRE, "Validation failed"),
+ NONE(VaadinIcon.COG, "No status defined yet");
+
+ @Getter
+ private final VaadinIcon icon;
+ @Getter private final String statusString;
+ EmailConfirmationStatus(final VaadinIcon icon, final String statusString) {
+ this.icon = icon;
+ this.statusString = statusString;
+ }
+}
diff --git a/src/main/java/pm/axe/services/user/AccountService.java b/src/main/java/pm/axe/services/user/AccountService.java
index cc71af633..9a8e89685 100644
--- a/src/main/java/pm/axe/services/user/AccountService.java
+++ b/src/main/java/pm/axe/services/user/AccountService.java
@@ -209,6 +209,17 @@ public Optional getAccount(final User user, final AccountType accountTy
return accountDao.findByUserAndType(user, accountType);
}
+ /**
+ * Checks if Account with given {@link User} and {@link AccountType} exists.
+ *
+ * @param user account's owner
+ * @param accountType account's type
+ * @return true if exists, false if not.
+ */
+ public boolean isAccountExist(final User user, final AccountType accountType) {
+ return accountDao.existsByUserAndType(user, accountType);
+ }
+
/**
* Searches {@link Account} by plain-text {@link Account} name and {@link AccountType}.
*
@@ -295,4 +306,113 @@ public OperationResult deleteAccount(final Account account) {
return OperationResult.generalFail().withMessage(e.getMessage());
}
}
+
+ /**
+ * Updates {@link AccountType#EMAIL} {@link Account}.
+ * This method marks updated {@link AccountType#EMAIL} {@link Account} as unconfirmed.
+ *
+ * @param user {@link Account} owner.
+ *
+ * @param email non-empty string with new Email Address (plain value).
+ *
+ * @return {@link OperationResult#success()} with updated {@link Account} as payload,
+ * {@link OperationResult#malformedInput()}, when new email address not valid.
+ * {@link OperationResult#generalFail()} with {@link #ERR_ENCRYPTION_FAILED} message,
+ * when encryption failed.
+ */
+ public OperationResult updateEmailAccount(final User user, final String email) {
+ if (user == null) return OperationResult.malformedInput().withMessage("User cannot be NULL");
+ OperationResult validationResult = mailService.isEmailValid(email);
+ if (validationResult.notOk()) {
+ return validationResult;
+ }
+
+ Optional emailAccount = getAccount(user, AccountType.EMAIL);
+ boolean userHasEmailAccount;
+ if (emailAccount.isPresent()) {
+ userHasEmailAccount = true;
+ String encryptedEmail;
+ OperationResult encryptEmailResult = cryptTool.encrypt(email);
+ if (encryptEmailResult.ok()) {
+ encryptedEmail = encryptEmailResult.getStringPayload();
+ } else {
+ log.error("{} Email encryption failed. Value: {}. Error: {}",
+ TAG, email, encryptEmailResult.getMessage());
+ return OperationResult.generalFail().withMessage(ERR_ENCRYPTION_FAILED);
+ }
+ emailAccount.get().setAccountName(encryptedEmail);
+ emailAccount.get().setConfirmed(false);
+ } else {
+ userHasEmailAccount = false;
+ }
+
+ try {
+ if (userHasEmailAccount) {
+ accountDao.save(emailAccount.get());
+ log.info("{} Updated email account for {} {}", TAG, User.class.getSimpleName(), user.getUsername());
+ return OperationResult.success().addPayload(emailAccount.get());
+ } else {
+ return createEmailAccount(user, email);
+ }
+ } catch (CannotCreateTransactionException e) {
+ return OperationResult.databaseDown();
+ } catch (Exception e) {
+ log.debug("", e);
+ return OperationResult.generalFail().withMessage(e.getMessage());
+ }
+ }
+
+ /**
+ * This method restores values from old {@link Account}.
+ *
+ * @param oldAccount old {@link Account} to restore values from.
+ * @throws IllegalArgumentException when old account is NULL.
+ */
+ public void rollbackAccount(final Account oldAccount) {
+ if (oldAccount == null) throw new IllegalArgumentException("old account cannot be null");
+ Optional currentAccount = this.getAccount(oldAccount.getUser(), oldAccount.getType());
+ try {
+ if (currentAccount.isPresent()) {
+ currentAccount.get().copy(oldAccount);
+ } else {
+ accountDao.save(oldAccount);
+ }
+ log.info("{} Account {} rolled back", TAG, oldAccount);
+ } catch (CannotCreateTransactionException e) {
+ log.error("{} failed to rollback account: Database is Down", TAG);
+ } catch (Exception e) {
+ log.error("{} failed to rollback Account {}: got exception {}", TAG, oldAccount, e.getMessage());
+ }
+ }
+
+ /**
+ * Returns current email address.
+ *
+ * @param user email's owner
+ * @return {@link Optional} with email address or {@link Optional#empty()}.
+ */
+ public Optional getCurrentEmail(final User user) {
+ if (user == null) throw new IllegalArgumentException("User cannot be NULL");
+
+ Optional emailAccount = getAccount(user, AccountType.EMAIL);
+ if (emailAccount.isEmpty()) return Optional.empty();
+ Optional plainTextEmail = decryptAccountName(emailAccount.get());
+ if (plainTextEmail.isEmpty()) return Optional.empty();
+ if (StringUtils.isNotBlank(plainTextEmail.get())) {
+ return plainTextEmail;
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Is current {@link User} confirmed.
+ *
+ * @param user email's owner
+ * @return true if email confirmed, false if email not confirmed or email doesn't exist.
+ */
+ public boolean isCurrentEmailConfirmed(final User user) {
+ Optional emailAccount = getAccount(user, AccountType.EMAIL);
+ return emailAccount.map(Account::isConfirmed).orElse(false);
+ }
}
diff --git a/src/main/java/pm/axe/services/user/TokenService.java b/src/main/java/pm/axe/services/user/TokenService.java
index 5651f1807..ada86ff0a 100644
--- a/src/main/java/pm/axe/services/user/TokenService.java
+++ b/src/main/java/pm/axe/services/user/TokenService.java
@@ -24,8 +24,6 @@
public class TokenService {
private static final String TAG = "[" + TokenService.class.getSimpleName() + "]";
- private static final String ERR_USER_ALREADY_HAS_TOKEN = "User already has token";
-
private final TokenDao tokenDao;
/**
@@ -36,17 +34,19 @@ public class TokenService {
* @return {@link OperationResult} with created {@link Token} or {@link OperationResult} with error.
*/
public OperationResult createConfirmationToken(final User user, final Account account) {
- boolean userAlreadyHasConfirmationToken =
- tokenDao.existsByTokenTypeAndUser(TokenType.ACCOUNT_CONFIRMATION_TOKEN, user);
- if (userAlreadyHasConfirmationToken) {
- return OperationResult.banned().withMessage(ERR_USER_ALREADY_HAS_TOKEN);
+ Optional optionalToken =
+ tokenDao.findByTokenTypeAndUserAndConfirmationFor(TokenType.ACCOUNT_CONFIRMATION_TOKEN, user, account);
+ Token confirmationToken;
+ if (optionalToken.isPresent()) {
+ //use it again
+ confirmationToken = optionalToken.get();
+ } else {
+ //create new
+ confirmationToken = Token.create(TokenType.ACCOUNT_CONFIRMATION_TOKEN).forUser(user);
+ confirmationToken.setConfirmationFor(account);
+ verifyTokenValueIsUnique(confirmationToken);
}
- Token confirmationToken = Token.create(TokenType.ACCOUNT_CONFIRMATION_TOKEN).forUser(user);
- confirmationToken.setConfirmationFor(account);
-
- verifyTokenValueIsUnique(confirmationToken);
-
try {
tokenDao.save(confirmationToken);
return OperationResult.success().addPayload(confirmationToken);
@@ -172,7 +172,22 @@ public Optional getToken(final String tokenString) {
return token.isPresent() ? returnOnlyValidToken(token.get()) : Optional.empty();
}
+ /**
+ * Gets Token by {@link User} and {@link TokenType}.
+ *
+ * @param user {@link Token}'s owner
+ * @param tokenType {@link Token}'s type
+ *
+ * @return {@link Optional} with valid {@link Token} or {@link Optional#empty()}
+ * @throws IllegalArgumentException when user or token typer params are null
+ */
+ public Optional getToken(final User user, final TokenType tokenType) {
+ if (user == null) throw new IllegalArgumentException("user cannot be null");
+ if (tokenType == null) throw new IllegalArgumentException("token type cannot be null");
+ Token token = tokenDao.findByTokenTypeAndUser(tokenType, user);
+ return token != null ? returnOnlyValidToken(token) : Optional.empty();
+ }
/**
* Provides {@link User}'s {@link TokenType#TELEGRAM_CONFIRMATION_TOKEN} token.
diff --git a/src/main/java/pm/axe/services/user/UserOperationsService.java b/src/main/java/pm/axe/services/user/UserOperationsService.java
index c709da78a..3c7e1f1ce 100644
--- a/src/main/java/pm/axe/services/user/UserOperationsService.java
+++ b/src/main/java/pm/axe/services/user/UserOperationsService.java
@@ -108,24 +108,14 @@ public OperationResult registerUser(final RegisterUserInput input) {
}
//Create and send - confirmation email for Accounts with Email set.
if (userAccount.getType() == AccountType.EMAIL) {
- //Create Confirmation Token
- OperationResult createConfirmationTokenResult =
- tokenService.createConfirmationToken(createdUser, userAccount);
- if (createConfirmationTokenResult.notOk()) {
- log.error("{} failed to create confirmation token for {}. OpResult: {}",
- TAG, createdUser.getUsername(), createConfirmationTokenResult);
- return createConfirmationTokenResult;
- }
- Token confirmationToken = createConfirmationTokenResult.getPayload(Token.class);
- rollbackTasks.push(RollbackTask.create(Token.class, confirmationToken));
- //Send it
- log.info("{} Successfully created {}({}) for user '{}'",
- TAG, confirmationToken.getTokenType(), confirmationToken.getToken(), createdUser.getUsername());
- OperationResult sendResult = senders.getSender(AccountType.EMAIL).send(confirmationToken, input.getEmail());
- if (sendResult.notOk()) {
- log.warn("{} Unable to send created {} to {}. OpResult: {}",
- TAG, confirmationToken.getTokenType(), input.getEmail(), sendResult);
- log.warn("{} Requesting Rollback", TAG);
+ Optional confirmationToken = createConfirmationToken(userAccount);
+ if (confirmationToken.isPresent()) {
+ rollbackTasks.push(RollbackTask.create(Token.class, confirmationToken.get()));
+ OperationResult sendConfirmationLetterResult =
+ sendConfirmationLetter(confirmationToken.get(), input.getEmail(), userAccount);
+ if (sendConfirmationLetterResult.notOk()) return sendConfirmationLetterResult;
+ } else {
+ log.error("{} Failed to create confirmation {}", TAG, Token.class.getSimpleName());
rollbackService.rollback(rollbackTasks);
}
}
@@ -147,6 +137,66 @@ public OperationResult registerUser(final RegisterUserInput input) {
? success : success.addPayload(TELEGRAM_TOKEN_KEY, telegramConfirmationToken.getToken());
}
+ /**
+ * Creates Confirmation {@link Token}.
+ * @param userAccount {@link Account} to confirm with given Token.
+ *
+ * @return {@link Optional} with {@link Token} or {@link Optional#empty()} if failed t create token.
+ */
+ public Optional createConfirmationToken(final Account userAccount) {
+ if (userAccount == null) {
+ throw new IllegalArgumentException("User Account cannot be null");
+ }
+ OperationResult createConfirmationTokenResult =
+ tokenService.createConfirmationToken(userAccount.getUser(), userAccount);
+ if (createConfirmationTokenResult.notOk()) {
+ log.error("{} failed to create confirmation token for {}. OpResult: {}",
+ TAG, userAccount.getUser().getUsername(), createConfirmationTokenResult);
+ return Optional.empty();
+ }
+ return Optional.ofNullable(createConfirmationTokenResult.getPayload(Token.class));
+ }
+
+ /**
+ * Sends {@link TokenType#ACCOUNT_CONFIRMATION_TOKEN}.
+ *
+ * @param confirmationToken {@link TokenType#ACCOUNT_CONFIRMATION_TOKEN} {@link Token}
+ * @param email non-empty string with email
+ * @param userAccount {@link Account} to confirm with given Token
+ *
+ * @return {@link OperationResult#success()}, when email send,
+ * {@link OperationResult#malformedInput()}, when something is wrong with params,
+ * {@link OperationResult#generalFail()}, when failed to send letter.
+ */
+ public OperationResult sendConfirmationLetter(final Token confirmationToken, final String email,
+ final Account userAccount) {
+ //check inputs
+ if (StringUtils.isBlank(email)) {
+ return OperationResult.malformedInput().withMessage("Email cannot be empty");
+ }
+ if (userAccount == null) {
+ return OperationResult.malformedInput().withMessage("User Account cannot be null");
+ }
+ if (userAccount.getUser() == null) {
+ return OperationResult.malformedInput().withMessage("Given Account is not bound to any user");
+ }
+
+
+ //Send it
+ log.info("{} Successfully created {}({}) for user '{}'",
+ TAG, confirmationToken.getTokenType(), confirmationToken.getToken(),
+ userAccount.getUser().getUsername());
+ OperationResult sendResult = senders.getSender(AccountType.EMAIL).send(confirmationToken, email);
+ if (sendResult.notOk()) {
+ log.warn("{} Unable to send created {} to {}. OpResult: {}",
+ TAG, confirmationToken.getTokenType(), email, sendResult);
+ log.warn("{} Requesting Rollback", TAG);
+ rollbackService.rollback(rollbackTasks);
+ return sendResult;
+ }
+ return OperationResult.success();
+ }
+
/**
* Deletes User and its Accounts.
*
@@ -246,4 +296,92 @@ public OperationResult deleteUser(final User user, final boolean force) {
EventBus.getDefault().post(UserDeletedEvent.createWith(user));
return deletionResult;
}
+
+ /**
+ * Updates email and also resets channel.
+ *
+ * @param user email account owner
+ * @param email plain text email address
+ * @return {@link OperationResult} with update email {@link Account} or {@link OperationResult} with fail state.
+ */
+ public OperationResult updateEmailAccount(final User user, final String email) {
+ OperationResult accountUpdateResult = accountService.updateEmailAccount(user, email);
+ if (accountUpdateResult.notOk()) {
+ return accountUpdateResult;
+ }
+ Account account = accountUpdateResult.getPayload(Account.class);
+ OperationResult resetResult = resetChannels(account);
+ if (resetResult.notOk()) {
+ return resetResult;
+ }
+ return OperationResult.success().addPayload(account);
+ }
+
+ /**
+ * Deletes {@link Account} and resets channels in {@link UserSettings}.
+ *
+ * @param account non-empty {@link Account} to update.
+ * @return delete account {@link OperationResult}.
+ *
+ */
+ public OperationResult deleteAccountOnly(final Account account) {
+ if (account == null) throw new IllegalArgumentException("Account cannot be null");
+ if (account.getUser() == null) throw new IllegalStateException("Account has no owner");
+
+ Optional accountConfirmationToken = tokenService.getToken(account.getUser(),
+ TokenType.ACCOUNT_CONFIRMATION_TOKEN);
+ if (accountConfirmationToken.isPresent()) {
+ //because we need to delete it in @Sync manner
+ OperationResult tokenDeletionResult = tokenService.deleteToken(accountConfirmationToken.get().getToken());
+ if (tokenDeletionResult.notOk()) {
+ return tokenDeletionResult;
+ }
+ }
+
+ //also reset channels (tfa, reset)
+ OperationResult resetResult = resetChannels(account);
+ if (resetResult.notOk()) {
+ return resetResult;
+ }
+
+ //and finally, delete it
+ return accountService.deleteAccount(account);
+ }
+
+ private OperationResult resetChannels(final Account account) {
+ if (account == null) throw new IllegalArgumentException("account cannot be null");
+ if (account.getUser() == null) throw new IllegalStateException("account has no owner");
+
+ Optional userSettings = userSettingsService.getUserSettings(account.getUser());
+ if (userSettings.isPresent()) {
+ UserSettings us = userSettings.get();
+ if (us.getTfaChannel() == account.getType()) {
+ us.setTfaChannel(getAnotherConfirmedAccount(us.getUser()));
+ }
+ if (us.getPasswordResetChannel() == account.getType()) {
+ us.setPasswordResetChannel(getAnotherConfirmedAccount(us.getUser()));
+ }
+ return userSettingsService.updateUserSettings(us);
+ } else {
+ return OperationResult.generalFail().withMessage("UserSettings not found for "
+ + account.getUser().getUsername());
+ }
+ }
+
+ private AccountType getAnotherConfirmedAccount(final User user) {
+ if (user == null) throw new IllegalArgumentException("user cannot be null");
+ List confirmedAccounts = accountService.getAllAccountsLinkedWithUser(user).stream()
+ .filter(Account::isConfirmed).toList();
+ if (confirmedAccounts.size() > 1) {
+ //more than one confirmed account - user should decide, which one to use.
+ return AccountType.LOCAL;
+ } else if (confirmedAccounts.size() == 1) {
+ //just using another confirmed account
+ return confirmedAccounts.get(0).getType();
+ } else {
+ //no more confirmed accounts - reset
+ return AccountType.LOCAL;
+ }
+ }
+
}
diff --git a/src/main/java/pm/axe/services/user/UserService.java b/src/main/java/pm/axe/services/user/UserService.java
index 89a9f5e31..2db03583c 100644
--- a/src/main/java/pm/axe/services/user/UserService.java
+++ b/src/main/java/pm/axe/services/user/UserService.java
@@ -196,5 +196,33 @@ void confirmUser(final User user) {
userDao.update(user);
}
+ /**
+ * Updates username.
+ *
+ * @param user {@link User}, that should be updated.
+ * @param username non-empty string with new username.
+ *
+ * @return {@link OperationResult#malformedInput()} if username doesn't meet requirements
+ * or {@link OperationResult#success()} if update was successful.
+ */
+ public OperationResult updateUsername(final User user, final String username) {
+ if (user == null) return OperationResult.malformedInput().withMessage("User cannot be NULL");
+ OperationResult validationResult = UsernameValidator.isValid(username);
+ if (validationResult.notOk()) {
+ return validationResult;
+ }
+ user.setUsername(username);
+ try {
+ userDao.update(user);
+ return OperationResult.success();
+ } catch (CannotCreateTransactionException e) {
+ return OperationResult.databaseDown();
+ } catch (Exception e) {
+ log.error("{} Exception on updating {} record",
+ TAG, User.class.getSimpleName());
+ log.debug("", e);
+ return OperationResult.generalFail();
+ }
+ }
}
diff --git a/src/main/java/pm/axe/services/user/UserSettingsService.java b/src/main/java/pm/axe/services/user/UserSettingsService.java
index 8a468e5b5..06e56c364 100644
--- a/src/main/java/pm/axe/services/user/UserSettingsService.java
+++ b/src/main/java/pm/axe/services/user/UserSettingsService.java
@@ -62,6 +62,17 @@ public void deleteUserSettings(final UserSettings settings) {
userSettingsDao.delete(settings);
}
+ /**
+ * Checks if user has 2FA enabled or not.
+ *
+ * @param user {@link UserSettings} owner
+ * @return true - if 2FA is enabled for given {@link User}, false - in other cases.
+ */
+ public boolean isTfaEnabled(final User user) {
+ Optional userSettings = getUserSettings(user);
+ return userSettings.map(UserSettings::isTfaEnabled).orElse(false);
+ }
+
private OperationResult createOrUpdateUserSettings(final UserSettings userSettings) {
try {
userSettingsDao.save(userSettings);
@@ -77,4 +88,6 @@ private OperationResult createOrUpdateUserSettings(final UserSettings userSettin
return OperationResult.generalFail();
}
}
+
+
}
diff --git a/src/main/java/pm/axe/ui/elements/AppMenu.java b/src/main/java/pm/axe/ui/elements/AppMenu.java
index 67fa68ff9..3a328900b 100644
--- a/src/main/java/pm/axe/ui/elements/AppMenu.java
+++ b/src/main/java/pm/axe/ui/elements/AppMenu.java
@@ -11,6 +11,8 @@
import com.vaadin.flow.component.menubar.MenuBar;
import com.vaadin.flow.component.menubar.MenuBarVariant;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
+import pm.axe.ui.pages.user.LoginPage;
+import pm.axe.ui.pages.user.RegistrationPage;
import pm.axe.utils.ErrorUtils;
/**
@@ -18,6 +20,9 @@
*/
public final class AppMenu extends Composite {
+ private final Button loginButton = getLoginButton();
+ private final Button registerButton = getRegisterButton();
+
/**
* Creates menu, that should be shown to all visitors.
*
@@ -58,7 +63,7 @@ private AppMenu(final boolean isUserMenu) {
if (isUserMenu) {
userMenuButtons.add(getLogoutButton());
} else {
- userMenuButtons.add(getLoginButton(), getRegisterButton());
+ userMenuButtons.add(loginButton, registerButton);
}
//buttons
@@ -99,11 +104,11 @@ private void onLogoutButtonClicked(final ClickEvent