This repository has been archived on 2026-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
powersync-app/nymea-app/ui/system/UsersSettingsPage.qml

541 lines
19 KiB
QML

import QtQuick 2.5
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.1
import Nymea 1.0
import NymeaApp.Utils 1.0
import "../components"
SettingsPageBase {
id: root
title: qsTr("User settings")
UserManager {
id: userManager
engine: _engine
onChangePasswordReply: {
if (error !== UserManager.UserErrorNoError) {
var component = Qt.createComponent("../components/ErrorDialog.qml")
var text;
switch (error) {
case UserManager.UserErrorBadPassword:
text = qsTr("The given password is not valid.");
break;
case UserManager.UserErrorPermissionDenied:
text = qsTr("Permission denied.");
break;
case UserManager.UserErrorBackendError:
text = qsTr("The new password could not be stored.")
break;
default:
text = qsTr("Un unexpected error happened when changing the password. We're sorry for this. (Error code: %1)").arg(error);
break;
}
var popup = component.createObject(app, {text: text});
popup.open()
}
}
}
RowLayout {
Layout.margins: Style.margins
spacing: Style.margins
visible: !engine.jsonRpcClient.pushButtonAuthAvailable
ColorIcon {
size: Style.hugeIconSize
source: "../images/account.svg"
color: Style.accentColor
}
ColumnLayout {
Label {
Layout.fillWidth: true
text: userManager.userInfo.displayName || userManager.userInfo.username
font: Style.bigFont
}
Label {
Layout.fillWidth: true
text: userManager.userInfo.username
visible: userManager.userInfo.displayName !== ""
}
Label {
Layout.fillWidth: true
text: userManager.userInfo.email
font: Style.smallFont
}
}
}
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("Change password")
iconName: "../images/key.svg"
visible: !engine.jsonRpcClient.pushButtonAuthAvailable
onClicked: {
var page = pageStack.push(changePasswordComponent)
page.confirmed.connect(function(newPassword) {
userManager.changePassword(newPassword)
})
}
}
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("Edit user information")
iconName: "../images/edit.svg"
onClicked: pageStack.push(editUserInfoComponent)
visible: !engine.jsonRpcClient.pushButtonAuthAvailable
}
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("Manage authorized devices")
iconName: "../images/smartphone.svg"
onClicked: {
pageStack.push(manageTokensComponent)
}
}
SettingsPageSectionHeader {
text: qsTr("Admin")
visible: NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin) && !engine.jsonRpcClient.pushButtonAuthAvailable
}
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("Manage users")
visible: NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin) && !engine.jsonRpcClient.pushButtonAuthAvailable
iconName: "../images/contact-group.svg"
onClicked: {
pageStack.push(manageUsersComponent)
}
}
Component {
id: editUserInfoComponent
SettingsPageBase {
id: editUserInfoPage
title: qsTr("Edit user information")
GridLayout {
Layout.margins: Style.margins
columnSpacing: Style.margins
columns: 2
Label {
text: qsTr("Your name")
}
NymeaTextField {
id: displayNameTextField
Layout.fillWidth: true
text: userManager.userInfo.displayName
}
Label {
text: qsTr("Email")
}
NymeaTextField {
id: emailTextField
Layout.fillWidth: true
text: userManager.userInfo.email
}
}
Button {
Layout.fillWidth: true
Layout.margins: Style.margins
text: qsTr("OK")
onClicked: {
editUserInfoPage.busy = true
userManager.setUserInfo(userManager.userInfo.username, displayNameTextField.text, emailTextField.text)
}
}
Connections {
target: userManager
onSetUserInfoReply: {
editUserInfoPage.busy = false
if (error != UserManager.UserErrorNoError) {
var component = Qt.createComponent("../components/ErrorDialog.qml")
var text = qsTr("Un unexpected error happened when creating the user. We're sorry for this. (Error code: %1)").arg(error);
var popup = component.createObject(app, {text: text});
popup.open()
} else {
pageStack.pop()
}
}
}
}
}
Component {
id: changePasswordComponent
SettingsPageBase {
id: changePasswordPage
title: qsTr("Change password")
signal confirmed(string newPassword)
SettingsPageSectionHeader {
text: qsTr("Change password")
}
Label {
Layout.fillWidth: true
Layout.leftMargin: app.margins
Layout.rightMargin: app.margins
text: qsTr("Please enter the new password for %1").arg(userManager.userInfo.username)
wrapMode: Text.WordWrap
}
PasswordTextField {
id: passwordTextField
Layout.fillWidth: true
Layout.leftMargin: app.margins
Layout.rightMargin: app.margins
signup: true
}
Button {
text: qsTr("OK")
Layout.fillWidth: true
Layout.margins: app.margins
enabled: passwordTextField.isValid
onClicked: {
changePasswordPage.confirmed(passwordTextField.password)
pageStack.pop();
}
}
}
}
Component {
id: manageTokensComponent
SettingsPageBase {
id: manageTokensPage
title: qsTr("Device access")
Component {
id: confirmTokenDeletionComponent
NymeaDialog {
headerIcon: "../images/lock-closed.svg"
title: qsTr("Remove device access")
text: qsTr("Are you sure you want to remove %1 from accessing your %2 system?").arg("<b>" + tokenInfo.deviceName + "</b>").arg(Configuration.systemName)
property TokenInfo tokenInfo: null
standardButtons: Dialog.Yes | Dialog.No
onAccepted: {
userManager.removeToken(tokenInfo.id)
}
}
}
SettingsPageSectionHeader {
text: qsTr("Devices / Apps accessing %1").arg(Configuration.systemName)
}
Repeater {
model: userManager.tokenInfos
delegate: NymeaSwipeDelegate {
Layout.fillWidth: true
text: model.deviceName
subText: qsTr("Created on %1").arg(Qt.formatDateTime(model.creationTime, Qt.DefaultLocaleShortDate))
prominentSubText: false
progressive: false
canDelete: true
iconName: "../images/smartphone.svg"
onClicked: deleteClicked()
onDeleteClicked: {
var popup = confirmTokenDeletionComponent.createObject(manageTokensPage, {tokenInfo: userManager.tokenInfos.get(index)})
popup.open()
}
}
}
}
}
Component {
id: manageUsersComponent
SettingsPageBase {
id: manageUsersPage
header: NymeaHeader {
text: qsTr("Users")
onBackPressed: pageStack.pop()
HeaderButton {
imageSource: Qt.resolvedUrl("../images/add.svg")
onClicked: {
pageStack.push(addUserComponent)
}
}
}
SettingsPageSectionHeader {
text: qsTr("Manage users for this %1 system").arg(Configuration.systemName)
}
Repeater {
model: userManager.users
delegate: NymeaItemDelegate {
Layout.fillWidth: true
text: engine.jsonRpcClient.ensureServerVersion("6.0") && model.displayName !== "" ? model.displayName : model.username
subText: engine.jsonRpcClient.ensureServerVersion("6.0") && model.displayName ? model.username : ""
iconName: "/ui/images/account.svg"
iconColor: userManager.userInfo.scopes & UserInfo.PermissionScopeAdmin ? Style.accentColor : Style.iconColor
canDelete: true
onClicked: {
pageStack.push(userDetailsComponent, {userInfo: userManager.users.get(index)})
}
}
}
}
}
Component {
id: userDetailsComponent
SettingsPageBase {
id: userDetailsPage
title: qsTr("Manage %1").arg(userInfo.username)
property UserInfo userInfo: null
Component {
id: confirmUserDeletionComponent
NymeaDialog {
headerIcon: "../images/lock-closed.svg"
title: qsTr("Remove user")
text: qsTr("Are you sure you want to remove %1 from accessing your %2 system?").arg("<b>" + userInfo.username + "</b>").arg(Configuration.systemName)
property UserInfo userInfo: null
standardButtons: Dialog.Yes | Dialog.No
onAccepted: {
userDetailsPage.busy = true
userManager.removeUser(userInfo.username)
}
}
}
SettingsPageSectionHeader {
text: qsTr("User information for %1").arg(userDetailsPage.userInfo.username)
}
GridLayout {
Layout.leftMargin: Style.margins
Layout.rightMargin: Style.margins
columnSpacing: Style.margins
columns: 2
Label {
text: qsTr("Name")
}
NymeaTextField {
id: displayNameTextField
Layout.fillWidth: true
text: userDetailsPage.userInfo.displayName
}
Label {
text: qsTr("Email")
}
NymeaTextField {
id: emailTextField
Layout.fillWidth: true
text: userDetailsPage.userInfo.email
}
}
Button {
Layout.fillWidth: true
Layout.margins: Style.margins
text: qsTr("Save")
onClicked: {
userManager.setUserInfo(userDetailsPage.userInfo.username, displayNameTextField.text, emailTextField.text)
}
}
SettingsPageSectionHeader {
text: qsTr("Permissions")
}
Repeater {
model: NymeaUtils.scopesModel
delegate: CheckDelegate {
Layout.fillWidth: true
text: model.text
checked: (userDetailsPage.userInfo.scopes & model.scope) === model.scope
enabled: model.scope === UserInfo.PermissionScopeAdmin ||
((userDetailsPage.userInfo.scopes & UserInfo.PermissionScopeAdmin) !== UserInfo.PermissionScopeAdmin)
onClicked: {
print("scopes:", userDetailsPage.userInfo.scopes)
var scopes = userDetailsPage.userInfo.scopes
if (checked) {
scopes |= model.scope
} else {
scopes &= ~model.scope
scopes |= model.resetOnUnset
}
print("username:", userDetailsPage.userInfo.username)
print("new scopes:", scopes, UserInfo.PermissionScopeAdmin)
userManager.setUserScopes(userDetailsPage.userInfo.username, scopes)
}
}
}
SettingsPageSectionHeader {
text: qsTr("Remove")
}
Button {
Layout.fillWidth: true
Layout.leftMargin: Style.margins
Layout.rightMargin: Style.margins
text: qsTr("Remove this user")
onClicked: {
var popup = confirmUserDeletionComponent.createObject(userDetailsPage, {userInfo: userDetailsPage.userInfo})
popup.open()
}
}
Connections {
target: userManager
onRemoveUserReply: {
userDetailsPage.busy = false
if (error !== UserManager.UserErrorNoError) {
var component = Qt.createComponent("../components/ErrorDialog.qml")
var text = qsTr("Un unexpected error happened when creating the user. We're sorry for this. (Error code: %1)").arg(error);
var popup = component.createObject(app, {text: text});
popup.open()
} else {
pageStack.pop();
}
}
}
}
}
Component {
id: addUserComponent
SettingsPageBase {
id: createUserPage
title: qsTr("Add a user")
property var permissionScopes: UserInfo.PermissionScopeNone
SettingsPageSectionHeader {
text: qsTr("User information")
}
GridLayout {
Layout.fillWidth: true
Layout.leftMargin: Style.margins
Layout.rightMargin: Style.margins
columns: 2
Label {
text: qsTr("Username:") + "*"
}
TextField {
id: usernameTextField
Layout.fillWidth: true
inputMethodHints: Qt.ImhNoAutoUppercase
}
Label {
text: qsTr("Password:") + "*"
Layout.alignment: Qt.AlignTop
Layout.topMargin: Style.smallMargins
}
PasswordTextField {
id: passwordTextField
Layout.fillWidth: true
}
Label {
text: qsTr("Full name:")
}
TextField {
id: displayNameTextField
Layout.fillWidth: true
}
Label {
text: qsTr("e-mail:")
}
TextField {
id: emailTextField
Layout.fillWidth: true
}
}
SettingsPageSectionHeader {
text: qsTr("Permissions")
}
Repeater {
id: scopesRepeater
model: NymeaUtils.scopesModel
delegate: CheckDelegate {
Layout.fillWidth: true
text: model.text
checked: (createUserPage.permissionScopes & model.scope) === model.scope
onClicked: {
var scopes = createUserPage.permissionScopes
if (checked) {
scopes |= model.scope
} else {
scopes &= ~model.scope
scopes |= model.resetOnUnset
}
createUserPage.permissionScopes = scopes
}
}
}
Button {
text: qsTr("Create new user")
Layout.fillWidth: true
Layout.leftMargin: Style.margins
Layout.rightMargin: Style.margins
enabled: usernameTextField.displayText.length >= 3 && passwordTextField.isValid
onClicked: {
createUserPage.busy = true
userManager.createUser(usernameTextField.displayText, passwordTextField.password, displayNameTextField.text, emailTextField.text, createUserPage.permissionScopes)
}
}
Connections {
target: userManager
onCreateUserReply: {
createUserPage.busy = false
if (error !== UserManager.UserErrorNoError) {
var component = Qt.createComponent("../components/ErrorDialog.qml")
var text;
switch (error) {
case UserManager.UserErrorInvalidUserId:
text = qsTr("The given username is not valid. It needs to be at least three characters long and not contain special characters.");
break;
case UserManager.UserErrorDuplicateUserId:
text = qsTr("The given username is already in use. Please choose a different username.");
break;
case UserManager.UserErrorBadPassword:
text = qsTr("The given password is not valid.");
break;
case UserManager.UserErrorPermissionDenied:
text = qsTr("Permission denied.");
break;
default:
text = qsTr("Un unexpected error happened when creating the user. We're sorry for this. (Error code: %1)").arg(error);
break;
}
var popup = component.createObject(app, {text: text});
popup.open()
} else {
pageStack.pop();
}
}
}
}
}
}