using Inversion; using Invercargill.DataStructures; namespace Spry.Authentication { /** * Error domain for user-related operations. */ public errordomain UserError { USER_NOT_FOUND, DUPLICATE_USERNAME, DUPLICATE_EMAIL, INVALID_PASSWORD, INVALID_CREDENTIALS, USER_INACTIVE, PERMISSION_DENIED, STORAGE_ERROR } /** * UserService provides user management operations including CRUD, * password hashing, and authentication. * * This service uses the inject<> pattern for dependency injection. * All methods are async to work with the repository async API. */ public class UserService : GLib.Object { private UserRepository _repository = inject(); // ========================================================================= // User Creation // ========================================================================= /** * Creates a new user with the specified credentials. * * This method: * - Validates username uniqueness * - Validates email uniqueness * - Hashes password with Argon2id via libsodium * - Creates User with UUID and timestamps * * @param username The unique username * @param email The unique email address * @param password The plaintext password to hash * @return The created User * @throws UserError on validation or storage failure */ public async User create_user_async(string username, string email, string password) throws Error { // Validate username uniqueness if (yield username_exists_async(username)) { throw new UserError.DUPLICATE_USERNAME("Username already exists"); } // Validate email uniqueness if (yield email_exists_async(email)) { throw new UserError.DUPLICATE_EMAIL("Email already exists"); } // Hash password with Argon2id var password_hash = hash_password(password); if (password_hash == null) { throw new UserError.STORAGE_ERROR("Failed to hash password"); } // Create user via repository var user = yield _repository.create(username, email, (!)password_hash); return user; } // ========================================================================= // User Retrieval // ========================================================================= /** * Gets a user by their unique ID. * * @param user_id The user's unique identifier * @return The User, or null if not found * @throws Error on storage failure */ public async User? get_user_async(string user_id) throws Error { return yield _repository.get_by_id(user_id); } /** * Gets a user by their username. * * @param username The username to look up * @return The User, or null if not found * @throws Error on storage failure */ public async User? get_user_by_username_async(string username) throws Error { return yield _repository.get_by_username(username); } /** * Gets a user by their email address. * * @param email The email address to look up * @return The User, or null if not found * @throws Error on storage failure */ public async User? get_user_by_email_async(string email) throws Error { return yield _repository.get_by_email(email); } // ========================================================================= // User Update // ========================================================================= /** * Updates an existing user. * * This method: * - Updates the updated_at timestamp * - Handles username/email changes with uniqueness validation * * @param user The user to update * @throws Error on validation or storage failure */ public async void update_user_async(User user) throws Error { // Get existing user to check for changes var existing = yield get_user_async(user.id); if (existing == null) { throw new UserError.USER_NOT_FOUND("User not found"); } // Check if username changed if (existing.username != user.username) { // Check new username uniqueness var existing_with_username = yield get_user_by_username_async(user.username); if (existing_with_username != null && existing_with_username.id != user.id) { throw new UserError.DUPLICATE_USERNAME("Username already exists"); } } // Check if email changed if (existing.email != user.email) { // Check new email uniqueness var existing_with_email = yield get_user_by_email_async(user.email); if (existing_with_email != null && existing_with_email.id != user.id) { throw new UserError.DUPLICATE_EMAIL("Email already exists"); } } // Update timestamp user.updated_at = new DateTime.now_utc(); // Store updated user via repository yield _repository.update(user); } // ========================================================================= // User Deletion // ========================================================================= /** * Deletes a user by their unique ID. * * @param user_id The user's unique identifier * @throws Error on storage failure */ public async void delete_user_async(string user_id) throws Error { // Get user first (optional, for logging/cleanup) var user = yield get_user_async(user_id); if (user == null) { throw new UserError.USER_NOT_FOUND("User not found"); } // Delete user via repository yield _repository.delete(user_id); } // ========================================================================= // User Listing // ========================================================================= /** * Lists users with pagination support. * * Note: This method is not supported by the basic UserRepository interface. * Subclasses or extensions should implement this as needed. * * @param offset The number of users to skip * @param limit The maximum number of users to return * @return A Vector of users * @throws Error on storage failure */ public async Vector list_users_async(int offset = 0, int limit = 100) throws Error { // The basic UserRepository interface doesn't include list operations // This would need to be added to the interface or handled differently // For now, return an empty list as a placeholder return new Vector(); } // ========================================================================= // Password Management // ========================================================================= /** * Hashes a password using Argon2id via libsodium. * * @param password The plaintext password to hash * @return The hashed password string, or null on failure */ public string? hash_password(string password) { return Sodium.PasswordHashing.hash(password); } /** * Verifies a password against a stored hash. * * @param user The user to verify against * @param password The plaintext password to verify * @return true if the password matches, false otherwise */ public bool verify_password(User user, string password) { return Sodium.PasswordHashing.check(user.password_hash, password); } /** * Sets a new password for a user. * * @param user The user to update * @param new_password The new plaintext password * @throws Error on failure */ public async void set_password_async(User user, string new_password) throws Error { var password_hash = hash_password(new_password); if (password_hash == null) { throw new UserError.STORAGE_ERROR("Failed to hash password"); } user.password_hash = (!)password_hash; user.updated_at = new DateTime.now_utc(); yield update_user_async(user); } // ========================================================================= // Authentication // ========================================================================= /** * Authenticates a user by username/email and password. * * This method: * - Looks up user by username or email * - Verifies the password * - Returns the user if valid * * @param username_or_email The username or email address * @param password The plaintext password * @return The authenticated User, or null if authentication failed * @throws Error on storage failure */ public async User? authenticate_async(string username_or_email, string password) throws Error { // Try to find user by username first, then by email User? user = yield get_user_by_username_async(username_or_email); if (user == null) { user = yield get_user_by_email_async(username_or_email); } if (user == null) { return null; } // Verify password bool password_valid = verify_password(user, password); if (!password_valid) { return null; } return user; } // ========================================================================= // Utility Methods // ========================================================================= /** * Checks if a username already exists. * * @param username The username to check * @return true if the username exists * @throws Error on storage failure */ public async bool username_exists_async(string username) throws Error { return yield _repository.exists_by_username(username); } /** * Checks if an email already exists. * * @param email The email to check * @return true if the email exists * @throws Error on storage failure */ public async bool email_exists_async(string email) throws Error { return yield _repository.exists_by_email(email); } /** * Gets the total count of users. * * Note: This method is not supported by the basic UserRepository interface. * Subclasses or extensions should implement this as needed. * * @return The number of users * @throws Error on storage failure */ public async int user_count_async() throws Error { // The basic UserRepository interface doesn't include count operations // This would need to be added to the interface or handled differently return 0; } // ========================================================================= // Permission Operations // ========================================================================= /** * Adds a permission to a user. * * @param user The user to add the permission to * @param permission The permission to add * @throws Error on storage failure */ public async void add_permission_async(User user, string permission) throws Error { yield _repository.add_permission(user.id, permission); } /** * Removes a permission from a user. * * @param user The user to remove the permission from * @param permission The permission to remove * @throws Error on storage failure */ public async void remove_permission_async(User user, string permission) throws Error { yield _repository.remove_permission(user.id, permission); } /** * Checks if a user has a specific permission. * * @param user The user to check * @param permission The permission to check * @return true if the user has the permission * @throws Error on storage failure */ public async bool has_permission_async(User user, string permission) throws Error { return yield _repository.has_permission(user.id, permission); } /** * Gets all permissions for a user. * * @param user The user to get permissions for * @return A Vector of permission strings * @throws Error on storage failure */ public async Vector get_permissions_async(User user) throws Error { return yield _repository.get_permissions(user.id); } // ========================================================================= // App Data Operations // ========================================================================= /** * Sets an app data value for a user. * * @param user The user to set the app data for * @param key The app data key * @param value The app data value * @throws Error on storage failure */ public async void set_app_data_async(User user, string key, string value) throws Error { yield _repository.set_app_data(user.id, key, value); } /** * Gets an app data value for a user. * * @param user The user to get the app data for * @param key The app data key * @return The app data value, or null if not found * @throws Error on storage failure */ public async string? get_app_data_async(User user, string key) throws Error { return yield _repository.get_app_data(user.id, key); } } }