diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/feature/FeatureType.java b/sormas-api/src/main/java/de/symeda/sormas/api/feature/FeatureType.java index 7cb2d730131..ff780a60b60 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/feature/FeatureType.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/feature/FeatureType.java @@ -332,7 +332,8 @@ public enum FeatureType { CASE_SURVEILANCE, CONTACT_TRACING }, null, - ImmutableMap.of(FeatureTypeProperty.S2S_SHARING, Boolean.FALSE)); + ImmutableMap.of(FeatureTypeProperty.S2S_SHARING, Boolean.FALSE)), + SELF_PASSWORD_RESET(true, false, null, null,null); public static final FeatureType[] SURVEILLANCE_FEATURE_TYPES = { FeatureType.CASE_SURVEILANCE, diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Captions.java b/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Captions.java index f83b4c374ed..92283557fd8 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Captions.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Captions.java @@ -974,6 +974,7 @@ public interface Captions { String Configuration_LineListing = "Configuration.LineListing"; String Configuration_Outbreaks = "Configuration.Outbreaks"; String Configuration_PointsOfEntry = "Configuration.PointsOfEntry"; + String confirmPassword = "confirmPassword"; String confirmChangesField = "confirmChangesField"; String confirmChangesValue = "confirmChangesValue"; String Contact = "Contact"; @@ -1174,6 +1175,7 @@ public interface Captions { String countryArchivedCountries = "countryArchivedCountries"; String createSymptomJournalAccountButton = "createSymptomJournalAccountButton"; String creationDate = "creationDate"; + String currentPassword = "currentPassword"; String CustomizableEnum_hasDetails = "CustomizableEnum.hasDetails"; String CustomizableEnum_hasDetails_short = "CustomizableEnum.hasDetails.short"; String CustomizableEnumValue_active = "CustomizableEnumValue.active"; @@ -2140,6 +2142,7 @@ public interface Captions { String outbreakNormal = "outbreakNormal"; String outbreakOutbreak = "outbreakOutbreak"; String passportNumber = "passportNumber"; + String passwordStrength = "passwordStrength"; String PathogenTest = "PathogenTest"; String PathogenTest_cqValue = "PathogenTest.cqValue"; String PathogenTest_ctValueE = "PathogenTest.ctValueE"; @@ -3082,6 +3085,7 @@ public interface Captions { String treatmentOpenPrescription = "treatmentOpenPrescription"; String unassigned = "unassigned"; String unknown = "unknown"; + String updatePassword = "updatePassword"; String User = "User"; String User_active = "User.active"; String User_address = "User.address"; @@ -3100,6 +3104,7 @@ public interface Captions { String User_userName = "User.userName"; String User_userRoles = "User.userRoles"; String User_uuid = "User.uuid"; + String userGeneratePassword = "userGeneratePassword"; String userMyUserId = "userMyUserId"; String userNewUser = "userNewUser"; String userResetPassword = "userResetPassword"; diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Strings.java b/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Strings.java index ae1a8129c87..9eb5cedd332 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Strings.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Strings.java @@ -194,6 +194,8 @@ public interface Strings { String confirmationVaccinationStatusUpdate = "confirmationVaccinationStatusUpdate"; String confirmExternalMessageCorrectionThrough = "confirmExternalMessageCorrectionThrough"; String confirmNetworkDiagramTooManyContacts = "confirmNetworkDiagramTooManyContacts"; + String confirmPassword = "confirmPassword"; + String currentPassword = "currentPassword"; String date = "date"; String day = "day"; String DefaultPassword_newPassword = "DefaultPassword.newPassword"; @@ -491,6 +493,7 @@ public interface Strings { String headingcasesWithReferenceDefinitionFulfilled = "headingcasesWithReferenceDefinitionFulfilled"; String headingCaution = "headingCaution"; String headingChangeCaseDisease = "headingChangeCaseDisease"; + String headingChangePassword = "headingChangePassword"; String headingChangePathogenTestResult = "headingChangePathogenTestResult"; String headingClinicalMeasurements = "headingClinicalMeasurements"; String headingClinicalVisitsDeleted = "headingClinicalVisitsDeleted"; @@ -886,6 +889,7 @@ public interface Strings { String headingUpdatedPersonInformation = "headingUpdatedPersonInformation"; String headingUpdatedSampleInformation = "headingUpdatedSampleInformation"; String headingUpdatePassword = "headingUpdatePassword"; + String headingUpdatePasswordFailed = "headingUpdatePasswordFailed"; String headingUpdatePersonContactDetails = "headingUpdatePersonContactDetails"; String headingUploadSuccess = "headingUploadSuccess"; String headingUserData = "headingUserData"; @@ -1453,6 +1457,8 @@ public interface Strings { String messageMissingDateFilter = "messageMissingDateFilter"; String messageMissingEpiWeekFilter = "messageMissingEpiWeekFilter"; String messageMultipleSampleReports = "messageMultipleSampleReports"; + String messageNewPasswordDoesNotMatchFailed = "messageNewPasswordDoesNotMatchFailed"; + String messageNewPasswordFailed = "messageNewPasswordFailed"; String messageNoCaseFound = "messageNoCaseFound"; String messageNoCaseFoundToLinkImmunization = "messageNoCaseFoundToLinkImmunization"; String messageNoCasesSelected = "messageNoCasesSelected"; @@ -1490,6 +1496,8 @@ public interface Strings { String messageNoVisitsSelected = "messageNoVisitsSelected"; String messageOtherDeleteReasonNotFilled = "messageOtherDeleteReasonNotFilled"; String messageOutbreakSaved = "messageOutbreakSaved"; + String messagePasswordChange = "messagePasswordChange"; + String messagePasswordFailed = "messagePasswordFailed"; String messagePasswordReset = "messagePasswordReset"; String messagePasswordResetEmailLink = "messagePasswordResetEmailLink"; String messagePathogenTestSaved = "messagePathogenTestSaved"; @@ -1609,6 +1617,7 @@ public interface Strings { String messageVisitsDeleted = "messageVisitsDeleted"; String messageVisitsWithWrongStatusNotCancelled = "messageVisitsWithWrongStatusNotCancelled"; String messageVisitsWithWrongStatusNotSetToLost = "messageVisitsWithWrongStatusNotSetToLost"; + String messageWrongCurrentPassword = "messageWrongCurrentPassword"; String messageWrongFileType = "messageWrongFileType"; String messageWrongTemplateFileType = "messageWrongTemplateFileType"; String min = "min"; @@ -1674,6 +1683,7 @@ public interface Strings { String of = "of"; String on = "on"; String or = "or"; + String passwordStrength = "passwordStrength"; String pathogenTestDeletedDuringLabMessageConversion = "pathogenTestDeletedDuringLabMessageConversion"; String pleaseSpecify = "pleaseSpecify"; String populationDataByArea = "populationDataByArea"; @@ -1849,6 +1859,7 @@ public interface Strings { String unsavedChanges_warningMessage = "unsavedChanges.warningMessage"; String unsavedChanges_warningTitle = "unsavedChanges.warningTitle"; String until = "until"; + String updatePassword = "updatePassword"; String uuidOf = "uuidOf"; String warning = "warning"; String warningDashboardMapTooManyMarkers = "warningDashboardMapTooManyMarkers"; diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/user/UserDto.java b/sormas-api/src/main/java/de/symeda/sormas/api/user/UserDto.java index 2691c7186cc..ab9462a6465 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/user/UserDto.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/user/UserDto.java @@ -69,9 +69,17 @@ public class UserDto extends EntityDto { public static final String HAS_CONSENTED_TO_GDPR = "hasConsentedToGdpr"; public static final String EXTERNAL_ID = "externalId"; public static final String JURISDICTION_LEVEL = "jurisdictionLevel"; + public static final String PASSWORD = "currentPassword"; + public static final String NEW_PASSWORD = "newPassword"; + public static final String CONFIRM_PASSWORD = "confirmPassword"; private boolean active = true; + private String currentPassword; + private String newPassword; + private String confirmPassword; + private String passwordStrength; + @Size(max = FieldConstraints.CHARACTER_LIMIT_DEFAULT, message = Validations.textTooLong) @AuditIncludeProperty private String userName; @@ -296,4 +304,37 @@ public JurisdictionLevel getJurisdictionLevel() { public void setJurisdictionLevel(JurisdictionLevel jurisdictionLevel) { this.jurisdictionLevel = jurisdictionLevel; } + + public void setCurrentPassword(String currentPassword) { + this.currentPassword = currentPassword; + } + + public String getCurrentPassword() { + return currentPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } + + public String getNewPassword() { + return newPassword; + } + + public void setConfirmPassword(String confirmPassword) { + this.confirmPassword = confirmPassword; + } + + public String getConfirmPassword() { + return confirmPassword; + } + + public void setPasswordStrength(String passwordStrength) { + this.passwordStrength = passwordStrength; + } + + public String getPasswordStrength() { + return passwordStrength; + } + } diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/user/UserFacade.java b/sormas-api/src/main/java/de/symeda/sormas/api/user/UserFacade.java index 8dd66e15369..85dc1c60d70 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/user/UserFacade.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/user/UserFacade.java @@ -52,12 +52,22 @@ public interface UserFacade { String resetPassword(String uuid); + String updateUserPassword(String uuid, String newPassword); + + boolean validateCurrentPassword(String password); + + String checkPasswordStrength(String password); + + boolean isPasswordWeak(String password); + + String generatePassword(); + List getAllAfter(Date date); UserDto getByUserName(String userName); /** - * + * * @param regionRef * reference of the region to be filtered for. When this region is null, it is not filtered in this regard. * NOTE: some users don't have a region (often users with NATIONAL_USER role, for example). They will @@ -79,7 +89,7 @@ public interface UserFacade { long count(UserCriteria userCriteria); /** - * + * * @param district * reference of the district to be filtered for. When this district is null, it is not filtered in this regard. * NOTE: some users don't have a district (often users with NATIONAL_USER role, for example). They will @@ -93,7 +103,7 @@ public interface UserFacade { List getUserRefsByDistrict(DistrictReferenceDto district, Disease limitedDisease, UserRight... userRights); /** - * + * * @param district * reference of the district to be filtered for. When this district is null, it is not filtered in this regard. * NOTE: some users don't have a district (often users with NATIONAL_USER role, for example). They will @@ -160,7 +170,7 @@ List getUserRefsByInfrastructure( /** * Retrieves the user rights of the user specified by the passed UUID, or those of the current user if no UUID is specified. * Requesting the user rights of another user without the rights to view users and user roles results in an AccessDeniedException. - * + * * @param userUuid * The UUID of the user to request the user rights for * @return A set containing the user rights associated to all user roles assigned to the user diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/user/UserPasswordChangeDto.java b/sormas-api/src/main/java/de/symeda/sormas/api/user/UserPasswordChangeDto.java new file mode 100644 index 00000000000..c907669d19b --- /dev/null +++ b/sormas-api/src/main/java/de/symeda/sormas/api/user/UserPasswordChangeDto.java @@ -0,0 +1,50 @@ +/* + * SORMAS® - Surveillance Outbreak Response Management & Analysis System + * Copyright © 2016-2023 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.symeda.sormas.api.user; + +import java.io.Serializable; + +public class UserPasswordChangeDto implements Serializable { + + private static final long serialVersionUID = 6269655187128160377L; + + private String uuid; + private String newPassword; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } + + public UserPasswordChangeDto(String uuid, String newPassword) { + this.uuid = uuid; + this.newPassword = newPassword; + } + + public UserPasswordChangeDto() { + } +} diff --git a/sormas-api/src/main/resources/captions.properties b/sormas-api/src/main/resources/captions.properties index c7bfc406ffc..088c7c1c170 100644 --- a/sormas-api/src/main/resources/captions.properties +++ b/sormas-api/src/main/resources/captions.properties @@ -2984,6 +2984,7 @@ TreatmentExport.caseName=Case name userNewUser=New user userMyUserId=My user ID userResetPassword=Create new password +userGeneratePassword=Generate new password userUpdatePasswordConfirmation=Really update password? sync=Sync syncUsers=Sync Users @@ -3009,6 +3010,11 @@ User.district=District User.community=Community User.externalId=External ID userRestrictDiseases=Restrict access to specific diseases +currentPassword=Enter Current Password +updatePassword=Enter new Password +confirmPassword=Confirm new Password +passwordStrength=Password strength + # Vaccination vaccinationNewVaccination=New vaccination vaccinationNoVaccinationsForPerson=There are no vaccinations for this person diff --git a/sormas-api/src/main/resources/strings.properties b/sormas-api/src/main/resources/strings.properties index 05833787bce..8b268811b93 100644 --- a/sormas-api/src/main/resources/strings.properties +++ b/sormas-api/src/main/resources/strings.properties @@ -631,6 +631,7 @@ headingLineListing = Line listing headingLineListingImport = Line listing import headingLocation = Location headingLoginFailed = Login failed +headingUpdatePasswordFailed = Update password failed headingMaternalHistory = Maternal history headingMedicalInformation = Additional medical information headingMissingDateFilter = Missing date filter @@ -662,6 +663,10 @@ headingOutbreakIn = Outbreak in headingPathogenTestsDeleted = Pathogen tests deleted headingPaperFormDates = Reception dates of paper form headingPersonData = Person data +currentPassword=Enter Current Password +updatePassword=Enter new Password +confirmPassword=Confirm new Password +passwordStrength=Password strength headingPersonInformation = Person information headingPersonOccupation = Occupation & education headingPickEventGroup = Pick event group @@ -714,6 +719,7 @@ headingReferCaseFromPointOfEntry = Refer case from point of entry headingTreatments = Executed treatments headingTreatmentsDeleted = Treatments deleted headingUpdatePassword = Update password +headingChangePassword = Change password headingUploadSuccess = Upload Successful headingUserData = User data headingVaccination = Vaccination @@ -1330,6 +1336,9 @@ messageIncompleteGpsCoordinates = GPS coordinates are incomplete messageIncorrectDateRange = Date from is after date to messageExternalMessagesAssigned = The assignee has been changed for all selected messages messageLoginFailed = Please check your username and password and try again +messageNewPasswordDoesNotMatchFailed = New password and Confirm password does not match +messagePasswordFailed = Incorrect Password Please check your Password and try again +messageNewPasswordFailed = Password should contain some text, \n some numbers \n and should be more than 8 characters messageMissingCases = Please generate some cases before generating contacts messageMissingDateFilter = Please fill in both date filter fields messageMissingEpiWeekFilter = Please fill in both epi week filter fields @@ -1359,6 +1368,7 @@ messageNoUsersSelected = You have not selected any users messageNoUserSelected = No user has been selected messageOutbreakSaved = Outbreak information saved messagePasswordReset = User's password was reset +messagePasswordChange = User's password was changed successfully messagePasswordResetEmailLink = A link to reset the password was sent to the user's email messagePathogenTestSaved = Pathogen test saved. The classification of its associated case was automatically changed to %s. messagePathogenTestSavedShort = Pathogen test saved @@ -1407,6 +1417,7 @@ messageVaccinationOutsideJurisdictionDeletionDenied = The vaccination outside us messageVisitsDeleted = All selected eligible visits have been deleted messageVisitsWithWrongStatusNotCancelled = Follow-up visits with CANCELLED or NO FOLLOW-UP status can not be cancelled messageVisitsWithWrongStatusNotSetToLost = Follow-up visits with NO FOLLOW-UP status can not be set to lost +messageWrongCurrentPassword = Wrong Current Password messageWrongFileType = Please provide a .csv file containing the data you want to import. It's recommended to use the import template file as a starting point. messageWrongTemplateFileType=For %s, please provide a .%s file. messageLineListingDisabled = Line listing has been disabled diff --git a/sormas-app/app/src/main/AndroidManifest.xml b/sormas-app/app/src/main/AndroidManifest.xml index 18d3de04c70..866ec00ebf0 100644 --- a/sormas-app/app/src/main/AndroidManifest.xml +++ b/sormas-app/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ + + + saveNewPassword(String uuid, String newPassword) throws NoConnectionException { + + return RetroProvider.getUserFacade().saveNewPassword(new UserPasswordChangeDto(uuid, newPassword)); + } + + public static Call generatePassword() throws NoConnectionException { + + return RetroProvider.getUserFacade().generatePassword(); + } + public static boolean isRestrictedToAssignEntities(User user) { if (user != null && !user.getUserRoles().isEmpty()) { return user.getUserRoles().stream().allMatch(UserRole::isRestrictAccessToAssignedEntities); diff --git a/sormas-app/app/src/main/java/de/symeda/sormas/app/login/ChangePasswordActivity.java b/sormas-app/app/src/main/java/de/symeda/sormas/app/login/ChangePasswordActivity.java new file mode 100644 index 00000000000..8685c33a182 --- /dev/null +++ b/sormas-app/app/src/main/java/de/symeda/sormas/app/login/ChangePasswordActivity.java @@ -0,0 +1,368 @@ +/* + * SORMAS® - Surveillance Outbreak Response Management & Analysis System + * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package de.symeda.sormas.app.login; + +import static de.symeda.sormas.app.backend.config.ConfigProvider.setNewPassword; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.core.app.ActivityCompat; +import androidx.databinding.DataBindingUtil; + +import de.symeda.sormas.api.utils.DataHelper; +import de.symeda.sormas.app.BaseLocalizedActivity; +import de.symeda.sormas.app.R; +import de.symeda.sormas.app.backend.config.ConfigProvider; +import de.symeda.sormas.app.backend.user.UserDtoHelper; +import de.symeda.sormas.app.core.NotificationContext; +import de.symeda.sormas.app.core.notification.NotificationHelper; +import de.symeda.sormas.app.core.notification.NotificationType; +import de.symeda.sormas.app.databinding.ActivityChangePasswordLayoutBinding; +import de.symeda.sormas.app.rest.RetroProvider; +import de.symeda.sormas.app.settings.SettingsActivity; +import de.symeda.sormas.app.util.NavigationHelper; +import de.symeda.sormas.app.util.SoftKeyboardHelper; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +@SuppressLint("ResourceType") +public class ChangePasswordActivity extends BaseLocalizedActivity implements ActivityCompat.OnRequestPermissionsResultCallback, NotificationContext { + + public static final String TAG = ChangePasswordActivity.class.getSimpleName(); + + private boolean isAtLeast8 = false; + private boolean hasUppercase = false; + private boolean hasNumber = false; + private boolean hasLowerCaseCharacter = false; + private boolean isGood = false; + private ActivityChangePasswordLayoutBinding binding; + private ProgressBar preloader; + private View fragmentFrame; + private boolean isPasswordGenerated = false; + + @RequiresApi(api = Build.VERSION_CODES.R) + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_change_password_layout); + + ChangePasswordViewModel changePasswordViewModel = new ChangePasswordViewModel(); + + binding = DataBindingUtil.setContentView(this, R.layout.activity_change_password_layout); + binding.setData(changePasswordViewModel); + + binding.changePasswordConfirmPassword.setLiveValidationDisabled(true); + binding.changePasswordCurrentPassword.setLiveValidationDisabled(true); + binding.changePasswordNewPassword.setLiveValidationDisabled(true); + + preloader = findViewById(R.id.preloader); + fragmentFrame = findViewById(R.id.fragment_frame); + + showPreloader(); + + } + + @Override + public void onPause() { + + super.onPause(); + SoftKeyboardHelper.hideKeyboard(this, binding.changePasswordCurrentPassword.getWindowToken()); + } + + @Override + protected void onDestroy() { + + hidePreloader(); + super.onDestroy(); + } + + @Override + protected void onResume() { + + super.onResume(); + } + + public void backToSettings(View view) { + + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + } + + @SuppressLint("ResourceType") + public void passwordValidationCheck(View view) { + + String newPassword = binding.changePasswordNewPassword.getValue(); + + isAtLeast8 = newPassword.length() >= 8; + hasUppercase = newPassword.matches("(.*[A-Z].*)"); + hasNumber = newPassword.matches("(.*[0-9].*)"); + hasLowerCaseCharacter = newPassword.matches(".*[a-z].*"); + if (isAtLeast8 && hasNumber && hasUppercase && hasLowerCaseCharacter) { + isGood = true; + binding.actionPasswordStrength.setVisibility(view.getVisibility()); + binding.actionPasswordStrength.setText(R.string.message_password_strong); + binding.actionPasswordStrength.setTextColor(Color.parseColor(getString(R.color.successBackground))); + } else { + binding.actionPasswordStrength.setVisibility(view.getVisibility()); + binding.actionPasswordStrength.setText(R.string.message_password_weak); + NotificationHelper.showNotification(binding, NotificationType.ERROR, R.string.additional_message_passord_weak); + binding.actionPasswordStrength.setTextColor(Color.parseColor(getString(R.color.errorBackground))); + } + } + + @SuppressLint("ResourceType") + public void generatePassword(View view) { + + if (DataHelper.isNullOrEmpty(ConfigProvider.getServerRestUrl())) { + NavigationHelper.goToSettings(this); + return; + } + try { + RetroProvider.connectAsyncHandled(this, true, true, result -> { + if (Boolean.TRUE.equals(result)) { + try { + executeGeneratePasswordCall(UserDtoHelper.generatePassword()); + } catch (Exception e) { + binding.actionPasswordStrength.setVisibility(view.getVisibility()); + binding.actionPasswordStrength.setText(e.toString()); + binding.actionPasswordStrength.setTextColor(Color.parseColor(getString(R.color.brightYellow))); + } + RetroProvider.disconnect(); + } + }); + } catch (Exception e) { + binding.actionPasswordStrength.setVisibility(view.getVisibility()); + binding.actionPasswordStrength.setText(e.toString()); + binding.actionPasswordStrength.setTextColor(Color.parseColor(getString(R.color.brightYellow))); + } + } + + @Override + public View getRootView() { + + if (binding != null) + return binding.getRoot(); + + return null; + } + + private void executeGeneratePasswordCall(Call call) { + + try { + call.enqueue(new Callback<>() { + + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.code() != 200) { + NotificationHelper.showNotification(binding, NotificationType.ERROR, getString(R.string.message_could_not_generate_password)); + } + var generatedPassword = response.body(); + binding.changePasswordNewPassword.setValue(generatedPassword); + binding.changePasswordConfirmPassword.setValue(generatedPassword); + isPasswordGenerated = true; + Toast.makeText(getApplicationContext(), getString(R.string.message_password_generated), Toast.LENGTH_SHORT).show(); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + NotificationHelper.showNotification(binding, NotificationType.ERROR, R.string.message_could_not_generate_password); + } + }); + } catch (Exception e) { + NotificationHelper.showNotification(binding, NotificationType.ERROR, R.string.message_could_not_generate_password); + } + } + + private void executeSaveNewPasswordCall(Call call, Activity activity) { + + try { + showPreloader(); + + call.enqueue(new Callback() { + + @Override + public void onResponse(Call call, Response response) { + hidePreloader(); + + if (response.code() != 200) { + NotificationHelper.showNotification(binding, NotificationType.ERROR, R.string.message_could_not_save_password); + } else { + String message; + String title; + title = getString(R.string.heading_change_password); + + if (isPasswordGenerated) { + String generatedPassword = binding.changePasswordNewPassword.getValue(); + message = generatedPassword; + alertDialog(activity, message, title); + } else { + message = getString(R.string.message_password_changed); + alertDialog(activity, message, title); + } + NotificationHelper.showNotification(binding, NotificationType.SUCCESS, R.string.message_password_changed); + } + + } + + @Override + public void onFailure(Call call, Throwable t) { + String message = getString(R.string.error_server_connection); + String title = getString(R.string.heading_change_password); + alertDialog(activity, message, title); + } + }); + } catch (Exception e) { + Log.d("Call", call.toString()); + } + + } + + public void alertDialog(Activity activity, String message, String title) { + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + LayoutInflater inflater = activity.getLayoutInflater(); + View dialogView = inflater.inflate(R.layout.dialog_change_password, null); + + TextView passwordTextView = dialogView.findViewById(R.id.passwordTextView); + + if (isPasswordGenerated) { + passwordTextView.setText(message); + } + + builder.setTitle(title); + + passwordTextView.setOnClickListener(v -> { + // Copy password to clipboard + ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(getString(R.string.heading_password), message); + clipboard.setPrimaryClip(clip); + Toast.makeText(activity, R.string.message_password_copied_to_clipbord, Toast.LENGTH_SHORT).show(); + }); + + builder.setView(dialogView); + builder.setCancelable(true); + builder.setPositiveButton(activity.getString(R.string.action_ok), new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + finish(); + } + }); + + AlertDialog dialog = builder.create(); + dialog.show(); + } + + /** + * When clicked on submit + **/ + public void savePassword(View view) { + + binding.changePasswordNewPassword.disableErrorState(); + binding.changePasswordCurrentPassword.disableErrorState(); + binding.changePasswordConfirmPassword.disableErrorState(); + + if (DataHelper.isNullOrEmpty(ConfigProvider.getServerRestUrl())) { + NavigationHelper.goToSettings(this); + return; + } + + String currentPassword = binding.changePasswordCurrentPassword.getValue(); + String newPassword = binding.changePasswordNewPassword.getValue(); + String confirmPassword = binding.changePasswordConfirmPassword.getValue(); + String configPassword = ConfigProvider.getPassword(); + + boolean isValid = true; + + if (currentPassword == null || currentPassword.trim().isEmpty()) { + binding.changePasswordCurrentPassword.enableErrorState(R.string.error_current_password_empty); + isValid = false; + } else if (newPassword == null || newPassword.trim().isEmpty()) { + binding.changePasswordNewPassword.enableErrorState(R.string.error_new_password_empty); + isValid = false; + } else if (confirmPassword == null || confirmPassword.trim().isEmpty()) { + binding.changePasswordConfirmPassword.enableErrorState(R.string.error_confirm_password_empty); + isValid = false; + } else if (!configPassword.equals(currentPassword)) { + binding.changePasswordCurrentPassword.enableErrorState(R.string.error_current_password_incorrect); + NotificationHelper.showNotification(binding, NotificationType.ERROR, R.string.error_current_password_incorrect); + isValid = false; + } else if (!newPassword.equals(confirmPassword)) { + binding.changePasswordConfirmPassword.enableErrorState(R.string.error_passwords_do_not_match); + NotificationHelper.showNotification(binding, NotificationType.ERROR, R.string.error_passwords_do_not_match); + isValid = false; + } + + if (isValid) { + passwordValidationCheck(view); + } + + if (isValid && isGood) { + RetroProvider.connectAsyncHandled(this, true, true, result -> { + if (Boolean.TRUE.equals(result)) { + try { + executeSaveNewPasswordCall(UserDtoHelper.saveNewPassword(ConfigProvider.getUser().getUuid(), newPassword), this); + setNewPassword(newPassword); + } catch (Exception e) { + binding.actionPasswordStrength.setVisibility(View.VISIBLE); + binding.actionPasswordStrength.setText(e.toString()); + binding.actionPasswordStrength.setTextColor(Color.parseColor(getString(R.color.brightYellow))); + } + RetroProvider.disconnect(); + } + }); + } + } + + public void showPreloader() { + + if (fragmentFrame != null) { + fragmentFrame.setVisibility(View.GONE); + } + if (preloader != null) { + preloader.setVisibility(View.VISIBLE); + } + } + + public void hidePreloader() { + + if (preloader != null) { + preloader.setVisibility(View.GONE); + } + if (fragmentFrame != null) { + fragmentFrame.setVisibility(View.VISIBLE); + } + } +} diff --git a/sormas-app/app/src/main/java/de/symeda/sormas/app/login/ChangePasswordViewModel.java b/sormas-app/app/src/main/java/de/symeda/sormas/app/login/ChangePasswordViewModel.java new file mode 100644 index 00000000000..a0c3307e79d --- /dev/null +++ b/sormas-app/app/src/main/java/de/symeda/sormas/app/login/ChangePasswordViewModel.java @@ -0,0 +1,55 @@ +/* + * SORMAS® - Surveillance Outbreak Response Management & Analysis System + * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.symeda.sormas.app.login; + +import androidx.databinding.BaseObservable; + +public class ChangePasswordViewModel extends BaseObservable { + + private String currentPassword; + private String newPassword; + private String confirmPassword; + + public String getCurrentPassword() { + + return currentPassword; + } + + public void setCurrentPassword(String currentPassword) { + + this.currentPassword = currentPassword; + } + + public String getNewPassword() { + + return newPassword; + } + + public void setNewPassword(String newPassword) { + + this.newPassword = newPassword; + } + + public String getConfirmPassword() { + + return confirmPassword; + } + + public void setConfirmPassword(String confirmPassword) { + + this.confirmPassword = confirmPassword; + } +} diff --git a/sormas-app/app/src/main/java/de/symeda/sormas/app/rest/UserFacadeRetro.java b/sormas-app/app/src/main/java/de/symeda/sormas/app/rest/UserFacadeRetro.java index 790099a2015..95a4b59d658 100644 --- a/sormas-app/app/src/main/java/de/symeda/sormas/app/rest/UserFacadeRetro.java +++ b/sormas-app/app/src/main/java/de/symeda/sormas/app/rest/UserFacadeRetro.java @@ -23,6 +23,7 @@ import retrofit2.http.GET; import retrofit2.http.POST; import retrofit2.http.Path; +import de.symeda.sormas.api.user.UserPasswordChangeDto; /** * Created by Martin Wahnschaffe on 07.06.2016. @@ -37,4 +38,10 @@ public interface UserFacadeRetro { @GET("users/uuids") Call> pullUuids(); + + @POST("users/saveNewPassword") + Call saveNewPassword(@Body UserPasswordChangeDto userPasswordChangeDto); + + @GET("users/generatePassword") + Call generatePassword(); } diff --git a/sormas-app/app/src/main/java/de/symeda/sormas/app/settings/SettingsFragment.java b/sormas-app/app/src/main/java/de/symeda/sormas/app/settings/SettingsFragment.java index ac66cad5939..ec1fae1de90 100644 --- a/sormas-app/app/src/main/java/de/symeda/sormas/app/settings/SettingsFragment.java +++ b/sormas-app/app/src/main/java/de/symeda/sormas/app/settings/SettingsFragment.java @@ -63,6 +63,8 @@ import de.symeda.sormas.app.util.Callback; import de.symeda.sormas.app.util.DataUtils; import de.symeda.sormas.app.util.SoftKeyboardHelper; +import de.symeda.sormas.app.login.ChangePasswordActivity; +import de.symeda.sormas.api.feature.FeatureType; /** * TODO SettingsFragment should probably not be a BaseLandingFragment, but a BaseFragment @@ -89,6 +91,7 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, binding.settingsServerUrl.setValue(ConfigProvider.getServerRestUrl()); binding.changePin.setOnClickListener(v -> changePIN()); + binding.changePassword.setOnClickListener(v -> changePassword()); binding.resynchronizeData.setOnClickListener(v -> repullData()); binding.showSyncLog.setOnClickListener(v -> openSyncLog()); binding.logout.setOnClickListener(v -> logout()); @@ -157,6 +160,8 @@ public void onResume() { binding.settingsServerUrlInfo.setVisibility(!hasServerUrl() ? View.VISIBLE : View.GONE); binding.settingsServerUrl.setVisibility(!hasServerUrl() || isShowDevOptions() ? View.VISIBLE : View.GONE); binding.changePin.setVisibility(hasUser ? View.VISIBLE : View.GONE); + binding.changePassword.setVisibility( + hasUser && !DatabaseHelper.getFeatureConfigurationDao().isFeatureDisabled(FeatureType.SELF_PASSWORD_RESET) ? View.VISIBLE : View.GONE); binding.resynchronizeData.setVisibility(hasUser ? View.VISIBLE : View.GONE); binding.showSyncLog.setVisibility(hasUser ? View.VISIBLE : View.GONE); binding.logout.setVisibility(hasUser && isShowDevOptions() ? View.VISIBLE : View.GONE); @@ -184,6 +189,12 @@ public void changePIN() { startActivity(intent); } + public void changePassword() { + + Intent intent = new Intent(getActivity(), ChangePasswordActivity.class); + startActivity(intent); + } + private void repullData() { checkAndShowUnsynchronizedChangesDialog(() -> showRepullDataConfirmationDialog(), "SYNC"); } diff --git a/sormas-app/app/src/main/res/layout/activity_change_password_layout.xml b/sormas-app/app/src/main/res/layout/activity_change_password_layout.xml new file mode 100644 index 00000000000..0fda7252b32 --- /dev/null +++ b/sormas-app/app/src/main/res/layout/activity_change_password_layout.xml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +