mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-07 19:59:14 -04:00
feat(Users): add user management UI in DMS Settings
This commit is contained in:
@@ -555,5 +555,20 @@ FocusScope {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: usersLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 35
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: UsersTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item)
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,6 +293,12 @@ Rectangle {
|
||||
"tabIndex": 20,
|
||||
"updaterOnly": true
|
||||
},
|
||||
{
|
||||
"id": "users",
|
||||
"text": I18n.tr("Users"),
|
||||
"icon": "manage_accounts",
|
||||
"tabIndex": 35
|
||||
},
|
||||
{
|
||||
"id": "window_rules",
|
||||
"text": I18n.tr("Window Rules"),
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Settings.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string statusText: ""
|
||||
property bool statusIsError: false
|
||||
property bool operationPending: false
|
||||
property string pendingUsername: ""
|
||||
property string pendingPassword: ""
|
||||
property string pendingConfirm: ""
|
||||
property bool pendingAdmin: false
|
||||
|
||||
function _resetForm() {
|
||||
pendingUsername = "";
|
||||
pendingPassword = "";
|
||||
pendingConfirm = "";
|
||||
pendingAdmin = false;
|
||||
usernameField.text = "";
|
||||
passwordField.text = "";
|
||||
confirmField.text = "";
|
||||
}
|
||||
|
||||
function _passwordsMatch() {
|
||||
return pendingPassword.length > 0 && pendingPassword === pendingConfirm;
|
||||
}
|
||||
|
||||
function _createCanProceed() {
|
||||
return !operationPending && UsersService.isValidUsername(pendingUsername) && !UsersService.userExists(pendingUsername) && _passwordsMatch();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: UsersService
|
||||
function onOperationCompleted(op, username, success, message) {
|
||||
root.operationPending = false;
|
||||
root.statusIsError = !success;
|
||||
if (success) {
|
||||
root.statusText = message + (username ? (" — " + username) : "");
|
||||
if (op === "create")
|
||||
root._resetForm();
|
||||
} else {
|
||||
root.statusText = (username ? (username + ": ") : "") + message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmModal {
|
||||
id: deleteUserConfirm
|
||||
}
|
||||
|
||||
ConfirmModal {
|
||||
id: adminToggleConfirm
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentHeight: mainColumn.height + Theme.spacingXL
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
topPadding: 4
|
||||
width: Math.min(600, parent.width - Theme.spacingL * 2)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
visible: !PolkitService.polkitAvailable
|
||||
text: I18n.tr("Polkit integration is disabled. User management requires Polkit to elevate privileges.")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.error
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "group"
|
||||
title: I18n.tr("Existing Users")
|
||||
settingKey: "usersList"
|
||||
visible: PolkitService.polkitAvailable
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Administrator group:")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: UsersService.adminGroup
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Theme.spacingM
|
||||
height: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: UsersService.refreshing ? I18n.tr("Refreshing…") : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: UsersService.users
|
||||
|
||||
Rectangle {
|
||||
id: userRow
|
||||
required property var modelData
|
||||
width: parent.width
|
||||
height: Math.max(48, rowContent.implicitHeight + Theme.spacingS * 2)
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHighest
|
||||
|
||||
readonly property bool isLastAdmin: modelData.isAdmin && UsersService.adminMembers.length <= 1
|
||||
|
||||
Row {
|
||||
id: rowContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "account_circle"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - actionButtons.width - Theme.spacingM * 3
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: userRow.modelData.username
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: userRow.modelData.isAdmin
|
||||
width: adminChipText.implicitWidth + Theme.spacingS * 2
|
||||
height: adminChipText.implicitHeight + Theme.spacingXS * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.primary, 0.15)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
id: adminChipText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("admin")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: userRow.modelData.gecos && userRow.modelData.gecos.length > 0 ? userRow.modelData.gecos + " · UID " + userRow.modelData.uid : "UID " + userRow.modelData.uid
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: actionButtons
|
||||
spacing: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankActionButton {
|
||||
id: adminToggleBtn
|
||||
readonly property bool actionBlocked: root.operationPending || (userRow.isLastAdmin && userRow.modelData.isAdmin)
|
||||
buttonSize: 36
|
||||
iconSize: 20
|
||||
iconName: userRow.modelData.isAdmin ? "shield_person" : "shield"
|
||||
iconColor: userRow.modelData.isAdmin ? Theme.primary : Theme.surfaceVariantText
|
||||
opacity: actionBlocked ? 0.4 : 1.0
|
||||
tooltipText: (userRow.isLastAdmin && userRow.modelData.isAdmin) ? I18n.tr("Cannot remove the only administrator") : (userRow.modelData.isAdmin ? I18n.tr("Remove admin") : I18n.tr("Make admin"))
|
||||
tooltipSide: "left"
|
||||
onClicked: {
|
||||
if (actionBlocked)
|
||||
return;
|
||||
const makeAdmin = !userRow.modelData.isAdmin;
|
||||
adminToggleConfirm.showWithOptions({
|
||||
title: makeAdmin ? I18n.tr("Grant admin?") : I18n.tr("Remove admin?"),
|
||||
message: makeAdmin ? I18n.tr("Add \"%1\" to the %2 group?").arg(userRow.modelData.username).arg(UsersService.adminGroup) : I18n.tr("Remove \"%1\" from the %2 group?").arg(userRow.modelData.username).arg(UsersService.adminGroup),
|
||||
confirmText: makeAdmin ? I18n.tr("Grant") : I18n.tr("Remove"),
|
||||
confirmColor: Theme.primary,
|
||||
onConfirm: () => {
|
||||
root.operationPending = true;
|
||||
root.statusText = "";
|
||||
UsersService.setAdmin(userRow.modelData.username, makeAdmin, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: deleteBtn
|
||||
readonly property bool actionBlocked: root.operationPending || !UsersService.canDelete(userRow.modelData.username)
|
||||
buttonSize: 36
|
||||
iconSize: 20
|
||||
iconName: "delete"
|
||||
iconColor: Theme.error
|
||||
opacity: actionBlocked ? 0.4 : 1.0
|
||||
tooltipText: userRow.isLastAdmin ? I18n.tr("Cannot delete the only administrator") : I18n.tr("Delete user")
|
||||
tooltipSide: "left"
|
||||
onClicked: {
|
||||
if (actionBlocked)
|
||||
return;
|
||||
deleteUserConfirm.showWithOptions({
|
||||
title: I18n.tr("Delete user?"),
|
||||
message: I18n.tr("Delete \"%1\" and remove the home directory? This cannot be undone.").arg(userRow.modelData.username),
|
||||
confirmText: I18n.tr("Delete"),
|
||||
confirmColor: Theme.primary,
|
||||
onConfirm: () => {
|
||||
root.operationPending = true;
|
||||
root.statusText = "";
|
||||
UsersService.deleteUser(userRow.modelData.username, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
visible: UsersService.users.length === 0 && !UsersService.refreshing
|
||||
text: I18n.tr("No human user accounts found.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "person_add"
|
||||
title: I18n.tr("Create User")
|
||||
settingKey: "createUser"
|
||||
visible: PolkitService.polkitAvailable
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Username")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: usernameField
|
||||
width: parent.width
|
||||
placeholderText: I18n.tr("e.g. alice")
|
||||
backgroundColor: Theme.surfaceContainerHighest
|
||||
normalBorderColor: usernameInvalid ? Theme.error : Theme.outlineMedium
|
||||
focusedBorderColor: usernameInvalid ? Theme.error : Theme.primary
|
||||
|
||||
readonly property bool usernameInvalid: text.length > 0 && (!UsersService.isValidUsername(text) || UsersService.userExists(text))
|
||||
|
||||
onTextEdited: {
|
||||
root.pendingUsername = text.trim();
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
visible: usernameField.text.length > 0 && !UsersService.isValidUsername(usernameField.text)
|
||||
text: I18n.tr("Username must start with a lowercase letter or underscore and contain only lowercase letters, digits, hyphens, or underscores.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
visible: usernameField.text.length > 0 && UsersService.isValidUsername(usernameField.text) && UsersService.userExists(usernameField.text)
|
||||
text: I18n.tr("A user with that name already exists.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Password")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: passwordField
|
||||
width: parent.width
|
||||
placeholderText: I18n.tr("Set initial password")
|
||||
echoMode: TextInput.Password
|
||||
showPasswordToggle: true
|
||||
backgroundColor: Theme.surfaceContainerHighest
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
onTextEdited: root.pendingPassword = text
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Confirm password")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: confirmField
|
||||
width: parent.width
|
||||
placeholderText: I18n.tr("Re-enter password")
|
||||
echoMode: TextInput.Password
|
||||
showPasswordToggle: true
|
||||
backgroundColor: Theme.surfaceContainerHighest
|
||||
normalBorderColor: confirmMismatch ? Theme.error : Theme.outlineMedium
|
||||
focusedBorderColor: confirmMismatch ? Theme.error : Theme.primary
|
||||
|
||||
readonly property bool confirmMismatch: text.length > 0 && text !== passwordField.text
|
||||
|
||||
onTextEdited: root.pendingConfirm = text
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
visible: confirmField.text.length > 0 && confirmField.text !== passwordField.text
|
||||
text: I18n.tr("Passwords do not match.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "createUserAdmin"
|
||||
tags: ["user", "admin", "sudo", "wheel"]
|
||||
text: I18n.tr("Grant administrator privileges")
|
||||
description: I18n.tr("Add the new user to the %1 group so they can use sudo.").arg(UsersService.adminGroup)
|
||||
checked: root.pendingAdmin
|
||||
onToggled: checked => root.pendingAdmin = checked
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankButton {
|
||||
text: root.operationPending ? I18n.tr("Working…") : I18n.tr("Create User")
|
||||
iconName: "person_add"
|
||||
backgroundColor: Theme.primary
|
||||
textColor: Theme.primaryText
|
||||
enabled: root._createCanProceed()
|
||||
onClicked: {
|
||||
if (!root._createCanProceed())
|
||||
return;
|
||||
root.operationPending = true;
|
||||
root.statusText = "";
|
||||
UsersService.createUser(root.pendingUsername, root.pendingPassword, root.pendingAdmin, null);
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.statusText
|
||||
color: root.statusIsError ? Theme.error : Theme.primary
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width - parent.children[0].width - Theme.spacingM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var log: Log.scoped("UsersService")
|
||||
|
||||
property var users: []
|
||||
property string adminGroup: "wheel"
|
||||
property var adminMembers: []
|
||||
property bool refreshing: false
|
||||
|
||||
signal operationCompleted(string op, string username, bool success, string message)
|
||||
|
||||
readonly property var _usernameRegex: /^[a-z_][a-z0-9_-]{0,30}\$?$/
|
||||
|
||||
function isValidUsername(name) {
|
||||
if (typeof name !== "string")
|
||||
return false;
|
||||
return _usernameRegex.test(name);
|
||||
}
|
||||
|
||||
function userExists(name) {
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
if (users[i].username === name)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function _findUser(name) {
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
if (users[i].username === name)
|
||||
return users[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function canDelete(name) {
|
||||
const u = _findUser(name);
|
||||
if (!u)
|
||||
return false;
|
||||
if (u.isAdmin && adminMembers.length <= 1)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (refreshing)
|
||||
return;
|
||||
refreshing = true;
|
||||
_detectAdminGroup();
|
||||
}
|
||||
|
||||
function _detectAdminGroup() {
|
||||
Proc.runCommand("usersService-detectGroup", ["sh", "-c", "getent group wheel >/dev/null && echo wheel || (getent group sudo >/dev/null && echo sudo || echo wheel)"], (output, exitCode) => {
|
||||
const detected = (output || "").trim() || "wheel";
|
||||
root.adminGroup = detected;
|
||||
_loadAdminMembers();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function _loadAdminMembers() {
|
||||
Proc.runCommand("usersService-adminMembers", ["sh", "-c", "getent group " + root.adminGroup + " | awk -F: '{print $4}'"], (output, exitCode) => {
|
||||
const members = (output || "").trim().split(",").map(s => s.trim()).filter(s => s.length > 0);
|
||||
root.adminMembers = members;
|
||||
_loadUsers();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function _loadUsers() {
|
||||
Proc.runCommand("usersService-loadUsers", ["sh", "-c", "getent passwd | awk -F: '$3>=1000 && $3<60000 && $1!=\"nobody\" {print $1\":\"$3\":\"$5\":\"$6\":\"$7}'"], (output, exitCode) => {
|
||||
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
|
||||
const list = [];
|
||||
const adminSet = {};
|
||||
for (let i = 0; i < root.adminMembers.length; i++)
|
||||
adminSet[root.adminMembers[i]] = true;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const parts = lines[i].split(":");
|
||||
if (parts.length < 5)
|
||||
continue;
|
||||
const username = parts[0];
|
||||
list.push({
|
||||
username,
|
||||
uid: parseInt(parts[1], 10),
|
||||
gecos: (parts[2] || "").split(",")[0],
|
||||
home: parts[3] || "",
|
||||
shell: parts[4] || "",
|
||||
isAdmin: adminSet[username] === true
|
||||
});
|
||||
}
|
||||
list.sort((a, b) => a.username.localeCompare(b.username));
|
||||
root.users = list;
|
||||
root.refreshing = false;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function createUser(username, password, addToAdmin, callback) {
|
||||
if (!isValidUsername(username)) {
|
||||
_emit("create", username, false, I18n.tr("Invalid username"), callback);
|
||||
return;
|
||||
}
|
||||
if (!password || password.length < 1) {
|
||||
_emit("create", username, false, I18n.tr("Password cannot be empty"), callback);
|
||||
return;
|
||||
}
|
||||
if (userExists(username)) {
|
||||
_emit("create", username, false, I18n.tr("User already exists"), callback);
|
||||
return;
|
||||
}
|
||||
_runUseradd(username, password, addToAdmin === true, callback);
|
||||
}
|
||||
|
||||
function setPassword(username, newPassword, callback) {
|
||||
if (!isValidUsername(username) || !userExists(username)) {
|
||||
_emit("passwd", username, false, I18n.tr("User not found"), callback);
|
||||
return;
|
||||
}
|
||||
if (!newPassword || newPassword.length < 1) {
|
||||
_emit("passwd", username, false, I18n.tr("Password cannot be empty"), callback);
|
||||
return;
|
||||
}
|
||||
_runChpasswd(username, newPassword, "passwd", callback);
|
||||
}
|
||||
|
||||
function deleteUser(username, callback) {
|
||||
if (!userExists(username)) {
|
||||
_emit("delete", username, false, I18n.tr("User not found"), callback);
|
||||
return;
|
||||
}
|
||||
if (!canDelete(username)) {
|
||||
_emit("delete", username, false, I18n.tr("Cannot delete the only administrator"), callback);
|
||||
return;
|
||||
}
|
||||
_runUserdel(username, callback);
|
||||
}
|
||||
|
||||
function setAdmin(username, makeAdmin, callback) {
|
||||
if (!userExists(username)) {
|
||||
_emit("admin", username, false, I18n.tr("User not found"), callback);
|
||||
return;
|
||||
}
|
||||
if (!makeAdmin) {
|
||||
const u = _findUser(username);
|
||||
if (u && u.isAdmin && root.adminMembers.length <= 1) {
|
||||
_emit("admin", username, false, I18n.tr("Cannot remove the only administrator"), callback);
|
||||
return;
|
||||
}
|
||||
}
|
||||
_runAdminToggle(username, makeAdmin === true, callback);
|
||||
}
|
||||
|
||||
function _emit(op, username, success, message, callback) {
|
||||
root.operationCompleted(op, username, success, message);
|
||||
if (typeof callback === "function") {
|
||||
try {
|
||||
callback(success, message);
|
||||
} catch (e) {
|
||||
log.warn("UsersService callback error:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: useraddComp
|
||||
Process {
|
||||
id: useraddProc
|
||||
property string targetUser: ""
|
||||
property string targetPassword: ""
|
||||
property bool addAdmin: false
|
||||
property var cb: null
|
||||
property string capturedErr: ""
|
||||
running: false
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: useraddProc.capturedErr = text || ""
|
||||
}
|
||||
onExited: exitCode => {
|
||||
const svc = root;
|
||||
if (exitCode !== 0) {
|
||||
svc._emit("create", useraddProc.targetUser, false, (useraddProc.capturedErr || "").trim() || I18n.tr("useradd failed (exit %1)").arg(exitCode), useraddProc.cb);
|
||||
Qt.callLater(() => useraddProc.destroy());
|
||||
return;
|
||||
}
|
||||
const targetUser = useraddProc.targetUser;
|
||||
const targetPassword = useraddProc.targetPassword;
|
||||
const addAdmin = useraddProc.addAdmin;
|
||||
const outerCb = useraddProc.cb;
|
||||
Qt.callLater(() => useraddProc.destroy());
|
||||
|
||||
svc._runChpasswd(targetUser, targetPassword, "create", (pwOk, pwMsg) => {
|
||||
if (!pwOk) {
|
||||
svc._emit("create", targetUser, false, pwMsg, outerCb);
|
||||
return;
|
||||
}
|
||||
if (addAdmin) {
|
||||
svc._runAdminToggle(targetUser, true, (adminOk, adminMsg) => {
|
||||
if (adminOk) {
|
||||
svc._emit("create", targetUser, true, I18n.tr("User created with administrator privileges"), outerCb);
|
||||
} else {
|
||||
svc._emit("create", targetUser, false, adminMsg, outerCb);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
svc._emit("create", targetUser, true, I18n.tr("User created"), outerCb);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: chpasswdComp
|
||||
Process {
|
||||
id: chpasswdProc
|
||||
property string targetUser: ""
|
||||
property string targetPassword: ""
|
||||
property string op: "passwd"
|
||||
property var cb: null
|
||||
property string capturedErr: ""
|
||||
command: ["pkexec", "sh", "-c", "head -n1 | chpasswd"]
|
||||
stdinEnabled: true
|
||||
running: false
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: chpasswdProc.capturedErr = text || ""
|
||||
}
|
||||
onStarted: {
|
||||
chpasswdProc.write(chpasswdProc.targetUser + ":" + chpasswdProc.targetPassword + "\n");
|
||||
}
|
||||
onExited: exitCode => {
|
||||
const op = chpasswdProc.op;
|
||||
const targetUser = chpasswdProc.targetUser;
|
||||
const cb = chpasswdProc.cb;
|
||||
const err = (chpasswdProc.capturedErr || "").trim();
|
||||
Qt.callLater(() => chpasswdProc.destroy());
|
||||
|
||||
if (exitCode !== 0) {
|
||||
const msg = err || I18n.tr("Password change failed (exit %1)").arg(exitCode);
|
||||
if (op === "create") {
|
||||
if (typeof cb === "function")
|
||||
cb(false, msg);
|
||||
} else {
|
||||
root._emit("passwd", targetUser, false, msg, cb);
|
||||
}
|
||||
} else {
|
||||
root.refresh();
|
||||
if (op === "create") {
|
||||
if (typeof cb === "function")
|
||||
cb(true, I18n.tr("Password set"));
|
||||
} else {
|
||||
root._emit("passwd", targetUser, true, I18n.tr("Password updated"), cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: userdelComp
|
||||
Process {
|
||||
id: userdelProc
|
||||
property string targetUser: ""
|
||||
property var cb: null
|
||||
property string capturedErr: ""
|
||||
running: false
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: userdelProc.capturedErr = text || ""
|
||||
}
|
||||
onExited: exitCode => {
|
||||
const targetUser = userdelProc.targetUser;
|
||||
const cb = userdelProc.cb;
|
||||
const err = (userdelProc.capturedErr || "").trim();
|
||||
Qt.callLater(() => userdelProc.destroy());
|
||||
|
||||
if (exitCode !== 0) {
|
||||
root._emit("delete", targetUser, false, err || I18n.tr("userdel failed (exit %1)").arg(exitCode), cb);
|
||||
} else {
|
||||
root.refresh();
|
||||
root._emit("delete", targetUser, true, I18n.tr("User deleted"), cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: adminToggleComp
|
||||
Process {
|
||||
id: adminToggleProc
|
||||
property string targetUser: ""
|
||||
property bool makeAdmin: false
|
||||
property var cb: null
|
||||
property string capturedErr: ""
|
||||
running: false
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: adminToggleProc.capturedErr = text || ""
|
||||
}
|
||||
onExited: exitCode => {
|
||||
const targetUser = adminToggleProc.targetUser;
|
||||
const makeAdmin = adminToggleProc.makeAdmin;
|
||||
const cb = adminToggleProc.cb;
|
||||
const err = (adminToggleProc.capturedErr || "").trim();
|
||||
Qt.callLater(() => adminToggleProc.destroy());
|
||||
|
||||
if (exitCode !== 0) {
|
||||
root._emit("admin", targetUser, false, err || I18n.tr("usermod failed (exit %1)").arg(exitCode), cb);
|
||||
} else {
|
||||
root.refresh();
|
||||
root._emit("admin", targetUser, true, makeAdmin ? I18n.tr("Granted administrator privileges") : I18n.tr("Removed administrator privileges"), cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _runUseradd(username, password, addToAdmin, callback) {
|
||||
const proc = useraddComp.createObject(root, {
|
||||
command: ["pkexec", "useradd", "-m", "-s", "/bin/bash", username],
|
||||
targetUser: username,
|
||||
targetPassword: password,
|
||||
addAdmin: addToAdmin,
|
||||
cb: callback
|
||||
});
|
||||
proc.running = true;
|
||||
}
|
||||
|
||||
function _runChpasswd(username, password, op, callback) {
|
||||
const proc = chpasswdComp.createObject(root, {
|
||||
targetUser: username,
|
||||
targetPassword: password,
|
||||
op: op,
|
||||
cb: callback
|
||||
});
|
||||
proc.running = true;
|
||||
}
|
||||
|
||||
function _runUserdel(username, callback) {
|
||||
const proc = userdelComp.createObject(root, {
|
||||
command: ["pkexec", "userdel", "-r", username],
|
||||
targetUser: username,
|
||||
cb: callback
|
||||
});
|
||||
proc.running = true;
|
||||
}
|
||||
|
||||
function _runAdminToggle(username, makeAdmin, callback) {
|
||||
const cmd = makeAdmin ? ["pkexec", "usermod", "-aG", root.adminGroup, username] : ["pkexec", "gpasswd", "-d", username, root.adminGroup];
|
||||
const proc = adminToggleComp.createObject(root, {
|
||||
command: cmd,
|
||||
targetUser: username,
|
||||
makeAdmin: makeAdmin,
|
||||
cb: callback
|
||||
});
|
||||
proc.running = true;
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
}
|
||||
Reference in New Issue
Block a user