Forráskód Böngészése

refactor(auth): migrate to ORM-based data access layer

Replace manual repository pattern with InvercargillSql ORM integration:
- Remove UserRepository, SessionRepository and their SQL implementations
- Delete Session, PermissionService and related infrastructure
- Add UserEntity, UserPermissionEntity, and UserProjection for ORM mapping
- Refactor UserService to use OrmSession directly
- Update Identity interface to use Element-typed identifier instead of string
- Add invercargill-sql-inversion dependency for ORM integration
- Add AuthenticationModule and UserTableMigration for schema setup

BREAKING CHANGE: Identity.id changed from string to Element; sessions and permissions removed from Authentication module
Billy Barrow 1 hónapja
szülő
commit
bbb98f1c9c

+ 1 - 0
meson.build

@@ -14,6 +14,7 @@ astralis_dep = dependency('astralis-0.1')
 json_glib_dep = dependency('json-glib-1.0')
 invercargill_json_dep = dependency('invercargill-json')
 invercargill_sql_dep = dependency('invercargill-sql', required: true)
+invercargill_sql_inversion_dep = dependency('invercargill-sql-inversion', required: true)
 sqlite_dep = dependency('sqlite3')
 libxml_dep = dependency('libxml-2.0')
 sodium_vapi = files('vapi/libsodium.vapi')

+ 17 - 0
src/Authentication/AuthenticationModule.vala

@@ -0,0 +1,17 @@
+using InvercargillSqlInversion;
+using Inversion;
+
+namespace Spry.Authentication {
+
+    public class AuthenticationModule : Object, Module {
+
+        public void register_components (Inversion.Container container) throws Error {
+            var sql = container.configure_with<DatabaseConfigurator>();
+            sql.add_entity<UserEntity>(UserEntity.entity_mapping);
+            sql.add_entity<UserPermissionEntity>(UserPermissionEntity.entity_mapping);
+            sql.add_projection<UserProjection> (UserProjection.projection_mapping);
+        }
+
+    }
+
+}

+ 0 - 93
src/Authentication/CreateAuthTables.vala

@@ -1,93 +0,0 @@
-using InvercargillSql;
-
-namespace Spry.Authentication {
-
-    /**
-     * Creates the authentication database schema.
-     * Run once during application initialization.
-     */
-    public class CreateAuthTables : Object {
-
-        private Connection _connection;
-
-        // =========================================================================
-        // Constructor
-        // =========================================================================
-
-        public CreateAuthTables(Connection connection) {
-            _connection = connection;
-        }
-
-        // =========================================================================
-        // Migration
-        // =========================================================================
-
-        /**
-         * Creates all authentication tables if they don't exist.
-         */
-        public async void migrate() throws Error {
-            // Users table
-            yield _connection.create_command("""
-                CREATE TABLE IF NOT EXISTS users (
-                    id TEXT PRIMARY KEY,
-                    username TEXT NOT NULL UNIQUE,
-                    email TEXT NOT NULL UNIQUE,
-                    password_hash TEXT NOT NULL,
-                    created_at TEXT NOT NULL,
-                    updated_at TEXT NOT NULL
-                )
-            """).execute_non_query_async();
-
-            // User permissions table
-            yield _connection.create_command("""
-                CREATE TABLE IF NOT EXISTS user_permissions (
-                    id INTEGER PRIMARY KEY AUTOINCREMENT,
-                    user_id TEXT NOT NULL,
-                    permission TEXT NOT NULL,
-                    UNIQUE(user_id, permission),
-                    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
-                )
-            """).execute_non_query_async();
-
-            // User app data table
-            yield _connection.create_command("""
-                CREATE TABLE IF NOT EXISTS user_app_data (
-                    id INTEGER PRIMARY KEY AUTOINCREMENT,
-                    user_id TEXT NOT NULL,
-                    key TEXT NOT NULL,
-                    value TEXT,
-                    UNIQUE(user_id, key),
-                    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
-                )
-            """).execute_non_query_async();
-
-            // Sessions table
-            yield _connection.create_command("""
-                CREATE TABLE IF NOT EXISTS sessions (
-                    id TEXT PRIMARY KEY,
-                    user_id TEXT NOT NULL,
-                    created_at TEXT NOT NULL,
-                    expires_at TEXT NOT NULL,
-                    last_accessed_at TEXT NOT NULL,
-                    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
-                )
-            """).execute_non_query_async();
-
-            // Create indexes
-            yield create_index("idx_users_username", "users(username)");
-            yield create_index("idx_users_email", "users(email)");
-            yield create_index("idx_user_permissions_user_id", "user_permissions(user_id)");
-            yield create_index("idx_user_app_data_user_id", "user_app_data(user_id)");
-            yield create_index("idx_sessions_user_id", "sessions(user_id)");
-            yield create_index("idx_sessions_expires_at", "sessions(expires_at)");
-        }
-
-        /**
-         * Creates an index if it doesn't exist.
-         */
-        private async void create_index(string name, string definition) throws Error {
-            var sql = "CREATE INDEX IF NOT EXISTS %s ON %s".printf(name, definition);
-            yield _connection.create_command(sql).execute_non_query_async();
-        }
-    }
-}

+ 0 - 258
src/Authentication/PermissionService.vala

@@ -1,258 +0,0 @@
-using Invercargill.DataStructures;
-using Inversion;
-
-namespace Spry.Authentication {
-
-    /**
-     * PermissionService handles granular permissions keyed by string, with wildcard support.
-     *
-     * Features:
-     * - Check if user has exact or wildcard permission
-     * - Set and clear permissions on users
-     * - Support for "admin" super-user permission
-     * - Wildcard matching delegated to Authorisation.PermissionMatcher
-     * 
-     * This service uses the inject<> pattern for dependency injection.
-     * All methods that need to load user data are async.
-     */
-    public class PermissionService : GLib.Object {
-
-        // =========================================================================
-        // Permission Constants
-        // =========================================================================
-
-        /** Permission for general user management access */
-        public const string USER_MANAGEMENT = "user-management";
-
-        /** Permission to create new users */
-        public const string USER_CREATE = "user-create";
-
-        /** Permission to read/view user details */
-        public const string USER_READ = "user-read";
-
-        /** Permission to update existing users */
-        public const string USER_UPDATE = "user-update";
-
-        /** Permission to delete users */
-        public const string USER_DELETE = "user-delete";
-
-        /** Super-user permission that grants all other permissions */
-        public const string ADMIN = "admin";
-
-        // =========================================================================
-        // Dependencies
-        // =========================================================================
-
-        private UserService _user_service = inject<UserService>();
-
-        /**
-         * Creates a new PermissionService instance.
-         * Dependencies are injected via inject<>() pattern.
-         */
-        public PermissionService() {
-            // Dependencies injected via field initializers
-        }
-
-        // =========================================================================
-        // Permission Checking
-        // =========================================================================
-
-        /**
-         * Checks if a user has a specific permission.
-         *
-         * This method:
-         * - Returns true if user has "admin" permission (super-user)
-         * - Checks for exact permission match
-         * - Uses Authorisation.PermissionMatcher for wildcard matching
-         *
-         * @param user The user to check
-         * @param permission The permission to check for
-         * @return true if the user has the permission, false otherwise
-         */
-        public bool has_permission(User user, string permission) {
-            // Get the user's permissions as an array
-            var user_permissions = user.permissions;
-
-            // Use PermissionMatcher.any_matches for efficient checking
-            return Authorisation.PermissionMatcher.any_matches(user_permissions, permission);
-        }
-
-        /**
-         * Checks if a user (by ID) has a specific permission.
-         *
-         * @param user_id The user's unique identifier
-         * @param permission The permission to check for
-         * @return true if the user has the permission, false otherwise
-         * @throws Error on storage failure
-         */
-        public async bool has_permission_by_id_async(string user_id, string permission) throws Error {
-            var user = yield _user_service.get_user_async(user_id);
-            if (user == null) {
-                return false;
-            }
-            return has_permission(user, permission);
-        }
-
-        // =========================================================================
-        // Permission Setting
-        // =========================================================================
-
-        /**
-         * Sets (adds) a permission for a user.
-         *
-         * This method:
-         * - Adds the permission if not already present
-         * - Persists changes via UserService.update_user_async()
-         *
-         * @param user The user to update
-         * @param permission The permission to add
-         * @throws Error on failure
-         */
-        public async void set_permission_async(User user, string permission) throws Error {
-            // Check if permission already exists
-            if (user.has_permission(permission)) {
-                // Already has this permission
-                return;
-            }
-
-            // Add the permission
-            user.add_permission(permission);
-
-            // Persist changes
-            yield _user_service.update_user_async(user);
-        }
-
-        /**
-         * Sets (adds) a permission for a user (by ID).
-         *
-         * @param user_id The user's unique identifier
-         * @param permission The permission to add
-         * @throws Error on failure
-         */
-        public async void set_permission_by_id_async(string user_id, string permission) throws Error {
-            var user = yield _user_service.get_user_async(user_id);
-            if (user == null) {
-                throw new UserError.USER_NOT_FOUND("User not found");
-            }
-
-            yield set_permission_async(user, permission);
-        }
-
-        // =========================================================================
-        // Permission Clearing
-        // =========================================================================
-
-        /**
-         * Clears (removes) a permission from a user.
-         *
-         * This method:
-         * - Removes the permission if present
-         * - Persists changes via UserService.update_user_async()
-         *
-         * @param user The user to update
-         * @param permission The permission to remove
-         * @throws Error on failure
-         */
-        public async void clear_permission_async(User user, string permission) throws Error {
-            // Remove the permission
-            user.remove_permission(permission);
-
-            // Persist changes
-            yield _user_service.update_user_async(user);
-        }
-
-        /**
-         * Clears (removes) a permission from a user (by ID).
-         *
-         * @param user_id The user's unique identifier
-         * @param permission The permission to remove
-         * @throws Error on failure
-         */
-        public async void clear_permission_by_id_async(string user_id, string permission) throws Error {
-            var user = yield _user_service.get_user_async(user_id);
-            if (user == null) {
-                throw new UserError.USER_NOT_FOUND("User not found");
-            }
-
-            yield clear_permission_async(user, permission);
-        }
-
-        /**
-         * Clears all permissions from a user.
-         *
-         * @param user The user to update
-         * @throws Error on failure
-         */
-        public async void clear_all_permissions_async(User user) throws Error {
-            // Clear all permissions
-            user.clear_permissions();
-
-            // Persist changes
-            yield _user_service.update_user_async(user);
-        }
-
-        // =========================================================================
-        // Permission Retrieval
-        // =========================================================================
-
-        /**
-         * Gets all permissions for a user.
-         *
-         * @param user The user to get permissions for
-         * @return A Vector of permission strings
-         */
-        public Vector<string> get_permissions(User user) {
-            // Return a copy of the permissions vector
-            var result = new Vector<string>();
-            foreach (var perm in user.get_permissions_vector()) {
-                result.add(perm);
-            }
-            return result;
-        }
-
-        /**
-         * Gets all permissions for a user (by ID).
-         *
-         * @param user_id The user's unique identifier
-         * @return A Vector of permission strings
-         * @throws Error on storage failure
-         */
-        public async Vector<string> get_permissions_by_id_async(string user_id) throws Error {
-            var user = yield _user_service.get_user_async(user_id);
-            if (user == null) {
-                return new Vector<string>();
-            }
-            return get_permissions(user);
-        }
-
-        // =========================================================================
-        // Wildcard Matching (delegated to PermissionMatcher)
-        // =========================================================================
-
-        /**
-         * Checks if a permission pattern matches a specific permission.
-         *
-         * This method delegates to Authorisation.PermissionMatcher.matches().
-         *
-         * Wildcard matching rules:
-         * - "*" matches everything
-         * - "prefix-*" matches any permission starting with "prefix-"
-         * - Without wildcard, requires exact match
-         * - "admin" matches everything (super-user)
-         *
-         * Examples:
-         * - permission_matches("*", "anything") → true
-         * - permission_matches("user-*", "user-create") → true
-         * - permission_matches("user-*", "user-delete") → true
-         * - permission_matches("user-*", "admin") → false
-         * - permission_matches("user-create", "user-create") → true
-         *
-         * @param pattern The pattern to match against (may contain wildcard)
-         * @param permission The permission to check
-         * @return true if the pattern matches the permission
-         */
-        public bool permission_matches(string pattern, string permission) {
-            return Authorisation.PermissionMatcher.matches(pattern, permission);
-        }
-    }
-}

+ 0 - 98
src/Authentication/Session.vala

@@ -1,98 +0,0 @@
-using Invercargill.DataStructures;
-using Json;
-
-namespace Spry.Authentication {
-
-    public class Session : GLib.Object {
-        // Identity
-        public string id { get; set; }
-        public string user_id { get; set; }
-
-        // Timing
-        public DateTime created_at { get; set; }
-        public DateTime expires_at { get; set; }
-
-        // Optional tracking
-        public string? ip_address { get; set; }
-        public string? user_agent { get; set; }
-
-        public Session() {
-            id = "";
-            user_id = "";
-            created_at = new DateTime.now_utc();
-            expires_at = new DateTime.now_utc();
-        }
-
-        public bool is_expired() {
-            return expires_at.compare(new DateTime.now_utc()) <= 0;
-        }
-
-        public static Session from_json(Json.Object obj) {
-            var session = new Session();
-
-            // Required string fields - use has_member and null coalescing for safety
-            session.id = obj.has_member("id") ? (obj.get_string_member("id") ?? "") : "";
-            session.user_id = obj.has_member("user_id") ? (obj.get_string_member("user_id") ?? "") : "";
-
-            // created_at - check member exists and value is not null/empty
-            if (obj.has_member("created_at")) {
-                var created_str = obj.get_string_member("created_at");
-                if (created_str != null && created_str.length > 0) {
-                    session.created_at = new DateTime.from_iso8601(created_str, new TimeZone.utc());
-                }
-            }
-
-            // expires_at - check member exists and value is not null/empty
-            if (obj.has_member("expires_at")) {
-                var expires_str = obj.get_string_member("expires_at");
-                if (expires_str != null && expires_str.length > 0) {
-                    session.expires_at = new DateTime.from_iso8601(expires_str, new TimeZone.utc());
-                }
-            }
-
-            // ip_address (optional) - check member exists and is not null
-            if (obj.has_member("ip_address")) {
-                var member = obj.get_member("ip_address");
-                if (member != null && member.get_node_type() == Json.NodeType.VALUE) {
-                    session.ip_address = obj.get_string_member("ip_address");
-                }
-            }
-
-            // user_agent (optional) - check member exists and is not null
-            if (obj.has_member("user_agent")) {
-                var member = obj.get_member("user_agent");
-                if (member != null && member.get_node_type() == Json.NodeType.VALUE) {
-                    session.user_agent = obj.get_string_member("user_agent");
-                }
-            }
-
-            return session;
-        }
-
-        public Json.Object to_json() {
-            var obj = new Json.Object();
-
-            // Use null coalescing to ensure we never pass null to set_string_member
-            obj.set_string_member("id", id ?? "");
-            obj.set_string_member("user_id", user_id ?? "");
-            obj.set_string_member("created_at", created_at != null ? created_at.format_iso8601() : new DateTime.now_utc().format_iso8601());
-            obj.set_string_member("expires_at", expires_at != null ? expires_at.format_iso8601() : new DateTime.now_utc().format_iso8601());
-
-            // ip_address (optional)
-            if (ip_address != null) {
-                obj.set_string_member("ip_address", (!)ip_address);
-            } else {
-                obj.set_null_member("ip_address");
-            }
-
-            // user_agent (optional)
-            if (user_agent != null) {
-                obj.set_string_member("user_agent", (!)user_agent);
-            } else {
-                obj.set_null_member("user_agent");
-            }
-
-            return obj;
-        }
-    }
-}

+ 0 - 83
src/Authentication/SessionRepository.vala

@@ -1,83 +0,0 @@
-using Invercargill.DataStructures;
-
-namespace Spry.Authentication {
-
-    /**
-     * Repository interface for Session persistence operations.
-     * Abstracts the storage mechanism from the service layer.
-     */
-    public interface SessionRepository : Object {
-
-        // =========================================================================
-        // Retrieval Operations
-        // =========================================================================
-
-        /**
-         * Gets a session by its unique ID.
-         *
-         * @param id The session's unique identifier
-         * @return The Session, or null if not found
-         * @throws Error on storage failure
-         */
-        public abstract async Session? get_by_id(string id) throws Error;
-
-        /**
-         * Gets all sessions for a user.
-         *
-         * @param user_id The user's unique identifier
-         * @return A Vector of sessions
-         * @throws Error on storage failure
-         */
-        public abstract async Vector<Session> get_by_user_id(string user_id) throws Error;
-
-        // =========================================================================
-        // Mutation Operations
-        // =========================================================================
-
-        /**
-         * Creates a new session.
-         *
-         * @param id The session's unique identifier (pre-generated)
-         * @param user_id The user's unique identifier
-         * @param expires_at When the session expires
-         * @return The created Session
-         * @throws Error on storage failure
-         */
-        public abstract async Session create(string id, string user_id, DateTime expires_at) throws Error;
-
-        /**
-         * Updates an existing session.
-         *
-         * @param session The session to update
-         * @throws Error on storage failure
-         */
-        public abstract async void update(Session session) throws Error;
-
-        /**
-         * Deletes a session by its unique ID.
-         *
-         * @param id The session's unique identifier
-         * @throws Error on storage failure
-         */
-        public abstract async void delete(string id) throws Error;
-
-        /**
-         * Deletes all sessions for a user.
-         *
-         * @param user_id The user's unique identifier
-         * @throws Error on storage failure
-         */
-        public abstract async void delete_by_user_id(string user_id) throws Error;
-
-        // =========================================================================
-        // Cleanup Operations
-        // =========================================================================
-
-        /**
-         * Removes all expired sessions from storage.
-         *
-         * @throws Error on storage failure
-         */
-        public abstract async void delete_expired() throws Error;
-    }
-}

+ 0 - 576
src/Authentication/SessionService.vala

@@ -1,576 +0,0 @@
-using Inversion;
-using InvercargillJson;
-using Invercargill.DataStructures;
-using Json;
-using Astralis;
-
-namespace Spry.Authentication {
-
-    /**
-     * Error domain for session-related operations.
-     */
-    public errordomain SessionError {
-        SESSION_NOT_FOUND,
-        SESSION_EXPIRED,
-        INVALID_SESSION_TOKEN,
-        COOKIE_NOT_FOUND,
-        STORAGE_ERROR
-    }
-
-    /**
-     * Result of session token validation containing session and user info.
-     */
-    public class SessionValidationResult : GLib.Object {
-        /**
-         * Whether the session token was successfully validated.
-         */
-        public bool is_valid { get; set; }
-
-        /**
-         * The session object if validation was successful.
-         */
-        public Session? session { get; set; }
-
-        /**
-         * The user object if validation was successful.
-         */
-        public User? user { get; set; }
-
-        /**
-         * Error message describing why validation failed.
-         */
-        public string? error_message { get; set; }
-
-        /**
-         * Creates a successful validation result.
-         */
-        public SessionValidationResult.success(Session session, User? user = null) {
-            GLib.Object(
-                is_valid: true,
-                session: session,
-                user: user,
-                error_message: null
-            );
-        }
-
-        /**
-         * Creates a failed validation result.
-         */
-        public SessionValidationResult.failure(string error_message) {
-            GLib.Object(
-                is_valid: false,
-                session: null,
-                user: null,
-                error_message: error_message
-            );
-        }
-    }
-
-    /**
-     * Result of authenticating a request.
-     */
-    public class AuthResult : GLib.Object {
-        /**
-         * Whether the request was successfully authenticated.
-         */
-        public bool is_authenticated { get; set; }
-
-        /**
-         * The authenticated user, or null if not authenticated.
-         */
-        public User? user { get; set; }
-
-        /**
-         * The session associated with the request, or null if not authenticated.
-         */
-        public Session? session { get; set; }
-
-        /**
-         * Error message describing why authentication failed.
-         */
-        public string? error_message { get; set; }
-
-        /**
-         * Creates a successful authentication result.
-         */
-        public AuthResult.success(User user, Session session) {
-            GLib.Object(
-                is_authenticated: true,
-                user: user,
-                session: session,
-                error_message: null
-            );
-        }
-
-        /**
-         * Creates a failed authentication result.
-         */
-        public AuthResult.failure(string error_message) {
-            GLib.Object(
-                is_authenticated: false,
-                user: null,
-                session: null,
-                error_message: error_message
-            );
-        }
-    }
-
-    /**
-     * SessionService handles session creation, validation, cookie management, and cleanup.
-     *
-     * Cookie Configuration:
-     * - Cookie name: spry_session (configurable)
-     * - HttpOnly: true
-     * - Secure: true (configurable for development)
-     * - SameSite: Strict
-     * - Path: /
-     * 
-     * Integration with Authorisation:
-     * - Uses AuthorisationTokenService for token generation/validation
-     * - Sessions can be converted to AuthorisationTokens for the Authorisation system
-     * 
-     * This service uses the inject<> pattern for dependency injection.
-     * All methods are async to work with the repository async API.
-     */
-    public class SessionService : GLib.Object {
-
-        private const string NAMESPACE = "session-token";
-
-        private SessionRepository _repository = inject<SessionRepository>();
-        private CryptographyProvider _crypto = inject<CryptographyProvider>();
-        private Authorisation.AuthorisationTokenService? _token_service = inject<Authorisation.AuthorisationTokenService>();
-
-        // Cookie configuration
-        private string _cookie_name = "spry_session";
-        private bool _cookie_secure = true;
-        private TimeSpan _session_duration = TimeSpan.HOUR * 24;
-
-        /**
-         * The name of the session cookie.
-         */
-        public string cookie_name { 
-            get { return _cookie_name; } 
-            set { _cookie_name = value; }
-        }
-
-        /**
-         * Whether the session cookie should be Secure.
-         */
-        public bool cookie_secure { 
-            get { return _cookie_secure; } 
-            set { _cookie_secure = value; }
-        }
-
-        /**
-         * The session expiry duration.
-         */
-        public TimeSpan session_duration { 
-            get { return _session_duration; } 
-            set { _session_duration = value; }
-        }
-
-        /**
-         * Creates a new SessionService instance with default configuration.
-         * Use properties to customize cookie name, security, and duration.
-         */
-        public SessionService() {
-            // Default configuration - use properties to customize
-        }
-
-        // =========================================================================
-        // Session Creation
-        // =========================================================================
-
-        /**
-         * Creates a new session for a user.
-         *
-         * This method:
-         * - Generates a UUID for the session
-         * - Sets expiry based on configured duration
-         * - Stores optional IP and user agent
-         *
-         * @param user_id The user's unique identifier
-         * @param ip_address Optional IP address for tracking
-         * @param user_agent Optional user agent for tracking
-         * @return The created Session
-         * @throws Error on failure
-         */
-        public async Session create_session_async(string user_id, string? ip_address = null, string? user_agent = null) throws Error {
-            stdout.printf("SESSION DEBUG: create_session_async() called for user: %s\n", user_id);
-            stdout.printf("SESSION DEBUG: IP address: %s, User-Agent: %s\n",
-                ip_address ?? "null", user_agent ?? "null");
-            
-            // Generate UUID for session
-            var session_id = generate_uuid();
-            stdout.printf("SESSION DEBUG: Generated session ID: %s\n", session_id);
-
-            // Calculate expiry
-            var expires_at = new DateTime.now_utc().add(_session_duration);
-            stdout.printf("SESSION DEBUG: Session expires at: %s\n", expires_at.format_iso8601());
-
-            // Create session via repository
-            stdout.printf("SESSION DEBUG: Creating session via repository...\n");
-            var session = yield _repository.create(session_id, user_id, expires_at);
-            stdout.printf("SESSION DEBUG: Session created via repository\n");
-
-            // Set optional fields
-            if (ip_address != null) {
-                session.ip_address = ip_address;
-            }
-            if (user_agent != null) {
-                session.user_agent = user_agent;
-            }
-
-            // Update session with optional fields if any were set
-            if (ip_address != null || user_agent != null) {
-                stdout.printf("SESSION DEBUG: Updating session with IP/User-Agent...\n");
-                yield _repository.update(session);
-            }
-
-            stdout.printf("SESSION DEBUG: Session created successfully: %s\n", session_id);
-            return session;
-        }
-
-        // =========================================================================
-        // Session Token Generation
-        // =========================================================================
-
-        /**
-         * Generates a signed and encrypted session token.
-         *
-         * Creates a JSON payload with session_id, user_id, expires_at
-         * and uses CryptographyProvider.author() with the session-token namespace.
-         *
-         * Note: For integration with the Authorisation system, use
-         * generate_authorisation_token() which creates an AuthorisationToken.
-         *
-         * @param session The session to generate a token for
-         * @return The encrypted token string (URL-safe Base64)
-         */
-        public string generate_session_token(Session session) {
-            stdout.printf("SESSION DEBUG: generate_session_token() called for session: %s\n", session.id);
-            stdout.printf("SESSION DEBUG: Session user_id: %s, expires_at: %s\n",
-                session.user_id, session.expires_at.format_iso8601());
-            
-            // Create JSON payload
-            var payload_obj = new Json.Object();
-            payload_obj.set_string_member("session_id", session.id);
-            payload_obj.set_string_member("user_id", session.user_id);
-            payload_obj.set_string_member("expires_at", session.expires_at.format_iso8601());
-
-            // Wrap object in a node for serialization
-            var node = new Json.Node(Json.NodeType.OBJECT);
-            node.set_object(payload_obj);
-            var payload = Json.to_string(node, false);
-            stdout.printf("SESSION DEBUG: Token payload JSON: %s\n", payload);
-
-            // Sign and seal the token
-            try {
-                stdout.printf("SESSION DEBUG: Calling author()...\n");
-                var blob = _crypto.author(NAMESPACE, payload);
-                var token = Base64.encode(blob).replace("+", "-").replace("/", "_");
-                stdout.printf("SESSION DEBUG: Token generated successfully (length: %d)\n", token.length);
-                return token;
-            } catch (Error e) {
-                warning("Failed to generate session token: %s", e.message);
-                return "";
-            }
-        }
-
-        /**
-         * Generates an AuthorisationToken for a user session.
-         * 
-         * This method creates a token compatible with the Authorisation system,
-         * using the AuthorisationTokenService if available.
-         *
-         * @param user The authenticated user
-         * @param session The session for the user
-         * @return The encrypted authorisation token string
-         */
-        public string generate_authorisation_token(User user, Session session) {
-            // If AuthorisationTokenService is available, use it
-            if (_token_service != null) {
-                return _token_service.generate_token(user, session.expires_at);
-            }
-            
-            // Fall back to session token generation
-            return generate_session_token(session);
-        }
-
-        // =========================================================================
-        // Session Validation
-        // =========================================================================
-
-        /**
-         * Validates a session token and returns the result.
-         *
-         * This method:
-         * - Uses CryptographyProvider.read() with session-token namespace
-         * - Checks expiry
-         * - Loads session from storage
-         * - Verifies session exists and matches token data
-         *
-         * @param token The encrypted token string (URL-safe Base64)
-         * @return A SessionValidationResult with session and user info
-         */
-        public async SessionValidationResult validate_session_token_async(string token) throws Error {
-            // Decode Base64
-            var decoded = Base64.decode(token.replace("-", "+").replace("_", "/"));
-            
-            // Decrypt and verify the token
-            var payload = _crypto.read(NAMESPACE, decoded);
-
-            if (payload == null) {
-                return new SessionValidationResult.failure("Could not decrypt token");
-            }
-
-            var json = new JsonElement.from_string((!)payload);
-            var obj = json.as<JsonObject>();
-
-            var session_id = obj.get("session_id").as<string>();
-            var user_id = obj.get("user_id").as<string>();
-            var expires_at_str = obj.get("expires_at").as<string>();
-            var expires_at = new DateTime.from_iso8601(expires_at_str, new TimeZone.utc());
-
-            // Check expiry
-            if (expires_at.compare(new DateTime.now_utc()) <= 0) {
-                return new SessionValidationResult.failure("Session has expired");
-            }
-
-            // Load session from storage
-            var session = yield get_session_async(session_id);
-            if (session == null) {
-                return new SessionValidationResult.failure("Session not found");
-            }
-
-            // Verify session matches token data
-            if (session.user_id != user_id) {
-                return new SessionValidationResult.failure("Session user mismatch");
-            }
-
-            return new SessionValidationResult.success(session);
-        }
-
-        // =========================================================================
-        // Session Retrieval
-        // =========================================================================
-
-        /**
-         * Gets a session by its unique ID.
-         *
-         * @param session_id The session's unique identifier
-         * @return The Session, or null if not found or expired
-         * @throws Error on storage failure
-         */
-        public async Session? get_session_async(string session_id) throws Error {
-            var session = yield _repository.get_by_id(session_id);
-
-            // Don't return expired sessions
-            if (session != null && session.is_expired()) {
-                return null;
-            }
-
-            return session;
-        }
-
-        /**
-         * Gets all sessions for a user.
-         *
-         * @param user_id The user's unique identifier
-         * @return A Vector of active (non-expired) sessions
-         * @throws Error on storage failure
-         */
-        public async Vector<Session> get_sessions_for_user_async(string user_id) throws Error {
-            var all_sessions = yield _repository.get_by_user_id(user_id);
-            
-            // Filter out expired sessions
-            var active_sessions = new Vector<Session>();
-            foreach (var session in all_sessions) {
-                if (!session.is_expired()) {
-                    active_sessions.add(session);
-                }
-            }
-
-            return active_sessions;
-        }
-
-        // =========================================================================
-        // Session Deletion
-        // =========================================================================
-
-        /**
-         * Deletes a session by its unique ID.
-         *
-         * @param session_id The session's unique identifier
-         * @throws Error on failure
-         */
-        public async void delete_session_async(string session_id) throws Error {
-            // Get session first to verify it exists
-            var session = yield get_session_async(session_id);
-            if (session == null) {
-                throw new SessionError.SESSION_NOT_FOUND("Session not found");
-            }
-
-            // Delete session via repository
-            yield _repository.delete(session_id);
-        }
-
-        /**
-         * Deletes all sessions for a user.
-         *
-         * @param user_id The user's unique identifier
-         * @throws Error on storage failure
-         */
-        public async void delete_all_sessions_for_user_async(string user_id) throws Error {
-            yield _repository.delete_by_user_id(user_id);
-        }
-
-        // =========================================================================
-        // Cookie Handling
-        // =========================================================================
-
-        /**
-         * Sets the session cookie on an HTTP response.
-         *
-         * Cookie configuration:
-         * - HttpOnly: true
-         * - Secure: configurable (default true)
-         * - SameSite: Strict
-         * - Path: /
-         *
-         * @param result The HttpResult to set the cookie header on
-         * @param token The session token to set
-         */
-        public void set_session_cookie(HttpResult result, string token) {
-            stdout.printf("SESSION DEBUG: set_session_cookie() called\n");
-            stdout.printf("SESSION DEBUG: Cookie name: %s, Token length: %d\n", _cookie_name, token.length);
-            
-            // Build the Set-Cookie header value manually
-            var max_age = (int)(_session_duration / TimeSpan.SECOND);
-            stdout.printf("SESSION DEBUG: Max-Age: %d seconds\n", max_age);
-            
-            var cookie_value = @"$_cookie_name=$token; Path=/; Max-Age=$max_age; HttpOnly";
-
-            if (_cookie_secure) {
-                cookie_value += "; Secure";
-            }
-
-            cookie_value += "; SameSite=Strict";
-
-            stdout.printf("SESSION DEBUG: Cookie header value (length: %d): %s\n",
-                cookie_value.length, cookie_value.substring(0, int.min(100, cookie_value.length)) + "...");
-            stdout.printf("SESSION DEBUG: Calling result.set_header('Set-Cookie', ...)\n");
-            result.set_header("Set-Cookie", cookie_value);
-            stdout.printf("SESSION DEBUG: Cookie header set successfully\n");
-        }
-
-        /**
-         * Clears the session cookie on an HTTP response.
-         *
-         * @param result The HttpResult to clear the cookie on
-         */
-        public void clear_session_cookie(HttpResult result) {
-            var cookie_value = @"$_cookie_name=; Path=/; Max-Age=0; HttpOnly";
-
-            if (_cookie_secure) {
-                cookie_value += "; Secure";
-            }
-
-            cookie_value += "; SameSite=Strict";
-
-            result.set_header("Set-Cookie", cookie_value);
-        }
-
-        /**
-         * Gets the session cookie value from an HTTP request.
-         *
-         * @param http_context The HttpContext containing the request
-         * @return The session cookie value, or null if not present
-         */
-        public string? get_session_cookie(HttpContext http_context) {
-            return http_context.request.get_cookie(_cookie_name);
-        }
-
-        // =========================================================================
-        // Session Cleanup
-        // =========================================================================
-
-        /**
-         * Removes all expired sessions from storage.
-         *
-         * @throws Error on storage failure
-         */
-        public async void cleanup_expired_sessions_async() throws Error {
-            yield _repository.delete_expired();
-        }
-
-        // =========================================================================
-        // Authentication Helper
-        // =========================================================================
-
-        /**
-         * Authenticates an HTTP request using the session cookie.
-         *
-         * This method:
-         * - Gets session cookie from request
-         * - Validates token
-         * - Loads user via UserService
-         * - Returns result with user and session
-         *
-         * @param http_context The HttpContext containing the request to authenticate
-         * @param user_service The UserService to load users from
-         * @return An AuthResult with authentication status and user/session info
-         * @throws Error on storage failure
-         */
-        public async AuthResult authenticate_request_async(HttpContext http_context, UserService user_service) throws Error {
-            // Get session cookie
-            var token = get_session_cookie(http_context);
-
-            if (token == null) {
-                return new AuthResult.failure("No session cookie found");
-            }
-
-            // Validate token
-            var validation = yield validate_session_token_async((!)token);
-
-            if (!validation.is_valid) {
-                return new AuthResult.failure(validation.error_message ?? "Invalid session");
-            }
-
-            var session = validation.session;
-            if (session == null) {
-                return new AuthResult.failure("Session not found");
-            }
-
-            // Load user
-            var user = yield user_service.get_user_async(session.user_id);
-            if (user == null) {
-                return new AuthResult.failure("User not found");
-            }
-
-            return new AuthResult.success(user, session);
-        }
-
-        // =========================================================================
-        // Private Helper Methods
-        // =========================================================================
-
-        private string generate_uuid() {
-            // Generate UUID v4 using libsodium random bytes
-            uint8[] bytes = new uint8[16];
-            Sodium.Random.random_bytes(bytes);
-
-            // Set version (4) and variant bits
-            bytes[6] = (bytes[6] & 0x0f) | 0x40;  // Version 4
-            bytes[8] = (bytes[8] & 0x3f) | 0x80;  // Variant 1
-
-            // Format as UUID string
-            return "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x".printf(
-                bytes[0], bytes[1], bytes[2], bytes[3],
-                bytes[4], bytes[5], bytes[6], bytes[7],
-                bytes[8], bytes[9], bytes[10], bytes[11],
-                bytes[12], bytes[13], bytes[14], bytes[15]
-            );
-        }
-    }
-}

+ 0 - 189
src/Authentication/SqlSessionRepository.vala

@@ -1,189 +0,0 @@
-using Invercargill;
-using Invercargill.DataStructures;
-using InvercargillSql;
-
-namespace Spry.Authentication {
-
-    /**
-     * SQL implementation of SessionRepository using InvercargillSql.
-     */
-    public class SqlSessionRepository : Object, SessionRepository {
-
-        private Connection _connection;
-
-        // =========================================================================
-        // Constructor
-        // =========================================================================
-
-        public SqlSessionRepository(Connection connection) {
-            _connection = connection;
-        }
-
-        // =========================================================================
-        // Retrieval Operations
-        // =========================================================================
-
-        public async Session? get_by_id(string id) throws Error {
-            var sql = "SELECT * FROM sessions WHERE id = :id";
-
-            var results = yield _connection.create_command(sql)
-                .with_parameter("id", id)
-                .execute_query_async();
-
-            var row = results.first_or_default();
-            if (row == null) {
-                return null;
-            }
-
-            return session_from_properties(row);
-        }
-
-        public async Vector<Session> get_by_user_id(string user_id) throws Error {
-            var sql = """
-                SELECT * FROM sessions
-                WHERE user_id = :user_id
-                ORDER BY created_at DESC
-            """;
-
-            var results = yield _connection.create_command(sql)
-                .with_parameter("user_id", user_id)
-                .execute_query_async();
-
-            var sessions = new Vector<Session>();
-            foreach (var row in results) {
-                sessions.add(session_from_properties(row));
-            }
-
-            return sessions;
-        }
-
-        // =========================================================================
-        // Mutation Operations
-        // =========================================================================
-
-        public async Session create(string id, string user_id, DateTime expires_at) throws Error {
-            var now = new DateTime.now_utc();
-
-            var sql = """
-                INSERT INTO sessions (id, user_id, created_at, expires_at, last_accessed_at)
-                VALUES (:id, :user_id, :created_at, :expires_at, :last_accessed_at)
-            """;
-
-            yield _connection.create_command(sql)
-                .with_parameter("id", id)
-                .with_parameter("user_id", user_id)
-                .with_parameter("created_at", now.format_iso8601())
-                .with_parameter("expires_at", expires_at.format_iso8601())
-                .with_parameter("last_accessed_at", now.format_iso8601())
-                .execute_non_query_async();
-
-            var session = new Session();
-            session.id = id;
-            session.user_id = user_id;
-            session.created_at = now;
-            session.expires_at = expires_at;
-
-            return session;
-        }
-
-        public async void update(Session session) throws Error {
-            var sql = """
-                UPDATE sessions SET
-                    expires_at = :expires_at
-                WHERE id = :id
-            """;
-
-            yield _connection.create_command(sql)
-                .with_parameter("id", session.id)
-                .with_parameter("expires_at", session.expires_at.format_iso8601())
-                .execute_non_query_async();
-        }
-
-        public async void delete(string id) throws Error {
-            var sql = "DELETE FROM sessions WHERE id = :id";
-
-            yield _connection.create_command(sql)
-                .with_parameter("id", id)
-                .execute_non_query_async();
-        }
-
-        public async void delete_by_user_id(string user_id) throws Error {
-            var sql = "DELETE FROM sessions WHERE user_id = :user_id";
-
-            yield _connection.create_command(sql)
-                .with_parameter("user_id", user_id)
-                .execute_non_query_async();
-        }
-
-        // =========================================================================
-        // Cleanup Operations
-        // =========================================================================
-
-        public async void delete_expired() throws Error {
-            var now = new DateTime.now_utc();
-
-            var sql = "DELETE FROM sessions WHERE expires_at < :now";
-
-            yield _connection.create_command(sql)
-                .with_parameter("now", now.format_iso8601())
-                .execute_non_query_async();
-        }
-
-        // =========================================================================
-        // Private Helpers
-        // =========================================================================
-
-        private Session session_from_properties(Properties props) {
-            var session = new Session();
-
-            // Required fields
-            session.id = get_string_or_empty(props, "id");
-            session.user_id = get_string_or_empty(props, "user_id");
-
-            // created_at
-            var created_str = get_string_or_empty(props, "created_at");
-            if (created_str.length > 0) {
-                session.created_at = new DateTime.from_iso8601(created_str, new TimeZone.utc());
-            }
-
-            // expires_at
-            var expires_str = get_string_or_empty(props, "expires_at");
-            if (expires_str.length > 0) {
-                session.expires_at = new DateTime.from_iso8601(expires_str, new TimeZone.utc());
-            }
-
-            // ip_address (nullable)
-            var ip_address = get_string_or_null(props, "ip_address");
-            session.ip_address = ip_address;
-
-            // user_agent (nullable)
-            var user_agent = get_string_or_null(props, "user_agent");
-            session.user_agent = user_agent;
-
-            return session;
-        }
-
-        private string get_string_or_empty(Properties props, string key) {
-            if (!props.has(key)) {
-                return "";
-            }
-            var elem = props.get(key);
-            if (elem == null) {
-                return "";
-            }
-            var str = elem.as<string>();
-            return str ?? "";
-        }
-
-        private string? get_string_or_null(Properties props, string key) {
-            if (!props.has(key)) {
-                return null;
-            }
-            var elem = props.get(key);
-            if (elem == null) {
-                return null;
-            }
-            return elem.as<string>();
-        }
-    }
-}

+ 0 - 401
src/Authentication/SqlUserRepository.vala

@@ -1,401 +0,0 @@
-using Invercargill;
-using Invercargill.DataStructures;
-using InvercargillSql;
-
-namespace Spry.Authentication {
-
-    /**
-     * SQL implementation of UserRepository using InvercargillSql.
-     */
-    public class SqlUserRepository : Object, UserRepository {
-
-        private Connection _connection;
-
-        // =========================================================================
-        // Constructor
-        // =========================================================================
-
-        public SqlUserRepository(Connection connection) {
-            _connection = connection;
-        }
-
-        // =========================================================================
-        // Retrieval Operations
-        // =========================================================================
-
-        public async User? get_by_id(string id) throws Error {
-            var sql = "SELECT * FROM users WHERE id = :id";
-
-            var results = yield _connection.create_command(sql)
-                .with_parameter("id", id)
-                .execute_query_async();
-
-            var row = results.first_or_default();
-            if (row == null) {
-                return null;
-            }
-
-            var user = user_from_properties(row);
-
-            // Load permissions
-            var permissions = yield get_permissions(id);
-            foreach (var perm in permissions) {
-                user.add_permission(perm);
-            }
-
-            // Load app data
-            var app_data = yield get_all_app_data(id);
-            foreach (var key in app_data.keys) {
-                user.app_data.set(key, app_data.get(key));
-            }
-
-            return user;
-        }
-
-        public async User? get_by_username(string username) throws Error {
-            var sql = "SELECT * FROM users WHERE username = :username";
-
-            var results = yield _connection.create_command(sql)
-                .with_parameter("username", username)
-                .execute_query_async();
-
-            var row = results.first_or_default();
-            if (row == null) {
-                return null;
-            }
-
-            var user = user_from_properties(row);
-
-            // Load permissions
-            var permissions = yield get_permissions(user.id);
-            foreach (var perm in permissions) {
-                user.add_permission(perm);
-            }
-
-            // Load app data
-            var app_data = yield get_all_app_data(user.id);
-            foreach (var key in app_data.keys) {
-                user.app_data.set(key, app_data.get(key));
-            }
-
-            return user;
-        }
-
-        public async User? get_by_email(string email) throws Error {
-            var sql = "SELECT * FROM users WHERE email = :email";
-
-            var results = yield _connection.create_command(sql)
-                .with_parameter("email", email)
-                .execute_query_async();
-
-            var row = results.first_or_default();
-            if (row == null) {
-                return null;
-            }
-
-            var user = user_from_properties(row);
-
-            // Load permissions
-            var permissions = yield get_permissions(user.id);
-            foreach (var perm in permissions) {
-                user.add_permission(perm);
-            }
-
-            // Load app data
-            var app_data = yield get_all_app_data(user.id);
-            foreach (var key in app_data.keys) {
-                user.app_data.set(key, app_data.get(key));
-            }
-
-            return user;
-        }
-
-        // =========================================================================
-        // Mutation Operations
-        // =========================================================================
-
-        public async User create(string username, string email, string password_hash) throws Error {
-            var id = generate_uuid();
-            var now = new DateTime.now_utc();
-
-            var sql = """
-                INSERT INTO users (id, username, email, password_hash, created_at, updated_at)
-                VALUES (:id, :username, :email, :password_hash, :created_at, :updated_at)
-            """;
-
-            yield _connection.create_command(sql)
-                .with_parameter("id", id)
-                .with_parameter("username", username)
-                .with_parameter("email", email)
-                .with_parameter("password_hash", password_hash)
-                .with_parameter("created_at", now.format_iso8601())
-                .with_parameter("updated_at", now.format_iso8601())
-                .execute_non_query_async();
-
-            var user = new User();
-            user.set_id(id);
-            user.set_username(username);
-            user.email = email;
-            user.password_hash = password_hash;
-            user.created_at = now;
-            user.updated_at = now;
-
-            return user;
-        }
-
-        public async void update(User user) throws Error {
-            var now = new DateTime.now_utc();
-
-            var sql = """
-                UPDATE users SET
-                    username = :username,
-                    email = :email,
-                    password_hash = :password_hash,
-                    updated_at = :updated_at
-                WHERE id = :id
-            """;
-
-            yield _connection.create_command(sql)
-                .with_parameter("id", user.id)
-                .with_parameter("username", user.username)
-                .with_parameter("email", user.email)
-                .with_parameter("password_hash", user.password_hash)
-                .with_parameter("updated_at", now.format_iso8601())
-                .execute_non_query_async();
-
-            user.updated_at = now;
-        }
-
-        public async void delete(string id) throws Error {
-            var sql = "DELETE FROM users WHERE id = :id";
-
-            yield _connection.create_command(sql)
-                .with_parameter("id", id)
-                .execute_non_query_async();
-        }
-
-        // =========================================================================
-        // Query Operations
-        // =========================================================================
-
-        public async bool exists_by_username(string username) throws Error {
-            var sql = "SELECT COUNT(*) FROM users WHERE username = :username";
-
-            var scalar = yield _connection.create_command(sql)
-                .with_parameter("username", username)
-                .execute_scalar_async();
-
-            if (scalar == null) {
-                return false;
-            }
-
-            return scalar.as<int64?>() > 0;
-        }
-
-        public async bool exists_by_email(string email) throws Error {
-            var sql = "SELECT COUNT(*) FROM users WHERE email = :email";
-
-            var scalar = yield _connection.create_command(sql)
-                .with_parameter("email", email)
-                .execute_scalar_async();
-
-            if (scalar == null) {
-                return false;
-            }
-
-            return scalar.as<int64?>() > 0;
-        }
-
-        // =========================================================================
-        // Permission Operations
-        // =========================================================================
-
-        public async void add_permission(string user_id, string permission) throws Error {
-            var sql = """
-                INSERT OR IGNORE INTO user_permissions (user_id, permission)
-                VALUES (:user_id, :permission)
-            """;
-
-            yield _connection.create_command(sql)
-                .with_parameter("user_id", user_id)
-                .with_parameter("permission", permission)
-                .execute_non_query_async();
-        }
-
-        public async void remove_permission(string user_id, string permission) throws Error {
-            var sql = """
-                DELETE FROM user_permissions
-                WHERE user_id = :user_id AND permission = :permission
-            """;
-
-            yield _connection.create_command(sql)
-                .with_parameter("user_id", user_id)
-                .with_parameter("permission", permission)
-                .execute_non_query_async();
-        }
-
-        public async bool has_permission(string user_id, string permission) throws Error {
-            var sql = """
-                SELECT COUNT(*) FROM user_permissions
-                WHERE user_id = :user_id AND permission = :permission
-            """;
-
-            var scalar = yield _connection.create_command(sql)
-                .with_parameter("user_id", user_id)
-                .with_parameter("permission", permission)
-                .execute_scalar_async();
-
-            if (scalar == null) {
-                return false;
-            }
-
-            return scalar.as<int64?>() > 0;
-        }
-
-        public async Vector<string> get_permissions(string user_id) throws Error {
-            var sql = "SELECT permission FROM user_permissions WHERE user_id = :user_id";
-
-            var results = yield _connection.create_command(sql)
-                .with_parameter("user_id", user_id)
-                .execute_query_async();
-
-            var permissions = new Vector<string>();
-            foreach (var row in results) {
-                var perm_elem = row.get("permission");
-                if (perm_elem != null) {
-                    var perm = perm_elem.as<string>();
-                    if (perm != null && perm.length > 0) {
-                        permissions.add(perm);
-                    }
-                }
-            }
-
-            return permissions;
-        }
-
-        // =========================================================================
-        // App Data Operations
-        // =========================================================================
-
-        public async void set_app_data(string user_id, string key, string value) throws Error {
-            var sql = """
-                INSERT OR REPLACE INTO user_app_data (user_id, key, value)
-                VALUES (:user_id, :key, :value)
-            """;
-
-            yield _connection.create_command(sql)
-                .with_parameter("user_id", user_id)
-                .with_parameter("key", key)
-                .with_parameter("value", value)
-                .execute_non_query_async();
-        }
-
-        public async string? get_app_data(string user_id, string key) throws Error {
-            var sql = """
-                SELECT value FROM user_app_data
-                WHERE user_id = :user_id AND key = :key
-            """;
-
-            var scalar = yield _connection.create_command(sql)
-                .with_parameter("user_id", user_id)
-                .with_parameter("key", key)
-                .execute_scalar_async();
-
-            if (scalar == null) {
-                return null;
-            }
-
-            return scalar.as<string>();
-        }
-
-        // =========================================================================
-        // Private Helpers
-        // =========================================================================
-
-        private User user_from_properties(Properties props) {
-            var user = new User();
-
-            // Required fields
-            user.set_id(get_string_or_empty(props, "id"));
-            user.set_username(get_string_or_empty(props, "username"));
-            user.email = get_string_or_empty(props, "email");
-            user.password_hash = get_string_or_empty(props, "password_hash");
-
-            // created_at
-            var created_str = get_string_or_empty(props, "created_at");
-            if (created_str.length > 0) {
-                user.created_at = new DateTime.from_iso8601(created_str, new TimeZone.utc());
-            }
-
-            // updated_at (nullable)
-            var updated_str = get_string_or_null(props, "updated_at");
-            if (updated_str != null && updated_str.length > 0) {
-                user.updated_at = new DateTime.from_iso8601(updated_str, new TimeZone.utc());
-            }
-
-            return user;
-        }
-
-        private string get_string_or_empty(Properties props, string key) {
-            if (!props.has(key)) {
-                return "";
-            }
-            var elem = props.get(key);
-            if (elem == null) {
-                return "";
-            }
-            var str = elem.as<string>();
-            return str ?? "";
-        }
-
-        private string? get_string_or_null(Properties props, string key) {
-            if (!props.has(key)) {
-                return null;
-            }
-            var elem = props.get(key);
-            if (elem == null) {
-                return null;
-            }
-            return elem.as<string>();
-        }
-
-        private async Dictionary<string, string> get_all_app_data(string user_id) throws Error {
-            var sql = "SELECT key, value FROM user_app_data WHERE user_id = :user_id";
-
-            var results = yield _connection.create_command(sql)
-                .with_parameter("user_id", user_id)
-                .execute_query_async();
-
-            var app_data = new Dictionary<string, string>();
-            foreach (var row in results) {
-                var key_elem = row.get("key");
-                var value_elem = row.get("value");
-
-                if (key_elem != null) {
-                    var key = key_elem.as<string>();
-                    var value = value_elem != null ? value_elem.as<string>() ?? "" : "";
-                    if (key != null && key.length > 0) {
-                        app_data.set(key, value);
-                    }
-                }
-            }
-
-            return app_data;
-        }
-
-        private string generate_uuid() {
-            uint8[] bytes = new uint8[16];
-            Sodium.Random.random_bytes(bytes);
-            // Set version 4 (random UUID)
-            bytes[6] = (bytes[6] & 0x0f) | 0x40;
-            // Set variant RFC 4122
-            bytes[8] = (bytes[8] & 0x3f) | 0x80;
-            return "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x".printf(
-                bytes[0], bytes[1], bytes[2], bytes[3],
-                bytes[4], bytes[5], bytes[6], bytes[7],
-                bytes[8], bytes[9], bytes[10], bytes[11],
-                bytes[12], bytes[13], bytes[14], bytes[15]
-            );
-        }
-    }
-}

+ 0 - 20
src/Authentication/User.vala

@@ -1,20 +0,0 @@
-using Invercargill;
-using Invercargill.DataStructures;
-using Json;
-
-namespace Spry.Authentication {
-
-    public class User : GLib.Object, Authorisation.Identity {
-
-        public string id { get; }
-        public string username { get; }
-        public Invercargill.ImmutableLot<string> permissions { owned get; }
-        public Invercargill.Properties data { owned get; }
-
-        public DateTime created_at { get; }
-        public DateTime updated_at { get; }
-
-        
-
-    }
-}

+ 35 - 0
src/Authentication/UserEntity.vala

@@ -0,0 +1,35 @@
+using InvercargillSql.Orm;
+using Spry.Authorisation;
+
+namespace Spry.Authentication {
+
+    public class UserEntity : Object {
+
+        public int64 id { get; set; }
+        public string username { get; set; }
+        public string email { get; set; }
+        public string forename { get; set; }
+        public string surname { get; set; }
+        public string password_hash { get; set; }
+        public DateTime date_of_birth { get; set; }
+        public DateTime created { get; set; }
+        public DateTime modified { get; set; }
+        public bool enabled { get; set; }
+
+        public static void entity_mapping(EntityMapperBuilder<UserEntity> cfg) {
+            cfg.table("spry_users")
+                .column<int64?>("id", o => o.id, (o, v) => o.id = v)
+                .column<string>("username", o => o.username, (o, v) => o.username = v)
+                .column<string>("email", o => o.email, (o, v) => o.email = v)
+                .column<string>("forename", o => o.forename, (o, v) => o.forename = v)
+                .column<string>("surname", o => o.surname, (o, v) => o.surname = v)
+                .column<string>("password_hash", o => o.password_hash, (o, v) => o.password_hash = v)
+                .column<DateTime>("date_of_birth", o => o.date_of_birth, (o, v) => o.date_of_birth = v)
+                .column<DateTime>("created", o => o.created, (o, v) => o.created = v)
+                .column<DateTime>("modified", o => o.modified, (o, v) => o.modified = v)
+                .column<bool>("enabled", o => o.enabled, (o, v) => o.enabled = v);
+        }
+
+    }
+
+}

+ 16 - 53
src/Authentication/UserIdentityProvider.vala

@@ -1,62 +1,25 @@
+using Spry.Authorisation;
+using InvercargillSql.Orm;
+using Invercargill;
 using Inversion;
 
 namespace Spry.Authentication {
 
-    /**
-     * UserIdentityProvider implements the IdentityProvider interface from the
-     * Authorisation system, bridging Authentication.User to Authorisation.Identity.
-     * 
-     * This allows the Authorisation system to retrieve Identity objects using
-     * the UserService for storage operations.
-     * 
-     * Usage:
-     * Register this as the IdentityProvider implementation in your IoC container:
-     *   container.register<Authorisation.IdentityProvider, UserIdentityProvider>();
-     * 
-     * Or use directly:
-     *   var provider = new UserIdentityProvider();
-     *   var identity = yield provider.get_identity_by_id("user-123");
-     */
-    public class UserIdentityProvider : GLib.Object, Authorisation.IdentityProvider {
+    class UserIdentityProvider : Object, IdentityProvider {
 
-        private UserService _user_service = inject<UserService>();
+        private OrmSession db = inject<OrmSession>();
 
-        /**
-         * Creates a new UserIdentityProvider.
-         * Dependencies are injected via inject<>() pattern.
-         */
-        public UserIdentityProvider() {
-            // Dependencies injected via field initializers
+        public async Authorisation.Identity? get_identity_by_identifier (Element id) throws Error {
+            return yield db.query<UserProjection>()
+                .where(@"id == $(id.as<int64?>())")
+                .first_async();
         }
-
-        /**
-         * Retrieves an Identity by its unique ID.
-         * 
-         * This method looks up a User by ID and returns it as an Identity.
-         * Since User implements Identity, no conversion is needed.
-         * 
-         * @param id The user's unique identifier
-         * @return The User as Identity, or null if not found
-         * @throws Error on retrieval failure
-         */
-        public async Authorisation.Identity? get_identity_by_id(string id) throws Error {
-            var user = yield _user_service.get_user_async(id);
-            return user; // User implements Identity
-        }
-
-        /**
-         * Retrieves an Identity by its username.
-         * 
-         * This method looks up a User by username and returns it as an Identity.
-         * Since User implements Identity, no conversion is needed.
-         * 
-         * @param username The username to look up
-         * @return The User as Identity, or null if not found
-         * @throws Error on retrieval failure
-         */
-        public async Authorisation.Identity? get_identity_by_username(string username) throws Error {
-            var user = yield _user_service.get_user_by_username_async(username);
-            return user; // User implements Identity
+        public async Authorisation.Identity? get_identity_by_username (string username) throws Error {
+            return yield db.query<UserProjection>()
+                .where(@"username == \"$(username)\"") // XXX FIXME once there is a better way to do this
+                .first_async();
         }
+        
     }
-}
+
+}

+ 20 - 0
src/Authentication/UserPermissionEntity.vala

@@ -0,0 +1,20 @@
+using InvercargillSql.Orm;
+
+namespace Spry.Authentication {
+
+    class UserPermissionEntity {
+
+        public int64 id { get; set; }
+        public int64 user_id { get; set; }
+        public string permission { get; set; }
+
+        public static void entity_mapping(EntityMapperBuilder<UserPermissionEntity> cfg) {
+            cfg.table("spry_user_permissions")
+                .column<int64?>("id", o => o.id, (o, v) => o.id = v)
+                .column<int64?>("user_id", o => o.user_id, (o, v) => o.user_id = v)
+                .column<string>("permission", o => o.permission, (o, v) => o.permission = v);
+        }
+
+    }
+
+}

+ 55 - 0
src/Authentication/UserProjection.vala

@@ -0,0 +1,55 @@
+using InvercargillSql.Orm.Projections;
+using Invercargill.DataStructures;
+using Spry.Authorisation;
+using Invercargill;
+
+namespace Spry.Authentication {
+
+    public class UserProjection : Object, Identity {
+
+        public Element identifier { owned get { return new NativeElement<int64?>(id); } }
+        public string username { get { return _username; } }
+        public ImmutableLot<string> permissions { owned get { return _permissions.to_immutable_buffer(); } }
+        public Invercargill.Properties data { owned get {
+            var props = new DataStructures.PropertyDictionary();
+            props.set_native<string>("email", email);
+            props.set_native<string>("forename", forename);
+            props.set_native<string>("surname", surname);
+            props.set_native<DateTime>("date_of_birth", date_of_birth);
+            props.set_native<DateTime>("created", created);
+            props.set_native<DateTime>("modified", modified);
+            return props;
+        }}
+
+        public int64 id { get; set; }
+        public string email { get; set; }
+        public string forename { get; set; }
+        public string surname { get; set; }
+        public string password_hash { get; set; }
+        public DateTime date_of_birth { get; set; }
+        public DateTime created { get; set; }
+        public DateTime modified { get; set; }
+        public bool enabled { get; set; }
+
+        private string _username;
+        private ImmutableBuffer<string> _permissions;
+
+        public static void projection_mapping(ProjectionBuilder<UserProjection> cfg) throws ProjectionError {
+            cfg.source<UserEntity>("u")
+                .select<int64?>("id", "u.id", (o, v) => o.id = v)
+                .select<string>("username", "u.username", (o, v) => o._username = v)
+                .select<string>("email", "u.email", (o, v) => o.email = v)
+                .select<string>("forename", "u.forename", (o, v) => o.forename = v)
+                .select<string>("surname", "u.surname", (o, v) => o.surname = v)
+                .select<string>("password_hash", "u.password_hash", (o, v) => o.password_hash = v)
+                .select<DateTime>("date_of_birth", "u.date_of_birth", (o, v) => o.date_of_birth = v)
+                .select<DateTime>("created", "u.created", (o, v) => o.created = v)
+                .select<DateTime>("modified", "u.modified", (o, v) => o.modified = v)
+                .select<bool>("enabled", "u.enabled", (o, v) => o.enabled = v)
+                .join<UserPermissionEntity>("p", "p.user_id == u.id")
+                .select_many<string>("permissions", "p.permission", (o, v) => o._permissions = v.to_immutable_buffer());
+        }
+
+    }
+
+}

+ 0 - 160
src/Authentication/UserRepository.vala

@@ -1,160 +0,0 @@
-using Invercargill.DataStructures;
-
-namespace Spry.Authentication {
-
-    /**
-     * Repository interface for User persistence operations.
-     * Abstracts the storage mechanism from the service layer.
-     */
-    public interface UserRepository : Object {
-
-        // =========================================================================
-        // Retrieval Operations
-        // =========================================================================
-
-        /**
-         * Gets a user by their unique ID.
-         *
-         * @param id The user's unique identifier
-         * @return The User, or null if not found
-         * @throws Error on storage failure
-         */
-        public abstract async User? get_by_id(string id) throws Error;
-
-        /**
-         * 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 abstract async User? get_by_username(string username) throws Error;
-
-        /**
-         * 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 abstract async User? get_by_email(string email) throws Error;
-
-        // =========================================================================
-        // Mutation Operations
-        // =========================================================================
-
-        /**
-         * Creates a new user.
-         *
-         * @param username The username for the new user
-         * @param email The email for the new user
-         * @param password_hash The hashed password
-         * @return The created User
-         * @throws Error on storage failure
-         */
-        public abstract async User create(string username, string email, string password_hash) throws Error;
-
-        /**
-         * Updates an existing user.
-         *
-         * @param user The user to update
-         * @throws Error on storage failure
-         */
-        public abstract async void update(User user) throws Error;
-
-        /**
-         * Deletes a user by their unique ID.
-         *
-         * @param id The user's unique identifier
-         * @throws Error on storage failure
-         */
-        public abstract async void delete(string id) throws Error;
-
-        // =========================================================================
-        // Query Operations
-        // =========================================================================
-
-        /**
-         * Checks if a username already exists.
-         *
-         * @param username The username to check
-         * @return true if the username exists
-         * @throws Error on storage failure
-         */
-        public abstract async bool exists_by_username(string username) throws Error;
-
-        /**
-         * Checks if an email already exists.
-         *
-         * @param email The email to check
-         * @return true if the email exists
-         * @throws Error on storage failure
-         */
-        public abstract async bool exists_by_email(string email) throws Error;
-
-        // =========================================================================
-        // Permission Operations
-        // =========================================================================
-
-        /**
-         * Adds a permission to a user.
-         *
-         * @param user_id The user's unique identifier
-         * @param permission The permission to add
-         * @throws Error on storage failure
-         */
-        public abstract async void add_permission(string user_id, string permission) throws Error;
-
-        /**
-         * Removes a permission from a user.
-         *
-         * @param user_id The user's unique identifier
-         * @param permission The permission to remove
-         * @throws Error on storage failure
-         */
-        public abstract async void remove_permission(string user_id, string permission) throws Error;
-
-        /**
-         * Checks if a user has a specific permission.
-         *
-         * @param user_id The user's unique identifier
-         * @param permission The permission to check
-         * @return true if the user has the permission
-         * @throws Error on storage failure
-         */
-        public abstract async bool has_permission(string user_id, string permission) throws Error;
-
-        /**
-         * Gets all permissions for a user.
-         *
-         * @param user_id The user's unique identifier
-         * @return A Vector of permission strings
-         * @throws Error on storage failure
-         */
-        public abstract async Vector<string> get_permissions(string user_id) throws Error;
-
-        // =========================================================================
-        // App Data Operations
-        // =========================================================================
-
-        /**
-         * Sets an app data value for a user.
-         *
-         * @param user_id The user's unique identifier
-         * @param key The app data key
-         * @param value The app data value
-         * @throws Error on storage failure
-         */
-        public abstract async void set_app_data(string user_id, string key, string value) throws Error;
-
-        /**
-         * Gets an app data value for a user.
-         *
-         * @param user_id The user's unique identifier
-         * @param key The app data key
-         * @return The app data value, or null if not found
-         * @throws Error on storage failure
-         */
-        public abstract async string? get_app_data(string user_id, string key) throws Error;
-    }
-}

+ 46 - 377
src/Authentication/UserService.vala

@@ -1,400 +1,69 @@
+using InvercargillSql.Orm;
+using Spry.Authorisation;
 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<UserRepository>();
-
-        // =========================================================================
-        // 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");
-            }
+    public class UserService : Object {
 
-            // 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<User> 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<User>();
-        }
-
-        // =========================================================================
-        // 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);
-        }
+        private OrmSession db = inject<OrmSession>();
+        private AuthorisationService authorisation_service = inject<AuthorisationService>();
 
-        /**
-         * 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;
-            }
+        public async AuthorisationToken? authenticate_user(string username, string password) throws Error {
+            var user = yield db.query<UserProjection>()
+                .where(@"username == \"$(username)\"")
+                .first_async();
 
-            // Verify password
-            bool password_valid = verify_password(user, password);
-            
-            if (!password_valid) {
+            if(!Sodium.PasswordHashing.check(user.password_hash, password)){
                 return null;
             }
 
-            return user;
+            return authorisation_service.authorise_identity(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;
-        }
+        public async UserEntity register_user(string username, string email, string forename, string surname, DateTime date_of_birth, string password, bool enabled = true) throws Error {
+            var user = new UserEntity() {
+                username = username,
+                email = email,
+                forename = forename,
+                surname = surname,
+                password_hash = Sodium.PasswordHashing.hash(password),
+                date_of_birth = date_of_birth,
+                created = new DateTime.now_utc(),
+                modified = new DateTime.now_utc(),
+                enabled = enabled,
+            };
 
-        // =========================================================================
-        // 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);
+            db.insert<UserEntity>(user);
+            return user;
         }
 
-        /**
-         * 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);
-        }
+        public async void set_password(int64 user_id, string password) throws Error {
+            var user = yield db.query<UserEntity>()
+                .where(@"id == $user_id")
+                .first_async();
 
-        /**
-         * 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<string> get_permissions_async(User user) throws Error {
-            return yield _repository.get_permissions(user.id);
+            user.password_hash = Sodium.PasswordHashing.hash(password);
+            user.modified = new DateTime.now_utc();
+            db.update<UserEntity>(user);
         }
 
-        // =========================================================================
-        // App Data Operations
-        // =========================================================================
+        public async UserEntity alter_user(int64 user_id, string username, string email, string forename, string surname, DateTime date_of_birth, bool enabled) throws Error {
+            var user = new UserEntity() {
+                username = username,
+                email = email,
+                forename = forename,
+                surname = surname,
+                date_of_birth = date_of_birth,
+                modified = new DateTime.now_utc(),
+                enabled = enabled,
+            };
 
-        /**
-         * 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);
+            db.update<UserEntity>(user);
+            return user;
         }
 
-        /**
-         * 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);
-        }
     }
-}
+
+}

+ 53 - 0
src/Authentication/UserTableMigration.vala

@@ -0,0 +1,53 @@
+using InvercargillSql.Migrations;
+
+namespace Spry.Authentication.Migrations {
+
+    public class UserTableMigration : Migration {
+
+        private int _version;
+        public UserTableMigration(int version) {
+            _version = version;
+        }
+
+        public override string name { get { return "Create Spry Authentication Tables"; } }
+        public override int version { get { return _version; } }
+
+        public override void up (InvercargillSql.Migrations.MigrationBuilder b) {
+            b.create_table ("spry_users",  (t) => {
+                t.column<int64?>("id")
+                    .auto_increment()
+                    .primary_key();
+                t.column<string>("username")
+                    .unique()
+                    .indexed();
+                t.column<string>("email")
+                    .unique()
+                    .indexed();
+                t.column<string>("forename");
+                t.column<string>("surname");
+                t.column<string>("password_hash");
+                t.column<DateTime>("date_of_birth");
+                t.column<DateTime>("created");
+                t.column<DateTime>("modified");
+                t.column<bool>("enabled");
+            });
+
+            b.create_table ("spry_user_permissions",  (t) => {
+                t.column<int64?>("id")
+                    .auto_increment()
+                    .primary_key();
+                t.column<int64?>("user_id")
+                    .references("spry_users", "id");
+                t.column<string>("permission")
+                    .indexed();
+            });
+        }
+
+        public override void down (InvercargillSql.Migrations.MigrationBuilder b) {
+            b.drop_table ("spry_user_permissions");
+            b.drop_table ("spry_users");
+        }
+
+    }
+
+}

+ 3 - 3
src/Authorisation/AuthorisationToken.vala

@@ -19,7 +19,7 @@ namespace Spry.Authorisation {
     public class AuthorisationToken {
 
         // Identity fields
-        public string user_id { get; private set; }
+        public Element user_identifier { get; private set; }
         public string username { get; private set; }
         public ImmutableLot<string> permissions { get; private set; }
         public DateTime issued_at { get; private set; }
@@ -35,7 +35,7 @@ namespace Spry.Authorisation {
          * @param duration Optional token duration (defaults to 24 hours)
          */
         internal AuthorisationToken(Identity identity, TimeSpan? duration = null) {
-            this.user_id = identity.id;
+            this.user_identifier = identity.identifier;
             this.username = identity.username;
             this.permissions = identity.permissions;
             this.data = identity.data;
@@ -66,7 +66,7 @@ namespace Spry.Authorisation {
          */
         public static PropertyMapper<AuthorisationToken> get_mapper() {
             return PropertyMapper.build_for<AuthorisationToken>(cfg => {
-                cfg.map<string>("uid", o => o.user_id, (o, v) => o.user_id = v);
+                cfg.map<Element>("uid", o => o.user_identifier, (o, v) => o.user_identifier = v);
                 cfg.map<string>("unm", o => o.username, (o, v) => o.username = v);
                 cfg.map_many<string>("prm", o => o.permissions, (o, v) => o.permissions = v.to_immutable_buffer());
                 cfg.map<Properties>("dat", o => o.data, (o, v) => o.data = v);

+ 1 - 1
src/Authorisation/Identity.vala

@@ -18,7 +18,7 @@ namespace Spry.Authorisation {
          * Unique identifier for this identity.
          * Used to look up the full identity object.
          */
-        public abstract string id { get; }
+        public abstract Element identifier { owned get; }
 
         /**
          * Human-readable name for this identity.

+ 2 - 1
src/Authorisation/IdentityProvider.vala

@@ -1,3 +1,4 @@
+using Invercargill;
 namespace Spry.Authorisation {
 
     /**
@@ -17,7 +18,7 @@ namespace Spry.Authorisation {
          * @return The Identity, or null if not found/inactive
          * @throws Error on retrieval failure
          */
-        public abstract async Identity? get_identity_by_id(string id) throws Error;
+        public abstract async Identity? get_identity_by_identifier(Element identifier) throws Error;
 
         /**
          * Retrieves an Identity by its username.

+ 6 - 14
src/meson.build

@@ -28,28 +28,20 @@ sources = files(
     'Authorisation/AuthorisationContext.vala',
     'Authorisation/AuthorisationPipelineComponent.vala',
     'Authorisation/AuthorisationService.vala',
-    'Authentication/User.vala',
-    'Authentication/Session.vala',
+    'Authentication/UserEntity.vala',
+    'Authentication/UserPermissionEntity.vala',
+    'Authentication/UserProjection.vala',
     'Authentication/UserService.vala',
-    'Authentication/SessionService.vala',
-    'Authentication/PermissionService.vala',
     'Authentication/UserIdentityProvider.vala',
-    'Authentication/UserRepository.vala',
-    'Authentication/SessionRepository.vala',
-    'Authentication/SqlUserRepository.vala',
-    'Authentication/SqlSessionRepository.vala',
-    'Authentication/CreateAuthTables.vala',
-    'Authentication/Components/LoginFormComponent.vala',
-    'Authentication/Components/UserManagementComponent.vala',
-    'Authentication/Components/UserDetailsComponent.vala',
-    'Authentication/Components/NewUserComponent.vala'
+    'Authentication/AuthenticationModule.vala',
+    'Authentication/UserTableMigration.vala',
 )
 
 
 library_version = meson.project_version()
 libspry = shared_library('spry-@0@'.format(library_version),
     sources,
-    dependencies: [glib_dep, gobject_dep, gio_dep, invercargill_dep, invercargill_json_dep, json_glib_dep, inversion_dep, libxml_dep, astralis_dep, sodium_deps],
+    dependencies: [glib_dep, gobject_dep, gio_dep, invercargill_dep, invercargill_json_dep, json_glib_dep, inversion_dep, libxml_dep, astralis_dep, sodium_deps, invercargill_sql_dep, sqlite_dep, invercargill_sql_inversion_dep],
     install: true,
     vala_gir: 'spry-@0@.gir'.format(library_version),
     install_dir: [true, true, true, true]