Skip to content

Commit ad23718

Browse files
committed
UserSettings: Add new user settings GUI application
Adds a new Users Settings application that provides a graphical interface for managing system user accounts, with feature parity with the useradd/userdel/usermod CLI utilities.
1 parent 985843f commit ad23718

19 files changed

+1000
-0
lines changed

Base/res/apps/UsersSettings.af

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[App]
2+
Name=Users Settings
3+
Executable=/bin/UsersSettings
4+
Category=Settings
5+
Description=Configure users
6+
ExcludeFromSystemMenu=true
7+
RequiresRoot=true
162 Bytes
Loading
273 Bytes
Loading
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
## Name
2+
3+
![Icon](/res/icons/16x16/app-user-settings.png) Users Settings
4+
5+
[Open](launch:///bin/UsersSettings)
6+
7+
## Synopsis
8+
9+
```**sh
10+
$ UsersSettings
11+
```
12+
13+
## Description
14+
15+
`Users Settings` is an application for managing user accounts on the system.
16+
17+
### User List
18+
19+
The left panel lists all user accounts present on the system. Selecting a user displays their details in the right panel.
20+
21+
### Adding a User
22+
23+
Click the **Add** button below the user list to open the _Add User_ dialog. Fill in:
24+
25+
- **Account Type**_Standard_ for a regular user or _Administrator_ to also grant membership of the `wheel` group.
26+
- **Full Name** — The user's display name (optional).
27+
- **Username** — The login name for the new account.
28+
- **Password** — The initial password for the account.
29+
30+
New users are automatically added to the `users`, `window`, `audio`, `lookup` and `phys` groups, so they can login to their own desktop environment.
31+
32+
### Editing a User
33+
34+
Select a user from the list to view and edit their details:
35+
36+
- **Full Name** — The user's display name stored in the GECOS field.
37+
- **Shell** — User's login shell.
38+
- **Account Type**_Standard_ or _Administrator_. Changing this adds or removes the user from the `wheel` group.
39+
40+
Click **Apply Changes** to save any modifications.
41+
42+
### Changing a Password
43+
44+
Click the **Change Password...** button in the user details panel to open the _Change Password_ dialog. Enter and confirm the new password, then click **OK**.
45+
46+
### Deleting a User
47+
48+
Select a user from the list and click the **Delete** button. A confirmation dialog will appear before the account and its home directory are permanently removed.

Userland/Applications/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ add_subdirectory(Terminal)
4545
add_subdirectory(TerminalSettings)
4646
add_subdirectory(TextEditor)
4747
add_subdirectory(ThemeEditor)
48+
add_subdirectory(UsersSettings)
4849
add_subdirectory(VideoPlayer)
4950
add_subdirectory(Weather)
5051
add_subdirectory(Welcome)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
serenity_component(
2+
UsersSettings
3+
RECOMMENDED
4+
TARGETS UsersSettings
5+
)
6+
7+
compile_gml(UsersTab.gml UsersTabGML.cpp)
8+
compile_gml(UserAddDialog.gml UserAddDialogGML.cpp)
9+
compile_gml(UserDetailsWidget.gml UserDetailsWidgetGML.cpp)
10+
compile_gml(ChangePasswordDialog.gml ChangePasswordDialogGML.cpp)
11+
12+
set(SOURCES
13+
main.cpp
14+
UsersTab.cpp
15+
UsersTabGML.cpp
16+
UserAddDialog.cpp
17+
UserAddDialogGML.cpp
18+
UserDetailsWidget.cpp
19+
UserDetailsWidgetGML.cpp
20+
ChangePasswordDialogGML.cpp
21+
)
22+
23+
serenity_app(UsersSettings ICON app-user-settings)
24+
target_link_libraries(UsersSettings PRIVATE LibCore LibGfx LibGUI LibMain)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
@UsersSettings::ChangePasswordDialog {
2+
fixed_width: 280
3+
fixed_height: 85
4+
fill_with_background_color: true
5+
layout: @GUI::VerticalBoxLayout {
6+
margins: [4]
7+
}
8+
9+
@GUI::Widget {
10+
fixed_height: 24
11+
layout: @GUI::HorizontalBoxLayout {}
12+
13+
@GUI::Label {
14+
text: "New Password:"
15+
text_alignment: "CenterLeft"
16+
fixed_width: 120
17+
}
18+
19+
@GUI::PasswordBox {
20+
name: "new_password_textbox"
21+
}
22+
}
23+
24+
@GUI::Widget {
25+
fixed_height: 24
26+
layout: @GUI::HorizontalBoxLayout {}
27+
28+
@GUI::Label {
29+
text: "Confirm Password:"
30+
text_alignment: "CenterLeft"
31+
fixed_width: 120
32+
}
33+
34+
@GUI::PasswordBox {
35+
name: "confirm_password_textbox"
36+
}
37+
}
38+
39+
@GUI::Widget {
40+
fixed_height: 24
41+
layout: @GUI::HorizontalBoxLayout {}
42+
43+
@GUI::Layout::Spacer {}
44+
45+
@GUI::DialogButton {
46+
name: "ok_button"
47+
text: "OK"
48+
fixed_width: 75
49+
}
50+
51+
@GUI::DialogButton {
52+
name: "cancel_button"
53+
text: "Cancel"
54+
fixed_width: 75
55+
}
56+
}
57+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (c) 2026, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#pragma once
8+
9+
#include <LibGUI/Widget.h>
10+
11+
namespace UsersSettings {
12+
13+
class ChangePasswordDialog final : public GUI::Widget {
14+
C_OBJECT(ChangePasswordDialog)
15+
public:
16+
static ErrorOr<NonnullRefPtr<ChangePasswordDialog>> try_create();
17+
18+
private:
19+
ChangePasswordDialog() = default;
20+
};
21+
22+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright (c) 2026, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#pragma once
8+
9+
#include <AK/Array.h>
10+
#include <AK/StringView.h>
11+
12+
namespace UsersSettings {
13+
14+
static constexpr u32 MIN_NORMAL_UID = 100;
15+
static constexpr Array ACCOUNT_TYPE_NAMES = { "Standard"sv, "Administrator"sv };
16+
static constexpr StringView WHEEL_GROUP_NAME = "wheel"sv;
17+
static constexpr Array DEFAULT_USER_GROUPS = { "users"sv, "window"sv, "audio"sv, "lookup"sv, "phys"sv };
18+
19+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright (c) 2026, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#include "UserAddDialog.h"
8+
#include "Constants.h"
9+
#include <LibCore/Account.h>
10+
#include <LibCore/Command.h>
11+
#include <LibCore/SecretString.h>
12+
#include <LibCore/System.h>
13+
#include <LibGUI/Button.h>
14+
#include <LibGUI/Dialog.h>
15+
#include <LibGUI/ItemListModel.h>
16+
#include <LibGUI/MessageBox.h>
17+
18+
namespace UsersSettings {
19+
20+
ErrorOr<void> UserAddDialog::initialize()
21+
{
22+
m_account_type_combobox = find_descendant_of_type_named<GUI::ComboBox>("account_type_combobox");
23+
m_full_name_textbox = find_descendant_of_type_named<GUI::TextBox>("full_name_textbox");
24+
m_username_textbox = find_descendant_of_type_named<GUI::TextBox>("username_textbox");
25+
m_password_textbox = find_descendant_of_type_named<GUI::PasswordBox>("password_textbox");
26+
27+
m_account_type_combobox->set_model(*GUI::ItemListModel<StringView, Array<StringView, 2>>::create(ACCOUNT_TYPE_NAMES));
28+
m_account_type_combobox->set_only_allow_values_from_model(true);
29+
m_account_type_combobox->set_selected_index(0);
30+
return {};
31+
}
32+
33+
ErrorOr<Optional<String>> UserAddDialog::show(GUI::Window* parent_window)
34+
{
35+
auto dialog = TRY(GUI::Dialog::try_create(parent_window));
36+
dialog->set_title("Add User");
37+
dialog->resize(260, 136);
38+
dialog->set_resizable(false);
39+
40+
auto widget = TRY(UserAddDialog::try_create());
41+
dialog->set_main_widget(widget);
42+
43+
Optional<String> created_username;
44+
45+
auto& ok_button = *widget->find_descendant_of_type_named<GUI::Button>("ok_button");
46+
ok_button.on_click = [&](auto) {
47+
auto username = MUST(String::from_byte_string(widget->m_username_textbox->text()));
48+
if (username.is_empty()) {
49+
GUI::MessageBox::show(dialog, "Username must not be empty."sv, "Error"sv, GUI::MessageBox::Type::Error);
50+
return;
51+
}
52+
53+
if (auto result = widget->add_user(); result.is_error()) {
54+
GUI::MessageBox::show_error(dialog, MUST(String::formatted("Failed to add user: {}", result.error())));
55+
return;
56+
}
57+
58+
created_username = username;
59+
dialog->done(GUI::Dialog::ExecResult::OK);
60+
};
61+
ok_button.set_default(true);
62+
63+
auto& cancel_button = *widget->find_descendant_of_type_named<GUI::Button>("cancel_button");
64+
cancel_button.on_click = [dialog](auto) {
65+
dialog->done(GUI::Dialog::ExecResult::Cancel);
66+
};
67+
68+
dialog->exec();
69+
return created_username;
70+
}
71+
72+
ErrorOr<void> UserAddDialog::add_user()
73+
{
74+
auto full_name = TRY(String::from_byte_string(m_full_name_textbox->text()));
75+
auto username = TRY(String::from_byte_string(m_username_textbox->text()));
76+
auto password = Core::SecretString::take_ownership(m_password_textbox->text().to_byte_buffer());
77+
bool is_admin = m_account_type_combobox->selected_index() == 1;
78+
79+
// Create user via useradd.
80+
if (!full_name.is_empty()) {
81+
TRY(Core::command("useradd"sv, { "--create-home"sv, "--gecos"sv, full_name.bytes_as_string_view(), username.bytes_as_string_view() }, {}));
82+
} else {
83+
TRY(Core::command("useradd"sv, { "--create-home"sv, username.bytes_as_string_view() }, {}));
84+
}
85+
86+
// Update groups and password via Core::Account.
87+
auto account = TRY(Core::Account::from_name(username));
88+
for (auto group_name : DEFAULT_USER_GROUPS) {
89+
auto maybe_group = Core::System::getgrnam(group_name);
90+
if (!maybe_group.is_error() && maybe_group.value().has_value())
91+
account.add_extra_gid(maybe_group.value()->gr_gid);
92+
}
93+
if (is_admin) {
94+
auto maybe_wheel = Core::System::getgrnam(WHEEL_GROUP_NAME);
95+
if (!maybe_wheel.is_error() && maybe_wheel.value().has_value())
96+
account.add_extra_gid(maybe_wheel.value()->gr_gid);
97+
}
98+
TRY(account.set_password(password));
99+
TRY(account.sync());
100+
101+
return {};
102+
}
103+
104+
}

0 commit comments

Comments
 (0)