Program Listing for File profile_manager.hpp#

Return to documentation for file (src/auth/profile_manager.hpp)

#pragma once
#include <QObject>
#include <memory>
#include <vector>

#include "auth/profile.hpp"

namespace mosaic {

/// @brief Manages the on-disk profile manifest and per-profile settings directories.
///
/// @par Storage layout
/// @code
/// ~/.config/CSRU/mosaic/
///   profiles.json          ← auth manifest (hashed credentials)
///   profiles/
///     <username>/
///       settings.json      ← per-group AppSettings
///       mosaic.log
/// @endcode
///
/// @par Password security
/// PBKDF2-HMAC-SHA256, 100 000 iterations, 32-byte output key, 32-byte random
/// salt per profile.  This protects against accidental profile collisions on a
/// shared workstation; it is **not** designed to protect against an attacker
/// with file-system access.
///
/// @par Example
/// @code{.cpp}
/// ProfileManager mgr;
/// mgr.load();
///
/// using Res = ProfileManager::RegisterResult;
/// if (mgr.register_profile("cognitive_lab", "Cognitive Science Lab", "secret")
///         == Res::Ok) {
///     qDebug() << "profile created at" << ProfileManager::profile_dir("cognitive_lab");
/// }
///
/// if (mgr.verify("cognitive_lab", "secret")) {
///     mgr.touch("cognitive_lab");
///     auto path = ProfileManager::settings_path("cognitive_lab");
/// }
/// @endcode
///
/// @see Profile, AppSettings
class ProfileManager : public QObject {
    Q_OBJECT
   public:
    explicit ProfileManager(QObject* parent = nullptr);
    ~ProfileManager() override;

    /// @brief Loads the profiles manifest from disk.
    ///
    /// Creates an empty manifest if the file does not exist yet.
    /// Safe to call multiple times — each call replaces the in-memory list.
    void load();

    // ── Queries ────────────────────────────────────────────────────────────

    /// @returns A copy of all currently loaded profiles.
    [[nodiscard]] std::vector<Profile> profiles() const;

    /// @param username  Username to look up (case-sensitive).
    /// @returns         @c true if a profile with this username exists.
    [[nodiscard]] bool has_profile(const QString& username) const;

    /// @param username  Username to look up.
    /// @returns         Pointer to the matching Profile, or @c nullptr.
    ///
    /// @note The returned pointer is invalidated the next time the profile
    ///       list is modified (register, delete).  Copy if you need to keep it.
    [[nodiscard]] const Profile* find(const QString& username) const;

    // ── Registration ───────────────────────────────────────────────────────

    /// @brief Possible outcomes of register_profile().
    enum class RegisterResult {
        Ok,               ///< Profile was created successfully.
        UsernameTaken,    ///< A profile with that username already exists.
        UsernameInvalid,  ///< Username does not match ``[a-z0-9_]{3,32}``.
        PasswordTooShort, ///< Non-empty password is shorter than 4 characters.
    };

    /// @brief Registers a new research-group profile.
    ///
    /// Creates the profile directory, hashes the password (if non-empty),
    /// appends the profile to the manifest, and saves it to disk.
    ///
    /// @param username     Unique login key — ``[a-z0-9_]{3,32}``.
    /// @param displayName  Human-readable group name (any Unicode string).
    /// @param password     Plain-text password.  Pass an empty string for
    ///                     a password-free profile.
    /// @param role         Privilege level for the new profile (default: User).
    /// @returns            RegisterResult::Ok on success, or an error code.
    RegisterResult register_profile(const QString& username, const QString& displayName,
                                    const QString& password,
                                    Profile::Role role = Profile::Role::User);

    /// @returns @c true if at least one profile with Admin role exists.
    [[nodiscard]] bool has_admin() const;

    // ── Authentication ─────────────────────────────────────────────────────

    /// @brief Verifies a password against the stored hash.
    ///
    /// @param username  Profile to authenticate.
    /// @param password  Plain-text password entered by the user.
    /// @returns         @c true if the password matches, or if the profile
    ///                  has no password set.  @c false if the profile does
    ///                  not exist or the password is wrong.
    [[nodiscard]] bool verify(const QString& username, const QString& password) const;

    /// @brief Updates the @c lastLogin timestamp and saves the manifest.
    ///
    /// Call this immediately after a successful login so the login dialog
    /// can display accurate "last seen" information.
    ///
    /// @param username  The profile that just logged in.
    void touch(const QString& username);

    // ── Profile paths ──────────────────────────────────────────────────────

    /// @returns The root config directory: ``~/.config/CSRU/mosaic``.
    [[nodiscard]] static QString root_dir();

    /// @param username  Profile username.
    /// @returns         The profile-specific directory:
    ///                  ``~/.config/CSRU/mosaic/profiles/<username>``.
    [[nodiscard]] static QString profile_dir(const QString& username);

    /// @param username  Profile username.
    /// @returns         Full path to the profile's settings file:
    ///                  ``<profile_dir>/settings.json``.
    [[nodiscard]] static QString settings_path(const QString& username);

    /// @param username  Profile username.
    /// @returns         Full path to the profile's log file:
    ///                  ``<profile_dir>/mosaic.log``.
    [[nodiscard]] static QString log_path(const QString& username);

    // ── Mutation ───────────────────────────────────────────────────────────

    /// @brief Permanently deletes a profile from the manifest.
    ///
    /// @note This does **not** delete the profile directory or its settings.
    ///       The data remains on disk so it can be manually recovered.
    ///
    /// @param username  Profile to delete.
    /// @returns         @c true if the profile was found and removed.
    bool delete_profile(const QString& username);

    /// @brief Renames a profile's display name without changing its username.
    ///
    /// @param username    Profile to update.
    /// @param newDisplay  New human-readable name.
    /// @returns           @c true if the profile was found and updated.
    bool rename_display(const QString& username, const QString& newDisplay);

    /// @brief Changes a profile's password.  Pass empty to remove password protection.
    ///
    /// @param username     Profile to update.
    /// @param newPassword  New plain-text password, or empty string to clear.
    /// @returns            @c true on success.
    bool change_password(const QString& username, const QString& newPassword);

    /// @brief Updates a profile's privilege role.
    ///
    /// @param username  Profile to update.
    /// @param role      New role.
    /// @returns         @c true on success.
    bool set_role(const QString& username, Profile::Role role);

    /// @brief Updates a profile's institution / lab name.
    ///
    /// @param username     Profile to update.
    /// @param institution  New institution string (any Unicode).
    /// @returns            @c true on success.
    bool set_institution(const QString& username, const QString& institution);

    /// @brief Returns the hex accent colour for a given position in the palette.
    ///
    /// Colours are assigned round-robin so consecutive profiles get distinct
    /// colours.
    ///
    /// @param profileIndex  Zero-based index (wraps around the 8-colour palette).
    /// @returns             A hex RGB string like ``"#5566dd"``.
    [[nodiscard]] static QString next_accent_colour(int profileIndex);

   signals:
    /// Emitted after any change to the profile list (register, delete, rename).
    void profiles_changed();

   private:
    [[nodiscard]] bool save() const;
    [[nodiscard]] static QString hash_password(const QString& password, const QByteArray& salt);
    [[nodiscard]] static QString generate_initials(const QString& displayName);

    struct Impl;
    std::unique_ptr<Impl> d;
};

} // namespace mosaic