diff options
Diffstat (limited to 'raveos-calamares-module/raveusers')
| -rw-r--r-- | raveos-calamares-module/raveusers/CMakeLists.txt | 37 | ||||
| -rw-r--r-- | raveos-calamares-module/raveusers/RaveUsersBackend.cpp | 242 | ||||
| -rw-r--r-- | raveos-calamares-module/raveusers/RaveUsersBackend.h | 89 | ||||
| -rw-r--r-- | raveos-calamares-module/raveusers/RaveUsersJobs.cpp | 294 | ||||
| -rw-r--r-- | raveos-calamares-module/raveusers/RaveUsersJobs.h | 67 | ||||
| -rw-r--r-- | raveos-calamares-module/raveusers/RaveUsersViewStep.cpp | 253 | ||||
| -rw-r--r-- | raveos-calamares-module/raveusers/RaveUsersViewStep.h | 47 | ||||
| -rw-r--r-- | raveos-calamares-module/raveusers/module.desc | 5 | ||||
| -rw-r--r-- | raveos-calamares-module/raveusers/raveusers.conf | 26 | ||||
| -rw-r--r-- | raveos-calamares-module/raveusers/ui/PasswordField.qml | 41 | ||||
| -rw-r--r-- | raveos-calamares-module/raveusers/ui/Translations.qml | 58 | ||||
| -rw-r--r-- | raveos-calamares-module/raveusers/ui/qmldir | 2 | ||||
| -rwxr-xr-x | raveos-calamares-module/raveusers/ui/raveos-logo.png | bin | 0 -> 302684 bytes | |||
| -rw-r--r-- | raveos-calamares-module/raveusers/ui/raveusers.qml | 286 |
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 Binary files differnew file mode 100755 index 0000000..9903765 --- /dev/null +++ b/raveos-calamares-module/raveusers/ui/raveos-logo.png 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 + } + } + } + } + } +} |