summaryrefslogtreecommitdiff
path: root/raveos-calamares-module/raveusers
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-calamares-module/raveusers')
-rw-r--r--raveos-calamares-module/raveusers/CMakeLists.txt37
-rw-r--r--raveos-calamares-module/raveusers/RaveUsersBackend.cpp242
-rw-r--r--raveos-calamares-module/raveusers/RaveUsersBackend.h89
-rw-r--r--raveos-calamares-module/raveusers/RaveUsersJobs.cpp294
-rw-r--r--raveos-calamares-module/raveusers/RaveUsersJobs.h67
-rw-r--r--raveos-calamares-module/raveusers/RaveUsersViewStep.cpp253
-rw-r--r--raveos-calamares-module/raveusers/RaveUsersViewStep.h47
-rw-r--r--raveos-calamares-module/raveusers/module.desc5
-rw-r--r--raveos-calamares-module/raveusers/raveusers.conf26
-rw-r--r--raveos-calamares-module/raveusers/ui/PasswordField.qml41
-rw-r--r--raveos-calamares-module/raveusers/ui/Translations.qml58
-rw-r--r--raveos-calamares-module/raveusers/ui/qmldir2
-rwxr-xr-xraveos-calamares-module/raveusers/ui/raveos-logo.pngbin0 -> 302684 bytes
-rw-r--r--raveos-calamares-module/raveusers/ui/raveusers.qml286
14 files changed, 1447 insertions, 0 deletions
diff --git a/raveos-calamares-module/raveusers/CMakeLists.txt b/raveos-calamares-module/raveusers/CMakeLists.txt
new file mode 100644
index 0000000..680056a
--- /dev/null
+++ b/raveos-calamares-module/raveusers/CMakeLists.txt
@@ -0,0 +1,37 @@
+find_package(Qt6 REQUIRED COMPONENTS Core Widgets Quick QuickWidgets)
+
+calamares_add_plugin(raveusers
+ TYPE viewmodule
+ EXPORT_MACRO PLUGINDLLEXPORT_PRO
+ SOURCES
+ RaveUsersViewStep.cpp
+ RaveUsersBackend.cpp
+ RaveUsersJobs.cpp
+ LINK_LIBRARIES
+ Qt6::Core
+ Qt6::Widgets
+ Qt6::Quick
+ Qt6::QuickWidgets
+ Calamares::calamares
+ crypt
+)
+
+install(
+ FILES module.desc
+ DESTINATION lib/calamares/modules/raveusers
+)
+
+install(
+ FILES raveusers.conf
+ DESTINATION share/calamares/modules
+)
+
+install(
+ FILES
+ ui/raveusers.qml
+ ui/PasswordField.qml
+ ui/Translations.qml
+ ui/qmldir
+ ui/raveos-logo.png
+ DESTINATION lib/calamares/modules/raveusers/ui
+)
diff --git a/raveos-calamares-module/raveusers/RaveUsersBackend.cpp b/raveos-calamares-module/raveusers/RaveUsersBackend.cpp
new file mode 100644
index 0000000..6f8eea6
--- /dev/null
+++ b/raveos-calamares-module/raveusers/RaveUsersBackend.cpp
@@ -0,0 +1,242 @@
+#include "RaveUsersBackend.h"
+
+#include <QRegularExpression>
+
+namespace
+{
+const QRegularExpression USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" );
+constexpr int USERNAME_MAX_LENGTH = 31;
+
+const QRegularExpression HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" );
+constexpr int HOSTNAME_MIN_LENGTH = 2;
+constexpr int HOSTNAME_MAX_LENGTH = 63;
+}
+
+RaveUsersBackend::RaveUsersBackend( QObject* parent )
+ : QObject( parent )
+ , m_forbiddenLoginNames { QStringLiteral( "root" ), QStringLiteral( "nobody" ) }
+ , m_forbiddenHostNames { QStringLiteral( "localhost" ) }
+{
+ m_defaultGroups = { "lp", "video", "network", "storage", "wheel", "audio" };
+}
+
+QString
+RaveUsersBackend::usernameStatus() const
+{
+ if ( m_username.isEmpty() )
+ {
+ return QString();
+ }
+ if ( m_username.length() > USERNAME_MAX_LENGTH )
+ {
+ return tr( "Your username is too long." );
+ }
+ if ( m_username.indexOf( QRegularExpression( "^[a-z_]" ) ) != 0 )
+ {
+ return tr( "Your username must start with a lowercase letter or underscore." );
+ }
+ if ( m_username.indexOf( USERNAME_RX ) != 0 )
+ {
+ return tr( "Only lowercase letters, numbers, underscore and hyphen are allowed." );
+ }
+ if ( m_forbiddenLoginNames.contains( m_username, Qt::CaseInsensitive ) )
+ {
+ return tr( "'%1' is not allowed as username." ).arg( m_username );
+ }
+ return QString();
+}
+
+QString
+RaveUsersBackend::hostnameStatus() const
+{
+ if ( m_hostname.isEmpty() )
+ {
+ return QString();
+ }
+ if ( m_hostname.length() < HOSTNAME_MIN_LENGTH )
+ {
+ return tr( "Your hostname is too short." );
+ }
+ if ( m_hostname.length() > HOSTNAME_MAX_LENGTH )
+ {
+ return tr( "Your hostname is too long." );
+ }
+ if ( m_forbiddenHostNames.contains( m_hostname, Qt::CaseInsensitive ) )
+ {
+ return tr( "'%1' is not allowed as hostname." ).arg( m_hostname );
+ }
+ if ( m_hostname.indexOf( HOSTNAME_RX ) != 0 )
+ {
+ return tr( "Only letters, numbers, underscore and hyphen are allowed." );
+ }
+ return QString();
+}
+
+QString
+RaveUsersBackend::passwordStatus() const
+{
+ if ( m_password.isEmpty() )
+ {
+ return QString();
+ }
+ if ( m_password != m_passwordSecondary )
+ {
+ return tr( "Your passwords do not match!" );
+ }
+ return QString();
+}
+
+QString
+RaveUsersBackend::rootPasswordStatus() const
+{
+ if ( m_reuseRootPassword )
+ {
+ return QString();
+ }
+ if ( m_rootPassword.isEmpty() )
+ {
+ return QString();
+ }
+ if ( m_rootPassword != m_rootPasswordSecondary )
+ {
+ return tr( "Your root passwords do not match!" );
+ }
+ return QString();
+}
+
+bool
+RaveUsersBackend::isReady() const
+{
+ if ( m_username.isEmpty() || !usernameStatus().isEmpty() )
+ {
+ return false;
+ }
+ if ( m_hostname.isEmpty() || !hostnameStatus().isEmpty() )
+ {
+ return false;
+ }
+ if ( m_password.isEmpty() || !passwordStatus().isEmpty() )
+ {
+ return false;
+ }
+ if ( m_setRootPassword && !m_reuseRootPassword )
+ {
+ if ( m_rootPassword.isEmpty() || !rootPasswordStatus().isEmpty() )
+ {
+ return false;
+ }
+ }
+ return true;
+}
+
+QStringList
+RaveUsersBackend::groupsForThisUser() const
+{
+ QStringList groups = m_defaultGroups;
+ groups.removeDuplicates();
+ return groups;
+}
+
+void
+RaveUsersBackend::setUsername( const QString& s )
+{
+ if ( s != m_username )
+ {
+ m_username = s;
+ emit usernameChanged();
+ emit statusChanged();
+
+ if ( !m_customHostname )
+ {
+ const QString generated = s.isEmpty() ? QString() : s + QStringLiteral( "-RaveOS-PC" );
+ if ( generated != m_hostname )
+ {
+ m_hostname = generated;
+ emit hostnameChanged();
+ emit statusChanged();
+ }
+ }
+
+ checkReady();
+ }
+}
+
+void
+RaveUsersBackend::setHostname( const QString& s )
+{
+ m_customHostname = true;
+ if ( s != m_hostname )
+ {
+ m_hostname = s;
+ emit hostnameChanged();
+ emit statusChanged();
+ checkReady();
+ }
+}
+
+void
+RaveUsersBackend::setPassword( const QString& s )
+{
+ if ( s != m_password )
+ {
+ m_password = s;
+ emit statusChanged();
+ checkReady();
+ }
+}
+
+void
+RaveUsersBackend::setPasswordSecondary( const QString& s )
+{
+ if ( s != m_passwordSecondary )
+ {
+ m_passwordSecondary = s;
+ emit statusChanged();
+ checkReady();
+ }
+}
+
+void
+RaveUsersBackend::setRootPasswordText( const QString& s )
+{
+ if ( s != m_rootPassword )
+ {
+ m_rootPassword = s;
+ emit statusChanged();
+ checkReady();
+ }
+}
+
+void
+RaveUsersBackend::setRootPasswordSecondaryText( const QString& s )
+{
+ if ( s != m_rootPasswordSecondary )
+ {
+ m_rootPasswordSecondary = s;
+ emit statusChanged();
+ checkReady();
+ }
+}
+
+void
+RaveUsersBackend::setReuseRootPassword( bool b )
+{
+ if ( b != m_reuseRootPassword )
+ {
+ m_reuseRootPassword = b;
+ emit reuseRootPasswordChanged();
+ emit statusChanged();
+ checkReady();
+ }
+}
+
+void
+RaveUsersBackend::checkReady()
+{
+ const bool ready = isReady();
+ if ( ready != m_lastReady )
+ {
+ m_lastReady = ready;
+ emit readyChanged();
+ }
+}
diff --git a/raveos-calamares-module/raveusers/RaveUsersBackend.h b/raveos-calamares-module/raveusers/RaveUsersBackend.h
new file mode 100644
index 0000000..4e06f47
--- /dev/null
+++ b/raveos-calamares-module/raveusers/RaveUsersBackend.h
@@ -0,0 +1,89 @@
+#pragma once
+
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+class RaveUsersBackend : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY( QString username READ username NOTIFY usernameChanged )
+ Q_PROPERTY( QString hostname READ hostname NOTIFY hostnameChanged )
+ Q_PROPERTY( QString usernameStatus READ usernameStatus NOTIFY statusChanged )
+ Q_PROPERTY( QString hostnameStatus READ hostnameStatus NOTIFY statusChanged )
+ Q_PROPERTY( QString passwordStatus READ passwordStatus NOTIFY statusChanged )
+ Q_PROPERTY( QString rootPasswordStatus READ rootPasswordStatus NOTIFY statusChanged )
+ Q_PROPERTY( bool ready READ isReady NOTIFY readyChanged )
+ Q_PROPERTY( bool reuseRootPassword READ reuseRootPassword NOTIFY reuseRootPasswordChanged )
+ Q_PROPERTY( bool rootReuseVisible READ rootReuseVisible CONSTANT )
+
+public:
+ explicit RaveUsersBackend( QObject* parent = nullptr );
+
+ QString username() const { return m_username; }
+ QString hostname() const { return m_hostname; }
+ QString usernameStatus() const;
+ QString hostnameStatus() const;
+ QString passwordStatus() const;
+ QString rootPasswordStatus() const;
+ bool isReady() const;
+ bool reuseRootPassword() const { return m_reuseRootPassword; }
+ bool rootReuseVisible() const { return m_setRootPassword; }
+
+ QString userPassword() const { return m_password; }
+ QString rootPassword() const { return m_reuseRootPassword ? m_password : m_rootPassword; }
+
+ QString userShell() const { return m_userShell; }
+ QStringList groupsForThisUser() const;
+ QString sudoersGroup() const { return m_sudoersGroup; }
+ bool sudoersConfigureWithGroup() const { return m_sudoersConfigureWithGroup; }
+ int homeUMask() const { return m_homeUMask; }
+ QStringList defaultGroups() const { return m_defaultGroups; }
+
+ Q_INVOKABLE void setUsername( const QString& s );
+ Q_INVOKABLE void setHostname( const QString& s );
+ Q_INVOKABLE void setPassword( const QString& s );
+ Q_INVOKABLE void setPasswordSecondary( const QString& s );
+ Q_INVOKABLE void setRootPasswordText( const QString& s );
+ Q_INVOKABLE void setRootPasswordSecondaryText( const QString& s );
+ Q_INVOKABLE void setReuseRootPassword( bool b );
+
+ void setForbiddenLoginNames( const QStringList& names ) { m_forbiddenLoginNames = names; }
+ void setForbiddenHostNames( const QStringList& names ) { m_forbiddenHostNames = names; }
+ void setUserShell( const QString& shell ) { m_userShell = shell; }
+ void setSudoersGroup( const QString& group ) { m_sudoersGroup = group; }
+ void setSudoersConfigureWithGroup( bool b ) { m_sudoersConfigureWithGroup = b; }
+ void setSetRootPassword( bool b ) { m_setRootPassword = b; }
+ void setHomeUMask( int umask ) { m_homeUMask = umask; }
+ void setDefaultGroups( const QStringList& groups ) { m_defaultGroups = groups; }
+
+signals:
+ void usernameChanged();
+ void hostnameChanged();
+ void statusChanged();
+ void readyChanged();
+ void reuseRootPasswordChanged();
+
+private:
+ void checkReady();
+
+ QString m_username;
+ QString m_hostname;
+ QString m_password;
+ QString m_passwordSecondary;
+ QString m_rootPassword;
+ QString m_rootPasswordSecondary;
+ bool m_customHostname = false;
+ bool m_reuseRootPassword = true;
+ bool m_setRootPassword = true;
+
+ QStringList m_forbiddenLoginNames;
+ QStringList m_forbiddenHostNames;
+ QString m_userShell = QStringLiteral( "/bin/bash" );
+ QString m_sudoersGroup;
+ bool m_sudoersConfigureWithGroup = true;
+ int m_homeUMask = -1;
+ QStringList m_defaultGroups;
+ bool m_lastReady = false;
+};
diff --git a/raveos-calamares-module/raveusers/RaveUsersJobs.cpp b/raveos-calamares-module/raveusers/RaveUsersJobs.cpp
new file mode 100644
index 0000000..1923c38
--- /dev/null
+++ b/raveos-calamares-module/raveusers/RaveUsersJobs.cpp
@@ -0,0 +1,294 @@
+#include "RaveUsersJobs.h"
+
+#include "RaveUsersBackend.h"
+
+#include "GlobalStorage.h"
+#include "JobQueue.h"
+#include "utils/Logger.h"
+#include "utils/Permissions.h"
+#include "utils/System.h"
+
+#include <QDir>
+#include <QFile>
+#include <QRandomGenerator>
+#include <QSet>
+
+#include <crypt.h>
+
+using WriteMode = Calamares::System::WriteMode;
+
+static QDir
+targetRootDir()
+{
+ Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
+ return QDir( gs->value( "rootMountPoint" ).toString() );
+}
+
+RaveUsersCreateUserJob::RaveUsersCreateUserJob( const RaveUsersBackend* backend )
+ : m_backend( backend )
+{
+}
+
+QString
+RaveUsersCreateUserJob::prettyName() const
+{
+ return tr( "Create user %1" ).arg( m_backend->username() );
+}
+
+QString
+RaveUsersCreateUserJob::prettyStatusMessage() const
+{
+ return tr( "Creating user %1…", "@status" ).arg( m_backend->username() );
+}
+
+Calamares::JobResult
+RaveUsersCreateUserJob::exec()
+{
+ const QString login = m_backend->username();
+
+ QStringList useraddCommand { "useradd", "-m", "-U" };
+ const QString shell = m_backend->userShell();
+ if ( !shell.isEmpty() )
+ {
+ useraddCommand << "-s" << shell;
+ }
+
+ useraddCommand << "-c" << login;
+ if ( m_backend->homeUMask() >= 0 )
+ {
+ useraddCommand << "-K" << QStringLiteral( "UMASK=%1" ).arg( m_backend->homeUMask(), 3, 8, QChar( '0' ) );
+ }
+ useraddCommand << login;
+
+ auto result = Calamares::System::instance()->targetEnvCommand( useraddCommand );
+ if ( result.getExitCode() )
+ {
+ cError() << "useradd failed" << result.getExitCode();
+ return result.explainProcess( useraddCommand, std::chrono::seconds( 10 ) );
+ }
+
+ const QStringList groups = m_backend->groupsForThisUser();
+ if ( !groups.isEmpty() )
+ {
+ QStringList usermodCommand { "usermod", "-aG", groups.join( ',' ), login };
+ result = Calamares::System::instance()->targetEnvCommand( usermodCommand );
+ if ( result.getExitCode() )
+ {
+ cError() << "usermod failed" << result.getExitCode();
+ return result.explainProcess( usermodCommand, std::chrono::seconds( 10 ) );
+ }
+ }
+
+ const QString userGroup = QStringLiteral( "%1:%2" ).arg( login, login );
+ const QString homeDir = QStringLiteral( "/home/%1" ).arg( login );
+ result = Calamares::System::instance()->targetEnvCommand( { "chown", "-R", userGroup, homeDir } );
+ if ( result.getExitCode() )
+ {
+ cError() << "chown failed" << result.getExitCode();
+ return result.explainProcess( "chown", std::chrono::seconds( 10 ) );
+ }
+
+ return Calamares::JobResult::ok();
+}
+
+RaveUsersSetPasswordJob::RaveUsersSetPasswordJob( const QString& userName, const QString& password )
+ : m_userName( userName )
+ , m_password( password )
+{
+}
+
+QString
+RaveUsersSetPasswordJob::prettyName() const
+{
+ return tr( "Set password for user %1" ).arg( m_userName );
+}
+
+QString
+RaveUsersSetPasswordJob::prettyStatusMessage() const
+{
+ return tr( "Setting password for user %1…", "@status" ).arg( m_userName );
+}
+
+static QString
+makeSha512Salt( int length = 16 )
+{
+ static const QString chars = QStringLiteral( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./" );
+ QString salt;
+ salt.reserve( length );
+ for ( int i = 0; i < length; ++i )
+ {
+ salt += chars.at( QRandomGenerator::global()->bounded( chars.size() ) );
+ }
+ return QStringLiteral( "$6$" ) + salt + QLatin1Char( '$' );
+}
+
+Calamares::JobResult
+RaveUsersSetPasswordJob::exec()
+{
+ QDir destDir = targetRootDir();
+ if ( !destDir.exists() )
+ {
+ return Calamares::JobResult::error( tr( "Bad destination system path." ),
+ tr( "rootMountPoint is %1" ).arg( destDir.absolutePath() ) );
+ }
+
+ if ( m_userName == QLatin1String( "root" ) && m_password.isEmpty() )
+ {
+ int ec = Calamares::System::instance()->targetEnvCall( { "usermod", "-p", "!", m_userName } );
+ if ( ec )
+ {
+ return Calamares::JobResult::error( tr( "Cannot disable root account." ),
+ tr( "usermod terminated with error code %1." ).arg( ec ) );
+ }
+ return Calamares::JobResult::ok();
+ }
+
+ const QString salt = makeSha512Salt();
+ const QByteArray hash = crypt( m_password.toUtf8().constData(), salt.toUtf8().constData() );
+ if ( hash.isEmpty() )
+ {
+ return Calamares::JobResult::error( tr( "Cannot hash password." ) );
+ }
+
+ int ec = Calamares::System::instance()->targetEnvCall(
+ { "usermod", "-p", QString::fromLatin1( hash ), m_userName } );
+ if ( ec )
+ {
+ return Calamares::JobResult::error( tr( "Cannot set password for user %1." ).arg( m_userName ),
+ tr( "usermod terminated with error code %1." ).arg( ec ) );
+ }
+
+ return Calamares::JobResult::ok();
+}
+
+RaveUsersSetHostnameJob::RaveUsersSetHostnameJob( const QString& hostname )
+ : m_hostname( hostname )
+{
+}
+
+QString
+RaveUsersSetHostnameJob::prettyName() const
+{
+ return tr( "Set hostname %1" ).arg( m_hostname );
+}
+
+Calamares::JobResult
+RaveUsersSetHostnameJob::exec()
+{
+ if ( !Calamares::System::instance()->createTargetFile( QStringLiteral( "/etc/hostname" ),
+ ( m_hostname + '\n' ).toUtf8(),
+ WriteMode::Overwrite ) )
+ {
+ return Calamares::JobResult::error( tr( "Cannot write hostname to target system" ) );
+ }
+
+ const QString hosts = QStringLiteral( R"(# Standard host addresses
+127.0.0.1 localhost
+::1 localhost ip6-localhost ip6-loopback
+ff02::1 ip6-allnodes
+ff02::2 ip6-allrouters
+# This host address
+127.0.1.1 %1
+)" )
+ .arg( m_hostname );
+
+ if ( !Calamares::System::instance()->createTargetFile(
+ QStringLiteral( "/etc/hosts" ), hosts.toUtf8(), WriteMode::Overwrite ) )
+ {
+ return Calamares::JobResult::error( tr( "Cannot write hosts file to target system" ) );
+ }
+
+ return Calamares::JobResult::ok();
+}
+
+RaveUsersSetupSudoJob::RaveUsersSetupSudoJob( const QString& group, bool configureWithGroup )
+ : m_group( group )
+ , m_configureWithGroup( configureWithGroup )
+{
+}
+
+QString
+RaveUsersSetupSudoJob::prettyName() const
+{
+ return tr( "Configuring sudo users…", "@status" );
+}
+
+Calamares::JobResult
+RaveUsersSetupSudoJob::exec()
+{
+ if ( m_group.isEmpty() )
+ {
+ return Calamares::JobResult::ok();
+ }
+
+ const QString designator = m_configureWithGroup ? QStringLiteral( "(ALL:ALL)" ) : QStringLiteral( "(ALL)" );
+ const QString line = QChar( '%' ) + QStringLiteral( "%1 ALL=%2 ALL\n" ).arg( m_group, designator );
+
+ auto fileResult = Calamares::System::instance()->createTargetFile(
+ QStringLiteral( "/etc/sudoers.d/10-installer" ), line.toUtf8().constData(), WriteMode::Overwrite );
+
+ if ( fileResult )
+ {
+ if ( !Calamares::Permissions::apply( fileResult.path(), 0440 ) )
+ {
+ return Calamares::JobResult::error( tr( "Cannot chmod sudoers file." ) );
+ }
+ }
+ else
+ {
+ return Calamares::JobResult::error( tr( "Cannot create sudoers file for writing." ) );
+ }
+
+ return Calamares::JobResult::ok();
+}
+
+RaveUsersSetupGroupsJob::RaveUsersSetupGroupsJob( const QStringList& groups )
+ : m_groups( groups )
+{
+}
+
+QString
+RaveUsersSetupGroupsJob::prettyName() const
+{
+ return tr( "Preparing groups…", "@status" );
+}
+
+Calamares::JobResult
+RaveUsersSetupGroupsJob::exec()
+{
+ QDir targetRoot = targetRootDir();
+
+ QSet< QString > existing;
+ QFile groupFile( targetRoot.absoluteFilePath( "etc/group" ) );
+ if ( groupFile.open( QIODevice::ReadOnly | QIODevice::Text ) )
+ {
+ while ( !groupFile.atEnd() )
+ {
+ const QString line = QString::fromLocal8Bit( groupFile.readLine() );
+ if ( line.startsWith( '#' ) )
+ {
+ continue;
+ }
+ const int idx = line.indexOf( ':' );
+ if ( idx > 0 )
+ {
+ existing.insert( line.left( idx ) );
+ }
+ }
+ }
+
+ for ( const QString& group : m_groups )
+ {
+ if ( group.isEmpty() || existing.contains( group ) )
+ {
+ continue;
+ }
+ QStringList cmd { "groupadd", "--system", group };
+ if ( Calamares::System::instance()->targetEnvCall( cmd ) )
+ {
+ cWarning() << "groupadd failed for" << group;
+ }
+ }
+
+ return Calamares::JobResult::ok();
+}
diff --git a/raveos-calamares-module/raveusers/RaveUsersJobs.h b/raveos-calamares-module/raveusers/RaveUsersJobs.h
new file mode 100644
index 0000000..c98a0b2
--- /dev/null
+++ b/raveos-calamares-module/raveusers/RaveUsersJobs.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include <Job.h>
+
+#include <QString>
+#include <QStringList>
+
+class RaveUsersBackend;
+
+class RaveUsersCreateUserJob : public Calamares::Job
+{
+public:
+ explicit RaveUsersCreateUserJob( const RaveUsersBackend* backend );
+ QString prettyName() const override;
+ QString prettyStatusMessage() const override;
+ Calamares::JobResult exec() override;
+
+private:
+ const RaveUsersBackend* m_backend;
+};
+
+class RaveUsersSetPasswordJob : public Calamares::Job
+{
+public:
+ RaveUsersSetPasswordJob( const QString& userName, const QString& password );
+ QString prettyName() const override;
+ QString prettyStatusMessage() const override;
+ Calamares::JobResult exec() override;
+
+private:
+ QString m_userName;
+ QString m_password;
+};
+
+class RaveUsersSetHostnameJob : public Calamares::Job
+{
+public:
+ explicit RaveUsersSetHostnameJob( const QString& hostname );
+ QString prettyName() const override;
+ Calamares::JobResult exec() override;
+
+private:
+ QString m_hostname;
+};
+
+class RaveUsersSetupSudoJob : public Calamares::Job
+{
+public:
+ RaveUsersSetupSudoJob( const QString& group, bool configureWithGroup );
+ QString prettyName() const override;
+ Calamares::JobResult exec() override;
+
+private:
+ QString m_group;
+ bool m_configureWithGroup;
+};
+
+class RaveUsersSetupGroupsJob : public Calamares::Job
+{
+public:
+ explicit RaveUsersSetupGroupsJob( const QStringList& groups );
+ QString prettyName() const override;
+ Calamares::JobResult exec() override;
+
+private:
+ QStringList m_groups;
+};
diff --git a/raveos-calamares-module/raveusers/RaveUsersViewStep.cpp b/raveos-calamares-module/raveusers/RaveUsersViewStep.cpp
new file mode 100644
index 0000000..4e21e65
--- /dev/null
+++ b/raveos-calamares-module/raveusers/RaveUsersViewStep.cpp
@@ -0,0 +1,253 @@
+#include "RaveUsersViewStep.h"
+
+#include "RaveUsersBackend.h"
+#include "RaveUsersJobs.h"
+
+#include "GlobalStorage.h"
+#include "JobQueue.h"
+#include "utils/Logger.h"
+#include "utils/String.h"
+#include "utils/Variant.h"
+
+#include <QDir>
+#include <QFile>
+#include <QLocale>
+#include <QQmlContext>
+#include <QQmlEngine>
+#include <QQuickItem>
+#include <QQuickWidget>
+#include <QVBoxLayout>
+
+CALAMARES_PLUGIN_FACTORY_DEFINITION( RaveUsersViewStepFactory, registerPlugin< RaveUsersViewStep >(); )
+
+RaveUsersViewStep::RaveUsersViewStep( QObject* parent )
+ : Calamares::ViewStep( parent )
+ , m_backend( new RaveUsersBackend( this ) )
+{
+ connect( m_backend, &RaveUsersBackend::readyChanged, this, [this]() {
+ emit nextStatusChanged( m_backend->isReady() );
+ } );
+}
+
+RaveUsersViewStep::~RaveUsersViewStep()
+{
+ if ( m_widget && m_widget->parent() == nullptr )
+ {
+ delete m_widget;
+ }
+}
+
+QString
+RaveUsersViewStep::prettyName() const
+{
+ return tr( "User" );
+}
+
+void
+RaveUsersViewStep::buildWidget()
+{
+ m_widget = new QWidget();
+ auto* layout = new QVBoxLayout( m_widget );
+ layout->setContentsMargins( 0, 0, 0, 0 );
+
+ m_quickWidget = new QQuickWidget();
+ m_quickWidget->setResizeMode( QQuickWidget::SizeRootObjectToView );
+
+ m_quickWidget->setClearColor( QColor( 0x40, 0x40, 0x40 ) );
+
+ QString modulePath = QStringLiteral( "/usr/lib/calamares/modules/raveusers/ui" );
+ if ( !QDir( modulePath ).exists() )
+ {
+ modulePath = QDir::currentPath() + QStringLiteral( "/raveusers/ui" );
+ }
+
+ m_quickWidget->engine()->addImportPath( modulePath );
+ m_quickWidget->rootContext()->setContextProperty( "backend", m_backend );
+
+ m_locale = getLocale();
+ m_quickWidget->rootContext()->setContextProperty( "systemLocale", m_locale );
+
+ QString qmlPath = QStringLiteral( "/usr/lib/calamares/modules/raveusers/ui/raveusers.qml" );
+ if ( !QFile::exists( qmlPath ) )
+ {
+ qmlPath = QDir::currentPath() + QStringLiteral( "/raveusers/ui/raveusers.qml" );
+ }
+
+ m_quickWidget->setSource( QUrl::fromLocalFile( qmlPath ) );
+ layout->addWidget( m_quickWidget );
+}
+
+QWidget*
+RaveUsersViewStep::widget()
+{
+ if ( !m_widget )
+ {
+ buildWidget();
+ }
+ return m_widget;
+}
+
+bool
+RaveUsersViewStep::isNextEnabled() const
+{
+ return m_backend && m_backend->isReady();
+}
+
+bool
+RaveUsersViewStep::isBackEnabled() const
+{
+ return true;
+}
+
+bool
+RaveUsersViewStep::isAtBeginning() const
+{
+ return true;
+}
+
+bool
+RaveUsersViewStep::isAtEnd() const
+{
+ return true;
+}
+
+Calamares::JobList
+RaveUsersViewStep::jobs() const
+{
+ Calamares::JobList jobs;
+
+ if ( !m_backend || !m_backend->isReady() )
+ {
+ return jobs;
+ }
+
+ if ( !m_backend->sudoersGroup().isEmpty() )
+ {
+ jobs.append( Calamares::job_ptr(
+ new RaveUsersSetupSudoJob( m_backend->sudoersGroup(), m_backend->sudoersConfigureWithGroup() ) ) );
+ }
+
+ jobs.append( Calamares::job_ptr( new RaveUsersSetupGroupsJob( m_backend->defaultGroups() ) ) );
+ jobs.append( Calamares::job_ptr( new RaveUsersCreateUserJob( m_backend ) ) );
+ jobs.append( Calamares::job_ptr( new RaveUsersSetPasswordJob( m_backend->username(), m_backend->userPassword() ) ) );
+ jobs.append( Calamares::job_ptr( new RaveUsersSetPasswordJob( QStringLiteral( "root" ), m_backend->rootPassword() ) ) );
+ jobs.append( Calamares::job_ptr( new RaveUsersSetHostnameJob( m_backend->hostname() ) ) );
+
+ return jobs;
+}
+
+void
+RaveUsersViewStep::onActivate()
+{
+ const QString newLocale = getLocale();
+ if ( newLocale != m_locale )
+ {
+ m_locale = newLocale;
+ if ( m_quickWidget )
+ {
+ m_quickWidget->rootContext()->setContextProperty( "systemLocale", m_locale );
+ }
+ }
+
+ if ( m_quickWidget )
+ {
+ QQuickItem* root = m_quickWidget->rootObject();
+ if ( root )
+ {
+ QMetaObject::invokeMethod( root, "updateLanguage", Qt::DirectConnection );
+ }
+ }
+}
+
+void
+RaveUsersViewStep::onLeave()
+{
+ if ( !m_backend || !m_backend->isReady() )
+ {
+ return;
+ }
+
+ Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
+ if ( !gs )
+ {
+ return;
+ }
+
+ const QString username = m_backend->username();
+ gs->insert( "username", username );
+ gs->insert( "hostname", m_backend->hostname() );
+ gs->insert( "fullname", username );
+ gs->insert( "password", Calamares::String::obscure( m_backend->userPassword() ) );
+ gs->insert( "setRootPassword", m_backend->rootReuseVisible() );
+ gs->insert( "reuseRootPassword", m_backend->reuseRootPassword() );
+}
+
+void
+RaveUsersViewStep::setConfigurationMap( const QVariantMap& config )
+{
+ const QVariantMap userSettings = config.value( "user" ).toMap();
+ QString shell = Calamares::getString( userSettings, "shell" );
+ m_backend->setUserShell( shell.isEmpty() ? QStringLiteral( "/bin/bash" ) : shell );
+
+ QStringList forbiddenLogin = Calamares::getStringList( userSettings, "forbidden_names" );
+ forbiddenLogin << QStringLiteral( "root" ) << QStringLiteral( "nobody" );
+ forbiddenLogin.removeDuplicates();
+ m_backend->setForbiddenLoginNames( forbiddenLogin );
+
+ const QVariantMap hostnameSettings = config.value( "hostname" ).toMap();
+ QStringList forbiddenHost = Calamares::getStringList( hostnameSettings, "forbidden_names" );
+ forbiddenHost << QStringLiteral( "localhost" );
+ forbiddenHost.removeDuplicates();
+ m_backend->setForbiddenHostNames( forbiddenHost );
+
+ m_backend->setSudoersGroup( Calamares::getString( config, "sudoersGroup" ) );
+ m_backend->setSudoersConfigureWithGroup( Calamares::getBool( config, "sudoersConfigureWithGroup", true ) );
+ m_backend->setSetRootPassword( Calamares::getBool( config, "setRootPassword", true ) );
+
+ QStringList groups;
+ for ( const auto& v : config.value( "defaultGroups" ).toList() )
+ {
+ if ( v.typeId() == QMetaType::QString )
+ {
+ groups << v.toString();
+ }
+ else if ( v.typeId() == QMetaType::QVariantMap )
+ {
+ const QString name = v.toMap().value( "name" ).toString();
+ if ( !name.isEmpty() )
+ {
+ groups << name;
+ }
+ }
+ }
+ if ( !groups.isEmpty() )
+ {
+ m_backend->setDefaultGroups( groups );
+ }
+}
+
+QString
+RaveUsersViewStep::getLocale() const
+{
+ Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
+ if ( gs )
+ {
+ if ( gs->contains( "locale" ) )
+ {
+ const QString loc = gs->value( "locale" ).toString();
+ if ( !loc.isEmpty() )
+ {
+ return loc;
+ }
+ }
+ if ( gs->contains( "localeConf" ) )
+ {
+ const QString lang = gs->value( "localeConf" ).toMap().value( "LANG" ).toString();
+ if ( !lang.isEmpty() )
+ {
+ return lang;
+ }
+ }
+ }
+ return QLocale::system().name();
+}
diff --git a/raveos-calamares-module/raveusers/RaveUsersViewStep.h b/raveos-calamares-module/raveusers/RaveUsersViewStep.h
new file mode 100644
index 0000000..67593ac
--- /dev/null
+++ b/raveos-calamares-module/raveusers/RaveUsersViewStep.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <QObject>
+#include <QString>
+#include <QStringList>
+#include <QWidget>
+
+#include <Job.h>
+#include <utils/PluginFactory.h>
+#include <viewpages/ViewStep.h>
+
+class QQuickWidget;
+class RaveUsersBackend;
+
+class PLUGINDLLEXPORT RaveUsersViewStep : public Calamares::ViewStep
+{
+ Q_OBJECT
+
+public:
+ explicit RaveUsersViewStep( QObject* parent = nullptr );
+ ~RaveUsersViewStep() override;
+
+ QString prettyName() const override;
+ QWidget* widget() override;
+
+ bool isNextEnabled() const override;
+ bool isBackEnabled() const override;
+ bool isAtBeginning() const override;
+ bool isAtEnd() const override;
+
+ Calamares::JobList jobs() const override;
+
+ void setConfigurationMap( const QVariantMap& configurationMap ) override;
+ void onActivate() override;
+ void onLeave() override;
+
+private:
+ void buildWidget();
+ QString getLocale() const;
+
+ QWidget* m_widget = nullptr;
+ QQuickWidget* m_quickWidget = nullptr;
+ RaveUsersBackend* m_backend = nullptr;
+ QString m_locale;
+};
+
+CALAMARES_PLUGIN_FACTORY_DECLARATION( RaveUsersViewStepFactory )
diff --git a/raveos-calamares-module/raveusers/module.desc b/raveos-calamares-module/raveusers/module.desc
new file mode 100644
index 0000000..143b7ec
--- /dev/null
+++ b/raveos-calamares-module/raveusers/module.desc
@@ -0,0 +1,5 @@
+---
+type: "viewmodule"
+name: "raveusers"
+interface: "qtplugin"
+load: "libcalamares_viewmodule_raveusers.so"
diff --git a/raveos-calamares-module/raveusers/raveusers.conf b/raveos-calamares-module/raveusers/raveusers.conf
new file mode 100644
index 0000000..8985cbe
--- /dev/null
+++ b/raveos-calamares-module/raveusers/raveusers.conf
@@ -0,0 +1,26 @@
+---
+defaultGroups:
+ - name: users
+ must_exist: true
+ system: true
+ - lp
+ - video
+ - network
+ - storage
+ - name: wheel
+ must_exist: false
+ system: true
+ - audio
+ - plugdev
+
+sudoersGroup: wheel
+sudoersConfigureWithGroup: true
+
+setRootPassword: true
+
+user:
+ shell: /bin/bash
+ forbidden_names: [ root ]
+
+hostname:
+ forbidden_names: [ localhost ]
diff --git a/raveos-calamares-module/raveusers/ui/PasswordField.qml b/raveos-calamares-module/raveusers/ui/PasswordField.qml
new file mode 100644
index 0000000..0dffb49
--- /dev/null
+++ b/raveos-calamares-module/raveusers/ui/PasswordField.qml
@@ -0,0 +1,41 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+
+TextField {
+ id: field
+ property bool reveal: false
+ signal revealClicked()
+
+ implicitHeight: 52
+ leftPadding: 14
+ rightPadding: 46
+ echoMode: reveal ? TextInput.Normal : TextInput.Password
+ color: "#ffffff"
+ placeholderTextColor: "#5b6270"
+ font.pixelSize: 15
+
+ background: Rectangle {
+ radius: 7
+ color: "#303030"
+ border.color: field.activeFocus ? "#3d7839" : "#303030"
+ border.width: field.activeFocus ? 1.5 : 1
+ }
+
+ Text {
+ text: "\uD83D\uDC41"
+ font.pixelSize: 16
+ color: field.reveal ? "#3d7839" : "#9ca3af"
+ anchors.right: parent.right
+ anchors.rightMargin: 14
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ MouseArea {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ width: 40
+ height: 40
+ cursorShape: Qt.PointingHandCursor
+ onClicked: field.revealClicked()
+ }
+}
diff --git a/raveos-calamares-module/raveusers/ui/Translations.qml b/raveos-calamares-module/raveusers/ui/Translations.qml
new file mode 100644
index 0000000..cb267b4
--- /dev/null
+++ b/raveos-calamares-module/raveusers/ui/Translations.qml
@@ -0,0 +1,58 @@
+pragma Singleton
+import QtQuick 2.15
+
+QtObject {
+ id: root
+
+ property string title: "Create user"
+ property string description: "Set up the computer's primary user account. This account has full administrator (root / sudo) privileges."
+ property string usernameLabel: "Username"
+ property string hostnameLabel: "Computer name"
+ property string passwordLabel: "Password"
+ property string passwordPlaceholder: "Password"
+ property string passwordConfirmLabel: "Confirm password"
+ property string reuseRootLabel: "Use the same password for the administrator (root)"
+ property string rootPasswordLabel: "Root password"
+ property string rootPasswordPlaceholder: "Root password"
+ property string rootPasswordConfirmLabel: "Confirm root password"
+
+ function setLanguage(locale) {
+ if (locale.startsWith("hu")) {
+ title = "Felhasználó létrehozása"
+ description = "Állítsd be a számítógép elsődleges felhasználói fiókját. Ez a fiók teljes rendszergazdai (root / sudo) jogosultsággal rendelkezik."
+ usernameLabel = "Felhasználónév"
+ hostnameLabel = "Számítógépnév"
+ passwordLabel = "Jelszó"
+ passwordPlaceholder = "Jelszó"
+ passwordConfirmLabel = "Jelszó megerősítése"
+ reuseRootLabel = "Ugyanaz a jelszó az adminisztrátorhoz (root)"
+ rootPasswordLabel = "Root jelszó"
+ rootPasswordPlaceholder = "Root jelszó"
+ rootPasswordConfirmLabel = "Root jelszó megerősítése"
+ } else if (locale.startsWith("de")) {
+ title = "Benutzer erstellen"
+ description = "Richte das primäre Benutzerkonto des Computers ein. Dieses Konto hat volle Administratorrechte (root / sudo)."
+ usernameLabel = "Benutzername"
+ hostnameLabel = "Computername"
+ passwordLabel = "Passwort"
+ passwordPlaceholder = "Passwort"
+ passwordConfirmLabel = "Passwort bestätigen"
+ reuseRootLabel = "Dasselbe Passwort für den Administrator (root)"
+ rootPasswordLabel = "Root-Passwort"
+ rootPasswordPlaceholder = "Root-Passwort"
+ rootPasswordConfirmLabel = "Root-Passwort bestätigen"
+ } else {
+ title = "Create user"
+ description = "Set up the computer's primary user account. This account has full administrator (root / sudo) privileges."
+ usernameLabel = "Username"
+ hostnameLabel = "Computer name"
+ passwordLabel = "Password"
+ passwordPlaceholder = "Password"
+ passwordConfirmLabel = "Confirm password"
+ reuseRootLabel = "Use the same password for the administrator (root)"
+ rootPasswordLabel = "Root password"
+ rootPasswordPlaceholder = "Root password"
+ rootPasswordConfirmLabel = "Confirm root password"
+ }
+ }
+}
diff --git a/raveos-calamares-module/raveusers/ui/qmldir b/raveos-calamares-module/raveusers/ui/qmldir
new file mode 100644
index 0000000..c87b787
--- /dev/null
+++ b/raveos-calamares-module/raveusers/ui/qmldir
@@ -0,0 +1,2 @@
+module RaveUsersModule
+singleton Translations 1.0 Translations.qml
diff --git a/raveos-calamares-module/raveusers/ui/raveos-logo.png b/raveos-calamares-module/raveusers/ui/raveos-logo.png
new file mode 100755
index 0000000..9903765
--- /dev/null
+++ b/raveos-calamares-module/raveusers/ui/raveos-logo.png
Binary files differ
diff --git a/raveos-calamares-module/raveusers/ui/raveusers.qml b/raveos-calamares-module/raveusers/ui/raveusers.qml
new file mode 100644
index 0000000..25e090f
--- /dev/null
+++ b/raveos-calamares-module/raveusers/ui/raveusers.qml
@@ -0,0 +1,286 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import "." as RaveUsersModule
+
+Rectangle {
+ id: root
+ color: "#2B2B2B"
+ radius: 14
+
+ readonly property color cCardBg: "#20232a"
+ readonly property color cCardBorder: "#333947"
+ readonly property color cInputBg: "#303030"
+ readonly property color cInputBorder: "#303030"
+ readonly property color cAccentGreen: "#3d7839"
+ readonly property color cTextWhite: "#ffffff"
+ readonly property color cTextMuted: "#8b93a3"
+ readonly property color cTextFaint: "#5b6270"
+ readonly property color cErrorRed: "#ff6b6b"
+
+ property bool showUserPassword: false
+ property bool showRootPassword: false
+
+ function updateLanguage() {
+ RaveUsersModule.Translations.setLanguage(systemLocale)
+ }
+
+ Component.onCompleted: {
+ RaveUsersModule.Translations.setLanguage(systemLocale)
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: 40
+ spacing: 0
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ ColumnLayout {
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.topMargin: 10
+ width: Math.min(parent.width - 40, 540)
+ spacing: 16
+
+ Image {
+ source: "raveos-logo.png"
+ sourceSize.width: 96
+ sourceSize.height: 96
+ fillMode: Image.PreserveAspectFit
+ Layout.preferredWidth: 96
+ Layout.preferredHeight: 96
+ Layout.alignment: Qt.AlignHCenter
+ }
+
+ Text {
+ text: RaveUsersModule.Translations.title
+ font.pixelSize: 28
+ font.bold: true
+ color: cTextWhite
+ Layout.alignment: Qt.AlignHCenter
+ }
+
+ Text {
+ text: RaveUsersModule.Translations.description
+ font.pixelSize: 15
+ lineHeight: 1.45
+ color: cTextMuted
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignHCenter
+ }
+
+ Item { height: 6 }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 6
+
+ Text {
+ text: RaveUsersModule.Translations.usernameLabel
+ color: "#c7ccd6"
+ font.pixelSize: 14
+ font.weight: Font.Medium
+ }
+
+ TextField {
+ id: usernameField
+ Layout.fillWidth: true
+ implicitHeight: 52
+ leftPadding: 14
+ rightPadding: 14
+ text: backend.username
+ placeholderText: "raveuser"
+ color: cTextWhite
+ placeholderTextColor: "#5b6270"
+ font.pixelSize: 15
+ background: Rectangle {
+ radius: 7
+ color: cInputBg
+ border.color: usernameField.activeFocus ? cAccentGreen : cInputBorder
+ border.width: usernameField.activeFocus ? 1.5 : 1
+ }
+ onTextEdited: backend.setUsername(text)
+ }
+
+ Text {
+ visible: backend.usernameStatus !== ""
+ text: backend.usernameStatus
+ color: cErrorRed
+ font.pixelSize: 12
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 6
+
+ Text {
+ text: RaveUsersModule.Translations.hostnameLabel
+ color: "#c7ccd6"
+ font.pixelSize: 14
+ font.weight: Font.Medium
+ }
+
+ TextField {
+ id: hostnameField
+ Layout.fillWidth: true
+ implicitHeight: 52
+ leftPadding: 14
+ rightPadding: 14
+ text: backend.hostname
+ placeholderText: "raveos-pc"
+ color: cTextWhite
+ placeholderTextColor: "#5b6270"
+ font.pixelSize: 15
+ background: Rectangle {
+ radius: 7
+ color: cInputBg
+ border.color: hostnameField.activeFocus ? cAccentGreen : cInputBorder
+ border.width: hostnameField.activeFocus ? 1.5 : 1
+ }
+ onTextEdited: backend.setHostname(text)
+ }
+
+ Text {
+ visible: backend.hostnameStatus !== ""
+ text: backend.hostnameStatus
+ color: cErrorRed
+ font.pixelSize: 12
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 6
+
+ Text {
+ text: RaveUsersModule.Translations.passwordLabel
+ color: "#c7ccd6"
+ font.pixelSize: 14
+ font.weight: Font.Medium
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 12
+
+ PasswordField {
+ Layout.fillWidth: true
+ reveal: showUserPassword
+ onRevealClicked: showUserPassword = !showUserPassword
+ placeholderText: RaveUsersModule.Translations.passwordPlaceholder
+ onTextEdited: backend.setPassword(text)
+ }
+
+ PasswordField {
+ Layout.fillWidth: true
+ reveal: showUserPassword
+ onRevealClicked: showUserPassword = !showUserPassword
+ placeholderText: RaveUsersModule.Translations.passwordConfirmLabel
+ onTextEdited: backend.setPasswordSecondary(text)
+ }
+ }
+
+ Text {
+ visible: backend.passwordStatus !== ""
+ text: backend.passwordStatus
+ color: cErrorRed
+ font.pixelSize: 12
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.topMargin: 2
+ Layout.bottomMargin: 2
+ height: 1
+ color: cCardBorder
+ opacity: 0.6
+ }
+
+ CheckBox {
+ id: reuseRootCheck
+ Layout.fillWidth: true
+ visible: backend.rootReuseVisible
+ text: RaveUsersModule.Translations.reuseRootLabel
+ checked: backend.reuseRootPassword
+ onToggled: backend.setReuseRootPassword(checked)
+
+ indicator: Rectangle {
+ implicitWidth: 20
+ implicitHeight: 20
+ x: reuseRootCheck.leftPadding
+ y: parent.height / 2 - height / 2
+ radius: 4
+ color: reuseRootCheck.checked ? cAccentGreen : cInputBg
+ border.color: reuseRootCheck.checked ? cAccentGreen : cInputBorder
+ border.width: 1
+
+ Text {
+ anchors.centerIn: parent
+ text: "\u2713"
+ font.pixelSize: 14
+ font.bold: true
+ color: "#0f1713"
+ visible: reuseRootCheck.checked
+ }
+ }
+
+ contentItem: Text {
+ text: reuseRootCheck.text
+ color: "#d7dbe2"
+ font.pixelSize: 14
+ verticalAlignment: Text.AlignVCenter
+ leftPadding: reuseRootCheck.indicator.width + 12
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 6
+ visible: !backend.reuseRootPassword
+
+ Text {
+ text: RaveUsersModule.Translations.rootPasswordLabel
+ color: "#c7ccd6"
+ font.pixelSize: 14
+ font.weight: Font.Medium
+ Layout.topMargin: 4
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 12
+
+ PasswordField {
+ Layout.fillWidth: true
+ reveal: showRootPassword
+ onRevealClicked: showRootPassword = !showRootPassword
+ placeholderText: RaveUsersModule.Translations.rootPasswordPlaceholder
+ onTextEdited: backend.setRootPasswordText(text)
+ }
+
+ PasswordField {
+ Layout.fillWidth: true
+ reveal: showRootPassword
+ onRevealClicked: showRootPassword = !showRootPassword
+ placeholderText: RaveUsersModule.Translations.rootPasswordConfirmLabel
+ onTextEdited: backend.setRootPasswordSecondaryText(text)
+ }
+ }
+
+ Text {
+ visible: backend.rootPasswordStatus !== ""
+ text: backend.rootPasswordStatus
+ color: cErrorRed
+ font.pixelSize: 12
+ }
+ }
+ }
+ }
+ }
+}