Skip to content
Snippets Groups Projects
Commit 02863e3b authored by Sebastian's avatar Sebastian Committed by Mikolai Gütschow
Browse files

Add settings entry and dialog for changing the password

parent 81dde14d
No related branches found
No related tags found
1 merge request!177Add settings entry and dialog for changing the password
......@@ -33,15 +33,21 @@ fun DialogButton(
onClick: () -> Unit,
text: String,
type: ButtonType,
enabled: Boolean = true,
) {
TextButton(onClick = onClick) {
TextButton(
onClick = onClick,
enabled = enabled,
colors = ButtonDefaults.textButtonColors(
contentColor = when (type) {
ButtonType.NEUTRAL -> MaterialTheme.colors.buttonTextPositive
ButtonType.DESTRUCTIVE -> MaterialTheme.colors.buttonTextNegative
}
)
) {
Text(
text.uppercase(InternationalizationUtils.locale),
style = MaterialTheme.typography.button,
color = when (type) {
ButtonType.NEUTRAL -> MaterialTheme.colors.buttonTextPositive
ButtonType.DESTRUCTIVE -> MaterialTheme.colors.buttonTextNegative
},
)
}
}
/*
* Briar Desktop
* Copyright (C) 2021-2022 The Briar Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.briarproject.briar.desktop.settings
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.AlertDialog
import androidx.compose.material.ButtonType
import androidx.compose.material.DialogButton
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.InitialFocusState
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedPasswordTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import org.briarproject.bramble.api.crypto.DecryptionResult
import org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_PASSWORD
import org.briarproject.briar.desktop.login.StrengthMeter
import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
import org.briarproject.briar.desktop.utils.PreviewUtils.preview
import java.lang.Float.min
@Suppress("HardCodedStringLiteral")
fun main() = preview(
"visible" to true,
) {
val (oldPassword, setOldPassword) = remember { mutableStateOf("") }
val (password, setPassword) = remember { mutableStateOf("") }
val (passwordConfirmation, setPasswordConfirmation) = remember { mutableStateOf("") }
val passwordStrength = derivedStateOf {
min(password.length / 10f, 1f)
}
val passwordTooWeakError = derivedStateOf {
passwordStrength.value < 0.6f
}
val (submitError, setSubmitError) = remember { mutableStateOf<DecryptionResult?>(null) }
ChangePasswordDialog(
isVisible = getBooleanParameter("visible"),
close = { setBooleanParameter("visible", false) },
onChange = {
// use password with 8 or less to test failure
if (password.length <= 8) {
setSubmitError(INVALID_PASSWORD)
false
} else {
true
}
},
oldPassword = oldPassword,
setOldPassword = setOldPassword,
password = password,
setPassword = setPassword,
passwordConfirmation = passwordConfirmation,
setPasswordConfirmation = setPasswordConfirmation,
passwordStrength = passwordStrength.value,
passwordTooWeakError = passwordTooWeakError.value,
passwordsDontMatchError = password != passwordConfirmation,
buttonEnabled = !passwordTooWeakError.value && password == passwordConfirmation,
submitError = submitError,
)
}
@Composable
fun ChangePasswordDialog(
isVisible: Boolean,
close: () -> Unit,
viewHolder: ChangePasswordSubViewModel,
) {
ChangePasswordDialog(
isVisible = isVisible,
close = close,
onChange = viewHolder::confirmChange,
oldPassword = viewHolder.oldPassword.value,
setOldPassword = viewHolder::setOldPassword,
password = viewHolder.password.value,
setPassword = viewHolder::setPassword,
passwordConfirmation = viewHolder.passwordConfirmation.value,
setPasswordConfirmation = viewHolder::setPasswordConfirmation,
passwordStrength = viewHolder.passwordStrength.value,
passwordTooWeakError = viewHolder.passwordTooWeakError.value,
passwordsDontMatchError = viewHolder.passwordMatchError.value,
buttonEnabled = viewHolder.buttonEnabled.value,
submitError = viewHolder.submitError.value,
)
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ChangePasswordDialog(
isVisible: Boolean,
close: () -> Unit,
onChange: () -> Boolean,
oldPassword: String,
setOldPassword: (String) -> Unit,
password: String,
setPassword: (String) -> Unit,
passwordConfirmation: String,
setPasswordConfirmation: (String) -> Unit,
passwordStrength: Float,
passwordTooWeakError: Boolean,
passwordsDontMatchError: Boolean,
buttonEnabled: Boolean,
submitError: DecryptionResult?,
) {
if (!isVisible) return
val onClose = {
setOldPassword("")
setPassword("")
setPasswordConfirmation("")
close()
}
val onSubmit = {
if (onChange()) {
onClose()
}
}
AlertDialog(
onDismissRequest = onClose,
title = {
Text(
text = i18n("settings.security.password.change"),
modifier = Modifier.width(IntrinsicSize.Max),
style = MaterialTheme.typography.h6,
)
},
text = {
PasswordForm(
oldPassword,
setOldPassword,
password,
setPassword,
passwordConfirmation,
setPasswordConfirmation,
passwordStrength,
passwordTooWeakError,
passwordsDontMatchError,
onSubmit,
submitError,
)
},
dismissButton = {
DialogButton(
onClick = onClose,
text = i18n("cancel"),
type = ButtonType.NEUTRAL,
)
},
confirmButton = {
DialogButton(
onClick = onSubmit,
text = i18n("change"),
type = ButtonType.NEUTRAL,
enabled = buttonEnabled,
)
},
)
}
@Composable
fun PasswordForm(
oldPassword: String,
setOldPassword: (String) -> Unit,
password: String,
setPassword: (String) -> Unit,
passwordConfirmation: String,
setPasswordConfirmation: (String) -> Unit,
passwordStrength: Float,
passwordTooWeakError: Boolean,
passwordsDontMatchError: Boolean,
onSubmit: () -> Unit,
onSubmitError: DecryptionResult?,
) {
val initialFocusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
Column {
OutlinedPasswordTextField(
value = oldPassword,
onValueChange = setOldPassword,
label = { Text(i18n("settings.security.password.current")) },
singleLine = true,
isError = onSubmitError == INVALID_PASSWORD,
showErrorWhen = InitialFocusState.FROM_START,
errorMessage = i18n("startup.error.password_wrong"),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next),
modifier = Modifier.fillMaxWidth().focusRequester(initialFocusRequester),
onEnter = { focusManager.moveFocus(FocusDirection.Next) },
)
Box(
modifier = Modifier.fillMaxWidth().requiredHeight(24.dp),
contentAlignment = Alignment.Center
) {
if (password.isNotEmpty())
StrengthMeter(passwordStrength, Modifier.fillMaxWidth())
}
OutlinedPasswordTextField(
value = password,
onValueChange = setPassword,
label = { Text(i18n("settings.security.password.choose")) },
singleLine = true,
isError = passwordTooWeakError,
showErrorWhen = InitialFocusState.AFTER_FOCUS_LOST_ONCE,
errorMessage = i18n("startup.error.password_too_weak"),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next),
modifier = Modifier.fillMaxWidth(),
onEnter = { focusManager.moveFocus(FocusDirection.Next) },
)
OutlinedPasswordTextField(
value = passwordConfirmation,
onValueChange = setPasswordConfirmation,
label = { Text(i18n("settings.security.password.confirm")) },
singleLine = true,
isError = passwordsDontMatchError,
showErrorWhen = InitialFocusState.AFTER_FIRST_FOCUSSED,
errorMessage = i18n("startup.error.passwords_not_match"),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
modifier = Modifier.fillMaxWidth(),
onEnter = onSubmit,
)
}
LaunchedEffect(Unit) {
initialFocusRequester.requestFocus()
}
}
/*
* Briar Desktop
* Copyright (C) 2021-2022 The Briar Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.briarproject.briar.desktop.settings
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import org.briarproject.bramble.api.account.AccountManager
import org.briarproject.bramble.api.crypto.DecryptionException
import org.briarproject.bramble.api.crypto.DecryptionResult
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK
import org.briarproject.briar.desktop.viewmodel.asState
class ChangePasswordSubViewModel(
private val accountManager: AccountManager,
private val passwordStrengthEstimator: PasswordStrengthEstimator,
) {
private val _oldPassword = mutableStateOf("")
private val _password = mutableStateOf("")
private val _passwordConfirmation = mutableStateOf("")
private val _submitError = mutableStateOf<DecryptionResult?>(null)
val oldPassword = _oldPassword.asState()
val password = _password.asState()
val passwordConfirmation = _passwordConfirmation.asState()
val passwordStrength = derivedStateOf {
passwordStrengthEstimator.estimateStrength(_password.value)
}
val passwordTooWeakError = derivedStateOf {
password.value.isNotEmpty() && passwordStrength.value < QUITE_WEAK
}
val passwordMatchError = derivedStateOf {
passwordConfirmation.value.isNotEmpty() && password.value != passwordConfirmation.value
}
val buttonEnabled = derivedStateOf {
password.value.isNotEmpty() && passwordConfirmation.value.isNotEmpty() &&
!passwordTooWeakError.value && !passwordMatchError.value
}
val submitError = _submitError.asState()
fun setOldPassword(password: String) {
_oldPassword.value = password
}
fun setPassword(password: String) {
_password.value = password
}
fun setPasswordConfirmation(passwordConfirmation: String) {
_passwordConfirmation.value = passwordConfirmation
}
fun confirmChange(): Boolean {
if (!buttonEnabled.value) return false
return try {
accountManager.changePassword(oldPassword.value, _password.value)
_submitError.value = null
true
} catch (e: DecryptionException) {
_submitError.value = e.decryptionResult
false
}
}
}
......@@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.OutlinedExposedDropDownMenu
import androidx.compose.material.Surface
import androidx.compose.material.Text
......@@ -73,6 +74,14 @@ fun SettingDetails(viewModel: SettingsViewModel) {
modifier = Modifier.widthIn(min = 200.dp)
)
}
DetailItem {
Text(i18n("settings.security.title"))
OutlinedButton(onClick = viewModel::showChangePasswordDialog) {
Text(i18n("settings.security.password.change"))
}
}
}
}
SettingCategory.CONNECTIONS -> {
......
......@@ -25,6 +25,12 @@ import androidx.compose.ui.Modifier
@Composable
fun SettingsScreen(viewModel: SettingsViewModel) {
ChangePasswordDialog(
viewModel.changePasswordDialogVisible.value,
close = viewModel::dismissChangePasswordDialog,
viewHolder = viewModel.changePasswordSubViewModel,
)
Row(Modifier.fillMaxSize()) {
/* TODO: Currently commented out because there are settings in just a single category
SettingOptionsList(
......
......@@ -19,6 +19,8 @@
package org.briarproject.briar.desktop.settings
import androidx.compose.runtime.mutableStateOf
import org.briarproject.bramble.api.account.AccountManager
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator
import org.briarproject.briar.desktop.viewmodel.ViewModel
import org.briarproject.briar.desktop.viewmodel.asState
import javax.inject.Inject
......@@ -36,6 +38,8 @@ class SettingsViewModel
@Inject
constructor(
private val unencryptedSettings: UnencryptedSettings,
private val accountManager: AccountManager,
private val passwordStrengthEstimator: PasswordStrengthEstimator,
) : ViewModel {
private val _selectedSetting = mutableStateOf(SettingCategory.DISPLAY)
val selectedSetting = _selectedSetting.asState()
......@@ -49,6 +53,11 @@ constructor(
private val _selectedLanguage = mutableStateOf(unencryptedSettings.language)
val selectedLanguage = _selectedLanguage.asState()
private val _changePasswordDialogVisible = mutableStateOf(false)
val changePasswordDialogVisible = _changePasswordDialogVisible.asState()
val changePasswordSubViewModel = ChangePasswordSubViewModel(accountManager, passwordStrengthEstimator)
fun selectSetting(selectedOption: SettingCategory) {
_selectedSetting.value = selectedOption
}
......@@ -62,4 +71,12 @@ constructor(
unencryptedSettings.language = language
_selectedLanguage.value = language
}
fun showChangePasswordDialog() {
_changePasswordDialogVisible.value = true
}
fun dismissChangePasswordDialog() {
_changePasswordDialogVisible.value = false
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment