diff --git a/docs/config.md b/docs/config.md index ff2d54fb4..51c917032 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,5 +1,5 @@ - + ## AuthMe Configuration The first time you run AuthMe it will create a config.yml file in the plugins/AuthMe folder, @@ -323,6 +323,8 @@ Email: mailSMTP: 'smtp.gmail.com' # Email SMTP server port mailPort: 465 + # Only affects port 25: enable TLS/STARTTLS? + useTls: true # Email account which sends the mails mailAccount: '' # Email account password @@ -440,6 +442,8 @@ Security: length: 8 # How many hours is a recovery code valid for? validForHours: 4 + # Max number of tries to enter recovery code + maxTries: 3 emailRecovery: # Seconds a user has to wait for before a password recovery mail may be sent again # This prevents an attacker from abusing AuthMe's email feature. @@ -460,4 +464,4 @@ To change settings on a running server, save your changes to config.yml and use --- -This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sat Feb 25 21:59:18 CET 2017 +This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Mon Mar 06 13:51:04 EST 2017 diff --git a/src/main/java/fr/xephi/authme/command/executable/email/ProcessCodeCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/ProcessCodeCommand.java new file mode 100644 index 000000000..8a7bbf3f1 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/email/ProcessCodeCommand.java @@ -0,0 +1,57 @@ +package fr.xephi.authme.command.executable.email; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.PasswordRecoveryService; +import fr.xephi.authme.service.RecoveryCodeService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command for submitting email recovery code. + */ +public class ProcessCodeCommand extends PlayerCommand { + + @Inject + private CommonService commonService; + + @Inject + private DataSource dataSource; + + @Inject + private RecoveryCodeService codeService; + + @Inject + private PasswordRecoveryService recoveryService; + + @Override + protected void runCommand(Player player, List arguments) { + String name = player.getName(); + String code = arguments.get(0); + + if (codeService.hasTriesLeft(name)) { + if (codeService.isCodeValid(name, code)) { + PlayerAuth auth = dataSource.getAuth(name); + String email = auth.getEmail(); + if (email == null || "your@email.com".equalsIgnoreCase(email)) { + commonService.send(player, MessageKey.INVALID_EMAIL); + return; + } + + recoveryService.generateAndSendNewPassword(player, email); + codeService.removeCode(name); + } else { + commonService.send(player, MessageKey.INCORRECT_RECOVERY_CODE, + Integer.toString(codeService.getTriesLeft(name))); + } + } else { + codeService.removeCode(name); + commonService.send(player, MessageKey.RECOVERY_TRIES_EXCEEDED); + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java index b3fb3b62b..455fc6134 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java @@ -5,34 +5,20 @@ import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.initialization.Reloadable; import fr.xephi.authme.mail.EmailService; import fr.xephi.authme.message.MessageKey; -import fr.xephi.authme.message.Messages; -import fr.xephi.authme.security.PasswordSecurity; -import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.PasswordRecoveryService; import fr.xephi.authme.service.RecoveryCodeService; -import fr.xephi.authme.settings.properties.SecuritySettings; -import fr.xephi.authme.util.RandomStringUtils; -import fr.xephi.authme.util.expiring.Duration; -import fr.xephi.authme.util.expiring.ExpiringSet; import org.bukkit.entity.Player; -import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.List; -import java.util.concurrent.TimeUnit; - -import static fr.xephi.authme.settings.properties.EmailSettings.RECOVERY_PASSWORD_LENGTH; /** * Command for password recovery by email. */ -public class RecoverEmailCommand extends PlayerCommand implements Reloadable { - - @Inject - private PasswordSecurity passwordSecurity; +public class RecoverEmailCommand extends PlayerCommand { @Inject private CommonService commonService; @@ -47,18 +33,10 @@ public class RecoverEmailCommand extends PlayerCommand implements Reloadable { private EmailService emailService; @Inject - private RecoveryCodeService recoveryCodeService; + private PasswordRecoveryService recoveryService; @Inject - private Messages messages; - - private ExpiringSet emailCooldown; - - @PostConstruct - private void initEmailCooldownSet() { - emailCooldown = new ExpiringSet<>( - commonService.getProperty(SecuritySettings.EMAIL_RECOVERY_COOLDOWN_SECONDS), TimeUnit.SECONDS); - } + private RecoveryCodeService recoveryCodeService; @Override protected void runCommand(Player player, List arguments) { @@ -89,73 +67,9 @@ public class RecoverEmailCommand extends PlayerCommand implements Reloadable { if (recoveryCodeService.isRecoveryCodeNeeded()) { // Process /email recovery addr@example.com - if (arguments.size() == 1) { - createAndSendRecoveryCode(player, email); - } else { - // Process /email recovery addr@example.com 12394 - processRecoveryCode(player, arguments.get(1), email); - } + recoveryService.createAndSendRecoveryCode(player, email); } else { - boolean maySendMail = checkEmailCooldown(player); - if (maySendMail) { - generateAndSendNewPassword(player, email); - } + recoveryService.generateAndSendNewPassword(player, email); } } - - @Override - public void reload() { - emailCooldown.setExpiration( - commonService.getProperty(SecuritySettings.EMAIL_RECOVERY_COOLDOWN_SECONDS), TimeUnit.SECONDS); - } - - private void createAndSendRecoveryCode(Player player, String email) { - if (!checkEmailCooldown(player)) { - return; - } - - String recoveryCode = recoveryCodeService.generateCode(player.getName()); - boolean couldSendMail = emailService.sendRecoveryCode(player.getName(), email, recoveryCode); - if (couldSendMail) { - commonService.send(player, MessageKey.RECOVERY_CODE_SENT); - emailCooldown.add(player.getName().toLowerCase()); - } else { - commonService.send(player, MessageKey.EMAIL_SEND_FAILURE); - } - } - - private void processRecoveryCode(Player player, String code, String email) { - final String name = player.getName(); - if (recoveryCodeService.isCodeValid(name, code)) { - generateAndSendNewPassword(player, email); - recoveryCodeService.removeCode(name); - } else { - commonService.send(player, MessageKey.INCORRECT_RECOVERY_CODE); - } - } - - private void generateAndSendNewPassword(Player player, String email) { - String name = player.getName(); - String thePass = RandomStringUtils.generate(commonService.getProperty(RECOVERY_PASSWORD_LENGTH)); - HashedPassword hashNew = passwordSecurity.computeHash(thePass, name); - - dataSource.updatePassword(name, hashNew); - boolean couldSendMail = emailService.sendPasswordMail(name, email, thePass); - if (couldSendMail) { - commonService.send(player, MessageKey.RECOVERY_EMAIL_SENT_MESSAGE); - emailCooldown.add(player.getName().toLowerCase()); - } else { - commonService.send(player, MessageKey.EMAIL_SEND_FAILURE); - } - } - - private boolean checkEmailCooldown(Player player) { - Duration waitDuration = emailCooldown.getExpiration(player.getName().toLowerCase()); - if (waitDuration.getDuration() > 0) { - String durationText = messages.formatDuration(waitDuration); - messages.send(player, MessageKey.EMAIL_COOLDOWN_ERROR, durationText); - return false; - } - return true; - } } diff --git a/src/main/java/fr/xephi/authme/message/MessageKey.java b/src/main/java/fr/xephi/authme/message/MessageKey.java index e8c3939a9..36c4a9cda 100644 --- a/src/main/java/fr/xephi/authme/message/MessageKey.java +++ b/src/main/java/fr/xephi/authme/message/MessageKey.java @@ -224,8 +224,11 @@ public enum MessageKey { /** A recovery code to reset your password has been sent to your email. */ RECOVERY_CODE_SENT("recovery_code_sent"), - /** The recovery code is not correct! Use "/email recovery [email]" to generate a new one */ - INCORRECT_RECOVERY_CODE("recovery_code_incorrect"), + /** The recovery code is not correct! You have %count tries remaining. */ + INCORRECT_RECOVERY_CODE("recovery_code_incorrect", "%count"), + + /** You have exceeded the maximum number of attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one. */ + RECOVERY_TRIES_EXCEEDED("recovery_tries_exceeded"), /** An email was already sent recently. You must wait %time before you can send a new one. */ EMAIL_COOLDOWN_ERROR("email_cooldown_error", "%time"), diff --git a/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java b/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java new file mode 100644 index 000000000..272f6a952 --- /dev/null +++ b/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java @@ -0,0 +1,125 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.mail.EmailService; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.RandomStringUtils; +import fr.xephi.authme.util.expiring.Duration; +import fr.xephi.authme.util.expiring.ExpiringSet; +import org.bukkit.entity.Player; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.concurrent.TimeUnit; + +import static fr.xephi.authme.settings.properties.EmailSettings.RECOVERY_PASSWORD_LENGTH; + +/** + * Manager for password recovery. + */ +public class PasswordRecoveryService implements Reloadable { + + @Inject + private CommonService commonService; + + @Inject + private RecoveryCodeService codeService; + + @Inject + private DataSource dataSource; + + @Inject + private EmailService emailService; + + @Inject + private PasswordSecurity passwordSecurity; + + @Inject + private RecoveryCodeService recoveryCodeService; + + @Inject + private Messages messages; + + private ExpiringSet emailCooldown; + + @PostConstruct + private void initEmailCooldownSet() { + emailCooldown = new ExpiringSet<>( + commonService.getProperty(SecuritySettings.EMAIL_RECOVERY_COOLDOWN_SECONDS), TimeUnit.SECONDS); + } + + /** + * Create a new recovery code and send it to the player + * via email. + * + * @param player The player getting the code. + * @param email The email to send the code to. + */ + public void createAndSendRecoveryCode(Player player, String email) { + if (!checkEmailCooldown(player)) { + return; + } + + String recoveryCode = recoveryCodeService.generateCode(player.getName()); + boolean couldSendMail = emailService.sendRecoveryCode(player.getName(), email, recoveryCode); + if (couldSendMail) { + commonService.send(player, MessageKey.RECOVERY_CODE_SENT); + emailCooldown.add(player.getName().toLowerCase()); + } else { + commonService.send(player, MessageKey.EMAIL_SEND_FAILURE); + } + } + + /** + * Generate a new password and send it to the player via + * email. This will update the database with the new password. + * + * @param player The player recovering their password. + * @param email The email to send the password to. + */ + public void generateAndSendNewPassword(Player player, String email) { + if (!checkEmailCooldown(player)) { + return; + } + + String name = player.getName(); + String thePass = RandomStringUtils.generate(commonService.getProperty(RECOVERY_PASSWORD_LENGTH)); + HashedPassword hashNew = passwordSecurity.computeHash(thePass, name); + + dataSource.updatePassword(name, hashNew); + boolean couldSendMail = emailService.sendPasswordMail(name, email, thePass); + if (couldSendMail) { + commonService.send(player, MessageKey.RECOVERY_EMAIL_SENT_MESSAGE); + emailCooldown.add(player.getName().toLowerCase()); + } else { + commonService.send(player, MessageKey.EMAIL_SEND_FAILURE); + } + } + + /** + * Check if a player is able to have emails sent. + * + * @param player The player to check. + * @return True if the player is not on cooldown. + */ + public boolean checkEmailCooldown(Player player) { + Duration waitDuration = emailCooldown.getExpiration(player.getName().toLowerCase()); + if (waitDuration.getDuration() > 0) { + String durationText = messages.formatDuration(waitDuration); + messages.send(player, MessageKey.EMAIL_COOLDOWN_ERROR, durationText); + return false; + } + return true; + } + + @Override + public void reload() { + emailCooldown.setExpiration( + commonService.getProperty(SecuritySettings.EMAIL_RECOVERY_COOLDOWN_SECONDS), TimeUnit.SECONDS); + } +} diff --git a/src/main/java/fr/xephi/authme/service/RecoveryCodeService.java b/src/main/java/fr/xephi/authme/service/RecoveryCodeService.java index cae8aaa73..dcccb36de 100644 --- a/src/main/java/fr/xephi/authme/service/RecoveryCodeService.java +++ b/src/main/java/fr/xephi/authme/service/RecoveryCodeService.java @@ -6,26 +6,29 @@ import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.util.RandomStringUtils; import fr.xephi.authme.util.expiring.ExpiringMap; +import fr.xephi.authme.util.expiring.TimedCounter; import javax.inject.Inject; import java.util.concurrent.TimeUnit; -import static fr.xephi.authme.settings.properties.SecuritySettings.RECOVERY_CODE_HOURS_VALID; - /** * Manager for recovery codes. */ public class RecoveryCodeService implements SettingsDependent, HasCleanup { private final ExpiringMap recoveryCodes; + private final TimedCounter playerTries; private int recoveryCodeLength; private int recoveryCodeExpiration; + private int recoveryCodeMaxTries; @Inject RecoveryCodeService(Settings settings) { recoveryCodeLength = settings.getProperty(SecuritySettings.RECOVERY_CODE_LENGTH); recoveryCodeExpiration = settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID); + recoveryCodeMaxTries = settings.getProperty(SecuritySettings.RECOVERY_CODE_MAX_TRIES); recoveryCodes = new ExpiringMap<>(recoveryCodeExpiration, TimeUnit.HOURS); + playerTries = new TimedCounter<>(recoveryCodeExpiration, TimeUnit.HOURS); } /** @@ -43,6 +46,8 @@ public class RecoveryCodeService implements SettingsDependent, HasCleanup { */ public String generateCode(String player) { String code = RandomStringUtils.generateHex(recoveryCodeLength); + + playerTries.put(player, recoveryCodeMaxTries); recoveryCodes.put(player, code); return code; } @@ -56,9 +61,30 @@ public class RecoveryCodeService implements SettingsDependent, HasCleanup { */ public boolean isCodeValid(String player, String code) { String storedCode = recoveryCodes.get(player); + playerTries.decrement(player); return storedCode != null && storedCode.equals(code); } + /** + * Checks whether a player has tries remaining to enter a code. + * + * @param player The player to check for. + * @return True if the player has tries left. + */ + public boolean hasTriesLeft(String player) { + return playerTries.get(player) > 0; + } + + /** + * Get the number of attempts a player has to enter a code. + * + * @param player The player to check for. + * @return The number of tries left. + */ + public int getTriesLeft(String player) { + return playerTries.get(player); + } + /** * Removes the player's recovery code if present. * @@ -66,17 +92,20 @@ public class RecoveryCodeService implements SettingsDependent, HasCleanup { */ public void removeCode(String player) { recoveryCodes.remove(player); + playerTries.remove(player); } @Override public void reload(Settings settings) { recoveryCodeLength = settings.getProperty(SecuritySettings.RECOVERY_CODE_LENGTH); - recoveryCodeExpiration = settings.getProperty(RECOVERY_CODE_HOURS_VALID); + recoveryCodeExpiration = settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID); + recoveryCodeMaxTries = settings.getProperty(SecuritySettings.RECOVERY_CODE_MAX_TRIES); recoveryCodes.setExpiration(recoveryCodeExpiration, TimeUnit.HOURS); } @Override public void performCleanup() { recoveryCodes.removeExpiredEntries(); + playerTries.removeExpiredEntries(); } } diff --git a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java index 496fb3b2b..0aa517aeb 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java @@ -114,6 +114,10 @@ public class SecuritySettings implements SettingsHolder { public static final Property RECOVERY_CODE_HOURS_VALID = newProperty("Security.recoveryCode.validForHours", 4); + @Comment("Max number of tries to enter recovery code") + public static final Property RECOVERY_CODE_MAX_TRIES = + newProperty("Security.recoveryCode.maxTries", 3); + @Comment({ "Seconds a user has to wait for before a password recovery mail may be sent again", "This prevents an attacker from abusing AuthMe's email feature." diff --git a/src/main/java/fr/xephi/authme/util/expiring/TimedCounter.java b/src/main/java/fr/xephi/authme/util/expiring/TimedCounter.java index c3ae908cd..80367bdd5 100644 --- a/src/main/java/fr/xephi/authme/util/expiring/TimedCounter.java +++ b/src/main/java/fr/xephi/authme/util/expiring/TimedCounter.java @@ -35,6 +35,21 @@ public class TimedCounter extends ExpiringMap { put(key, get(key) + 1); } + /** + * Decrements the value stored for the provided key. + * This method will NOT update the expiration. + * + * @param key the key to increment the counter for + */ + public void decrement(K key) { + ExpiringEntry e = entries.get(key); + + if (e != null) { + if (e.getValue() <= 0) { remove(key); } + else {entries.put(key, new ExpiringEntry<>(e.getValue() - 1, e.getExpiration())); } + } + } + /** * Calculates the total of all non-expired entries in this counter. * diff --git a/src/main/resources/messages/messages_bg.yml b/src/main/resources/messages/messages_bg.yml index 38e79face..c4042804a 100644 --- a/src/main/resources/messages/messages_bg.yml +++ b/src/main/resources/messages/messages_bg.yml @@ -45,7 +45,8 @@ unregistered: '&cУспешно от-регистриран!' # TODO accounts_owned_other: 'The player %name has %count accounts:' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use "/email recovery [email]" to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&fТвоята регистрация не е активирана, моля провери своя Имейл!' usage_unreg: '&cКоманда: /unregister парола' pwd_changed: '&cПаролата е променена!' diff --git a/src/main/resources/messages/messages_br.yml b/src/main/resources/messages/messages_br.yml index c213f49cb..e39c553c3 100644 --- a/src/main/resources/messages/messages_br.yml +++ b/src/main/resources/messages/messages_br.yml @@ -48,7 +48,9 @@ accounts_owned_self: 'Você tem %count contas:' accounts_owned_other: 'O jogador %name tem %count contas:' two_factor_create: '&2O seu código secreto é %code. Você pode verificá-lo a partir daqui %url' recovery_code_sent: 'Um código de recuperação para redefinir sua senha foi enviada para o seu e-mail.' +# TODO: Missing tags %count recovery_code_incorrect: 'O código de recuperação esta incorreto! Use /email recovery [email] para gerar um novo!' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&cA sua conta ainda não está ativada, por favor, verifique seus e-mails!' usage_unreg: '&cUse: /unregister ' pwd_changed: '&2Senha alterada com sucesso!' diff --git a/src/main/resources/messages/messages_cz.yml b/src/main/resources/messages/messages_cz.yml index 586a7376f..fb8ef0f8b 100644 --- a/src/main/resources/messages/messages_cz.yml +++ b/src/main/resources/messages/messages_cz.yml @@ -44,7 +44,9 @@ accounts_owned_self: 'Vlastníš tyto účty (%count):' accounts_owned_other: 'Hráč %name vlastní tyto účty (%count):' two_factor_create: '&2Tvůj tajný kód je %code. Můžeš ho oskenovat zde %url' recovery_code_sent: 'Kód pro obnovení hesla byl odeslán na váš email.' +# TODO: Missing tags %count recovery_code_incorrect: 'Kód pro není správný! Použijte příkaz /email recovery [email] pro vygenerování nového.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&cTvůj účet není aktivovaný, zkontroluj si svůj E-mail.' usage_unreg: '&cPoužij: "/unregister TvojeHeslo".' pwd_changed: '&cHeslo změněno!' diff --git a/src/main/resources/messages/messages_de.yml b/src/main/resources/messages/messages_de.yml index 648a5153f..02fcfad49 100644 --- a/src/main/resources/messages/messages_de.yml +++ b/src/main/resources/messages/messages_de.yml @@ -44,7 +44,9 @@ accounts_owned_self: 'Du besitzt %count Accounts:' accounts_owned_other: 'Der Spieler %name hat %count Accounts:' two_factor_create: '&2Dein geheimer Code ist %code. Du kannst ihn hier abfragen: %url' recovery_code_sent: 'Ein Wiederherstellungscode zum Zurücksetzen deines Passworts wurde an deine E-Mail-Adresse geschickt.' +# TODO: Missing tags %count recovery_code_incorrect: 'Der Wiederherstellungscode stimmt nicht! Nutze /email recovery [email] um einen neuen zu generieren.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&cDein Account wurde noch nicht aktiviert. Bitte prüfe deine E-Mails!' usage_unreg: '&cBenutze: /unregister ' pwd_changed: '&2Passwort geändert!' diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index c71902341..cf22eb0b3 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -44,7 +44,8 @@ accounts_owned_self: 'You own %count accounts:' accounts_owned_other: 'The player %name has %count accounts:' two_factor_create: '&2Your secret code is %code. You can scan it from here %url' recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -recovery_code_incorrect: 'The recovery code is not correct! Use "/email recovery [email]" to generate a new one' +recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&cYour account isn''t activated yet, please check your emails!' usage_unreg: '&cUsage: /unregister ' pwd_changed: '&2Password changed successfully!' diff --git a/src/main/resources/messages/messages_es.yml b/src/main/resources/messages/messages_es.yml index ac7415718..6c6508057 100644 --- a/src/main/resources/messages/messages_es.yml +++ b/src/main/resources/messages/messages_es.yml @@ -47,7 +47,9 @@ accounts_owned_self: 'Eres propietario de %count cuentas:' accounts_owned_other: 'El jugador %name tiene %count cuentas:' two_factor_create: '&2Tu código secreto es %code. Lo puedes escanear desde aquí %url' recovery_code_sent: 'El código de recuperación para recuperar tu contraseña se ha enviado a tu correo.' +# TODO: Missing tags %count recovery_code_incorrect: '¡El código de recuperación no es correcto! Usa "/email recovery [email]" para generar uno nuevo' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&fTu cuenta no está activada aún, ¡revisa tu correo!' usage_unreg: '&cUso: /unregister contraseña' pwd_changed: '&c¡Contraseña cambiada!' diff --git a/src/main/resources/messages/messages_eu.yml b/src/main/resources/messages/messages_eu.yml index 05473e68a..648d7f209 100644 --- a/src/main/resources/messages/messages_eu.yml +++ b/src/main/resources/messages/messages_eu.yml @@ -45,7 +45,8 @@ unregistered: '&cZure erregistroa ezabatu duzu!' # TODO accounts_owned_other: 'The player %name has %count accounts:' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use "/email recovery [email]" to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&fZure kontua aktibatu gabe dago, konfirmatu zure emaila!' usage_unreg: '&cErabili: /unregister password' pwd_changed: '&cPasahitza aldatu duzu!' diff --git a/src/main/resources/messages/messages_fi.yml b/src/main/resources/messages/messages_fi.yml index a48ac74db..e3a44396e 100644 --- a/src/main/resources/messages/messages_fi.yml +++ b/src/main/resources/messages/messages_fi.yml @@ -45,7 +45,8 @@ unregistered: '&cPelaajatili poistettu onnistuneesti!' # TODO accounts_owned_other: 'The player %name has %count accounts:' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use "/email recovery [email]" to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&fKäyttäjäsi ei ole vahvistettu!' usage_unreg: '&cKäyttötapa: /unregister password' pwd_changed: '&cSalasana vaihdettu!!' diff --git a/src/main/resources/messages/messages_fr.yml b/src/main/resources/messages/messages_fr.yml index 9070db19d..9a5b6dcad 100644 --- a/src/main/resources/messages/messages_fr.yml +++ b/src/main/resources/messages/messages_fr.yml @@ -49,7 +49,9 @@ accounts_owned_self: 'Vous avez %count comptes:' accounts_owned_other: 'Le joueur %name a %count comptes:' two_factor_create: '&aVotre code secret est &2%code&a. Vous pouvez le scanner depuis &2%url' recovery_code_sent: 'Un code de récupération a été envoyé à votre adresse email afin de réinitialiser votre mot de passe.' +# TODO: Missing tags %count recovery_code_incorrect: '&cLe code de réinitialisation est incorrect!%nl%Faites "/email recovery [email]" pour en générer un nouveau.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&fCe compte n''est pas actif, consultez vos emails !' usage_unreg: '&cPour supprimer votre compte, utilisez "/unregister "' pwd_changed: '&aMot de passe changé avec succès !' diff --git a/src/main/resources/messages/messages_gl.yml b/src/main/resources/messages/messages_gl.yml index 1c0570306..da7270bc0 100644 --- a/src/main/resources/messages/messages_gl.yml +++ b/src/main/resources/messages/messages_gl.yml @@ -45,7 +45,8 @@ unregistered: '&cFeito! Xa non estás rexistrado!' # TODO accounts_owned_other: 'The player %name has %count accounts:' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use "/email recovery [email]" to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&fA túa conta aínda non está activada, comproba a túa bandexa de correo!!' usage_unreg: '&cUso: /unregister ' pwd_changed: '&cCambiouse o contrasinal!' diff --git a/src/main/resources/messages/messages_hu.yml b/src/main/resources/messages/messages_hu.yml index dc62f8c18..a77dfd6c2 100644 --- a/src/main/resources/messages/messages_hu.yml +++ b/src/main/resources/messages/messages_hu.yml @@ -44,7 +44,9 @@ accounts_owned_self: '%count db regisztrációd van:' accounts_owned_other: 'A %name nevű játékosnak, %count db regisztrációja van:' two_factor_create: '&2A te titkos kódod a következő: %code. Vagy skenneld be a következő oldalról: %url' recovery_code_sent: 'A jelszavad visszaállításához szükséges kódot sikeresen kiküldtük az email címedre!' +# TODO: Missing tags %count recovery_code_incorrect: 'A visszaállító kód helytelen volt! Használd a következő parancsot: /email recovery [email címed] egy új generálásához' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&cA felhasználód aktiválása még nem történt meg, ellenőrizd a megadott emailed!' usage_unreg: '&cHasználat: "/unregister "' pwd_changed: '&cJelszó sikeresen megváltoztatva!' diff --git a/src/main/resources/messages/messages_id.yml b/src/main/resources/messages/messages_id.yml index 90db87bd8..c5c9caf67 100644 --- a/src/main/resources/messages/messages_id.yml +++ b/src/main/resources/messages/messages_id.yml @@ -45,7 +45,8 @@ unregistered: '&cUnregister berhasil!' # TODO accounts_owned_other: 'The player %name has %count accounts:' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use "/email recovery [email]" to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&cAkunmu belum diaktifkan, silahkan periksa email kamu!' # TODO usage_unreg: '&cUsage: /unregister ' pwd_changed: '&2Berhasil mengubah password!' diff --git a/src/main/resources/messages/messages_it.yml b/src/main/resources/messages/messages_it.yml index 989b555ba..246fa3bce 100644 --- a/src/main/resources/messages/messages_it.yml +++ b/src/main/resources/messages/messages_it.yml @@ -46,7 +46,9 @@ accounts_owned_self: 'Possiedi %count account:' accounts_owned_other: 'Il giocatore %name possiede %count account:' two_factor_create: '&2Il tuo codice segreto è: &f%code%%nl%&2Puoi anche scannerizzare il codice QR da qui: &f%url' recovery_code_sent: 'Una email contenente il codice di recupero per reimpostare la tua password è stata appena inviata al tuo indirizzo email.' +# TODO: Missing tags %count recovery_code_incorrect: 'Il codice di recupero inserito non è corretto! Scrivi "/email recovery " per generarne uno nuovo' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&cIl tuo account non è stato ancora verificato, controlla fra le tue email per scoprire come attivarlo!' usage_unreg: '&cUtilizzo: /unregister ' pwd_changed: '&2Password cambiata correttamente!' diff --git a/src/main/resources/messages/messages_ko.yml b/src/main/resources/messages/messages_ko.yml index a65d93526..cfb6fcb57 100644 --- a/src/main/resources/messages/messages_ko.yml +++ b/src/main/resources/messages/messages_ko.yml @@ -49,7 +49,8 @@ unregistered: '&c성공적으로 탈퇴했습니다!' # TODO accounts_owned_other: 'The player %name has %count accounts:' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use "/email recovery [email]" to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&f당신의 계정은 아직 활성화되어있지 않습니다, 당신의 이메일을 확인해보세요!' usage_unreg: '&c사용법: /unregister 비밀번호' pwd_changed: '&c비밀번호를 변경했습니다!' diff --git a/src/main/resources/messages/messages_lt.yml b/src/main/resources/messages/messages_lt.yml index d75dec4a1..8c144a808 100644 --- a/src/main/resources/messages/messages_lt.yml +++ b/src/main/resources/messages/messages_lt.yml @@ -45,7 +45,8 @@ unregistered: '&aSekmingai issiregistravote!' # TODO accounts_owned_other: 'The player %name has %count accounts:' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use "/email recovery [email]" to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&aJusu vartotojas nera patvirtintas, patikrinkite el.pasta.' usage_unreg: '&ePanaikinti registracija: "/unregister slaptazodis"' pwd_changed: '&aSlaptazodis pakeistas' diff --git a/src/main/resources/messages/messages_nl.yml b/src/main/resources/messages/messages_nl.yml index 630fadd6e..8ebff6db5 100644 --- a/src/main/resources/messages/messages_nl.yml +++ b/src/main/resources/messages/messages_nl.yml @@ -44,7 +44,9 @@ accounts_owned_self: 'Je bezit %count accounts:' accounts_owned_other: 'De speler %name heeft %count accounts:' two_factor_create: '&2Je geheime code is %code. Je kunt hem scannen op %url' recovery_code_sent: 'Een herstelcode voor je wachtwoord is naar je mailbox gestuurd.' +# TODO: Missing tags %count recovery_code_incorrect: 'De herstelcode is niet correct! Gebruik "/email recovery [email]" om een nieuwe te krijgen' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: 'Je account is nog niet geactiveerd, controleer je mailbox!' usage_unreg: '&cGebruik: /unregister password' pwd_changed: '&cWachtwoord succesvol aangepast!' diff --git a/src/main/resources/messages/messages_pl.yml b/src/main/resources/messages/messages_pl.yml index 35622e5d4..4600c6585 100644 --- a/src/main/resources/messages/messages_pl.yml +++ b/src/main/resources/messages/messages_pl.yml @@ -45,7 +45,9 @@ accounts_owned_self: 'Posiadasz %count kont:' accounts_owned_other: 'Gracz %name posiada %count kont:' two_factor_create: '&2Twoj sekretny kod to %code. Mozesz zeskanowac go tutaj %url' recovery_code_sent: 'Kod odzyskiwania hasla zostal wyslany na adres email przypisany do konta.' +# TODO: Missing tags %count recovery_code_incorrect: 'Kod odzyskiwania hasla jest bledny! Uzyj /email recovery [email] aby wygenerowac nowy.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&fTwoje konto nie zostalo aktywowane! Sprawdz maila.' usage_unreg: '&cUzycie: /unregister haslo' pwd_changed: '&fHaslo zostalo zmienione!' diff --git a/src/main/resources/messages/messages_pt.yml b/src/main/resources/messages/messages_pt.yml index fef35f74f..f4af6e811 100644 --- a/src/main/resources/messages/messages_pt.yml +++ b/src/main/resources/messages/messages_pt.yml @@ -45,7 +45,8 @@ unregistered: '&cRegisto eliminado com sucesso!' # TODO accounts_owned_other: 'The player %name has %count accounts:' two_factor_create: '&2O seu código secreto é o %code. Você pode verificá-lo a partir daqui %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use "/email recovery [email]" to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&fA sua conta não foi ainda activada, verifique o seu email onde irá receber indicações para activação de conta. ' usage_unreg: '&cUse: /unregister password' pwd_changed: '&cPassword alterada!' diff --git a/src/main/resources/messages/messages_ro.yml b/src/main/resources/messages/messages_ro.yml index 703b93f59..6df8a7c18 100644 --- a/src/main/resources/messages/messages_ro.yml +++ b/src/main/resources/messages/messages_ro.yml @@ -44,7 +44,9 @@ accounts_owned_self: 'Detii %count conturi:' accounts_owned_other: 'Jucatorul %name are %count conturi:' two_factor_create: '&2Codul tau secret este %code. Il poti scana de aici %url' recovery_code_sent: 'Un cod de recuperare a parolei a fost trimis catre email-ul tau.' +# TODO: Missing tags %count recovery_code_incorrect: 'Codul de recuperare nu este corect! Foloseste /email recovery [email] pentru a genera unul nou.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&cContul tau nu este activat, te rugam verifica-ti email-ul!' usage_unreg: '&cFoloseste comanda: /unregister ' pwd_changed: '&2Parola a fost inregistrata cu succes!' diff --git a/src/main/resources/messages/messages_ru.yml b/src/main/resources/messages/messages_ru.yml index ed4e1729f..17c2170cc 100644 --- a/src/main/resources/messages/messages_ru.yml +++ b/src/main/resources/messages/messages_ru.yml @@ -44,7 +44,9 @@ accounts_owned_self: 'Вы являетесь владельцем %count акк accounts_owned_other: 'Игрок %name имеет %count аккаунтов:' two_factor_create: '&2Ваш секретный код %code. Вы должны просканировать его здесь %url' recovery_code_sent: 'Код восстановления для сброса пароля был отправлен на вашу электронную почту.' +# TODO: Missing tags %count recovery_code_incorrect: 'Код восстановления неверный! Введите /email recovery <Ваш Email>, чтобы отправить новый код' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&6Ваш аккаунт еще не активирован! Проверьте вашу почту!' usage_unreg: '&cИспользование: &e/unregister <Пароль>' pwd_changed: '&2Пароль изменен!' diff --git a/src/main/resources/messages/messages_sk.yml b/src/main/resources/messages/messages_sk.yml index b49f8cf5c..901741e27 100644 --- a/src/main/resources/messages/messages_sk.yml +++ b/src/main/resources/messages/messages_sk.yml @@ -49,7 +49,8 @@ unregistered: '&cUcet bol vymazany!' # TODO accounts_owned_other: 'The player %name has %count accounts:' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use "/email recovery [email]" to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&fUcet nie je aktivny. Prezri si svoj e-mail!' usage_unreg: '&cPríkaz: /unregister heslo' pwd_changed: '&cHeslo zmenené!' diff --git a/src/main/resources/messages/messages_tr.yml b/src/main/resources/messages/messages_tr.yml index 3431e99e6..9bb62f1f0 100644 --- a/src/main/resources/messages/messages_tr.yml +++ b/src/main/resources/messages/messages_tr.yml @@ -44,7 +44,9 @@ accounts_owned_self: 'Sen %count hesaba sahipsin:' accounts_owned_other: 'Oyuncu %name %count hesaba sahip:' two_factor_create: '&2Gizli kodunuz %code. Buradan test edebilirsin, %url' recovery_code_sent: 'Sifre sifirlama kodu eposta adresinize gonderildi.' +# TODO: Missing tags %count recovery_code_incorrect: 'Kod dogru degil! Kullanim "/email recovery [eposta]" ile yeni bir kod olustur' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&cHeabiniz henuz aktif edilmemis, e-postanizi kontrol edin!' usage_unreg: '&cKullanim: /unregister ' pwd_changed: '&2Sifre basariyla degistirildi!' diff --git a/src/main/resources/messages/messages_uk.yml b/src/main/resources/messages/messages_uk.yml index 35dbbffee..dc63d3167 100644 --- a/src/main/resources/messages/messages_uk.yml +++ b/src/main/resources/messages/messages_uk.yml @@ -44,7 +44,8 @@ accounts_owned_self: 'Кількість ваших твінк‒акаунті accounts_owned_other: 'Кількість твінк‒акаунтів гравця %name: %count' two_factor_create: '&2Ваш секретний код — %code %nl%&2Можете зкопіювати його за цим посиланням — %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use "/email recovery [email]" to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&cВаш акаунт ще не активовано. Будь ласка, провірте свою електронну пошту!' usage_unreg: '&cСинтаксис: /unregister <пароль>' pwd_changed: '&2Пароль успішно змінено!' diff --git a/src/main/resources/messages/messages_vn.yml b/src/main/resources/messages/messages_vn.yml index 7f68be063..b51f4ced5 100644 --- a/src/main/resources/messages/messages_vn.yml +++ b/src/main/resources/messages/messages_vn.yml @@ -44,7 +44,9 @@ accounts_owned_self: 'Bạn sở hữu %count tài khoản:' accounts_owned_other: 'Người chơi %name có %count tài khoản:' two_factor_create: '&2Mã bí mật của bạn là %code. Bạn có thể quét nó tại đây %url' recovery_code_sent: 'Một mã khôi phục mật khẩu đã được gửi đến địa chỉ email của bạn.' +# TODO: Missing tags %count recovery_code_incorrect: 'Mã khôi phục không đúng! Dùng lệnh /email recovery [email] để tạo một mã mới' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&cTài khoản của bạn chưa được kích hoạt, vui lòng kiểm tra email!' usage_unreg: '&cSử dụng: /unregister ' pwd_changed: '&2Thay đổi mật khẩu thành công!' diff --git a/src/main/resources/messages/messages_zhcn.yml b/src/main/resources/messages/messages_zhcn.yml index 63922334d..0c0e8d0ab 100644 --- a/src/main/resources/messages/messages_zhcn.yml +++ b/src/main/resources/messages/messages_zhcn.yml @@ -45,7 +45,9 @@ accounts_owned_self: '您拥有 %count 个账户:' accounts_owned_other: '玩家 %name 拥有 %count 个账户:' two_factor_create: '&8[&6玩家系统&8] &2你的代码是 %code,你可以使用 %url 来扫描' recovery_code_sent: '一个用于重置您的密码的验证码已发到您的邮箱' +# TODO: Missing tags %count recovery_code_incorrect: '验证码不正确! 使用 /email recovery [email] 以生成新的验证码' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&8[&6玩家系统&8] &f你的帐号还未激活,请查看你的邮箱!' usage_unreg: '&8[&6玩家系统&8] &c正确用法:“/unregister <密码>”' pwd_changed: '&8[&6玩家系统&8] &c密码已成功修改!' diff --git a/src/main/resources/messages/messages_zhhk.yml b/src/main/resources/messages/messages_zhhk.yml index 896003d4f..7a3335a2e 100644 --- a/src/main/resources/messages/messages_zhhk.yml +++ b/src/main/resources/messages/messages_zhhk.yml @@ -49,7 +49,8 @@ unregistered: '&8[&6用戶系統&8] &c你已成功刪除會員註冊記錄。' # TODO accounts_owned_other: 'The player %name has %count accounts:' two_factor_create: '&8[&6用戶系統 - 兩步驗證碼&8] &b你的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use "/email recovery [email]" to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&8[&6用戶系統&8] &f你的帳戶還沒有經過電郵驗證 !' usage_unreg: '&8[&6用戶系統&8] &f用法: 《 /unregister <密碼> 》' pwd_changed: '&8[&6用戶系統&8] &c你成功更換了你的密碼 !' diff --git a/src/main/resources/messages/messages_zhmc.yml b/src/main/resources/messages/messages_zhmc.yml index 92f9113ea..3eeee554a 100644 --- a/src/main/resources/messages/messages_zhmc.yml +++ b/src/main/resources/messages/messages_zhmc.yml @@ -44,7 +44,9 @@ accounts_owned_self: '您擁有 %count 個帳戶:' accounts_owned_other: '玩家 %name 擁有 %count 個帳戶:' two_factor_create: '&2您的密碼是 %code。您可以從這裡掃描 %url' recovery_code_sent: '已將重設密碼的恢復代碼發送到您的電子郵件。' +# TODO: Missing tags %count recovery_code_incorrect: '恢復代碼錯誤!使用指令: "/email recovery [電郵地址]" 生成新的一個恢復代碼。' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&c你的帳戶未激活,請確認電郵!' usage_unreg: '&c使用方法: "/unregister <你的密碼>"' pwd_changed: '&2密碼已更變!' diff --git a/src/main/resources/messages/messages_zhtw.yml b/src/main/resources/messages/messages_zhtw.yml index b2cff0384..e1129db75 100644 --- a/src/main/resources/messages/messages_zhtw.yml +++ b/src/main/resources/messages/messages_zhtw.yml @@ -49,7 +49,8 @@ unregistered: '&b【AuthMe】&6你已經成功取消註冊。' # TODO accounts_owned_other: 'The player %name has %count accounts:' two_factor_create: '&b【AuthMe - 兩步驗證碼】&b你的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use "/email recovery [email]" to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' vb_nonActiv: '&b【AuthMe】&6你的帳號還沒有經過驗證! 檢查看看你的電子信箱 (Email) 吧!' usage_unreg: '&b【AuthMe】&6用法: &c"/unregister <密碼>"' pwd_changed: '&b【AuthMe】&6密碼變更成功!' diff --git a/src/test/java/fr/xephi/authme/command/executable/email/ProcessCodeCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/email/ProcessCodeCommandTest.java new file mode 100644 index 000000000..076839ee7 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/email/ProcessCodeCommandTest.java @@ -0,0 +1,126 @@ +package fr.xephi.authme.command.executable.email; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.PasswordRecoveryService; +import fr.xephi.authme.service.RecoveryCodeService; +import org.bukkit.entity.Player; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Tests for {@link ProcessCodeCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class ProcessCodeCommandTest { + + @InjectMocks + private ProcessCodeCommand command; + + @Mock + private CommonService commonService; + + @Mock + private DataSource dataSource; + + @Mock + private RecoveryCodeService codeService; + + @Mock + private PasswordRecoveryService recoveryService; + + private static final String DEFAULT_EMAIL = "your@email.com"; + + @Test + public void shouldSendErrorForInvalidRecoveryCode() { + // given + String name = "Vultur3"; + Player sender = mock(Player.class); + given(sender.getName()).willReturn(name); + given(codeService.hasTriesLeft(name)).willReturn(true); + given(codeService.isCodeValid(name, "bogus")).willReturn(false); + given(codeService.getTriesLeft(name)).willReturn(2); + + // when + command.executeCommand(sender, Collections.singletonList("bogus")); + + // then + verify(commonService).send(sender, MessageKey.INCORRECT_RECOVERY_CODE, "2"); + verifyNoMoreInteractions(recoveryService); + } + + @Test + public void shouldSendErrorForNoMoreTries() { + // given + String name = "BobbY"; + Player sender = mock(Player.class); + given(sender.getName()).willReturn(name); + given(codeService.hasTriesLeft(name)).willReturn(false); + + // when + command.executeCommand(sender, Collections.singletonList("bogus")); + + // then + verify(commonService).send(sender, MessageKey.RECOVERY_TRIES_EXCEEDED); + verify(codeService).removeCode(name); + verifyNoMoreInteractions(recoveryService); + } + + @Test + public void shouldHandleDefaultEmail() { + // given + String name = "Tract0r"; + Player sender = mock(Player.class); + given(sender.getName()).willReturn(name); + given(dataSource.getAuth(name)).willReturn(newAuthWithEmail(DEFAULT_EMAIL)); + given(codeService.hasTriesLeft(name)).willReturn(true); + given(codeService.isCodeValid(name, "actual")).willReturn(true); + + // when + command.executeCommand(sender, Collections.singletonList("actual")); + + // then + verify(dataSource).getAuth(name); + verifyNoMoreInteractions(dataSource); + verify(commonService).send(sender, MessageKey.INVALID_EMAIL); + } + + @Test + public void shouldGenerateAndSendPassword() { + // given + String name = "GenericName"; + Player sender = mock(Player.class); + given(sender.getName()).willReturn(name); + String email = "ran-out@example.com"; + PlayerAuth auth = newAuthWithEmail(email); + given(dataSource.getAuth(name)).willReturn(auth); + given(codeService.hasTriesLeft(name)).willReturn(true); + given(codeService.isCodeValid(name, "actual")).willReturn(true); + + // when + command.executeCommand(sender, Collections.singletonList("actual")); + + // then + verify(recoveryService).generateAndSendNewPassword(sender, email); + verify(codeService).removeCode(name); + } + + private static PlayerAuth newAuthWithEmail(String email) { + return PlayerAuth.builder() + .name("name") + .email(email) + .build(); + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java index 5b1020780..8b026fc52 100644 --- a/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java @@ -9,38 +9,22 @@ import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.mail.EmailService; import fr.xephi.authme.message.MessageKey; -import fr.xephi.authme.message.Messages; import fr.xephi.authme.security.PasswordSecurity; -import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.PasswordRecoveryService; import fr.xephi.authme.service.RecoveryCodeService; -import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.settings.properties.SecuritySettings; -import fr.xephi.authme.util.expiring.Duration; import org.bukkit.entity.Player; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.Mockito; -import java.util.Arrays; import java.util.Collections; -import java.util.concurrent.TimeUnit; -import static fr.xephi.authme.AuthMeMatchers.stringWithLength; -import static org.hamcrest.Matchers.both; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.lessThan; -import static org.junit.Assert.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.only; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; @@ -70,13 +54,13 @@ public class RecoverEmailCommandTest { @Mock private EmailService emailService; + + @Mock + private PasswordRecoveryService recoveryService; @Mock private RecoveryCodeService recoveryCodeService; - @Mock - private Messages messages; - @BeforeClass public static void initLogger() { TestHelper.setupLogger(); @@ -200,85 +184,21 @@ public class RecoverEmailCommandTest { // then verify(emailService).hasAllInformation(); verify(dataSource).getAuth(name); - verify(recoveryCodeService).generateCode(name); - verify(commonService).send(sender, MessageKey.RECOVERY_CODE_SENT); - verify(emailService).sendRecoveryCode(name, email, code); - } - - @Test - public void shouldSendErrorForInvalidRecoveryCode() { - // given - String name = "Vultur3"; - Player sender = mock(Player.class); - given(sender.getName()).willReturn(name); - given(emailService.hasAllInformation()).willReturn(true); - given(playerCache.isAuthenticated(name)).willReturn(false); - String email = "vulture@example.com"; - PlayerAuth auth = newAuthWithEmail(email); - given(dataSource.getAuth(name)).willReturn(auth); - given(recoveryCodeService.isRecoveryCodeNeeded()).willReturn(true); - given(recoveryCodeService.isCodeValid(name, "bogus")).willReturn(false); - - // when - command.executeCommand(sender, Arrays.asList(email, "bogus")); - - // then - verify(emailService).hasAllInformation(); - verify(dataSource, only()).getAuth(name); - verify(commonService).send(sender, MessageKey.INCORRECT_RECOVERY_CODE); - verifyNoMoreInteractions(emailService); - } - - @Test - public void shouldResetPasswordAndSendEmail() { - // given - String name = "Vultur3"; - Player sender = mock(Player.class); - given(sender.getName()).willReturn(name); - given(emailService.hasAllInformation()).willReturn(true); - given(emailService.sendPasswordMail(anyString(), anyString(), anyString())).willReturn(true); - given(playerCache.isAuthenticated(name)).willReturn(false); - String email = "vulture@example.com"; - String code = "A6EF3AC8"; - PlayerAuth auth = newAuthWithEmail(email); - given(dataSource.getAuth(name)).willReturn(auth); - given(commonService.getProperty(EmailSettings.RECOVERY_PASSWORD_LENGTH)).willReturn(20); - given(passwordSecurity.computeHash(anyString(), eq(name))) - .willAnswer(invocation -> new HashedPassword(invocation.getArgument(0))); - given(recoveryCodeService.isRecoveryCodeNeeded()).willReturn(true); - given(recoveryCodeService.isCodeValid(name, code)).willReturn(true); - - // when - command.executeCommand(sender, Arrays.asList(email, code)); - - // then - verify(emailService).hasAllInformation(); - verify(dataSource).getAuth(name); - ArgumentCaptor passwordCaptor = ArgumentCaptor.forClass(String.class); - verify(passwordSecurity).computeHash(passwordCaptor.capture(), eq(name)); - String generatedPassword = passwordCaptor.getValue(); - assertThat(generatedPassword, stringWithLength(20)); - verify(dataSource).updatePassword(eq(name), any(HashedPassword.class)); - verify(recoveryCodeService).removeCode(name); - verify(emailService).sendPasswordMail(name, email, generatedPassword); - verify(commonService).send(sender, MessageKey.RECOVERY_EMAIL_SENT_MESSAGE); + verify(recoveryService).createAndSendRecoveryCode(sender, email); } @Test public void shouldGenerateNewPasswordWithoutRecoveryCode() { // given - String name = "sh4rK"; + String name = "Vultur3"; Player sender = mock(Player.class); given(sender.getName()).willReturn(name); given(emailService.hasAllInformation()).willReturn(true); given(emailService.sendPasswordMail(anyString(), anyString(), anyString())).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); - String email = "shark@example.org"; + String email = "vulture@example.com"; PlayerAuth auth = newAuthWithEmail(email); given(dataSource.getAuth(name)).willReturn(auth); - given(commonService.getProperty(EmailSettings.RECOVERY_PASSWORD_LENGTH)).willReturn(20); - given(passwordSecurity.computeHash(anyString(), eq(name))) - .willAnswer(invocation -> new HashedPassword(invocation.getArgument(0))); given(recoveryCodeService.isRecoveryCodeNeeded()).willReturn(false); // when @@ -287,49 +207,9 @@ public class RecoverEmailCommandTest { // then verify(emailService).hasAllInformation(); verify(dataSource).getAuth(name); - ArgumentCaptor passwordCaptor = ArgumentCaptor.forClass(String.class); - verify(passwordSecurity).computeHash(passwordCaptor.capture(), eq(name)); - String generatedPassword = passwordCaptor.getValue(); - assertThat(generatedPassword, stringWithLength(20)); - verify(dataSource).updatePassword(eq(name), any(HashedPassword.class)); - verify(emailService).sendPasswordMail(name, email, generatedPassword); - verify(commonService).send(sender, MessageKey.RECOVERY_EMAIL_SENT_MESSAGE); + verify(recoveryService).generateAndSendNewPassword(sender, email); } - @Test - public void shouldNotSendEmailIfCooldownCheckFails() { - // given - String name = "feverRay"; - Player sender = mock(Player.class); - given(sender.getName()).willReturn(name); - given(emailService.hasAllInformation()).willReturn(true); - given(emailService.sendRecoveryCode(anyString(), anyString(), anyString())).willReturn(true); - given(playerCache.isAuthenticated(name)).willReturn(false); - String email = "mymail@example.org"; - PlayerAuth auth = newAuthWithEmail(email); - given(dataSource.getAuth(name)).willReturn(auth); - given(recoveryCodeService.isRecoveryCodeNeeded()).willReturn(true); - given(recoveryCodeService.generateCode(anyString())).willReturn("Code"); - // Trigger sending of recovery code - command.executeCommand(sender, Collections.singletonList(email)); - - Mockito.reset(emailService, commonService); - given(emailService.hasAllInformation()).willReturn(true); - given(messages.formatDuration(any(Duration.class))).willReturn("8 minutes"); - - // when - command.executeCommand(sender, Collections.singletonList(email)); - - // then - verify(emailService, only()).hasAllInformation(); - ArgumentCaptor durationCaptor = ArgumentCaptor.forClass(Duration.class); - verify(messages).formatDuration(durationCaptor.capture()); - assertThat(durationCaptor.getValue().getDuration(), both(lessThan(41L)).and(greaterThan(36L))); - assertThat(durationCaptor.getValue().getTimeUnit(), equalTo(TimeUnit.SECONDS)); - verify(messages).send(sender, MessageKey.EMAIL_COOLDOWN_ERROR, "8 minutes"); - } - - private static PlayerAuth newAuthWithEmail(String email) { return PlayerAuth.builder() .name("name") diff --git a/src/test/java/fr/xephi/authme/service/PasswordRecoveryServiceTest.java b/src/test/java/fr/xephi/authme/service/PasswordRecoveryServiceTest.java new file mode 100644 index 000000000..c7c1419d1 --- /dev/null +++ b/src/test/java/fr/xephi/authme/service/PasswordRecoveryServiceTest.java @@ -0,0 +1,54 @@ +package fr.xephi.authme.service; + +import ch.jalu.injector.testing.BeforeInjecting; +import ch.jalu.injector.testing.InjectDelayed; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.mail.EmailService; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.settings.properties.SecuritySettings; +import org.junit.Ignore; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.BDDMockito.given; + +/** + * Tests for {@link PasswordRecoveryService}. + */ +@Ignore +@RunWith(MockitoJUnitRunner.class) +public class PasswordRecoveryServiceTest { + + @InjectDelayed + private PasswordRecoveryService recoveryService; + + @Mock + private CommonService commonService; + + @Mock + private RecoveryCodeService codeService; + + @Mock + private DataSource dataSource; + + @Mock + private EmailService emailService; + + @Mock + private PasswordSecurity passwordSecurity; + + @Mock + private RecoveryCodeService recoveryCodeService; + + @Mock + private Messages messages; + + @BeforeInjecting + public void initSettings() { + given(commonService.getProperty(SecuritySettings.EMAIL_RECOVERY_COOLDOWN_SECONDS)).willReturn(40); + } + + //TODO: Write tests +} diff --git a/src/test/java/fr/xephi/authme/service/RecoveryCodeServiceTest.java b/src/test/java/fr/xephi/authme/service/RecoveryCodeServiceTest.java index eb8af0dc3..acd26d933 100644 --- a/src/test/java/fr/xephi/authme/service/RecoveryCodeServiceTest.java +++ b/src/test/java/fr/xephi/authme/service/RecoveryCodeServiceTest.java @@ -33,6 +33,7 @@ public class RecoveryCodeServiceTest { public void initSettings() { given(settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID)).willReturn(4); given(settings.getProperty(SecuritySettings.RECOVERY_CODE_LENGTH)).willReturn(5); + given(settings.getProperty(SecuritySettings.RECOVERY_CODE_MAX_TRIES)).willReturn(3); } @Test @@ -62,6 +63,35 @@ public class RecoveryCodeServiceTest { assertThat(code, stringWithLength(5)); } + @Test + public void playerHasTriesLeft() { + // given + String player = "Dusty"; + recoveryCodeService.generateCode(player); + + // when + boolean result = recoveryCodeService.hasTriesLeft(player); + + // then + assertThat(result, equalTo(true)); + } + + @Test + public void playerHasNoTriesLeft() { + // given + String player = "Dusty"; + recoveryCodeService.generateCode(player); + recoveryCodeService.isCodeValid(player, "1st try"); + recoveryCodeService.isCodeValid(player, "2nd try"); + recoveryCodeService.isCodeValid(player, "3rd try"); + + // when + boolean result = recoveryCodeService.hasTriesLeft(player); + + // then + assertThat(result, equalTo(false)); + } + @Test public void shouldRecognizeCorrectCode() { // given @@ -87,10 +117,15 @@ public class RecoveryCodeServiceTest { // then assertThat(recoveryCodeService.isCodeValid(player, code), equalTo(false)); assertThat(getCodeMap().get(player), nullValue()); + assertThat(getTriesCounter().get(player), equalTo(0)); } private ExpiringMap getCodeMap() { return ReflectionTestUtils.getFieldValue(RecoveryCodeService.class, recoveryCodeService, "recoveryCodes"); } + + private ExpiringMap getTriesCounter() { + return ReflectionTestUtils.getFieldValue(RecoveryCodeService.class, recoveryCodeService, "playerTries"); + } }