Просмотр исходного кода

refactor(auth): convert user services to async with dependency injection

Convert UserService, SessionService, and PermissionService to async
methods that work with Implexus async API. Replace constructor injection
with inject<>() pattern for dependency management.

Add ResponseState class to Component for managing HTTP response state
(headers, cookies, redirects) during action handlers. This enables
proper session cookie handling and redirects from login forms.

Improve JSON parsing safety in User and Session models to handle null
values and missing fields gracefully. Fix XPath selector in Component
for global element extraction.

Update all user components to use async service methods and underscore
prefixed private fields with dependency injection.
Billy Barrow 1 месяц назад
Родитель
Сommit
3049cdb7eb

+ 935 - 0
examples/UsersExample.vala

@@ -0,0 +1,935 @@
+using Astralis;
+using Invercargill;
+using Invercargill.DataStructures;
+using Inversion;
+using Implexus;
+using Implexus.Core;
+using Implexus.Engine;
+using Implexus.Migrations;
+using Spry;
+using Spry.Users;
+using Spry.Users.Components;
+
+/**
+ * UsersExample.vala - Complete example demonstrating the Spry Users system
+ * 
+ * This example demonstrates:
+ * 1. Application Migration - Extends UsersMigration with a specific version
+ * 2. Service Setup - Using Inversion's inject<T>() pattern
+ * 3. User Registration - Creating a new user with username/email/password
+ * 4. User Authentication - Login flow with session creation
+ * 5. Permission Management - Setting and checking permissions
+ * 6. Protected Content - Pages that require authentication
+ * 7. Login Form - Using the built-in LoginFormComponent
+ * 8. User Management - Using UserManagementPage (for users with permission)
+ * 
+ * Route structure:
+ *   /              -> HomePage (public landing page)
+ *   /register      -> RegisterPage (create new account)
+ *   /login         -> LoginPage (uses LoginFormComponent)
+ *   /logout        -> LogoutEndpoint (clears session and redirects)
+ *   /dashboard     -> DashboardPage (protected - requires authentication)
+ *   /admin/users   -> UserManagementPage (protected - requires "user-management" permission)
+ */
+
+// =============================================================================
+// MIGRATION - Sets up the Users system storage structure
+// =============================================================================
+
+/**
+ * MyAppUsersMigration - Application-specific migration for the Users system
+ * 
+ * This creates:
+ * - /spry/users/users - Container for user documents
+ * - /spry/users/sessions - Container for session documents
+ * - /spry/users/users/by_username - Catalogue for username lookups
+ * - /spry/users/users/by_email - Catalogue for email lookups
+ * 
+ * The version string must be unique within your application's migration system.
+ */
+public class MyAppUsersMigration : UsersMigration {
+    public override string version { owned get { return "2026031501"; } }
+}
+
+// =============================================================================
+// STYLESHEETS - CSS content served as FastResources
+// =============================================================================
+
+private const string MAIN_CSS = """
+/* Base Reset & Layout */
+* { box-sizing: border-box; margin: 0; padding: 0; }
+html, body {
+    height: 100%;
+}
+body {
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+    line-height: 1.6;
+    background: #f5f7fa;
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+}
+
+/* Header */
+header {
+    background: #2c3e50;
+    color: white;
+    padding: 1rem 2rem;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    flex-shrink: 0;
+}
+header nav a {
+    color: white;
+    margin-right: 1.5rem;
+    text-decoration: none;
+    font-weight: 500;
+    transition: opacity 0.2s;
+}
+header nav a:hover { opacity: 0.8; }
+header nav a:last-child { margin-right: 0; }
+header .user-info {
+    font-size: 0.9rem;
+}
+header .user-info a {
+    margin-left: 1rem;
+    color: #3498db;
+}
+
+/* Main Content */
+main.container {
+    flex: 1;
+    max-width: 800px;
+    width: 100%;
+    margin: 0 auto;
+    padding: 2rem 1rem;
+}
+
+/* Cards */
+.card {
+    background: white;
+    border-radius: 12px;
+    padding: 2rem;
+    margin-bottom: 1.5rem;
+    box-shadow: 0 2px 8px rgba(0,0,0,0.08);
+}
+
+/* Typography */
+h1 { color: #2c3e50; margin-bottom: 1rem; }
+h2 { color: #34495e; margin-bottom: 0.75rem; margin-top: 1.5rem; }
+p { color: #555; margin-bottom: 1rem; }
+ul { margin-left: 1.5rem; margin-bottom: 1rem; }
+li { margin-bottom: 0.5rem; color: #555; }
+
+/* Links */
+a { color: #3498db; text-decoration: none; transition: color 0.2s; }
+a:hover { color: #2980b9; text-decoration: underline; }
+
+/* Forms */
+.form-group {
+    margin-bottom: 1.25rem;
+}
+.form-group label {
+    display: block;
+    margin-bottom: 0.5rem;
+    font-weight: 500;
+    color: #34495e;
+}
+.form-group input {
+    width: 100%;
+    padding: 0.75rem;
+    border: 2px solid #e0e0e0;
+    border-radius: 6px;
+    font-size: 1rem;
+    transition: border-color 0.2s;
+}
+.form-group input:focus {
+    outline: none;
+    border-color: #3498db;
+}
+.form-group-checkbox {
+    display: flex;
+    align-items: center;
+    gap: 0.5rem;
+}
+.form-group-checkbox input {
+    width: auto;
+}
+.form-group-checkbox label {
+    margin: 0;
+    font-weight: normal;
+}
+
+/* Buttons */
+button, .btn {
+    background: #3498db;
+    color: white;
+    border: none;
+    padding: 0.75rem 1.5rem;
+    border-radius: 6px;
+    cursor: pointer;
+    font-size: 1rem;
+    font-weight: 500;
+    transition: all 0.2s;
+    text-decoration: none;
+    display: inline-block;
+}
+button:hover, .btn:hover {
+    background: #2980b9;
+    text-decoration: none;
+}
+.btn-secondary {
+    background: #6c757d;
+}
+.btn-secondary:hover {
+    background: #545b62;
+}
+
+/* Alerts */
+.alert {
+    padding: 1rem;
+    border-radius: 6px;
+    margin-bottom: 1rem;
+}
+.alert-success {
+    background: #d4edda;
+    color: #155724;
+    border: 1px solid #c3e6cb;
+}
+.alert-error {
+    background: #f8d7da;
+    color: #721c24;
+    border: 1px solid #f5c6cb;
+}
+.error-message {
+    color: #dc3545;
+    font-size: 0.9rem;
+    margin-top: 0.5rem;
+}
+
+/* Login Form */
+.spry-login-form {
+    max-width: 400px;
+}
+.login-btn {
+    width: 100%;
+    margin-top: 1rem;
+}
+
+/* Footer */
+footer {
+    background: #2c3e50;
+    color: rgba(255,255,255,0.7);
+    padding: 1.5rem;
+    text-align: center;
+    flex-shrink: 0;
+}
+
+/* Dashboard */
+.welcome-section {
+    margin-bottom: 2rem;
+}
+.permission-list {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 0.5rem;
+    margin-top: 0.5rem;
+}
+.permission-badge {
+    background: #e9ecef;
+    color: #495057;
+    padding: 0.25rem 0.75rem;
+    border-radius: 20px;
+    font-size: 0.85rem;
+}
+.permission-badge.admin {
+    background: #ffc107;
+    color: #856404;
+}
+""";
+
+// =============================================================================
+// PAGE TEMPLATE - Provides consistent layout for all pages
+// =============================================================================
+
+/**
+ * MainLayoutTemplate - Base template for all pages
+ * 
+ * Provides:
+ * - HTML document structure
+ * - Common <head> elements (scripts, styles)
+ * - Site-wide header with navigation (changes based on auth state)
+ * - Site-wide footer
+ */
+public class MainLayoutTemplate : PageTemplate {
+    
+    private SessionService _session_service = inject<SessionService>();
+    private UserService _user_service = inject<UserService>();
+    private HttpContext _http_context = inject<HttpContext>();
+    
+    public User? current_user { get; private set; }
+    
+    public override string markup { get {
+        return """
+        <!DOCTYPE html>
+        <html lang="en">
+        <head>
+            <meta charset="UTF-8">
+            <meta name="viewport" content="width=device-width, initial-scale=1.0">
+            <title>Spry Users Example</title>
+            <link rel="stylesheet" href="/styles/main.css">
+            <script spry-res="htmx.js"></script>
+        </head>
+        <body>
+            <header>
+                <nav>
+                    <a href="/">Home</a>
+                    <a href="/dashboard">Dashboard</a>
+                    <a href="/admin/users">User Admin</a>
+                </nav>
+                <div class="user-info" sid="user-info">
+                    <!-- Will be populated based on auth state -->
+                </div>
+            </header>
+            <main class="container">
+                <spry-template-outlet />
+            </main>
+            <footer>
+                <p>Built with Spry Framework - Users Example</p>
+            </footer>
+        </body>
+        </html>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        // Try to authenticate the request
+        var auth_result = yield _session_service.authenticate_request_async(_http_context, _user_service);
+        
+        if (auth_result.is_authenticated && auth_result.user != null) {
+            current_user = auth_result.user;
+            this["user-info"].inner_html = @"Logged in as $(auth_result.user.username) | <a href=\"/logout\">Logout</a>";
+        } else {
+            this["user-info"].inner_html = "<a href=\"/login\">Login</a> | <a href=\"/register\">Register</a>";
+        }
+    }
+}
+
+// =============================================================================
+// PAGE COMPONENTS - Public pages
+// =============================================================================
+
+/**
+ * HomePage - Public landing page
+ * 
+ * This page is accessible to everyone, authenticated or not.
+ * It provides an overview of the Users system features.
+ */
+public class HomePage : PageComponent {
+    
+    private SessionService _session_service = inject<SessionService>();
+    private UserService _user_service = inject<UserService>();
+    private HttpContext _http_context = inject<HttpContext>();
+    
+    public User? current_user { get; private set; }
+    
+    public const string ROUTE = "/";
+    
+    public override string markup { get {
+        return """
+        <div class="card">
+            <h1>Welcome to the Spry Users Example</h1>
+            <p>This example demonstrates the complete Spry Users system including:</p>
+            
+            <h2>Features</h2>
+            <ul>
+                <li><strong>User Registration</strong> - Create new accounts with username/email/password</li>
+                <li><strong>Authentication</strong> - Login with session management</li>
+                <li><strong>Permissions</strong> - Granular permission system with wildcard support</li>
+                <li><strong>Protected Pages</strong> - Pages that require authentication</li>
+                <li><strong>User Management</strong> - Admin interface for managing users</li>
+            </ul>
+            
+            <h2>Try It Out</h2>
+            <p sid="status-message"></p>
+            
+            <ul>
+                <li><a href="/register">Register</a> - Create a new account</li>
+                <li><a href="/login">Login</a> - Sign in to your account</li>
+                <li><a href="/dashboard">Dashboard</a> - Protected page (requires login)</li>
+                <li><a href="/admin/users">User Admin</a> - Admin page (requires permission)</li>
+            </ul>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        // Check if user is authenticated
+        var auth_result = yield _session_service.authenticate_request_async(_http_context, _user_service);
+        
+        if (auth_result.is_authenticated && auth_result.user != null) {
+            current_user = auth_result.user;
+            this["status-message"].text_content = @"You are logged in as <strong>$(auth_result.user.username)</strong>.";
+        } else {
+            this["status-message"].text_content = "You are not logged in. Register or login to access protected pages.";
+        }
+    }
+}
+
+/**
+ * RegisterPage - User registration page
+ * 
+ * Allows new users to create an account with username, email, and password.
+ * Demonstrates direct use of UserService.create_user_async().
+ */
+public class RegisterPage : PageComponent {
+    
+    private UserService _user_service = inject<UserService>();
+    private PermissionService _permission_service = inject<PermissionService>();
+    private HttpContext _http_context = inject<HttpContext>();
+    
+    public string? error_message { get; private set; default = null; }
+    public string? success_message { get; private set; default = null; }
+    public string preserved_username { get; private set; default = ""; }
+    public string preserved_email { get; private set; default = ""; }
+    
+    public const string ROUTE = "/register";
+    
+    public override string markup { get {
+        return """
+        <div class="card" sid="register-card" hx-swap="outerHTML">
+            <h1>Create Account</h1>
+            
+            <div spry-if="this.success_message != null" class="alert alert-success" sid="success-alert">
+                <span content-expr="this.success_message"></span>
+            </div>
+            
+            <form sid="register-form" spry-action=":Register" spry-target="register-card">
+                <div class="form-group">
+                    <label for="username">Username</label>
+                    <input type="text" name="username" sid="username-input" required 
+                           autocomplete="username" placeholder="Choose a username"/>
+                </div>
+                
+                <div class="form-group">
+                    <label for="email">Email</label>
+                    <input type="email" name="email" sid="email-input" required 
+                           autocomplete="email" placeholder="Enter your email"/>
+                </div>
+                
+                <div class="form-group">
+                    <label for="password">Password</label>
+                    <input type="password" name="password" sid="password-input" required 
+                           autocomplete="new-password" placeholder="Choose a password"/>
+                </div>
+                
+                <div class="form-group">
+                    <label for="confirm-password">Confirm Password</label>
+                    <input type="password" name="confirm_password" sid="confirm-password-input" required 
+                           autocomplete="new-password" placeholder="Confirm your password"/>
+                </div>
+                
+                <div spry-if="this.error_message != null" class="error-message" sid="error-container">
+                    <span content-expr="this.error_message"></span>
+                </div>
+                
+                <button type="submit">Create Account</button>
+            </form>
+            
+            <p style="margin-top: 1.5rem;">
+                Already have an account? <a href="/login">Login here</a>
+            </p>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        // Preserve form values after failed submission
+        if (preserved_username.length > 0) {
+            this["username-input"].set_attribute("value", preserved_username);
+        }
+        if (preserved_email.length > 0) {
+            this["email-input"].set_attribute("value", preserved_email);
+        }
+    }
+    
+    public async override void handle_action(string action) throws Error {
+        if (action == "Register") {
+            yield handle_register_async();
+        }
+    }
+    
+    private async void handle_register_async() throws Error {
+        var query = _http_context.request.query_params;
+        
+        // Get form values
+        var username = (query.get_any_or_default("username") ?? "").strip();
+        var email = (query.get_any_or_default("email") ?? "").strip();
+        var password = query.get_any_or_default("password") ?? "";
+        var confirm_password = query.get_any_or_default("confirm_password") ?? "";
+        
+        // Preserve values for re-display
+        preserved_username = username;
+        preserved_email = email;
+        
+        // Validate inputs
+        if (username.length < 3) {
+            error_message = "Username must be at least 3 characters";
+            return;
+        }
+        
+        if (!email.contains("@") || !email.contains(".")) {
+            error_message = "Please enter a valid email address";
+            return;
+        }
+        
+        if (password.length < 6) {
+            error_message = "Password must be at least 6 characters";
+            return;
+        }
+        
+        if (password != confirm_password) {
+            error_message = "Passwords do not match";
+            return;
+        }
+        
+        // Attempt to create user
+        try {
+            var user = yield _user_service.create_user_async(username, email, password);
+            
+            // Grant basic permissions to new users
+            yield _permission_service.set_permission_async(user, PermissionService.USER_READ);
+            
+            success_message = @"Account created successfully! You can now <a href=\"/login\">login</a>.";
+            error_message = null;
+            
+            // Clear preserved values on success
+            preserved_username = "";
+            preserved_email = "";
+            
+        } catch (UserError.DUPLICATE_USERNAME e) {
+            error_message = "Username already exists. Please choose another.";
+        } catch (UserError.DUPLICATE_EMAIL e) {
+            error_message = "Email already registered. Please use another or login.";
+        } catch (Error e) {
+            error_message = "Registration failed: %s".printf(e.message);
+        }
+    }
+}
+
+// =============================================================================
+// PAGE COMPONENTS - Authentication pages
+// =============================================================================
+
+/**
+ * LoginPage - Login page using the built-in LoginFormComponent
+ * 
+ * This page demonstrates how to use the LoginFormComponent for authentication.
+ * The component handles:
+ * - Form display and validation
+ * - Authentication via UserService
+ * - Session creation via SessionService
+ * - Cookie management
+ * - Redirect after successful login
+ */
+public class LoginPage : PageComponent {
+    
+    private ComponentFactory _factory = inject<ComponentFactory>();
+    private LoginFormComponent _login_form;
+    
+    public const string ROUTE = "/login";
+    
+    public override string markup { get {
+        return """
+        <div class="card">
+            <h1>Login</h1>
+            <spry-outlet sid="login-form-outlet"/>
+            <p style="margin-top: 1.5rem;">
+                Don't have an account? <a href="/register">Register here</a>
+            </p>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        // Create and configure the login form component
+        _login_form = _factory.create<LoginFormComponent>();
+        _login_form.redirect_url = "/dashboard";  // Redirect here after successful login
+        
+        // Add the form to our outlet
+        add_outlet_child("login-form-outlet", _login_form);
+        
+        // Share globals with the login form (required for action handling)
+        add_globals_from(_login_form);
+    }
+}
+
+/**
+ * LogoutEndpoint - Handles logout by clearing the session
+ *
+ * This demonstrates:
+ * - Getting the current session from the cookie
+ * - Deleting the session from storage
+ * - Clearing the session cookie
+ * - Returning a redirect response
+ */
+public class LogoutEndpoint : Object, Endpoint {
+    
+    private SessionService _session_service = inject<SessionService>();
+    private UserService _user_service = inject<UserService>();
+    
+    public async HttpResult handle_request(HttpContext http_context, RouteContext route_context) throws Error {
+        // Try to get the current session
+        var auth_result = yield _session_service.authenticate_request_async(http_context, _user_service);
+        
+        if (auth_result.is_authenticated && auth_result.session != null) {
+            // Delete the session from storage
+            yield _session_service.delete_session_async(auth_result.session.id);
+        }
+        
+        // Create redirect result with Location header
+        // Note: We use 302 (FOUND) redirect - StatusCode enum may not have FOUND, so we use the numeric value
+        var result = new HttpStringResult("Redirecting to home page...", 302);
+        result.set_header("Location", "/");
+        
+        // Clear the session cookie
+        _session_service.clear_session_cookie(result);
+        
+        return result;
+    }
+}
+
+// =============================================================================
+// PAGE COMPONENTS - Protected pages
+// =============================================================================
+
+/**
+ * DashboardPage - Protected dashboard page
+ * 
+ * This page demonstrates:
+ * - Checking authentication in prepare()
+ * - Redirecting unauthenticated users to login
+ * - Accessing the current user's information
+ * - Checking permissions
+ * - Displaying user-specific content
+ */
+public class DashboardPage : PageComponent {
+    
+    private SessionService _session_service = inject<SessionService>();
+    private UserService _user_service = inject<UserService>();
+    private PermissionService _permission_service = inject<PermissionService>();
+    private HttpContext _http_context = inject<HttpContext>();
+    
+    public User? current_user { get; private set; }
+    public bool is_admin { get; private set; default = false; }
+    public Vector<string> permissions { get; private set; }
+    
+    public const string ROUTE = "/dashboard";
+    
+    public override string markup { get {
+        return """
+        <div class="card">
+            <!-- Not authenticated message -->
+            <div spry-if="!this.is_authenticated" class="alert alert-error">
+                <h2>Authentication Required</h2>
+                <p>You must be logged in to view this page.</p>
+                <p style="margin-top: 1rem;">
+                    <a href="/login" class="btn">Login</a>
+                    <a href="/register" class="btn btn-secondary" style="margin-left: 0.5rem;">Register</a>
+                </p>
+            </div>
+            
+            <!-- Authenticated content -->
+            <div spry-if="this.is_authenticated">
+                <div class="welcome-section">
+                    <h1 sid="welcome-heading">Dashboard</h1>
+                    <p>Welcome to your dashboard! This page demonstrates protected content.</p>
+                </div>
+                
+                <h2>Your Profile</h2>
+                <ul>
+                    <li><strong>Username:</strong> <span sid="username"></span></li>
+                    <li><strong>Email:</strong> <span sid="email"></span></li>
+                    <li><strong>User ID:</strong> <span sid="user-id"></span></li>
+                    <li><strong>Account Created:</strong> <span sid="created-at"></span></li>
+                </ul>
+                
+                <h2>Your Permissions</h2>
+                <div class="permission-list" sid="permission-list">
+                    <!-- Permissions will be listed here -->
+                </div>
+                
+                <div spry-if="this.is_admin" class="alert alert-success" style="margin-top: 1.5rem;">
+                    <strong>Admin Access:</strong> You have admin privileges.
+                    <a href="/admin/users" style="color: #155724;">Go to User Management</a>
+                </div>
+                
+                <p style="margin-top: 1.5rem;">
+                    <a href="/" class="btn btn-secondary">Back to Home</a>
+                </p>
+            </div>
+        </div>
+        """;
+    }}
+    
+    // Track if user is authenticated (for template binding)
+    public bool is_authenticated { get; private set; default = false; }
+    
+    public override async void prepare() throws Error {
+        // Authenticate the request
+        var auth_result = yield _session_service.authenticate_request_async(_http_context, _user_service);
+        
+        if (!auth_result.is_authenticated || auth_result.user == null) {
+            // Not authenticated - show message and link to login
+            // Note: PageComponent doesn't have redirect(), so we show a message instead
+            is_authenticated = false;
+            return;
+        }
+        
+        is_authenticated = true;
+        
+        current_user = auth_result.user;
+        permissions = _permission_service.get_permissions(current_user);
+        is_admin = _permission_service.has_permission(current_user, PermissionService.ADMIN);
+        
+        // Populate user info
+        this["welcome-heading"].text_content = @"Welcome, $(current_user.username)!";
+        this["username"].text_content = current_user.username;
+        this["email"].text_content = current_user.email;
+        this["user-id"].text_content = current_user.id;
+        this["created-at"].text_content = current_user.created_at.format("%Y-%m-%d %H:%M:%S UTC");
+        
+        // Populate permissions
+        var perm_text = "";
+        foreach (var perm in permissions) {
+            var badge_class = perm == PermissionService.ADMIN ? "permission-badge admin" : "permission-badge";
+            perm_text += @"<span class=\"$badge_class\">$perm</span> ";
+        }
+        if (permissions.length == 0) {
+            perm_text = "<span class=\"permission-badge\">No permissions assigned</span>";
+        }
+        this["permission-list"].inner_html = perm_text;
+    }
+}
+
+// =============================================================================
+// SEED DATA - Creates initial admin user
+// =============================================================================
+
+/**
+ * SeedData - Creates initial users for testing
+ * 
+ * This demonstrates how to:
+ * - Check if users exist before creating
+ * - Create users programmatically
+ * - Grant permissions to users
+ */
+public class SeedData : Object {
+    
+    public static async void ensure_admin_exists(UserService user_service, PermissionService permission_service) throws Error {
+        // Check if admin user exists
+        var admin_user = yield user_service.get_user_by_username_async("admin");
+        
+        if (admin_user != null) {
+            print("Admin user already exists\n");
+            return;
+        }
+        
+        print("Creating admin user...\n");
+        
+        // Create admin user
+        admin_user = yield user_service.create_user_async("admin", "admin@example.com", "admin123");
+        
+        // Grant admin permissions
+        yield permission_service.set_permission_async(admin_user, PermissionService.ADMIN);
+        yield permission_service.set_permission_async(admin_user, PermissionService.USER_MANAGEMENT);
+        yield permission_service.set_permission_async(admin_user, PermissionService.USER_CREATE);
+        yield permission_service.set_permission_async(admin_user, PermissionService.USER_READ);
+        yield permission_service.set_permission_async(admin_user, PermissionService.USER_UPDATE);
+        yield permission_service.set_permission_async(admin_user, PermissionService.USER_DELETE);
+        
+        print("Admin user created with username 'admin' and password 'admin123'\n");
+        
+        // Create a regular test user
+        var test_user = yield user_service.get_user_by_username_async("testuser");
+        if (test_user == null) {
+            print("Creating test user...\n");
+            test_user = yield user_service.create_user_async("testuser", "test@example.com", "test123");
+            yield permission_service.set_permission_async(test_user, PermissionService.USER_READ);
+            print("Test user created with username 'testuser' and password 'test123'\n");
+        }
+    }
+}
+
+// =============================================================================
+// APPLICATION SETUP
+// =============================================================================
+
+// =============================================================================
+// ASYNC MAIN LOOP - Required for async initialization
+// =============================================================================
+
+private MainLoop main_loop;
+private Implexus.Core.Engine global_engine;
+
+public static int main(string[] args) {
+    int port = args.length > 1 ? int.parse(args[1]) : 8080;
+    
+    print("═══════════════════════════════════════════════════════════════\n");
+    print("              Spry Users Example - Complete Demo\n");
+    print("═══════════════════════════════════════════════════════════════\n");
+    print("  Port: %d\n", port);
+    print("═══════════════════════════════════════════════════════════════\n");
+    print("  Public Endpoints:\n");
+    print("    /              - Home page (public)\n");
+    print("    /register      - Create new account\n");
+    print("    /login         - Login page\n");
+    print("═══════════════════════════════════════════════════════════════\n");
+    print("  Protected Endpoints (requires login):\n");
+    print("    /dashboard     - User dashboard\n");
+    print("    /logout        - Logout and clear session\n");
+    print("═══════════════════════════════════════════════════════════════\n");
+    print("  Admin Endpoints (requires 'user-management' permission):\n");
+    print("    /admin/users   - User management page\n");
+    print("═══════════════════════════════════════════════════════════════\n");
+    print("  Default Users:\n");
+    print("    admin / admin123    - Has all permissions\n");
+    print("    testuser / test123  - Regular user\n");
+    print("═══════════════════════════════════════════════════════════════\n");
+    print("\nPress Ctrl+C to stop the server\n\n");
+    
+    main_loop = new MainLoop();
+    
+    try {
+        // 1. Create the embedded engine FIRST
+        print("Creating database engine...\n");
+        global_engine = EngineFactory.create_embedded();
+        
+        // 2. Run migrations to set up containers and catalogues
+        print("Running migrations...\n");
+        run_migrations.begin((obj, res) => {
+            try {
+                run_migrations.end(res);
+                
+                // 3. Now start the application
+                start_application.begin(port, (obj, res) => {
+                    try {
+                        start_application.end(res);
+                    } catch (Error e) {
+                        printerr("Application error: %s\n", e.message);
+                        main_loop.quit();
+                    }
+                });
+            } catch (Error e) {
+                printerr("Migration error: %s\n", e.message);
+                main_loop.quit();
+            }
+        });
+        
+        main_loop.run();
+        return 0;
+        
+    } catch (Error e) {
+        printerr("Error: %s\n", e.message);
+        return 1;
+    }
+}
+
+/**
+ * Run database migrations to set up the Users system structure.
+ */
+private async void run_migrations() throws Error {
+    var migration = new MyAppUsersMigration();
+    yield migration.up_async(global_engine);
+    print("Migrations completed successfully.\n");
+}
+
+/**
+ * Start the web application after migrations are complete.
+ */
+private async void start_application(int port) throws Error {
+    var application = new WebApplication(port);
+    
+    // Register the Engine in the container FIRST before any services
+    // This is critical - services use inject<Engine>() and need it to be available
+    application.add_singleton<Implexus.Core.Engine>(() => global_engine);
+    
+    // Enable compression
+    application.use_compression();
+    
+    // Add Spry module for component actions
+    application.add_module<SpryModule>();
+    
+    // Register Users system services
+    // These use inject<Engine>() internally, so Engine must be registered first
+    application.add_singleton<UserService>();
+    application.add_singleton<SessionService>();
+    application.add_singleton<PermissionService>();
+    
+    // Seed initial data (admin user, test user)
+    application.add_singleton<CryptographyProvider>();
+    seed_initial_data.begin(application.container);
+    
+    // Register template with route prefix
+    // MainLayoutTemplate applies to ALL routes (empty prefix)
+    var spry_cfg = application.configure_with<SpryConfigurator>();
+    spry_cfg.add_template<MainLayoutTemplate>("");
+    
+    // Register page components as endpoints
+    application.add_transient<HomePage>();
+    application.add_endpoint<HomePage>(new EndpointRoute(HomePage.ROUTE));
+    
+    application.add_transient<RegisterPage>();
+    application.add_endpoint<RegisterPage>(new EndpointRoute(RegisterPage.ROUTE));
+    
+    application.add_transient<LoginPage>();
+    application.add_endpoint<LoginPage>(new EndpointRoute(LoginPage.ROUTE));
+    
+    application.add_transient<DashboardPage>();
+    application.add_endpoint<DashboardPage>(new EndpointRoute(DashboardPage.ROUTE));
+    
+    // Register logout endpoint
+    application.add_endpoint<LogoutEndpoint>(new EndpointRoute("/logout"));
+    
+    // Register UserManagementPage (admin page)
+    application.add_transient<UserManagementPage>();
+    application.add_endpoint<UserManagementPage>(new EndpointRoute("/admin/users"));
+    
+    // Register LoginFormComponent (used by LoginPage)
+    application.add_transient<LoginFormComponent>();
+    
+    // Register child components used by UserManagementPage
+    application.add_transient<UserListComponent>();
+    application.add_transient<UserListItemComponent>();
+    application.add_transient<UserFormComponent>();
+    application.add_transient<PermissionEditorComponent>();
+    
+    // Register CSS as FastResource
+    application.add_startup_endpoint<FastResource>(new EndpointRoute("/styles/main.css"), () => {
+        try {
+            return new FastResource.from_string(MAIN_CSS)
+                .with_content_type("text/css; charset=utf-8")
+                .with_default_compressors();
+        } catch (Error e) {
+            error("Failed to create main CSS resource: %s", e.message);
+        }
+    });
+    
+    print("Starting web server on port %d...\n\n", port);
+    application.run();
+}
+
+/**
+ * Seed initial data (admin and test users).
+ */
+private async void seed_initial_data(Container container) {
+    try {
+        var scope = container.create_scope();
+        var user_service = scope.resolve<UserService>();
+        var permission_service = scope.resolve<PermissionService>();
+        yield SeedData.ensure_admin_exists(user_service, permission_service);
+    } catch (Error e) {
+        printerr("Warning: Failed to seed initial data: %s\n", e.message);
+    }
+}

+ 8 - 0
examples/meson.build

@@ -33,3 +33,11 @@ executable('progress-example',
     install: false
     install: false
 )
 )
 
 
+# UsersExample - demonstrates the complete Spry Users system
+# Includes: user registration, authentication, permissions, protected pages
+executable('users-example',
+    'UsersExample.vala',
+    dependencies: [spry_dep, spry_users_dep, astralis_dep, invercargill_dep, inversion_dep, implexus_dep],
+    install: false
+)
+

+ 406 - 0
plans/response-state-design.md

@@ -0,0 +1,406 @@
+# Response State Design for Spry Components
+
+## Problem Statement
+
+Authentication works correctly (user is found, password verified), but:
+1. **Cookie is not being set** - The `Set-Cookie` header is set on a discarded `HttpResult`
+2. **Redirect is not happening** - `hx-redirect` is set as an HTML attribute instead of an HTTP header
+
+### Root Cause Analysis
+
+#### Root Cause #1: Cookie Set on Discarded HttpResult
+
+```mermaid
+sequenceDiagram
+    participant CE as ComponentEndpoint
+    participant C as Component
+    participant SS as SessionService
+    
+    CE->>C: handle_action - login
+    C->>C: to_result - creates HttpResult A
+    C->>SS: set_session_cookie - Result A, token
+    SS->>SS: result.set_header - Set-Cookie on A
+    Note over C: Result A is discarded - async void!
+    C-->>CE: void return
+    CE->>C: to_result - creates NEW HttpResult B
+    C-->>CE: Result B - no cookie!
+```
+
+The problem: `handle_action()` is `async void` - it cannot return an `HttpResult`. The cookie is set on a temporary result that gets discarded.
+
+#### Root Cause #2: HTML Attribute vs HTTP Header
+
+```vala
+// WRONG - This sets an HTML attribute
+this[login-form].set_attribute(hx-redirect, redirect_url);
+
+// CORRECT - This sets an HTTP response header
+result.set_header(HX-Redirect, redirect_url);
+```
+
+HTMX requires `HX-Redirect` as an **HTTP response header**, not an HTML attribute.
+
+---
+
+## Proposed Solution: ResponseState
+
+### Design Overview
+
+Add a `ResponseState` class that components use to accumulate response modifications during action handling. The state is then applied when `to_result()` creates the final `HttpResult`.
+
+```mermaid
+sequenceDiagram
+    participant CE as ComponentEndpoint
+    participant C as Component
+    participant RS as ResponseState
+    participant SS as SessionService
+    
+    CE->>C: handle_action - login
+    C->>RS: add_header - Set-Cookie, token
+    C->>RS: redirect - /dashboard
+    C-->>CE: void return - state accumulated
+    CE->>C: to_result
+    C->>C: create HttpResult
+    C->>RS: apply_to - result
+    RS->>C: result.set_header - Set-Cookie
+    RS->>C: result.set_header - HX-Redirect
+    C-->>CE: Result with headers!
+```
+
+### Key Components
+
+#### 1. ResponseState Class
+
+A new class to hold response modifications:
+
+```vala
+namespace Spry {
+
+    /**
+     * Redirect type for component responses.
+     */
+    public enum RedirectType {
+        /** Client-side redirect using HX-Redirect header - default for HTMX */
+        CLIENT_SIDE,
+        /** HTTP 302 temporary redirect using Location header */
+        TEMPORARY,
+        /** HTTP 301 permanent redirect using Location header */
+        PERMANENT
+    }
+
+    /**
+     * Holds response state that will be applied to the final HttpResult.
+     * 
+     * Components can modify this during action handling to influence
+     * the HTTP response without needing to return an HttpResult directly.
+     */
+    public class ResponseState : GLib.Object {
+        
+        // Headers to add to the response
+        private HashTable<string, string> _headers;
+        
+        // HTTP status code override
+        private StatusCode? _status_code = null;
+        
+        // Redirect configuration
+        private string? _redirect_url = null;
+        private RedirectType _redirect_type = RedirectType.CLIENT_SIDE;
+        
+        /**
+         * Adds a header to the response.
+         * If the header already exists, it will be replaced.
+         */
+        public void set_header(string name, string value) { ... }
+        
+        /**
+         * Adds a header to the response.
+         * Multiple headers with the same name can be added.
+         */
+        public void add_header(string name, string value) { ... }
+        
+        /**
+         * Sets the HTTP status code for the response.
+         */
+        public void set_status(StatusCode status) { ... }
+        
+        /**
+         * Sets up a redirect.
+         * Default type is CLIENT_SIDE which uses HX-Redirect header.
+         */
+        public void redirect(string url, RedirectType type = RedirectType.CLIENT_SIDE) { ... }
+        
+        /**
+         * Applies all accumulated state to an HttpResult.
+         * Called internally by Component.to_result().
+         */
+        internal void apply_to(HttpResult result) { ... }
+        
+        /**
+         * Clears all accumulated state.
+         * Called after to_result() to prepare for next request.
+         */
+        internal void reset() { ... }
+    }
+}
+```
+
+#### 2. Component Changes
+
+Add `ResponseState` property and convenience methods to the base `Component` class:
+
+```vala
+public abstract class Component : Object, Renderable {
+    
+    // Response state for action handlers
+    private ResponseState _response_state = new ResponseState();
+    
+    /**
+     * The response state for this component.
+     * Modify during handle_action to influence the HTTP response.
+     */
+    public ResponseState response { get { return _response_state; } }
+    
+    // Convenience methods that delegate to response_state:
+    
+    /**
+     * Adds a header to the response.
+     */
+    protected void set_header(string name, string value) {
+        _response_state.set_header(name, value);
+    }
+    
+    /**
+     * Sets the HTTP status code.
+     */
+    protected void set_status(StatusCode status) {
+        _response_state.set_status(status);
+    }
+    
+    /**
+     * Sets up a redirect.
+     * Default type is CLIENT_SIDE which uses HX-Redirect header.
+     */
+    protected void redirect(string url, RedirectType type = RedirectType.CLIENT_SIDE) {
+        _response_state.redirect(url, type);
+    }
+    
+    // Modified to_result():
+    public async HttpResult to_result() throws Error {
+        var document = yield to_document();
+        var result = document.to_result(get_status());
+        
+        // Apply accumulated response state
+        _response_state.apply_to(result);
+        
+        // Reset state for next request
+        _response_state.reset();
+        
+        return result;
+    }
+}
+```
+
+---
+
+## Usage Examples
+
+### Example 1: Login with Cookie and Redirect
+
+```vala
+public class LoginFormComponent : Component {
+    
+    private SessionService _session_service = inject<SessionService>();
+    
+    public override async void handle_action(string action) throws Error {
+        if (action == "login") {
+            yield handle_login();
+        }
+    }
+    
+    private async void handle_login() throws Error {
+        // ... authenticate user ...
+        
+        // Create session
+        var session = yield _session_service.create_session_async(user.id);
+        var token = _session_service.generate_session_token(session);
+        
+        // Set cookie using convenience method
+        var cookie_value = @"spry_session=$token; Path=/; Max-Age=86400; HttpOnly; Secure; SameSite=Strict";
+        set_header("Set-Cookie", cookie_value);
+        
+        // Set up HTMX redirect
+        redirect("/dashboard");
+    }
+}
+```
+
+### Example 2: Unauthorized Response
+
+```vala
+public override async void handle_action(string action) throws Error {
+    if (!user_is_admin) {
+        set_status(StatusCode.UNAUTHORIZED);
+        error_message = "Admin access required";
+        return;
+    }
+    // ... handle action ...
+}
+```
+
+### Example 3: HTTP 302 Redirect
+
+```vala
+public override async void handle_action(string action) throws Error {
+    if (action == "logout") {
+        // Clear session
+        yield _session_service.delete_session_async(session_id);
+        
+        // HTTP redirect to login page
+        redirect("/login", RedirectType.TEMPORARY);
+    }
+}
+```
+
+### Example 4: Custom Headers
+
+```vala
+public override async void handle_action(string action) throws Error {
+    // Set custom response headers
+    set_header("X-Custom-Header", "value");
+    set_header("Cache-Control", "no-cache");
+    
+    // Multiple Set-Cookie headers
+    add_header("Set-Cookie", "pref=dark; Path=/");
+    add_header("Set-Cookie", "lang=en; Path=/");
+}
+```
+
+---
+
+## Implementation Details
+
+### Header Accumulation Strategy
+
+Headers use a `HashTable<string, string>` with the following behavior:
+- `set_header()`: Replaces existing header with same name
+- `add_header()`: Appends to header list - for headers that can appear multiple times like `Set-Cookie`
+
+### Redirect Implementation
+
+The `apply_to()` method handles redirects:
+
+```vala
+internal void apply_to(HttpResult result) {
+    // Apply headers first
+    foreach (var header in _headers) {
+        result.set_header(header.key, header.value);
+    }
+    
+    // Handle redirect
+    if (_redirect_url != null) {
+        switch (_redirect_type) {
+            case RedirectType.CLIENT_SIDE:
+                result.set_header("HX-Redirect", _redirect_url);
+                break;
+            case RedirectType.TEMPORARY:
+                result.set_header("Location", _redirect_url);
+                result.set_status(StatusCode.FOUND); // 302
+                break;
+            case RedirectType.PERMANENT:
+                result.set_header("Location", _redirect_url);
+                result.set_status(StatusCode.MOVED_PERMANENTLY); // 301
+                break;
+        }
+    }
+    
+    // Apply status code if set - redirects may override this
+    if (_status_code != null && _redirect_url == null) {
+        result.set_status(_status_code);
+    }
+}
+```
+
+### Interaction with get_status()
+
+The existing `get_status()` virtual method continues to work for initial page renders. `ResponseState` status only applies when set during action handling.
+
+Priority order:
+1. Redirect status (301/302) if redirect is set
+2. ResponseState status if set
+3. `get_status()` return value
+
+---
+
+## Files to Modify
+
+| File | Changes |
+|------|---------|
+| `src/ResponseState.vala` | **NEW** - ResponseState class |
+| `src/Component.vala` | Add ResponseState property, convenience methods, modify to_result |
+| `src/Users/Components/LoginFormComponent.vala` | Use new API for cookie and redirect |
+| `src/Users/SessionService.vala` | May need minor updates for cookie helper |
+| `src/meson.build` | Add ResponseState.vala to build |
+
+---
+
+## API Summary
+
+### New Classes
+
+- `Spry.ResponseState` - Holds response modifications
+- `Spry.RedirectType` - Enum for redirect types
+
+### Component Additions
+
+| Method | Description |
+|--------|-------------|
+| `response` | Property to access ResponseState |
+| `set_header(name, value)` | Set a response header |
+| `add_header(name, value)` | Add a response header |
+| `set_status(status)` | Set HTTP status code |
+| `redirect(url, type)` | Redirect with optional type - default is CLIENT_SIDE |
+
+### ResponseState Methods
+
+| Method | Description |
+|--------|-------------|
+| `set_header(name, value)` | Set/replace a header |
+| `add_header(name, value)` | Add a header |
+| `set_status(status)` | Set status code |
+| `redirect(url, type)` | Redirect with optional type - default is CLIENT_SIDE |
+| `has_modifications()` | Check if any state is set |
+
+---
+
+## Questions for Consideration
+
+1. **Should headers be accumulated or replaced by default?**
+   - Current design: `set_header` replaces, `add_header` appends
+   - Alternative: Always accumulate, provide `replace_header` for explicit replacement
+
+2. **Should there be a distinction between HTMX redirects and HTTP redirects?**
+   - Current design: Yes, via `RedirectType` enum
+   - Default is HTMX redirect since Spry is HTMX-focused
+
+3. **How should this interact with prepare and to_result?**
+   - Current design: State is applied in `to_result()`, then reset
+   - Alternative: Apply in `ComponentEndpoint` after `to_result()`
+
+4. **Should there be convenience methods like set_cookie?**
+   - Could add `set_cookie(name, value, options)` helper
+   - Would encapsulate cookie formatting logic
+
+5. **What happens if both redirect and status are set?**
+   - Current design: Redirect takes precedence for status code
+   - Headers are still applied
+
+---
+
+## Next Steps
+
+1. Review and approve this design
+2. Implement `ResponseState.vala`
+3. Modify `Component.vala` to integrate ResponseState
+4. Update `LoginFormComponent.vala` to use new API
+5. Test login flow with cookie and redirect
+6. Update documentation

+ 29 - 2
src/Component.vala

@@ -21,6 +21,25 @@ namespace Spry {
         private static Mutex templates_lock = Mutex();
         private static Mutex templates_lock = Mutex();
         public string context_key { get; internal set; default = Uuid.string_random(); }
         public string context_key { get; internal set; default = Uuid.string_random(); }
         
         
+        // Response state for action handlers
+        private ResponseState _response_state = new ResponseState();
+        
+        /**
+         * The response state for this component.
+         * Modify during handle_action to influence the HTTP response.
+         *
+         * Example:
+         * {{{
+         * public override async void handle_action(string action) throws Error {
+         *     if (action == "login") {
+         *         response.set_cookie("session", token, 86400);
+         *         response.redirect("/dashboard");
+         *     }
+         * }
+         * }}}
+         */
+        public ResponseState response { get { return _response_state; } }
+        
         public abstract string markup { get; }
         public abstract string markup { get; }
         public virtual StatusCode get_status() {
         public virtual StatusCode get_status() {
             return StatusCode.OK;
             return StatusCode.OK;
@@ -183,7 +202,7 @@ namespace Spry {
             yield transform_document(final_instance);
             yield transform_document(final_instance);
 
 
             // Extract out globals
             // Extract out globals
-            var globals = final_instance.select("//[@spry-global]");
+            var globals = final_instance.select("//*[@spry-global]");
             var globals_document = new MarkupDocument();
             var globals_document = new MarkupDocument();
             globals_document.body.append_nodes(globals);
             globals_document.body.append_nodes(globals);
             return globals_document;
             return globals_document;
@@ -192,7 +211,15 @@ namespace Spry {
 
 
         public async HttpResult to_result() throws Error {
         public async HttpResult to_result() throws Error {
             var document = yield to_document();
             var document = yield to_document();
-            return document.to_result(get_status());
+            var result = document.to_result(get_status());
+            
+            // Apply accumulated response state (headers, cookies, redirects)
+            _response_state.apply_headers(result);
+            
+            // Reset state for next request
+            _response_state.reset();
+            
+            return result;
         }
         }
 
 
         private class ComponentTemplate : MarkupTemplate {
         private class ComponentTemplate : MarkupTemplate {

+ 209 - 0
src/ResponseState.vala

@@ -0,0 +1,209 @@
+using Invercargill;
+using Invercargill.DataStructures;
+using Astralis;
+
+namespace Spry {
+
+    /**
+     * Redirect type for component responses.
+     */
+    public enum RedirectType {
+        /** Client-side redirect using HX-Redirect header - default for HTMX */
+        CLIENT_SIDE,
+        /** HTTP 302 temporary redirect using Location header */
+        TEMPORARY,
+        /** HTTP 301 permanent redirect using Location header */
+        PERMANENT
+    }
+
+    /**
+     * Holds response state that will be applied to the final HttpResult.
+     * 
+     * Components can modify this during action handling to influence
+     * the HTTP response without needing to return an HttpResult directly.
+     * 
+     * This is particularly useful for async void action handlers that need
+     * to set cookies, headers, or redirects on the response.
+     * 
+     * Example usage:
+     * {{{
+     * public override async void handle_action(string action) throws Error {
+     *     if (action == "login") {
+     *         // Set a cookie
+     *         response.set_cookie("session", token, 86400);
+     *         
+     *         // Redirect after login
+     *         response.redirect("/dashboard");
+     *     }
+     * }
+     * }}}
+     */
+    public class ResponseState : GLib.Object {
+        
+        // Headers to add to the response (name -> value)
+        private HashTable<string, string> _headers;
+        
+        // Cookies to set (each as a complete Set-Cookie header value)
+        private List<string> _cookies;
+        
+        // HTTP status code override
+        private StatusCode? _status_code = null;
+        
+        // Redirect configuration
+        private string? _redirect_url = null;
+        private RedirectType _redirect_type = RedirectType.CLIENT_SIDE;
+        
+        public ResponseState() {
+            _headers = new HashTable<string, string>(str_hash, str_equal);
+            _cookies = new List<string>();
+        }
+        
+        /**
+         * Sets a header on the response.
+         * If a header with this name already exists, it will be replaced.
+         */
+        public void set_header(string name, string value) {
+            _headers.insert(name, value);
+        }
+        
+        /**
+         * Adds a header to the response.
+         * Multiple headers with the same name can be added.
+         * Use this for headers like Set-Cookie that can appear multiple times.
+         */
+        public void add_header(string name, string value) {
+            // For Set-Cookie, we store in the cookies list
+            // For other headers, we just replace (since HttpResult doesn't support true multi-headers)
+            if (name == "Set-Cookie") {
+                _cookies.append(value);
+            } else {
+                _headers.insert(name, value);
+            }
+        }
+        
+        /**
+         * Sets the HTTP status code for the response.
+         */
+        public void set_status(StatusCode status) {
+            _status_code = status;
+        }
+        
+        /**
+         * Sets up a redirect.
+         * Default type is CLIENT_SIDE which uses HX-Redirect header for HTMX.
+         * 
+         * @param url The URL to redirect to
+         * @param type The type of redirect (default: CLIENT_SIDE for HTMX)
+         */
+        public void redirect(string url, RedirectType type = RedirectType.CLIENT_SIDE) {
+            _redirect_url = url;
+            _redirect_type = type;
+        }
+        
+        /**
+         * Convenience method to set a session cookie.
+         * 
+         * @param name Cookie name
+         * @param value Cookie value
+         * @param max_age Max age in seconds (-1 for session cookie)
+         * @param path Cookie path (default: "/")
+         * @param http_only Whether to set HttpOnly flag (default: true)
+         * @param secure Whether to set Secure flag (default: true)
+         * @param same_site SameSite value (default: "Strict")
+         */
+        public void set_cookie(
+            string name, 
+            string value, 
+            int max_age = -1,
+            string path = "/",
+            bool http_only = true,
+            bool secure = true,
+            string same_site = "Strict"
+        ) {
+            var cookie_parts = new StringBuilder();
+            cookie_parts.append_printf("%s=%s", name, value);
+            
+            if (path != null && path.length > 0) {
+                cookie_parts.append_printf("; Path=%s", path);
+            }
+            
+            if (max_age >= 0) {
+                cookie_parts.append_printf("; Max-Age=%d", max_age);
+            }
+            
+            if (http_only) {
+                cookie_parts.append("; HttpOnly");
+            }
+            
+            if (secure) {
+                cookie_parts.append("; Secure");
+            }
+            
+            if (same_site != null && same_site.length > 0) {
+                cookie_parts.append_printf("; SameSite=%s", same_site);
+            }
+            
+            _cookies.append(cookie_parts.str);
+        }
+        
+        /**
+         * Checks if any modifications have been made.
+         */
+        public bool has_modifications() {
+            uint headers_count = 0;
+            _headers.foreach((k, v) => { headers_count++; });
+            return headers_count > 0 ||
+                   _cookies.length() > 0 ||
+                   _status_code != null ||
+                   _redirect_url != null;
+        }
+        
+        /**
+         * Gets the status code if one was set.
+         */
+        public StatusCode? get_status_code() {
+            return _status_code;
+        }
+        
+        /**
+         * Checks if a redirect has been configured.
+         */
+        public bool has_redirect() {
+            return _redirect_url != null;
+        }
+        
+        /**
+         * Applies all accumulated headers to an HttpResult.
+         * Note: This only applies headers. Status code and redirect handling
+         * should be done by creating an appropriate HttpResult subclass.
+         */
+        internal void apply_headers(HttpResult result) {
+            // Apply single-value headers
+            _headers.foreach((key, value) => {
+                result.set_header(key, value);
+            });
+            
+            // Apply cookies as Set-Cookie headers
+            foreach (var cookie in _cookies) {
+                result.set_header("Set-Cookie", cookie);
+            }
+            
+            // Apply redirect header for HTMX
+            if (_redirect_url != null && _redirect_type == RedirectType.CLIENT_SIDE) {
+                result.set_header("HX-Redirect", _redirect_url);
+            }
+        }
+        
+        /**
+         * Clears all accumulated state.
+         * Called after to_result() to prepare for next request.
+         */
+        internal void reset() {
+            _headers.remove_all();
+            _cookies = new List<string>();
+            _status_code = null;
+            _redirect_url = null;
+            _redirect_type = RedirectType.CLIENT_SIDE;
+        }
+    }
+}

+ 49 - 31
src/Users/Components/LoginFormComponent.vala

@@ -28,13 +28,15 @@ namespace Spry.Users.Components {
      *   - redirect_url: URL to redirect after successful login (default: "/")
      *   - redirect_url: URL to redirect after successful login (default: "/")
      *   - login_action_name: Custom action name (default: "login")
      *   - login_action_name: Custom action name (default: "login")
      *   - Override markup property for custom styling
      *   - Override markup property for custom styling
+     *
+     * This component uses the inject<> pattern for dependency injection.
      */
      */
     public class LoginFormComponent : Component {
     public class LoginFormComponent : Component {
 
 
-        private UserService user_service = inject<UserService>();
-        private SessionService session_service = inject<SessionService>();
-        private PermissionService? permission_service = inject<PermissionService>();
-        private HttpContext http_context = inject<HttpContext>();
+        private UserService _user_service = inject<UserService>();
+        private SessionService _session_service = inject<SessionService>();
+        private PermissionService? _permission_service = inject<PermissionService>();
+        private HttpContext _http_context = inject<HttpContext>();
 
 
         // =========================================================================
         // =========================================================================
         // Configuration Properties
         // Configuration Properties
@@ -133,18 +135,12 @@ namespace Spry.Users.Components {
             if (preserved_username.length > 0) {
             if (preserved_username.length > 0) {
                 this["username-input"].set_attribute("value", preserved_username);
                 this["username-input"].set_attribute("value", preserved_username);
             }
             }
-
-            // If login was successful, add HX-Redirect header for client-side redirect
-            if (login_successful) {
-                // Set the redirect header - HTMX will handle the redirect
-                this["login-form"].set_attribute("hx-redirect", redirect_url);
-            }
         }
         }
 
 
         public async override void handle_action(string action) throws Error {
         public async override void handle_action(string action) throws Error {
             // Normalize action name comparison
             // Normalize action name comparison
             if (action.down() == login_action_name.down()) {
             if (action.down() == login_action_name.down()) {
-                yield handle_login();
+                yield handle_login_async();
             }
             }
         }
         }
 
 
@@ -152,8 +148,10 @@ namespace Spry.Users.Components {
         // Login Handler
         // Login Handler
         // =========================================================================
         // =========================================================================
 
 
-        private async void handle_login() throws Error {
-            var query = http_context.request.query_params;
+        private async void handle_login_async() throws Error {
+            stdout.printf("LOGIN DEBUG: handle_login_async() triggered\n");
+            
+            var query = _http_context.request.query_params;
 
 
             // Get form values
             // Get form values
             var username_raw = query.get_any_or_default("username");
             var username_raw = query.get_any_or_default("username");
@@ -164,70 +162,90 @@ namespace Spry.Users.Components {
             var password = password_raw ?? "";
             var password = password_raw ?? "";
             var remember_me = remember_me_raw == "on";
             var remember_me = remember_me_raw == "on";
 
 
+            stdout.printf("LOGIN DEBUG: Username/email received: '%s'\n", username);
+            stdout.printf("LOGIN DEBUG: Password length: %d\n", password.length);
+            stdout.printf("LOGIN DEBUG: Remember me: %s\n", remember_me ? "true" : "false");
+
             // Preserve username for re-display on error
             // Preserve username for re-display on error
             preserved_username = username;
             preserved_username = username;
 
 
             // Validate inputs
             // Validate inputs
             if (username.length == 0) {
             if (username.length == 0) {
+                stdout.printf("LOGIN DEBUG: Validation failed - empty username\n");
                 error_message = "Please enter your username or email";
                 error_message = "Please enter your username or email";
                 return;
                 return;
             }
             }
 
 
             if (password.length == 0) {
             if (password.length == 0) {
+                stdout.printf("LOGIN DEBUG: Validation failed - empty password\n");
                 error_message = "Please enter your password";
                 error_message = "Please enter your password";
                 return;
                 return;
             }
             }
 
 
             // Attempt authentication
             // Attempt authentication
-            var user = user_service.authenticate(username, password);
+            stdout.printf("LOGIN DEBUG: Calling authenticate_async()...\n");
+            var user = yield _user_service.authenticate_async(username, password);
+            stdout.printf("LOGIN DEBUG: authenticate_async() returned user: %s\n", user != null ? user.id : "null");
 
 
             if (user == null) {
             if (user == null) {
                 // Authentication failed - show generic error message
                 // Authentication failed - show generic error message
                 // (Don't reveal whether username or password was wrong for security)
                 // (Don't reveal whether username or password was wrong for security)
+                stdout.printf("LOGIN DEBUG: Authentication failed - invalid credentials\n");
                 error_message = "Invalid username or password";
                 error_message = "Invalid username or password";
                 return;
                 return;
             }
             }
 
 
-            // Check if user is active (if the User model supports this)
-            // Note: Current User model doesn't have is_active, but we check anyway
-            // for future compatibility
+            stdout.printf("LOGIN DEBUG: Authentication successful for user: %s (username: %s, email: %s)\n",
+                user.id, user.username, user.email);
 
 
             // Get client info for session tracking
             // Get client info for session tracking
-            var ip_address = http_context.request.remote_address;
-            var user_agent = http_context.request.headers.get_any_or_default("User-Agent");
+            var ip_address = _http_context.request.remote_address;
+            var user_agent = _http_context.request.headers.get_any_or_default("User-Agent");
+            stdout.printf("LOGIN DEBUG: Client IP: %s, User-Agent: %s\n",
+                ip_address ?? "null", user_agent ?? "null");
 
 
             // Create session
             // Create session
+            stdout.printf("LOGIN DEBUG: Creating session...\n");
             Session? session;
             Session? session;
             if (remember_me) {
             if (remember_me) {
                 // Create session with extended duration for "remember me"
                 // Create session with extended duration for "remember me"
-                // Note: Current SessionService uses configured duration; 
+                // Note: Current SessionService uses configured duration;
                 // for extended sessions, we'd need to modify SessionService
                 // for extended sessions, we'd need to modify SessionService
                 // For now, create a regular session
                 // For now, create a regular session
-                session = session_service.create_session(user.id, ip_address, user_agent);
+                session = yield _session_service.create_session_async(user.id, ip_address, user_agent);
             } else {
             } else {
-                session = session_service.create_session(user.id, ip_address, user_agent);
+                session = yield _session_service.create_session_async(user.id, ip_address, user_agent);
             }
             }
+            stdout.printf("LOGIN DEBUG: Session creation result: %s\n", session != null ? session.id : "null");
 
 
             if (session == null) {
             if (session == null) {
+                stdout.printf("LOGIN DEBUG: ERROR - Failed to create session\n");
                 error_message = "Failed to create session. Please try again.";
                 error_message = "Failed to create session. Please try again.";
                 return;
                 return;
             }
             }
 
 
             // Generate session token
             // Generate session token
-            var token = session_service.generate_session_token(session);
-
-            // Set session cookie via HttpResult
-            // We need to set the cookie header on the response
-            var result = yield to_result();
-            session_service.set_session_cookie(result, token);
-
-            // Mark login as successful - prepare() will add redirect header
+            stdout.printf("LOGIN DEBUG: Generating session token...\n");
+            var token = _session_service.generate_session_token(session);
+            stdout.printf("LOGIN DEBUG: Token generated (length: %d)\n", token.length);
+
+            // Set session cookie using ResponseState
+            // This accumulates the cookie header to be applied when to_result() is called
+            stdout.printf("LOGIN DEBUG: Setting session cookie via ResponseState...\n");
+            response.set_cookie("spry_session", token, 86400, "/", true, true, "Strict");
+            stdout.printf("LOGIN DEBUG: Session cookie set\n");
+
+            // Set up HTMX redirect using ResponseState
+            // This sets the HX-Redirect header for client-side redirect
+            response.redirect(redirect_url);
+            
             login_successful = true;
             login_successful = true;
             error_message = null;
             error_message = null;
+            stdout.printf("LOGIN DEBUG: Login successful! Redirect to: %s\n", redirect_url);
 
 
             // Optional: Check permissions if permission_service is available
             // Optional: Check permissions if permission_service is available
             // This can be used for post-login permission checks
             // This can be used for post-login permission checks
-            if (permission_service != null) {
+            if (_permission_service != null) {
                 // Subclasses can override to add permission checks
                 // Subclasses can override to add permission checks
                 // e.g., require certain permissions to access specific areas
                 // e.g., require certain permissions to access specific areas
             }
             }

+ 5 - 3
src/Users/Components/PermissionEditorComponent.vala

@@ -22,10 +22,12 @@ namespace Spry.Users.Components {
      *   editor.set_permissions(user.permissions);
      *   editor.set_permissions(user.permissions);
      *   // After user interaction:
      *   // After user interaction:
      *   var selected = editor.get_selected();
      *   var selected = editor.get_selected();
+     * 
+     * This component uses the inject<> pattern for dependency injection.
      */
      */
     public class PermissionEditorComponent : Component {
     public class PermissionEditorComponent : Component {
 
 
-        private HttpContext http_context = inject<HttpContext>();
+        private HttpContext _http_context = inject<HttpContext>();
 
 
         // =========================================================================
         // =========================================================================
         // Constants
         // Constants
@@ -106,7 +108,7 @@ namespace Spry.Users.Components {
             """;
             """;
         }}
         }}
 
 
-        public override async void prepare() throws Error {
+        public async override void prepare() throws Error {
             // Create checkboxes for common permissions
             // Create checkboxes for common permissions
             var common_items = new Series<Renderable>();
             var common_items = new Series<Renderable>();
             foreach (var perm in COMMON_PERMISSION_ARRAY) {
             foreach (var perm in COMMON_PERMISSION_ARRAY) {
@@ -128,7 +130,7 @@ namespace Spry.Users.Components {
         }
         }
 
 
         public async override void handle_action(string action) throws Error {
         public async override void handle_action(string action) throws Error {
-            var query = http_context.request.query_params;
+            var query = _http_context.request.query_params;
 
 
             if (action == "AddPermission") {
             if (action == "AddPermission") {
                 var new_perm = query.get_any_or_default("new_permission");
                 var new_perm = query.get_any_or_default("new_permission");

+ 16 - 30
src/Users/Components/UserFormComponent.vala

@@ -28,12 +28,14 @@ namespace Spry.Users.Components {
      *   // Edit mode:
      *   // Edit mode:
      *   var form = factory.create<UserFormComponent>();
      *   var form = factory.create<UserFormComponent>();
      *   form.set_user(existing_user);
      *   form.set_user(existing_user);
+     * 
+     * This component uses the inject<> pattern for dependency injection.
      */
      */
     public class UserFormComponent : Component {
     public class UserFormComponent : Component {
 
 
-        private UserService user_service = inject<UserService>();
-        private ComponentFactory factory = inject<ComponentFactory>();
-        private HttpContext http_context = inject<HttpContext>();
+        private UserService _user_service = inject<UserService>();
+        private ComponentFactory _factory = inject<ComponentFactory>();
+        private HttpContext _http_context = inject<HttpContext>();
 
 
         // =========================================================================
         // =========================================================================
         // State Properties
         // State Properties
@@ -220,7 +222,7 @@ namespace Spry.Users.Components {
             """;
             """;
         }}
         }}
 
 
-        public override async void prepare() throws Error {
+        public async override void prepare() throws Error {
             if (!_is_visible) {
             if (!_is_visible) {
                 return;
                 return;
             }
             }
@@ -244,7 +246,7 @@ namespace Spry.Users.Components {
         public async override void handle_action(string action) throws Error {
         public async override void handle_action(string action) throws Error {
             switch (action) {
             switch (action) {
                 case "Save":
                 case "Save":
-                    yield save_user();
+                    yield save_user_async();
                     break;
                     break;
                 case "Cancel":
                 case "Cancel":
                     hide();
                     hide();
@@ -256,8 +258,8 @@ namespace Spry.Users.Components {
         // Private Helpers
         // Private Helpers
         // =========================================================================
         // =========================================================================
 
 
-        private async void save_user() throws Error {
-            var query = http_context.request.query_params;
+        private async void save_user_async() throws Error {
+            var query = _http_context.request.query_params;
 
 
             // Get form values
             // Get form values
             var user_id = get_query_value(query, "user_id");
             var user_id = get_query_value(query, "user_id");
@@ -301,29 +303,19 @@ namespace Spry.Users.Components {
                         return;
                         return;
                     }
                     }
 
 
-                    string? create_error;
-                    var user = user_service.create_user(username, email, password, out create_error);
-
-                    if (user == null) {
-                        _error_message = create_error ?? "Failed to create user";
-                        return;
-                    }
+                    var user = yield _user_service.create_user_async(username, email, password);
 
 
                     // Set permissions if any were selected
                     // Set permissions if any were selected
                     if (permissions.length > 0) {
                     if (permissions.length > 0) {
                         user.permissions = permissions;
                         user.permissions = permissions;
-                        string? update_error;
-                        if (!user_service.update_user(user, out update_error)) {
-                            _error_message = update_error ?? "Failed to set permissions";
-                            return;
-                        }
+                        yield _user_service.update_user_async(user);
                     }
                     }
 
 
                     _success_message = "User created successfully";
                     _success_message = "User created successfully";
                     hide();
                     hide();
                 } else {
                 } else {
                     // Update existing user
                     // Update existing user
-                    var user = user_service.get_user(user_id);
+                    var user = yield _user_service.get_user_async(user_id);
                     if (user == null) {
                     if (user == null) {
                         _error_message = "User not found";
                         _error_message = "User not found";
                         return;
                         return;
@@ -341,23 +333,17 @@ namespace Spry.Users.Components {
                             _error_message = "New password must be at least 8 characters";
                             _error_message = "New password must be at least 8 characters";
                             return;
                             return;
                         }
                         }
-                        string? password_error;
-                        if (!user_service.set_password(user, new_password, out password_error)) {
-                            _error_message = password_error ?? "Failed to update password";
-                            return;
-                        }
+                        yield _user_service.set_password_async(user, new_password);
                     }
                     }
 
 
                     // Save changes
                     // Save changes
-                    string? update_error;
-                    if (!user_service.update_user(user, out update_error)) {
-                        _error_message = update_error ?? "Failed to update user";
-                        return;
-                    }
+                    yield _user_service.update_user_async(user);
 
 
                     _success_message = "User updated successfully";
                     _success_message = "User updated successfully";
                     hide();
                     hide();
                 }
                 }
+            } catch (UserError e) {
+                _error_message = e.message;
             } catch (Error e) {
             } catch (Error e) {
                 _error_message = "Error: %s".printf(e.message);
                 _error_message = "Error: %s".printf(e.message);
             }
             }

+ 16 - 14
src/Users/Components/UserListComponent.vala

@@ -21,12 +21,14 @@ namespace Spry.Users.Components {
      *
      *
      * Usage:
      * Usage:
      *   <spry-component name="UserListComponent" sid="user-list"/>
      *   <spry-component name="UserListComponent" sid="user-list"/>
+     * 
+     * This component uses the inject<> pattern for dependency injection.
      */
      */
     public class UserListComponent : Component {
     public class UserListComponent : Component {
 
 
-        private UserService user_service = inject<UserService>();
-        private ComponentFactory factory = inject<ComponentFactory>();
-        private HttpContext http_context = inject<HttpContext>();
+        private UserService _user_service = inject<UserService>();
+        private ComponentFactory _factory = inject<ComponentFactory>();
+        private HttpContext _http_context = inject<HttpContext>();
 
 
         // =========================================================================
         // =========================================================================
         // State Properties
         // State Properties
@@ -136,14 +138,14 @@ namespace Spry.Users.Components {
             """;
             """;
         }}
         }}
 
 
-        public override async void prepare() throws Error {
+        public async override void prepare() throws Error {
             // Load users based on current search and page
             // Load users based on current search and page
-            load_users();
+            yield load_users_async();
 
 
             // Create user item components
             // Create user item components
             var items = new Series<Renderable>();
             var items = new Series<Renderable>();
             foreach (var user in _users) {
             foreach (var user in _users) {
-                var item = factory.create<UserListItemComponent>();
+                var item = _factory.create<UserListItemComponent>();
                 item.set_user(user);
                 item.set_user(user);
                 item.show_actions = true;
                 item.show_actions = true;
                 items.add(item);
                 items.add(item);
@@ -157,7 +159,7 @@ namespace Spry.Users.Components {
         }
         }
 
 
         public async override void handle_action(string action) throws Error {
         public async override void handle_action(string action) throws Error {
-            var query = http_context.request.query_params;
+            var query = _http_context.request.query_params;
 
 
             switch (action) {
             switch (action) {
                 case "Search":
                 case "Search":
@@ -192,8 +194,8 @@ namespace Spry.Users.Components {
         /**
         /**
          * Refreshes the user list (useful after external changes).
          * Refreshes the user list (useful after external changes).
          */
          */
-        public void refresh() {
-            load_users();
+        public async void refresh_async() throws Error {
+            yield load_users_async();
         }
         }
 
 
         /**
         /**
@@ -247,15 +249,15 @@ namespace Spry.Users.Components {
         // Private Helpers
         // Private Helpers
         // =========================================================================
         // =========================================================================
 
 
-        private void load_users() {
+        private async void load_users_async() throws Error {
             // Get total count first
             // Get total count first
-            _total_count = user_service.user_count();
+            _total_count = yield _user_service.user_count_async();
 
 
             if (_search_query.length > 0) {
             if (_search_query.length > 0) {
                 // For search, we need to filter users
                 // For search, we need to filter users
                 // Current UserService doesn't have search_users, so we load all and filter
                 // Current UserService doesn't have search_users, so we load all and filter
-                // Future optimization: Add search_users method to UserService
-                var all_users = user_service.list_users(0, 1000); // Load all for filtering
+                // Future optimization: Add search_users_async method to UserService
+                var all_users = yield _user_service.list_users_async(0, 1000); // Load all for filtering
                 _users = new Vector<User>();
                 _users = new Vector<User>();
 
 
                 var query_lower = _search_query.down();
                 var query_lower = _search_query.down();
@@ -278,7 +280,7 @@ namespace Spry.Users.Components {
                 _users = paginated;
                 _users = paginated;
             } else {
             } else {
                 // Load users with pagination
                 // Load users with pagination
-                _users = user_service.list_users(_page * _page_size, _page_size);
+                _users = yield _user_service.list_users_async(_page * _page_size, _page_size);
             }
             }
         }
         }
     }
     }

+ 3 - 1
src/Users/Components/UserListItemComponent.vala

@@ -21,6 +21,8 @@ namespace Spry.Users.Components {
      *   var item = factory.create<UserListItemComponent>();
      *   var item = factory.create<UserListItemComponent>();
      *   item.set_user(user);
      *   item.set_user(user);
      *   item.show_actions = true;
      *   item.show_actions = true;
+     * 
+     * This component uses the inject<> pattern for dependency injection.
      */
      */
     public class UserListItemComponent : Component {
     public class UserListItemComponent : Component {
 
 
@@ -103,7 +105,7 @@ namespace Spry.Users.Components {
             """;
             """;
         }}
         }}
 
 
-        public override async void prepare() throws Error {
+        public async override void prepare() throws Error {
             if (_user == null) {
             if (_user == null) {
                 // Hide the row if no user is set
                 // Hide the row if no user is set
                 this["user-row"].set_attribute("spry-hidden", "true");
                 this["user-row"].set_attribute("spry-hidden", "true");

+ 99 - 266
src/Users/Components/UserManagementPage.vala

@@ -25,22 +25,24 @@ namespace Spry.Users.Components {
      * Usage:
      * Usage:
      *   // Register as a page:
      *   // Register as a page:
      *   spry_cfg.add_page<UserManagementPage>(new EndpointRoute("/admin/users"));
      *   spry_cfg.add_page<UserManagementPage>(new EndpointRoute("/admin/users"));
+     * 
+     * This component uses the inject<> pattern for dependency injection.
      */
      */
     public class UserManagementPage : PageComponent {
     public class UserManagementPage : PageComponent {
 
 
-        private PermissionService permission_service = inject<PermissionService>();
-        private UserService user_service = inject<UserService>();
-        private SessionService session_service = inject<SessionService>();
-        private ComponentFactory factory = inject<ComponentFactory>();
-        private HttpContext http_context = inject<HttpContext>();
+        private PermissionService _permission_service = inject<PermissionService>();
+        private UserService _user_service = inject<UserService>();
+        private SessionService _session_service = inject<SessionService>();
+        private ComponentFactory _factory = inject<ComponentFactory>();
+        private HttpContext _http_context = inject<HttpContext>();
 
 
         // =========================================================================
         // =========================================================================
-        // State Properties
+        // State Properties (must be public for template expression access)
         // =========================================================================
         // =========================================================================
 
 
-        private string? _success_message = null;
-        private string? _error_message = null;
-        private bool _access_denied = false;
+        public string? success_message { get; private set; default = null; }
+        public string? error_message { get; private set; default = null; }
+        public bool access_denied { get; private set; default = false; }
 
 
         // =========================================================================
         // =========================================================================
         // Component Implementation
         // Component Implementation
@@ -114,227 +116,45 @@ namespace Spry.Users.Components {
 
 
                     /* Access Denied */
                     /* Access Denied */
                     .access-denied {
                     .access-denied {
-                        text-align: center;
-                        padding: 4rem 2rem;
-                    }
-                    .access-denied h2 { color: #dc3545; }
-
-                    /* Modal */
-                    .modal-overlay {
-                        position: fixed;
-                        top: 0;
-                        left: 0;
-                        right: 0;
-                        bottom: 0;
-                        background: rgba(0,0,0,0.5);
-                        display: flex;
-                        align-items: center;
-                        justify-content: center;
-                        z-index: 1000;
-                    }
-                    .modal-content {
-                        background: white;
-                        padding: 2rem;
-                        border-radius: 8px;
-                        max-width: 500px;
-                        width: 90%;
-                        max-height: 90vh;
-                        overflow-y: auto;
-                        box-shadow: 0 4px 6px rgba(0,0,0,0.1);
-                    }
-                    .modal-header {
-                        display: flex;
-                        justify-content: space-between;
-                        align-items: center;
-                        margin-bottom: 1.5rem;
-                        padding-bottom: 1rem;
-                        border-bottom: 1px solid #e0e0e0;
-                    }
-                    .modal-header h3 { margin: 0; }
-                    .close-btn {
-                        background: none;
-                        border: none;
-                        font-size: 1.5rem;
-                        cursor: pointer;
-                        color: #666;
-                    }
-                    .close-btn:hover { color: #333; }
-
-                    /* Form */
-                    .form-group { margin-bottom: 1rem; }
-                    .form-group label {
-                        display: block;
-                        margin-bottom: 0.25rem;
-                        font-weight: 500;
-                    }
-                    .form-input {
-                        width: 100%;
-                        padding: 0.5rem;
-                        border: 1px solid #ccc;
-                        border-radius: 4px;
-                        font-size: 1rem;
-                    }
-                    .form-input:focus { outline: none; border-color: #007bff; }
-                    .form-hint { font-size: 0.75rem; color: #666; margin-top: 0.25rem; }
-                    .form-actions {
-                        display: flex;
-                        gap: 0.5rem;
-                        margin-top: 1.5rem;
-                        padding-top: 1rem;
-                        border-top: 1px solid #e0e0e0;
-                    }
-
-                    /* Badges */
-                    .badge {
-                        display: inline-block;
-                        padding: 0.25rem 0.5rem;
-                        border-radius: 4px;
-                        font-size: 0.75rem;
-                        margin-right: 0.25rem;
-                        margin-bottom: 0.25rem;
-                    }
-                    .badge-active { background: #d4edda; color: #155724; }
-                    .badge-inactive { background: #f8d7da; color: #721c24; }
-                    .badge-permission { background: #e9ecef; color: #495057; }
-
-                    /* Table */
-                    .table-container { overflow-x: auto; }
-                    .user-table {
-                        width: 100%;
-                        border-collapse: collapse;
-                        background: white;
-                        border-radius: 4px;
-                        box-shadow: 0 1px 3px rgba(0,0,0,0.1);
-                    }
-                    .user-table th,
-                    .user-table td {
-                        padding: 0.75rem;
-                        text-align: left;
-                        border-bottom: 1px solid #e0e0e0;
-                    }
-                    .user-table th {
-                        background: #f8f9fa;
-                        font-weight: 600;
-                    }
-                    .user-table tr:hover { background: #f8f9fa; }
-
-                    /* Search */
-                    .search-bar { margin-bottom: 1rem; }
-                    .search-form { display: flex; gap: 0.5rem; }
-                    .search-input {
-                        flex: 1;
-                        padding: 0.5rem;
-                        border: 1px solid #ccc;
-                        border-radius: 4px;
-                    }
-                    .btn-search { background: #28a745; color: white; }
-                    .btn-clear { background: #6c757d; color: white; }
-
-                    /* Pagination */
-                    .pagination {
-                        display: flex;
-                        justify-content: center;
-                        align-items: center;
-                        gap: 1rem;
-                        margin-top: 1rem;
-                    }
-                    .page-info { color: #666; }
-
-                    /* Empty State */
-                    .empty-state {
                         text-align: center;
                         text-align: center;
                         padding: 3rem;
                         padding: 3rem;
-                        background: white;
-                        border-radius: 4px;
-                    }
-                    .user-count {
-                        text-align: center;
-                        color: #666;
-                        margin-top: 1rem;
-                        font-size: 0.875rem;
-                    }
-
-                    /* Permission Editor */
-                    .permission-checkbox {
-                        display: inline-flex;
-                        align-items: center;
-                        margin-right: 1rem;
-                        margin-bottom: 0.5rem;
-                    }
-                    .permission-checkbox input { margin-right: 0.5rem; }
-                    .permission-tag {
-                        display: inline-flex;
-                        align-items: center;
-                        background: #e9ecef;
-                        padding: 0.25rem 0.5rem;
-                        border-radius: 4px;
-                        margin: 0.25rem;
-                    }
-                    .permission-tag .remove-tag {
-                        background: none;
-                        border: none;
-                        margin-left: 0.5rem;
-                        cursor: pointer;
-                        color: #666;
-                        font-size: 1rem;
-                        line-height: 1;
-                    }
-                    .permission-tag .remove-tag:hover { color: #dc3545; }
-                    .permission-input {
-                        padding: 0.5rem;
-                        border: 1px solid #ccc;
-                        border-radius: 4px;
                     }
                     }
-                    .btn-add { background: #28a745; color: white; }
-                    .help-text { color: #666; font-size: 0.75rem; margin-top: 0.5rem; display: block; }
-                    .permission-group { margin-bottom: 1rem; }
-                    .permission-group h4 { margin-bottom: 0.5rem; font-size: 0.875rem; }
+                    .access-denied h2 { color: #dc3545; }
                 </style>
                 </style>
             </head>
             </head>
             <body>
             <body>
-                <div class="admin-container" sid="admin-container">
+                <div class="admin-container">
+                    <div class="admin-header">
+                        <h1>User Management</h1>
+                        <button sid="create-btn" 
+                                spry-action=":CreateUser"
+                                class="btn btn-primary">
+                            + Create User
+                        </button>
+                    </div>
+
                     <!-- Access Denied Message -->
                     <!-- Access Denied Message -->
-                    <div spry-if="this._access_denied" class="access-denied" sid="access-denied">
+                    <div spry-if="this.access_denied" class="access-denied" sid="accessDeniedMsg">
                         <h2>Access Denied</h2>
                         <h2>Access Denied</h2>
-                        <p>You do not have permission to access the user management page.</p>
-                        <p>Please contact an administrator if you believe this is an error.</p>
+                        <p>You do not have permission to access this page.</p>
                     </div>
                     </div>
 
 
-                    <!-- Main Content (shown when authorized) -->
-                    <div spry-if="!this._access_denied">
-                        <header class="admin-header">
-                            <h1>User Management</h1>
-                            <div class="header-actions">
-                                <button sid="create-btn"
-                                        spry-action=":CreateUser"
-                                        hx-target="#user-form-container"
-                                        hx-swap="innerHTML"
-                                        class="btn btn-primary">
-                                    + Create User
-                                </button>
-                            </div>
-                        </header>
-
-                        <!-- Success Message -->
-                        <div spry-if="this._success_message != null"
-                             class="alert alert-success"
-                             sid="success-alert"
-                             spry-global="success-alert">
-                            <span content-expr="this._success_message"></span>
-                        </div>
+                    <!-- Success Message -->
+                    <div spry-if="this.success_message != null" class="alert alert-success" sid="successAlert">
+                        <span content-expr="this.success_message"></span>
+                    </div>
 
 
-                        <!-- Error Message -->
-                        <div spry-if="this._error_message != null"
-                             class="alert alert-error"
-                             sid="error-alert"
-                             spry-global="error-alert">
-                            <span content-expr="this._error_message"></span>
-                        </div>
+                    <!-- Error Message -->
+                    <div spry-if="this.error_message != null" class="alert alert-error" sid="errorAlert">
+                        <span content-expr="this.error_message"></span>
+                    </div>
 
 
+                    <!-- Main Content (hidden if access denied) -->
+                    <div spry-if="!this.access_denied" sid="mainContent">
                         <!-- User List Component -->
                         <!-- User List Component -->
                         <spry-component name="UserListComponent" sid="user-list"/>
                         <spry-component name="UserListComponent" sid="user-list"/>
 
 
-                        <!-- User Form Container (for modal) -->
+                        <!-- User Form Modal -->
                         <div id="user-form-container" sid="user-form-container">
                         <div id="user-form-container" sid="user-form-container">
                             <spry-component name="UserFormComponent" sid="user-form"/>
                             <spry-component name="UserFormComponent" sid="user-form"/>
                         </div>
                         </div>
@@ -345,60 +165,63 @@ namespace Spry.Users.Components {
             """;
             """;
         }}
         }}
 
 
-        public override async void prepare() throws Error {
+        public async override void prepare() throws Error {
             // Check permission
             // Check permission
-            var auth_result = session_service.authenticate_request(http_context, user_service);
+            var auth_result = yield _session_service.authenticate_request_async(_http_context, _user_service);
 
 
-            if (!auth_result.is_authenticated) {
-                _access_denied = true;
+            if (!auth_result.is_authenticated || auth_result.user == null) {
+                access_denied = true;
                 return;
                 return;
             }
             }
 
 
-            var current_user = auth_result.user;
-            if (current_user == null) {
-                _access_denied = true;
-                return;
-            }
+            var has_permission = yield _permission_service.has_permission_by_id_async(
+                auth_result.user.id,
+                PermissionService.USER_MANAGEMENT
+            );
 
 
-            // Check for user-management permission
-            if (!permission_service.has_permission(current_user, PermissionService.USER_MANAGEMENT)) {
-                _access_denied = true;
+            if (!has_permission) {
+                access_denied = true;
                 return;
                 return;
             }
             }
 
 
-            _access_denied = false;
+            // Share globals with child components
+            var user_list = get_component_child<UserListComponent>("user-list");
+            if (user_list != null) {
+                add_globals_from(user_list);
+            }
 
 
-            // Ensure form is hidden initially
             var user_form = get_component_child<UserFormComponent>("user-form");
             var user_form = get_component_child<UserFormComponent>("user-form");
-            user_form.hide();
+            if (user_form != null) {
+                add_globals_from(user_form);
+            }
         }
         }
 
 
         public async override void handle_action(string action) throws Error {
         public async override void handle_action(string action) throws Error {
-            // Don't process actions if access is denied
-            if (_access_denied) {
+            // Check permission for all actions
+            if (access_denied) {
                 return;
                 return;
             }
             }
 
 
-            var query = http_context.request.query_params;
+            var query = _http_context.request.query_params;
 
 
             switch (action) {
             switch (action) {
                 case "CreateUser":
                 case "CreateUser":
-                    handle_create_user();
+                    yield handle_create_user_async();
                     break;
                     break;
 
 
                 case "EditUser":
                 case "EditUser":
                     var user_id = get_query_value(query, "user_id");
                     var user_id = get_query_value(query, "user_id");
-                    handle_edit_user(user_id);
+                    yield handle_edit_user_async(user_id);
                     break;
                     break;
 
 
                 case "ToggleActive":
                 case "ToggleActive":
                     var user_id = get_query_value(query, "user_id");
                     var user_id = get_query_value(query, "user_id");
-                    yield handle_toggle_active(user_id);
+                    yield handle_toggle_active_async(user_id);
                     break;
                     break;
 
 
                 case "DeleteUser":
                 case "DeleteUser":
                     var user_id = get_query_value(query, "user_id");
                     var user_id = get_query_value(query, "user_id");
-                    yield handle_delete_user(user_id);
+                    yield handle_delete_user_async(user_id);
                     break;
                     break;
             }
             }
         }
         }
@@ -407,75 +230,85 @@ namespace Spry.Users.Components {
         // Action Handlers
         // Action Handlers
         // =========================================================================
         // =========================================================================
 
 
-        private void handle_create_user() throws Error {
+        private async void handle_create_user_async() throws Error {
             var user_form = get_component_child<UserFormComponent>("user-form");
             var user_form = get_component_child<UserFormComponent>("user-form");
-            user_form.show_create();
-            _success_message = null;
-            _error_message = null;
+            if (user_form != null) {
+                user_form.show_create();
+            }
+            success_message = null;
+            error_message = null;
         }
         }
 
 
-        private void handle_edit_user(string user_id) throws Error {
+        private async void handle_edit_user_async(string user_id) throws Error {
             if (user_id.length == 0) {
             if (user_id.length == 0) {
-                _error_message = "Invalid user ID";
+                error_message = "Invalid user ID";
                 return;
                 return;
             }
             }
 
 
-            var user = user_service.get_user(user_id);
+            var user = yield _user_service.get_user_async(user_id);
             if (user == null) {
             if (user == null) {
-                _error_message = "User not found";
+                error_message = "User not found";
                 return;
                 return;
             }
             }
 
 
             var user_form = get_component_child<UserFormComponent>("user-form");
             var user_form = get_component_child<UserFormComponent>("user-form");
-            user_form.set_user(user);
-            _success_message = null;
-            _error_message = null;
+            if (user_form != null) {
+                user_form.set_user(user);
+            }
+            success_message = null;
+            error_message = null;
         }
         }
 
 
-        private async void handle_toggle_active(string user_id) throws Error {
+        private async void handle_toggle_active_async(string user_id) throws Error {
             // Note: Current User model doesn't have is_active field
             // Note: Current User model doesn't have is_active field
             // This is a placeholder for future implementation
             // This is a placeholder for future implementation
-            _error_message = "Toggle active functionality not yet implemented";
-            _success_message = null;
+            error_message = "Toggle active functionality not yet implemented";
+            success_message = null;
 
 
             // Refresh the list
             // Refresh the list
             var user_list = get_component_child<UserListComponent>("user-list");
             var user_list = get_component_child<UserListComponent>("user-list");
-            add_globals_from(user_list);
+            if (user_list != null) {
+                add_globals_from(user_list);
+            }
         }
         }
 
 
-        private async void handle_delete_user(string user_id) throws Error {
+        private async void handle_delete_user_async(string user_id) throws Error {
             if (user_id.length == 0) {
             if (user_id.length == 0) {
-                _error_message = "Invalid user ID";
+                error_message = "Invalid user ID";
                 return;
                 return;
             }
             }
 
 
             // Prevent self-deletion
             // Prevent self-deletion
-            var auth_result = session_service.authenticate_request(http_context, user_service);
+            var auth_result = yield _session_service.authenticate_request_async(_http_context, _user_service);
             if (auth_result.is_authenticated && auth_result.user != null) {
             if (auth_result.is_authenticated && auth_result.user != null) {
                 if (auth_result.user.id == user_id) {
                 if (auth_result.user.id == user_id) {
-                    _error_message = "Cannot delete your own account";
-                    _success_message = null;
+                    error_message = "Cannot delete your own account";
+                    success_message = null;
 
 
                     // Refresh the list
                     // Refresh the list
                     var user_list = get_component_child<UserListComponent>("user-list");
                     var user_list = get_component_child<UserListComponent>("user-list");
-                    add_globals_from(user_list);
+                    if (user_list != null) {
+                        add_globals_from(user_list);
+                    }
                     return;
                     return;
                 }
                 }
             }
             }
 
 
             // Delete the user
             // Delete the user
-            string? delete_error;
-            if (!user_service.delete_user(user_id, out delete_error)) {
-                _error_message = delete_error ?? "Failed to delete user";
-                _success_message = null;
-            } else {
-                _success_message = "User deleted successfully";
-                _error_message = null;
+            try {
+                yield _user_service.delete_user_async(user_id);
+                success_message = "User deleted successfully";
+                error_message = null;
+            } catch (Error e) {
+                error_message = e.message;
+                success_message = null;
             }
             }
 
 
             // Refresh the list
             // Refresh the list
             var user_list = get_component_child<UserListComponent>("user-list");
             var user_list = get_component_child<UserListComponent>("user-list");
-            add_globals_from(user_list);
+            if (user_list != null) {
+                add_globals_from(user_list);
+            }
         }
         }
 
 
         // =========================================================================
         // =========================================================================

+ 49 - 47
src/Users/PermissionService.vala

@@ -1,4 +1,5 @@
 using Invercargill.DataStructures;
 using Invercargill.DataStructures;
+using Inversion;
 
 
 namespace Spry.Users {
 namespace Spry.Users {
 
 
@@ -10,6 +11,9 @@ namespace Spry.Users {
      * - Set and clear permissions on users
      * - Set and clear permissions on users
      * - Support for "admin" super-user permission
      * - Support for "admin" super-user permission
      * - Wildcard matching with trailing "*" (e.g., "user-*" matches "user-create")
      * - Wildcard matching with trailing "*" (e.g., "user-*" matches "user-create")
+     * 
+     * This service uses the inject<> pattern for dependency injection.
+     * All methods that need to load user data are async.
      */
      */
     public class PermissionService : GLib.Object {
     public class PermissionService : GLib.Object {
 
 
@@ -39,15 +43,14 @@ namespace Spry.Users {
         // Dependencies
         // Dependencies
         // =========================================================================
         // =========================================================================
 
 
-        private UserService _user_service;
+        private UserService _user_service = inject<UserService>();
 
 
         /**
         /**
          * Creates a new PermissionService instance.
          * Creates a new PermissionService instance.
-         *
-         * @param user_service The UserService for user operations
+         * Dependencies are injected via inject<>() pattern.
          */
          */
-        public PermissionService(UserService user_service) {
-            _user_service = user_service;
+        public PermissionService() {
+            // Dependencies injected via field initializers
         }
         }
 
 
         // =========================================================================
         // =========================================================================
@@ -94,13 +97,14 @@ namespace Spry.Users {
          * @param user_id The user's unique identifier
          * @param user_id The user's unique identifier
          * @param permission The permission to check for
          * @param permission The permission to check for
          * @return true if the user has the permission, false otherwise
          * @return true if the user has the permission, false otherwise
+         * @throws Error on storage failure
          */
          */
-        public bool has_permission_by_id(string user_id, string permission) {
-            var user = _user_service.get_user(user_id);
+        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) {
             if (user == null) {
                 return false;
                 return false;
             }
             }
-            return has_permission((!)user, permission);
+            return has_permission(user, permission);
         }
         }
 
 
         // =========================================================================
         // =========================================================================
@@ -112,21 +116,18 @@ namespace Spry.Users {
          *
          *
          * This method:
          * This method:
          * - Adds the permission if not already present
          * - Adds the permission if not already present
-         * - Persists changes via UserService.update_user()
+         * - Persists changes via UserService.update_user_async()
          *
          *
          * @param user The user to update
          * @param user The user to update
          * @param permission The permission to add
          * @param permission The permission to add
-         * @param error Output parameter for error message on failure
-         * @return true on success, false on failure
+         * @throws Error on failure
          */
          */
-        public bool set_permission(User user, string permission, out string? error = null) {
-            error = null;
-
+        public async void set_permission_async(User user, string permission) throws Error {
             // Check if permission already exists
             // Check if permission already exists
             foreach (var existing_perm in user.permissions) {
             foreach (var existing_perm in user.permissions) {
                 if (existing_perm == permission) {
                 if (existing_perm == permission) {
                     // Already has this permission
                     // Already has this permission
-                    return true;
+                    return;
                 }
                 }
             }
             }
 
 
@@ -134,7 +135,7 @@ namespace Spry.Users {
             user.permissions.add(permission);
             user.permissions.add(permission);
 
 
             // Persist changes
             // Persist changes
-            return _user_service.update_user(user, out error);
+            yield _user_service.update_user_async(user);
         }
         }
 
 
         /**
         /**
@@ -142,19 +143,15 @@ namespace Spry.Users {
          *
          *
          * @param user_id The user's unique identifier
          * @param user_id The user's unique identifier
          * @param permission The permission to add
          * @param permission The permission to add
-         * @param error Output parameter for error message on failure
-         * @return true on success, false on failure
+         * @throws Error on failure
          */
          */
-        public bool set_permission_by_id(string user_id, string permission, out string? error = null) {
-            error = null;
-
-            var user = _user_service.get_user(user_id);
+        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) {
             if (user == null) {
-                error = "User not found";
-                return false;
+                throw new UserError.USER_NOT_FOUND("User not found");
             }
             }
 
 
-            return set_permission((!)user, permission, out error);
+            yield set_permission_async(user, permission);
         }
         }
 
 
         // =========================================================================
         // =========================================================================
@@ -166,21 +163,18 @@ namespace Spry.Users {
          *
          *
          * This method:
          * This method:
          * - Removes the permission if present
          * - Removes the permission if present
-         * - Persists changes via UserService.update_user()
+         * - Persists changes via UserService.update_user_async()
          *
          *
          * @param user The user to update
          * @param user The user to update
          * @param permission The permission to remove
          * @param permission The permission to remove
-         * @param error Output parameter for error message on failure
-         * @return true on success, false on failure
+         * @throws Error on failure
          */
          */
-        public bool clear_permission(User user, string permission, out string? error = null) {
-            error = null;
-
+        public async void clear_permission_async(User user, string permission) throws Error {
             // Remove the permission
             // Remove the permission
             user.permissions.remove(permission);
             user.permissions.remove(permission);
 
 
             // Persist changes
             // Persist changes
-            return _user_service.update_user(user, out error);
+            yield _user_service.update_user_async(user);
         }
         }
 
 
         /**
         /**
@@ -188,36 +182,29 @@ namespace Spry.Users {
          *
          *
          * @param user_id The user's unique identifier
          * @param user_id The user's unique identifier
          * @param permission The permission to remove
          * @param permission The permission to remove
-         * @param error Output parameter for error message on failure
-         * @return true on success, false on failure
+         * @throws Error on failure
          */
          */
-        public bool clear_permission_by_id(string user_id, string permission, out string? error = null) {
-            error = null;
-
-            var user = _user_service.get_user(user_id);
+        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) {
             if (user == null) {
-                error = "User not found";
-                return false;
+                throw new UserError.USER_NOT_FOUND("User not found");
             }
             }
 
 
-            return clear_permission((!)user, permission, out error);
+            yield clear_permission_async(user, permission);
         }
         }
 
 
         /**
         /**
          * Clears all permissions from a user.
          * Clears all permissions from a user.
          *
          *
          * @param user The user to update
          * @param user The user to update
-         * @param error Output parameter for error message on failure
-         * @return true on success, false on failure
+         * @throws Error on failure
          */
          */
-        public bool clear_all_permissions(User user, out string? error = null) {
-            error = null;
-
+        public async void clear_all_permissions_async(User user) throws Error {
             // Clear all permissions
             // Clear all permissions
             user.permissions.clear();
             user.permissions.clear();
 
 
             // Persist changes
             // Persist changes
-            return _user_service.update_user(user, out error);
+            yield _user_service.update_user_async(user);
         }
         }
 
 
         // =========================================================================
         // =========================================================================
@@ -239,6 +226,21 @@ namespace Spry.Users {
             return result;
             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
         // Wildcard Matching
         // =========================================================================
         // =========================================================================

+ 30 - 22
src/Users/Session.vala

@@ -30,33 +30,40 @@ namespace Spry.Users {
         public static Session from_json(Json.Object obj) {
         public static Session from_json(Json.Object obj) {
             var session = new Session();
             var session = new Session();
 
 
-            session.id = obj.get_string_member("id");
-            session.user_id = obj.get_string_member("user_id");
+            // 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
+            // created_at - check member exists and value is not null/empty
             if (obj.has_member("created_at")) {
             if (obj.has_member("created_at")) {
-                session.created_at = new DateTime.from_iso8601(
-                    obj.get_string_member("created_at"),
-                    new TimeZone.utc()
-                );
+                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
+            // expires_at - check member exists and value is not null/empty
             if (obj.has_member("expires_at")) {
             if (obj.has_member("expires_at")) {
-                session.expires_at = new DateTime.from_iso8601(
-                    obj.get_string_member("expires_at"),
-                    new TimeZone.utc()
-                );
+                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)
-            if (obj.has_member("ip_address") && !obj.get_null_member("ip_address")) {
-                session.ip_address = obj.get_string_member("ip_address");
+            // 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)
-            if (obj.has_member("user_agent") && !obj.get_null_member("user_agent")) {
-                session.user_agent = obj.get_string_member("user_agent");
+            // 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;
             return session;
@@ -65,10 +72,11 @@ namespace Spry.Users {
         public Json.Object to_json() {
         public Json.Object to_json() {
             var obj = new Json.Object();
             var obj = new Json.Object();
 
 
-            obj.set_string_member("id", id);
-            obj.set_string_member("user_id", user_id);
-            obj.set_string_member("created_at", created_at.format_iso8601());
-            obj.set_string_member("expires_at", expires_at.format_iso8601());
+            // 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)
             // ip_address (optional)
             if (ip_address != null) {
             if (ip_address != null) {

+ 272 - 275
src/Users/SessionService.vala

@@ -1,6 +1,7 @@
 using Implexus.Core;
 using Implexus.Core;
 using Invercargill;
 using Invercargill;
 using Invercargill.DataStructures;
 using Invercargill.DataStructures;
+using Inversion;
 using InvercargillJson;
 using InvercargillJson;
 using Json;
 using Json;
 using Astralis;
 using Astralis;
@@ -129,11 +130,14 @@ namespace Spry.Users {
      * - Secure: true (configurable for development)
      * - Secure: true (configurable for development)
      * - SameSite: Strict
      * - SameSite: Strict
      * - Path: /
      * - Path: /
+     * 
+     * This service uses the inject<> pattern for dependency injection.
+     * All methods are async to work with the Implexus async API.
      */
      */
     public class SessionService : GLib.Object {
     public class SessionService : GLib.Object {
 
 
-        private Engine _engine;
-        private CryptographyProvider _crypto;
+        private Engine _engine = inject<Engine>();
+        private CryptographyProvider _crypto = inject<CryptographyProvider>();
 
 
         // Storage paths
         // Storage paths
         private const string BASE_PATH = "/spry/users";
         private const string BASE_PATH = "/spry/users";
@@ -142,37 +146,40 @@ namespace Spry.Users {
         private const string SESSION_TYPE_LABEL = "Session";
         private const string SESSION_TYPE_LABEL = "Session";
 
 
         // Cookie configuration
         // Cookie configuration
-        private string _cookie_name;
-        private bool _cookie_secure;
-        private TimeSpan _session_duration;
+        private string _cookie_name = "spry_session";
+        private bool _cookie_secure = true;
+        private TimeSpan _session_duration = TimeSpan.HOUR * 24;
 
 
         /**
         /**
-         * Creates a new SessionService instance.
-         *
-         * @param engine The Implexus engine for data storage
-         * @param crypto The cryptography provider for token operations
-         * @param session_duration Session expiry duration (default 24 hours)
-         * @param cookie_name Name of the session cookie (default "spry_session")
-         * @param cookie_secure Whether cookie should be Secure (default true)
+         * The name of the session cookie.
          */
          */
-        public SessionService(
-            Engine engine,
-            CryptographyProvider crypto,
-            TimeSpan? session_duration = null,
-            string cookie_name = "spry_session",
-            bool cookie_secure = true
-        ) {
-            _engine = engine;
-            _crypto = crypto;
-            _cookie_name = cookie_name;
-            _cookie_secure = cookie_secure;
-
-            if (session_duration != null) {
-                _session_duration = (!)session_duration;
-            } else {
-                // Default: 24 hours
-                _session_duration = TimeSpan.HOUR * 24;
-            }
+        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
         }
         }
 
 
         // =========================================================================
         // =========================================================================
@@ -191,42 +198,50 @@ namespace Spry.Users {
          * @param user_id The user's unique identifier
          * @param user_id The user's unique identifier
          * @param ip_address Optional IP address for tracking
          * @param ip_address Optional IP address for tracking
          * @param user_agent Optional user agent for tracking
          * @param user_agent Optional user agent for tracking
-         * @return The created Session, or null on failure
+         * @return The created Session
+         * @throws Error on failure
          */
          */
-        public Session? create_session(string user_id, string? ip_address = null, string? user_agent = null) {
-            try {
-                // Generate UUID for session
-                var session_id = generate_uuid();
-
-                // Create session object
-                var session = new Session();
-                session.id = session_id;
-                session.user_id = user_id;
-                session.created_at = new DateTime.now_utc();
-                session.expires_at = new DateTime.now_utc().add(_session_duration);
-                session.ip_address = ip_address;
-                session.user_agent = user_agent;
-
-                // Get or create storage containers
-                var sessions_container = get_or_create_sessions_container();
-
-                // Create session document
-                var session_doc = sessions_container.create_document(session_id, SESSION_TYPE_LABEL);
-
-                // Store session properties
-                store_session_in_document(session_doc, session);
-
-                // Update user sessions index
-                add_session_to_user_index(user_id, session_id);
-
-                return session;
-            } catch (EngineError e) {
-                warning("Failed to create session: %s", e.message);
-                return null;
-            } catch (Error e) {
-                warning("Failed to create session: %s", e.message);
-                return null;
-            }
+        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);
+
+            // Create session object
+            var session = new Session();
+            session.id = session_id;
+            session.user_id = user_id;
+            session.created_at = new DateTime.now_utc();
+            session.expires_at = new DateTime.now_utc().add(_session_duration);
+            session.ip_address = ip_address;
+            session.user_agent = user_agent;
+            stdout.printf("SESSION DEBUG: Session expires at: %s\n", session.expires_at.format_iso8601());
+
+            // Get or create storage containers
+            stdout.printf("SESSION DEBUG: Getting sessions container...\n");
+            var sessions_container = yield get_sessions_container_async();
+            stdout.printf("SESSION DEBUG: Sessions container obtained\n");
+
+            // Create session document
+            stdout.printf("SESSION DEBUG: Creating session document...\n");
+            var session_doc = yield sessions_container.create_document_async(session_id, SESSION_TYPE_LABEL);
+            stdout.printf("SESSION DEBUG: Session document created\n");
+
+            // Store session properties
+            stdout.printf("SESSION DEBUG: Storing session properties...\n");
+            yield store_session_in_document_async(session_doc, session);
+            stdout.printf("SESSION DEBUG: Session properties stored\n");
+
+            // Update user sessions index
+            stdout.printf("SESSION DEBUG: Updating user sessions index...\n");
+            yield add_session_to_user_index_async(user_id, session_id);
+            stdout.printf("SESSION DEBUG: User sessions index updated\n");
+
+            stdout.printf("SESSION DEBUG: Session created successfully: %s\n", session_id);
+            return session;
         }
         }
 
 
         // =========================================================================
         // =========================================================================
@@ -243,6 +258,10 @@ namespace Spry.Users {
          * @return The encrypted token string
          * @return The encrypted token string
          */
          */
         public string generate_session_token(Session session) {
         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
             // Create JSON payload
             var payload_obj = new Json.Object();
             var payload_obj = new Json.Object();
             payload_obj.set_string_member("session_id", session.id);
             payload_obj.set_string_member("session_id", session.id);
@@ -253,9 +272,13 @@ namespace Spry.Users {
             var node = new Json.Node(Json.NodeType.OBJECT);
             var node = new Json.Node(Json.NodeType.OBJECT);
             node.set_object(payload_obj);
             node.set_object(payload_obj);
             var payload = Json.to_string(node, false);
             var payload = Json.to_string(node, false);
+            stdout.printf("SESSION DEBUG: Token payload JSON: %s\n", payload);
 
 
             // Sign and seal the token with expiry
             // Sign and seal the token with expiry
-            return _crypto.sign_then_seal_token(payload, session.expires_at);
+            stdout.printf("SESSION DEBUG: Calling sign_then_seal_token()...\n");
+            var token = _crypto.sign_then_seal_token(payload, session.expires_at);
+            stdout.printf("SESSION DEBUG: Token generated successfully (length: %d)\n", token.length);
+            return token;
         }
         }
 
 
         // =========================================================================
         // =========================================================================
@@ -274,7 +297,7 @@ namespace Spry.Users {
          * @param token The encrypted token string
          * @param token The encrypted token string
          * @return A SessionValidationResult with session and user info
          * @return A SessionValidationResult with session and user info
          */
          */
-        public SessionValidationResult validate_session_token(string token) {
+        public async SessionValidationResult validate_session_token_async(string token) throws Error {
             // Decrypt and verify the token
             // Decrypt and verify the token
             var token_result = _crypto.unseal_then_verify_token(token);
             var token_result = _crypto.unseal_then_verify_token(token);
 
 
@@ -290,40 +313,36 @@ namespace Spry.Users {
             }
             }
 
 
             // Parse the payload
             // Parse the payload
-            try {
-                var payload = token_result.payload;
-                if (payload == null) {
-                    return new SessionValidationResult.failure("Empty token payload");
-                }
-
-                var json = new JsonElement.from_string((!)payload);
-                var obj = json.as<JsonObject>();
+            var payload = token_result.payload;
+            if (payload == null) {
+                return new SessionValidationResult.failure("Empty token payload");
+            }
 
 
-                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());
+            var json = new JsonElement.from_string((!)payload);
+            var obj = json.as<JsonObject>();
 
 
-                // Check expiry again
-                if (expires_at.compare(new DateTime.now_utc()) <= 0) {
-                    return new SessionValidationResult.failure("Session has expired");
-                }
+            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());
 
 
-                // Load session from storage
-                var session = get_session(session_id);
-                if (session == null) {
-                    return new SessionValidationResult.failure("Session not found");
-                }
+            // Check expiry again
+            if (expires_at.compare(new DateTime.now_utc()) <= 0) {
+                return new SessionValidationResult.failure("Session has expired");
+            }
 
 
-                // Verify session matches token data
-                if (session.user_id != user_id) {
-                    return new SessionValidationResult.failure("Session user mismatch");
-                }
+            // Load session from storage
+            var session = yield get_session_async(session_id);
+            if (session == null) {
+                return new SessionValidationResult.failure("Session not found");
+            }
 
 
-                return new SessionValidationResult.success(session);
-            } catch (Error e) {
-                return new SessionValidationResult.failure("Invalid token format: %s".printf(e.message));
+            // Verify session matches token data
+            if (session.user_id != user_id) {
+                return new SessionValidationResult.failure("Session user mismatch");
             }
             }
+
+            return new SessionValidationResult.success(session);
         }
         }
 
 
         // =========================================================================
         // =========================================================================
@@ -335,27 +354,24 @@ namespace Spry.Users {
          *
          *
          * @param session_id The session's unique identifier
          * @param session_id The session's unique identifier
          * @return The Session, or null if not found or expired
          * @return The Session, or null if not found or expired
+         * @throws Error on storage failure
          */
          */
-        public Session? get_session(string session_id) {
-            try {
-                var path = new EntityPath(@"$BASE_PATH/$SESSIONS_CONTAINER/$session_id");
-                var entity = _engine.get_entity_or_null(path);
+        public async Session? get_session_async(string session_id) throws Error {
+            var path = new EntityPath(@"$BASE_PATH/$SESSIONS_CONTAINER/$session_id");
+            var entity = yield _engine.get_entity_or_null_async(path);
 
 
-                if (entity == null || entity.entity_type != EntityType.DOCUMENT) {
-                    return null;
-                }
+            if (entity == null || entity.entity_type != EntityType.DOCUMENT) {
+                return null;
+            }
 
 
-                var session = load_session_from_document((!)entity);
+            var session = yield load_session_from_document_async((!)entity);
 
 
-                // Don't return expired sessions
-                if (session != null && session.is_expired()) {
-                    return null;
-                }
-
-                return session;
-            } catch (Error e) {
+            // Don't return expired sessions
+            if (session != null && session.is_expired()) {
                 return null;
                 return null;
             }
             }
+
+            return session;
         }
         }
 
 
         /**
         /**
@@ -363,21 +379,18 @@ namespace Spry.Users {
          *
          *
          * @param user_id The user's unique identifier
          * @param user_id The user's unique identifier
          * @return A Vector of active (non-expired) sessions
          * @return A Vector of active (non-expired) sessions
+         * @throws Error on storage failure
          */
          */
-        public Vector<Session> get_sessions_for_user(string user_id) {
+        public async Vector<Session> get_sessions_for_user_async(string user_id) throws Error {
             var sessions = new Vector<Session>();
             var sessions = new Vector<Session>();
 
 
-            try {
-                var session_ids = get_session_ids_for_user(user_id);
+            var session_ids = yield get_session_ids_for_user_async(user_id);
 
 
-                foreach (var session_id in session_ids) {
-                    var session = get_session(session_id);
-                    if (session != null) {
-                        sessions.add(session);
-                    }
+            foreach (var session_id in session_ids) {
+                var session = yield get_session_async(session_id);
+                if (session != null) {
+                    sessions.add(session);
                 }
                 }
-            } catch (Error e) {
-                // Return empty list on error
             }
             }
 
 
             return sessions;
             return sessions;
@@ -395,59 +408,49 @@ namespace Spry.Users {
          * - Updates the user sessions index
          * - Updates the user sessions index
          *
          *
          * @param session_id The session's unique identifier
          * @param session_id The session's unique identifier
-         * @return true on success, false on failure
+         * @throws Error on failure
          */
          */
-        public bool delete_session(string session_id) {
-            try {
-                // Get session first to update user index
-                var session = get_session(session_id);
-                if (session == null) {
-                    return false;
-                }
-
-                var user_id = session.user_id;
-
-                // Delete session document
-                var path = new EntityPath(@"$BASE_PATH/$SESSIONS_CONTAINER/$session_id");
-                var entity = _engine.get_entity_or_null(path);
+        public async void delete_session_async(string session_id) throws Error {
+            // Get session first to update user index
+            var session = yield get_session_async(session_id);
+            if (session == null) {
+                throw new SessionError.SESSION_NOT_FOUND("Session not found");
+            }
 
 
-                if (entity != null) {
-                    entity.delete();
-                }
+            var user_id = session.user_id;
 
 
-                // Update user sessions index
-                remove_session_from_user_index(user_id, session_id);
+            // Delete session document
+            var path = new EntityPath(@"$BASE_PATH/$SESSIONS_CONTAINER/$session_id");
+            var entity = yield _engine.get_entity_or_null_async(path);
 
 
-                return true;
-            } catch (Error e) {
-                warning("Failed to delete session: %s", e.message);
-                return false;
+            if (entity != null) {
+                yield entity.delete_async();
             }
             }
+
+            // Update user sessions index
+            yield remove_session_from_user_index_async(user_id, session_id);
         }
         }
 
 
         /**
         /**
          * Deletes all sessions for a user.
          * Deletes all sessions for a user.
          *
          *
          * @param user_id The user's unique identifier
          * @param user_id The user's unique identifier
+         * @throws Error on storage failure
          */
          */
-        public void delete_all_sessions_for_user(string user_id) {
-            try {
-                var session_ids = get_session_ids_for_user(user_id);
+        public async void delete_all_sessions_for_user_async(string user_id) throws Error {
+            var session_ids = yield get_session_ids_for_user_async(user_id);
 
 
-                foreach (var session_id in session_ids) {
-                    var path = new EntityPath(@"$BASE_PATH/$SESSIONS_CONTAINER/$session_id");
-                    var entity = _engine.get_entity_or_null(path);
+            foreach (var session_id in session_ids) {
+                var path = new EntityPath(@"$BASE_PATH/$SESSIONS_CONTAINER/$session_id");
+                var entity = yield _engine.get_entity_or_null_async(path);
 
 
-                    if (entity != null) {
-                        entity.delete();
-                    }
+                if (entity != null) {
+                    yield entity.delete_async();
                 }
                 }
-
-                // Clear user sessions index
-                clear_user_sessions_index(user_id);
-            } catch (Error e) {
-                warning("Failed to delete all sessions for user: %s", e.message);
             }
             }
+
+            // Clear user sessions index
+            yield clear_user_sessions_index_async(user_id);
         }
         }
 
 
         // =========================================================================
         // =========================================================================
@@ -467,8 +470,13 @@ namespace Spry.Users {
          * @param token The session token to set
          * @param token The session token to set
          */
          */
         public void set_session_cookie(HttpResult result, string token) {
         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
             // Build the Set-Cookie header value manually
             var max_age = (int)(_session_duration / TimeSpan.SECOND);
             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";
             var cookie_value = @"$_cookie_name=$token; Path=/; Max-Age=$max_age; HttpOnly";
 
 
             if (_cookie_secure) {
             if (_cookie_secure) {
@@ -477,7 +485,11 @@ namespace Spry.Users {
 
 
             cookie_value += "; SameSite=Strict";
             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);
             result.set_header("Set-Cookie", cookie_value);
+            stdout.printf("SESSION DEBUG: Cookie header set successfully\n");
         }
         }
 
 
         /**
         /**
@@ -516,35 +528,34 @@ namespace Spry.Users {
          *
          *
          * This method iterates through all sessions and removes those
          * This method iterates through all sessions and removes those
          * that have passed their expiry time.
          * that have passed their expiry time.
+         *
+         * @throws Error on storage failure
          */
          */
-        public void cleanup_expired_sessions() {
-            try {
-                var path = new EntityPath(@"$BASE_PATH/$SESSIONS_CONTAINER");
-                var container = _engine.get_entity_or_null(path);
+        public async void cleanup_expired_sessions_async() throws Error {
+            var path = new EntityPath(@"$BASE_PATH/$SESSIONS_CONTAINER");
+            var container = yield _engine.get_entity_or_null_async(path);
 
 
-                if (container == null) {
-                    return;
-                }
-
-                var expired_session_ids = new Vector<string>();
+            if (container == null) {
+                return;
+            }
 
 
-                foreach (var child in container.get_children()) {
-                    if (child.entity_type != EntityType.DOCUMENT) {
-                        continue;
-                    }
+            var expired_session_ids = new Vector<string>();
 
 
-                    var session = load_session_from_document(child);
-                    if (session != null && session.is_expired()) {
-                        expired_session_ids.add(session.id);
-                    }
+            var children = yield container.get_children_async();
+            foreach (var child in children) {
+                if (child.entity_type != EntityType.DOCUMENT) {
+                    continue;
                 }
                 }
 
 
-                // Delete expired sessions
-                foreach (var session_id in expired_session_ids) {
-                    delete_session(session_id);
+                var session = yield load_session_from_document_async(child);
+                if (session != null && session.is_expired()) {
+                    expired_session_ids.add(session.id);
                 }
                 }
-            } catch (Error e) {
-                warning("Failed to cleanup expired sessions: %s", e.message);
+            }
+
+            // Delete expired sessions
+            foreach (var session_id in expired_session_ids) {
+                yield delete_session_async(session_id);
             }
             }
         }
         }
 
 
@@ -564,8 +575,9 @@ namespace Spry.Users {
          * @param http_context The HttpContext containing the request to authenticate
          * @param http_context The HttpContext containing the request to authenticate
          * @param user_service The UserService to load users from
          * @param user_service The UserService to load users from
          * @return An AuthResult with authentication status and user/session info
          * @return An AuthResult with authentication status and user/session info
+         * @throws Error on storage failure
          */
          */
-        public AuthResult authenticate_request(HttpContext http_context, UserService user_service) {
+        public async AuthResult authenticate_request_async(HttpContext http_context, UserService user_service) throws Error {
             // Get session cookie
             // Get session cookie
             var token = get_session_cookie(http_context);
             var token = get_session_cookie(http_context);
 
 
@@ -574,7 +586,7 @@ namespace Spry.Users {
             }
             }
 
 
             // Validate token
             // Validate token
-            var validation = validate_session_token((!)token);
+            var validation = yield validate_session_token_async((!)token);
 
 
             if (!validation.is_valid) {
             if (!validation.is_valid) {
                 return new AuthResult.failure(validation.error_message ?? "Invalid session");
                 return new AuthResult.failure(validation.error_message ?? "Invalid session");
@@ -586,7 +598,7 @@ namespace Spry.Users {
             }
             }
 
 
             // Load user
             // Load user
-            var user = user_service.get_user(session.user_id);
+            var user = yield user_service.get_user_async(session.user_id);
             if (user == null) {
             if (user == null) {
                 return new AuthResult.failure("User not found");
                 return new AuthResult.failure("User not found");
             }
             }
@@ -598,71 +610,66 @@ namespace Spry.Users {
         // Private Helper Methods
         // Private Helper Methods
         // =========================================================================
         // =========================================================================
 
 
-        private Entity get_or_create_sessions_container() throws EngineError {
-            var root = _engine.get_root();
+        private async Entity get_sessions_container_async() throws Error {
+            var root = yield _engine.get_root_async();
 
 
             // Get or create /spry
             // Get or create /spry
-            Entity? spry = root.get_child("spry");
+            Entity? spry = yield root.get_child_async("spry");
             if (spry == null) {
             if (spry == null) {
-                spry = root.create_container("spry");
+                spry = yield root.create_container_async("spry");
             }
             }
 
 
             // Get or create /spry/users
             // Get or create /spry/users
-            Entity? users_base = spry.get_child("users");
+            Entity? users_base = yield spry.get_child_async("users");
             if (users_base == null) {
             if (users_base == null) {
-                users_base = spry.create_container("users");
+                users_base = yield spry.create_container_async("users");
             }
             }
 
 
             // Get or create /spry/users/sessions
             // Get or create /spry/users/sessions
-            Entity? sessions_container = users_base.get_child(SESSIONS_CONTAINER);
+            Entity? sessions_container = yield users_base.get_child_async(SESSIONS_CONTAINER);
             if (sessions_container == null) {
             if (sessions_container == null) {
-                sessions_container = users_base.create_container(SESSIONS_CONTAINER);
+                sessions_container = yield users_base.create_container_async(SESSIONS_CONTAINER);
             }
             }
 
 
             return (!)sessions_container;
             return (!)sessions_container;
         }
         }
 
 
-        private Entity? get_sessions_by_user_container() {
-            try {
-                var path = new EntityPath(@"$BASE_PATH/$SESSIONS_BY_USER_CONTAINER");
-                var container = _engine.get_entity_or_null(path);
-
-                if (container == null) {
-                    // Create it if it doesn't exist
-                    var root = _engine.get_root();
-                    Entity? spry = root.get_child("spry");
-                    if (spry == null) {
-                        spry = root.create_container("spry");
-                    }
+        private async Entity? get_sessions_by_user_container_async() throws Error {
+            var path = new EntityPath(@"$BASE_PATH/$SESSIONS_BY_USER_CONTAINER");
+            var container = yield _engine.get_entity_or_null_async(path);
 
 
-                    Entity? users_base = spry.get_child("users");
-                    if (users_base == null) {
-                        users_base = spry.create_container("users");
-                    }
+            if (container == null) {
+                // Create it if it doesn't exist
+                var root = yield _engine.get_root_async();
+                Entity? spry = yield root.get_child_async("spry");
+                if (spry == null) {
+                    spry = yield root.create_container_async("spry");
+                }
 
 
-                    container = users_base.create_container(SESSIONS_BY_USER_CONTAINER);
+                Entity? users_base = yield spry.get_child_async("users");
+                if (users_base == null) {
+                    users_base = yield spry.create_container_async("users");
                 }
                 }
 
 
-                return container;
-            } catch (Error e) {
-                warning("Failed to get sessions_by_user container: %s", e.message);
-                return null;
+                container = yield users_base.create_container_async(SESSIONS_BY_USER_CONTAINER);
             }
             }
+
+            return container;
         }
         }
 
 
-        private void store_session_in_document(Entity doc, Session session) throws EngineError {
+        private async void store_session_in_document_async(Entity doc, Session session) throws Error {
             var props = session.to_json();
             var props = session.to_json();
 
 
             // Store each JSON member as a property
             // Store each JSON member as a property
             foreach (var member_name in props.get_members()) {
             foreach (var member_name in props.get_members()) {
                 var element = json_to_element(props.get_member(member_name));
                 var element = json_to_element(props.get_member(member_name));
-                doc.set_entity_property(member_name, element);
+                yield doc.set_entity_property_async(member_name, element);
             }
             }
         }
         }
 
 
-        private Session? load_session_from_document(Entity doc) {
+        private async Session? load_session_from_document_async(Entity doc) {
             try {
             try {
-                var props = doc.properties;
+                var props = yield doc.get_properties_async();
                 var json_obj = properties_to_json(props);
                 var json_obj = properties_to_json(props);
                 return Session.from_json(json_obj);
                 return Session.from_json(json_obj);
             } catch (Error e) {
             } catch (Error e) {
@@ -714,15 +721,15 @@ namespace Spry.Users {
 
 
         private Json.Node element_to_json(Element element) {
         private Json.Node element_to_json(Element element) {
             if (element is NullElement) {
             if (element is NullElement) {
-                return new Json.Node.alloc();
+                return new Json.Node(Json.NodeType.NULL);
             }
             }
 
 
             // Try to get as string
             // Try to get as string
             if (element.assignable_to<string>()) {
             if (element.assignable_to<string>()) {
                 try {
                 try {
                     var str_value = element.as<string>();
                     var str_value = element.as<string>();
-                    var node = new Json.Node.alloc();
-                    node.set_string(str_value);
+                    var node = new Json.Node(Json.NodeType.VALUE);
+                    node.set_string(str_value ?? "");
                     return node;
                     return node;
                 } catch (Error e) {
                 } catch (Error e) {
                     // Fall through
                     // Fall through
@@ -733,7 +740,7 @@ namespace Spry.Users {
             if (element.assignable_to<bool>()) {
             if (element.assignable_to<bool>()) {
                 try {
                 try {
                     var bool_value = element.as<bool>();
                     var bool_value = element.as<bool>();
-                    var node = new Json.Node.alloc();
+                    var node = new Json.Node(Json.NodeType.VALUE);
                     node.set_boolean(bool_value);
                     node.set_boolean(bool_value);
                     return node;
                     return node;
                 } catch (Error e) {
                 } catch (Error e) {
@@ -746,7 +753,7 @@ namespace Spry.Users {
                 try {
                 try {
                     var int_value = element.as<int64?>();
                     var int_value = element.as<int64?>();
                     if (int_value != null) {
                     if (int_value != null) {
-                        var node = new Json.Node.alloc();
+                        var node = new Json.Node(Json.NodeType.VALUE);
                         node.set_int((!)int_value);
                         node.set_int((!)int_value);
                         return node;
                         return node;
                     }
                     }
@@ -760,7 +767,7 @@ namespace Spry.Users {
                 try {
                 try {
                     var double_value = element.as<double?>();
                     var double_value = element.as<double?>();
                     if (double_value != null) {
                     if (double_value != null) {
-                        var node = new Json.Node.alloc();
+                        var node = new Json.Node(Json.NodeType.VALUE);
                         node.set_double((!)double_value);
                         node.set_double((!)double_value);
                         return node;
                         return node;
                     }
                     }
@@ -777,7 +784,7 @@ namespace Spry.Users {
                     foreach (var item in series) {
                     foreach (var item in series) {
                         arr.add_element(element_to_json(item));
                         arr.add_element(element_to_json(item));
                     }
                     }
-                    var node = new Json.Node.alloc();
+                    var node = new Json.Node(Json.NodeType.ARRAY);
                     node.set_array(arr);
                     node.set_array(arr);
                     return node;
                     return node;
                 } catch (Error e) {
                 } catch (Error e) {
@@ -787,26 +794,26 @@ namespace Spry.Users {
 
 
             // Try as Properties for objects
             // Try as Properties for objects
             if (element is Properties) {
             if (element is Properties) {
-                var node = new Json.Node.alloc();
+                var node = new Json.Node(Json.NodeType.OBJECT);
                 node.set_object(properties_to_json((Properties)element));
                 node.set_object(properties_to_json((Properties)element));
                 return node;
                 return node;
             }
             }
 
 
             // Default: return null
             // Default: return null
-            return new Json.Node.alloc();
+            return new Json.Node(Json.NodeType.NULL);
         }
         }
 
 
-        private void add_session_to_user_index(string user_id, string session_id) throws EngineError {
-            var container = get_sessions_by_user_container();
+        private async void add_session_to_user_index_async(string user_id, string session_id) throws Error {
+            var container = yield get_sessions_by_user_container_async();
             if (container == null) return;
             if (container == null) return;
 
 
             // Get or create the user's session list document
             // Get or create the user's session list document
             var path = new EntityPath(@"$BASE_PATH/$SESSIONS_BY_USER_CONTAINER/$user_id");
             var path = new EntityPath(@"$BASE_PATH/$SESSIONS_BY_USER_CONTAINER/$user_id");
-            var existing = _engine.get_entity_or_null(path);
+            var existing = yield _engine.get_entity_or_null_async(path);
 
 
             Vector<string> session_ids;
             Vector<string> session_ids;
             if (existing != null) {
             if (existing != null) {
-                session_ids = load_session_ids_from_document((!)existing);
+                session_ids = yield load_session_ids_from_document_async((!)existing);
             } else {
             } else {
                 session_ids = new Vector<string>();
                 session_ids = new Vector<string>();
             }
             }
@@ -825,75 +832,65 @@ namespace Spry.Users {
             }
             }
 
 
             // Store updated list
             // Store updated list
-            var doc = existing;
-            if (doc == null) {
-                doc = container.create_document(user_id, "UserSessionsIndex");
+            Entity doc;
+            if (existing != null) {
+                doc = existing;
+            } else {
+                doc = yield container.create_document_async(user_id, "UserSessionsIndex");
             }
             }
 
 
-            store_session_ids_in_document(doc, session_ids);
+            yield store_session_ids_in_document_async(doc, session_ids);
         }
         }
 
 
-        private void remove_session_from_user_index(string user_id, string session_id) {
-            try {
-                var path = new EntityPath(@"$BASE_PATH/$SESSIONS_BY_USER_CONTAINER/$user_id");
-                var existing = _engine.get_entity_or_null(path);
+        private async void remove_session_from_user_index_async(string user_id, string session_id) throws Error {
+            var path = new EntityPath(@"$BASE_PATH/$SESSIONS_BY_USER_CONTAINER/$user_id");
+            var existing = yield _engine.get_entity_or_null_async(path);
 
 
-                if (existing == null) return;
+            if (existing == null) return;
 
 
-                var session_ids = load_session_ids_from_document((!)existing);
+            var session_ids = yield load_session_ids_from_document_async((!)existing);
 
 
-                // Remove the session ID
-                var new_ids = new Vector<string>();
-                foreach (var id in session_ids) {
-                    if (id != session_id) {
-                        new_ids.add(id);
-                    }
+            // Remove the session ID
+            var new_ids = new Vector<string>();
+            foreach (var id in session_ids) {
+                if (id != session_id) {
+                    new_ids.add(id);
                 }
                 }
+            }
 
 
-                if (new_ids.length == 0) {
-                    // Delete the index document if no sessions left
-                    existing.delete();
-                } else {
-                    store_session_ids_in_document((!)existing, new_ids);
-                }
-            } catch (Error e) {
-                warning("Failed to remove session from user index: %s", e.message);
+            if (new_ids.length == 0) {
+                // Delete the index document if no sessions left
+                yield existing.delete_async();
+            } else {
+                yield store_session_ids_in_document_async((!)existing, new_ids);
             }
             }
         }
         }
 
 
-        private void clear_user_sessions_index(string user_id) {
-            try {
-                var path = new EntityPath(@"$BASE_PATH/$SESSIONS_BY_USER_CONTAINER/$user_id");
-                var existing = _engine.get_entity_or_null(path);
+        private async void clear_user_sessions_index_async(string user_id) throws Error {
+            var path = new EntityPath(@"$BASE_PATH/$SESSIONS_BY_USER_CONTAINER/$user_id");
+            var existing = yield _engine.get_entity_or_null_async(path);
 
 
-                if (existing != null) {
-                    existing.delete();
-                }
-            } catch (Error e) {
-                warning("Failed to clear user sessions index: %s", e.message);
+            if (existing != null) {
+                yield existing.delete_async();
             }
             }
         }
         }
 
 
-        private Vector<string> get_session_ids_for_user(string user_id) {
-            try {
-                var path = new EntityPath(@"$BASE_PATH/$SESSIONS_BY_USER_CONTAINER/$user_id");
-                var existing = _engine.get_entity_or_null(path);
-
-                if (existing == null) {
-                    return new Vector<string>();
-                }
+        private async Vector<string> get_session_ids_for_user_async(string user_id) throws Error {
+            var path = new EntityPath(@"$BASE_PATH/$SESSIONS_BY_USER_CONTAINER/$user_id");
+            var existing = yield _engine.get_entity_or_null_async(path);
 
 
-                return load_session_ids_from_document((!)existing);
-            } catch (Error e) {
+            if (existing == null) {
                 return new Vector<string>();
                 return new Vector<string>();
             }
             }
+
+            return yield load_session_ids_from_document_async((!)existing);
         }
         }
 
 
-        private Vector<string> load_session_ids_from_document(Entity doc) {
+        private async Vector<string> load_session_ids_from_document_async(Entity doc) {
             var session_ids = new Vector<string>();
             var session_ids = new Vector<string>();
 
 
             try {
             try {
-                var props = doc.properties;
+                var props = yield doc.get_properties_async();
                 if (props.has("session_ids")) {
                 if (props.has("session_ids")) {
                     var element = props.get("session_ids");
                     var element = props.get("session_ids");
                     if (element != null && element.assignable_to<Series<Element>>()) {
                     if (element != null && element.assignable_to<Series<Element>>()) {
@@ -912,12 +909,12 @@ namespace Spry.Users {
             return session_ids;
             return session_ids;
         }
         }
 
 
-        private void store_session_ids_in_document(Entity doc, Vector<string> session_ids) throws EngineError {
+        private async void store_session_ids_in_document_async(Entity doc, Vector<string> session_ids) throws Error {
             var series = new Series<Element>();
             var series = new Series<Element>();
             foreach (var id in session_ids) {
             foreach (var id in session_ids) {
                 series.add(new NativeElement<string>(id));
                 series.add(new NativeElement<string>(id));
             }
             }
-            doc.set_entity_property("session_ids", new NativeElement<Series<Element>>(series));
+            yield doc.set_entity_property_async("session_ids", new NativeElement<Series<Element>>(series));
         }
         }
 
 
         private string generate_uuid() {
         private string generate_uuid() {

+ 62 - 38
src/Users/User.vala

@@ -32,41 +32,58 @@ namespace Spry.Users {
         public static User from_json(Json.Object obj) {
         public static User from_json(Json.Object obj) {
             var user = new User();
             var user = new User();
 
 
-            user.id = obj.get_string_member("id");
-            user.username = obj.get_string_member("username");
-            user.email = obj.get_string_member("email");
-            user.password_hash = obj.get_string_member("password_hash");
+            // Required string fields - use has_member and null coalescing for safety
+            user.id = obj.has_member("id") ? (obj.get_string_member("id") ?? "") : "";
+            user.username = obj.has_member("username") ? (obj.get_string_member("username") ?? "") : "";
+            user.email = obj.has_member("email") ? (obj.get_string_member("email") ?? "") : "";
+            user.password_hash = obj.has_member("password_hash") ? (obj.get_string_member("password_hash") ?? "") : "";
 
 
-            // created_at
+            // created_at - check member exists and value is not null/empty
             if (obj.has_member("created_at")) {
             if (obj.has_member("created_at")) {
-                user.created_at = new DateTime.from_iso8601(
-                    obj.get_string_member("created_at"),
-                    new TimeZone.utc()
-                );
+                var created_str = obj.get_string_member("created_at");
+                if (created_str != null && created_str.length > 0) {
+                    user.created_at = new DateTime.from_iso8601(created_str, new TimeZone.utc());
+                }
             }
             }
 
 
-            // updated_at (optional)
-            if (obj.has_member("updated_at") && !obj.get_null_member("updated_at")) {
-                user.updated_at = new DateTime.from_iso8601(
-                    obj.get_string_member("updated_at"),
-                    new TimeZone.utc()
-                );
+            // updated_at (optional) - check member exists, is not null, and has value
+            if (obj.has_member("updated_at")) {
+                var member = obj.get_member("updated_at");
+                if (member != null && member.get_node_type() == Json.NodeType.VALUE) {
+                    var updated_str = obj.get_string_member("updated_at");
+                    if (updated_str != null && updated_str.length > 0) {
+                        user.updated_at = new DateTime.from_iso8601(updated_str, new TimeZone.utc());
+                    }
+                }
             }
             }
 
 
-            // permissions (array of strings)
+            // permissions (array of strings) - check member exists and is array
             if (obj.has_member("permissions")) {
             if (obj.has_member("permissions")) {
-                var perms_array = obj.get_array_member("permissions");
-                foreach (var perm in perms_array.get_elements()) {
-                    user.permissions.add(perm.get_string());
+                var member = obj.get_member("permissions");
+                if (member != null && member.get_node_type() == Json.NodeType.ARRAY) {
+                    var perms_array = obj.get_array_member("permissions");
+                    if (perms_array != null) {
+                        foreach (var perm in perms_array.get_elements()) {
+                            var perm_str = perm.get_string();
+                            if (perm_str != null) {
+                                user.permissions.add(perm_str);
+                            }
+                        }
+                    }
                 }
                 }
             }
             }
 
 
-            // app_data (object with string values)
-            if (obj.has_member("app_data") && !obj.get_null_member("app_data")) {
-                var app_data_obj = obj.get_object_member("app_data");
-                foreach (var key in app_data_obj.get_members()) {
-                    var value = app_data_obj.get_string_member(key);
-                    user.app_data.set(key, value);
+            // app_data (object with string values) - check member exists and is object
+            if (obj.has_member("app_data")) {
+                var member = obj.get_member("app_data");
+                if (member != null && member.get_node_type() == Json.NodeType.OBJECT) {
+                    var app_data_obj = obj.get_object_member("app_data");
+                    if (app_data_obj != null) {
+                        foreach (var key in app_data_obj.get_members()) {
+                            var value = app_data_obj.get_string_member(key);
+                            user.app_data.set(key, value ?? "");
+                        }
+                    }
                 }
                 }
             }
             }
 
 
@@ -76,11 +93,12 @@ namespace Spry.Users {
         public Json.Object to_json() {
         public Json.Object to_json() {
             var obj = new Json.Object();
             var obj = new Json.Object();
 
 
-            obj.set_string_member("id", id);
-            obj.set_string_member("username", username);
-            obj.set_string_member("email", email);
-            obj.set_string_member("password_hash", password_hash);
-            obj.set_string_member("created_at", created_at.format_iso8601());
+            // Use null coalescing to ensure we never pass null to set_string_member
+            obj.set_string_member("id", id ?? "");
+            obj.set_string_member("username", username ?? "");
+            obj.set_string_member("email", email ?? "");
+            obj.set_string_member("password_hash", password_hash ?? "");
+            obj.set_string_member("created_at", created_at != null ? created_at.format_iso8601() : new DateTime.now_utc().format_iso8601());
 
 
             // updated_at (optional)
             // updated_at (optional)
             if (updated_at != null) {
             if (updated_at != null) {
@@ -89,19 +107,25 @@ namespace Spry.Users {
                 obj.set_null_member("updated_at");
                 obj.set_null_member("updated_at");
             }
             }
 
 
-            // permissions array
+            // permissions array - always create array even if empty
             var perms_array = new Json.Array();
             var perms_array = new Json.Array();
-            foreach (var perm in permissions) {
-                perms_array.add_string_element(perm);
+            if (permissions != null) {
+                foreach (var perm in permissions) {
+                    if (perm != null) {
+                        perms_array.add_string_element(perm);
+                    }
+                }
             }
             }
             obj.set_array_member("permissions", perms_array);
             obj.set_array_member("permissions", perms_array);
 
 
-            // app_data object
+            // app_data object - always create object even if empty
             var app_data_obj = new Json.Object();
             var app_data_obj = new Json.Object();
-            var iter = app_data.iterator();
-            while (iter.next()) {
-                var pair = iter.get();
-                app_data_obj.set_string_member(pair.key, pair.value);
+            if (app_data != null) {
+                var iter = app_data.iterator();
+                while (iter.next()) {
+                    var pair = iter.get();
+                    app_data_obj.set_string_member(pair.key ?? "", pair.value ?? "");
+                }
             }
             }
             obj.set_object_member("app_data", app_data_obj);
             obj.set_object_member("app_data", app_data_obj);
 
 

+ 237 - 385
src/Users/UserService.vala

@@ -1,6 +1,7 @@
 using Implexus.Core;
 using Implexus.Core;
 using Invercargill;
 using Invercargill;
 using Invercargill.DataStructures;
 using Invercargill.DataStructures;
+using Inversion;
 using Json;
 using Json;
 
 
 namespace Spry.Users {
 namespace Spry.Users {
@@ -25,32 +26,22 @@ namespace Spry.Users {
      *
      *
      * Storage paths:
      * Storage paths:
      * - Users: /spry/users/users/{user_id}
      * - Users: /spry/users/users/{user_id}
-     * - Username index: /spry/users/by_username/{username} → stores user_id
-     * - Email index: /spry/users/by_email/{email} → stores user_id
+     * - Username catalogue: /spry/users/users/by_username/{username} → returns user document
+     * - Email catalogue: /spry/users/users/by_email/{email} → returns user document
+     * 
+     * This service uses the inject<> pattern for dependency injection.
+     * All methods are async to work with the Implexus async API.
      */
      */
     public class UserService : GLib.Object {
     public class UserService : GLib.Object {
 
 
-        private Engine _engine;
-        private CryptographyProvider _crypto;
+        private Engine _engine = inject<Engine>();
+        private CryptographyProvider _crypto = inject<CryptographyProvider>();
 
 
         // Storage paths
         // Storage paths
         private const string BASE_PATH = "/spry/users";
         private const string BASE_PATH = "/spry/users";
         private const string USERS_CONTAINER = "users";
         private const string USERS_CONTAINER = "users";
-        private const string BY_USERNAME_CONTAINER = "by_username";
-        private const string BY_EMAIL_CONTAINER = "by_email";
         private const string USER_TYPE_LABEL = "User";
         private const string USER_TYPE_LABEL = "User";
 
 
-        /**
-         * Creates a new UserService instance.
-         *
-         * @param engine The Implexus engine for data storage
-         * @param crypto The cryptography provider for token operations
-         */
-        public UserService(Engine engine, CryptographyProvider crypto) {
-            _engine = engine;
-            _crypto = crypto;
-        }
-
         // =========================================================================
         // =========================================================================
         // User Creation
         // User Creation
         // =========================================================================
         // =========================================================================
@@ -59,73 +50,56 @@ namespace Spry.Users {
          * Creates a new user with the specified credentials.
          * Creates a new user with the specified credentials.
          *
          *
          * This method:
          * This method:
-         * - Validates username uniqueness (checks by_username index)
-         * - Validates email uniqueness (checks by_email index)
+         * - Validates username uniqueness (checks by_username catalogue)
+         * - Validates email uniqueness (checks by_email catalogue)
          * - Hashes password with Argon2id via libsodium
          * - Hashes password with Argon2id via libsodium
          * - Creates User with UUID and timestamps
          * - Creates User with UUID and timestamps
-         * - Stores user document and index entries
+         * - Stores user document (catalogues update automatically)
          *
          *
          * @param username The unique username
          * @param username The unique username
          * @param email The unique email address
          * @param email The unique email address
          * @param password The plaintext password to hash
          * @param password The plaintext password to hash
-         * @param error Output parameter for error message on failure
-         * @return The created User, or null on failure
+         * @return The created User
+         * @throws UserError on validation or storage failure
          */
          */
-        public User? create_user(string username, string email, string password, out string? error = null) {
-            error = null;
-
-            try {
-                // Validate username uniqueness
-                if (username_exists(username)) {
-                    error = "Username already exists";
-                    return null;
-                }
-
-                // Validate email uniqueness
-                if (email_exists(email)) {
-                    error = "Email already exists";
-                    return null;
-                }
+        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");
+            }
 
 
-                // Hash password with Argon2id
-                var password_hash = hash_password(password);
-                if (password_hash == null) {
-                    error = "Failed to hash password";
-                    return null;
-                }
+            // Validate email uniqueness
+            if (yield email_exists_async(email)) {
+                throw new UserError.DUPLICATE_EMAIL("Email already exists");
+            }
 
 
-                // Generate UUID for user
-                var user_id = generate_uuid();
+            // 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 object
-                var user = new User();
-                user.id = user_id;
-                user.username = username;
-                user.email = email;
-                user.password_hash = (!)password_hash;
-                user.created_at = new DateTime.now_utc();
+            // Generate UUID for user
+            var user_id = generate_uuid();
 
 
-                // Get or create storage containers
-                var users_container = get_or_create_users_container();
+            // Create user object
+            var user = new User();
+            user.id = user_id;
+            user.username = username;
+            user.email = email;
+            user.password_hash = (!)password_hash;
+            user.created_at = new DateTime.now_utc();
 
 
-                // Create user document
-                var user_doc = users_container.create_document(user_id, USER_TYPE_LABEL);
+            // Get users container
+            var users_container = yield get_users_container_async();
 
 
-                // Store user properties
-                store_user_in_document(user_doc, user);
+            // Create user document
+            var user_doc = yield users_container.create_document_async(user_id, USER_TYPE_LABEL);
 
 
-                // Update indexes
-                set_username_index(username, user_id);
-                set_email_index(email, user_id);
+            // Store user properties
+            yield store_user_in_document_async(user_doc, user);
 
 
-                return user;
-            } catch (EngineError e) {
-                error = "Storage error: %s".printf(e.message);
-                return null;
-            } catch (Error e) {
-                error = "Error: %s".printf(e.message);
-                return null;
-            }
+            return user;
         }
         }
 
 
         // =========================================================================
         // =========================================================================
@@ -137,48 +111,83 @@ namespace Spry.Users {
          *
          *
          * @param user_id The user's unique identifier
          * @param user_id The user's unique identifier
          * @return The User, or null if not found
          * @return The User, or null if not found
+         * @throws Error on storage failure
          */
          */
-        public User? get_user(string user_id) {
-            try {
-                var path = new EntityPath(@"$BASE_PATH/$USERS_CONTAINER/$user_id");
-                var entity = _engine.get_entity_or_null(path);
-
-                if (entity == null || entity.entity_type != EntityType.DOCUMENT) {
-                    return null;
-                }
+        public async User? get_user_async(string user_id) throws Error {
+            var path = new EntityPath(@"$BASE_PATH/$USERS_CONTAINER/$user_id");
+            var entity = yield _engine.get_entity_or_null_async(path);
 
 
-                return load_user_from_document((!)entity);
-            } catch (Error e) {
+            if (entity == null || entity.entity_type != EntityType.DOCUMENT) {
                 return null;
                 return null;
             }
             }
+
+            return yield load_user_from_document_async((!)entity);
         }
         }
 
 
         /**
         /**
-         * Gets a user by their username.
+         * Gets a user by their username using the catalogue.
+         *
+         * The catalogue path /spry/users/users/by_username/{username}
+         * returns the user document directly.
          *
          *
          * @param username The username to look up
          * @param username The username to look up
          * @return The User, or null if not found
          * @return The User, or null if not found
+         * @throws Error on storage failure
          */
          */
-        public User? get_user_by_username(string username) {
-            var user_id = get_user_id_by_username(username);
-            if (user_id == null) {
+        public async User? get_user_by_username_async(string username) throws Error {
+            // Use catalogue to get user directly
+            var path = new EntityPath(@"$BASE_PATH/$USERS_CONTAINER/by_username/$username");
+            var entity = yield _engine.get_entity_or_null_async(path);
+
+            if (entity == null) {
                 return null;
                 return null;
             }
             }
-            return get_user((!)user_id);
+
+            // Catalogue groups contain the actual user documents
+            if (entity.entity_type == EntityType.DOCUMENT) {
+                return yield load_user_from_document_async((!)entity);
+            }
+
+            // If it's a CatalogueGroup, get the first child (should only be one)
+            var children = yield entity.get_children_async();
+            if (children.length > 0) {
+                return yield load_user_from_document_async(children[0]);
+            }
+
+            return null;
         }
         }
 
 
         /**
         /**
-         * Gets a user by their email address.
+         * Gets a user by their email address using the catalogue.
+         *
+         * The catalogue path /spry/users/users/by_email/{email}
+         * returns the user document directly.
          *
          *
          * @param email The email address to look up
          * @param email The email address to look up
          * @return The User, or null if not found
          * @return The User, or null if not found
+         * @throws Error on storage failure
          */
          */
-        public User? get_user_by_email(string email) {
-            var user_id = get_user_id_by_email(email);
-            if (user_id == null) {
+        public async User? get_user_by_email_async(string email) throws Error {
+            // Use catalogue to get user directly
+            var path = new EntityPath(@"$BASE_PATH/$USERS_CONTAINER/by_email/$email");
+            var entity = yield _engine.get_entity_or_null_async(path);
+
+            if (entity == null) {
                 return null;
                 return null;
             }
             }
-            return get_user((!)user_id);
+
+            // Catalogue groups contain the actual user documents
+            if (entity.entity_type == EntityType.DOCUMENT) {
+                return yield load_user_from_document_async((!)entity);
+            }
+
+            // If it's a CatalogueGroup, get the first child (should only be one)
+            var children = yield entity.get_children_async();
+            if (children.length > 0) {
+                return yield load_user_from_document_async(children[0]);
+            }
+
+            return null;
         }
         }
 
 
         // =========================================================================
         // =========================================================================
@@ -190,66 +199,44 @@ namespace Spry.Users {
          *
          *
          * This method:
          * This method:
          * - Updates the updated_at timestamp
          * - Updates the updated_at timestamp
-         * - Handles username/email changes (updates indexes)
+         * - Handles username/email changes (catalogues update automatically)
          *
          *
          * @param user The user to update
          * @param user The user to update
-         * @param error Output parameter for error message on failure
-         * @return true on success, false on failure
+         * @throws Error on validation or storage failure
          */
          */
-        public bool update_user(User user, out string? error = null) {
-            error = null;
-
-            try {
-                // Get existing user to check for index changes
-                var existing = get_user(user.id);
-                if (existing == null) {
-                    error = "User not found";
-                    return false;
-                }
-
-                // Check if username changed
-                if (existing.username != user.username) {
-                    // Check new username uniqueness
-                    var existing_with_username = get_user_id_by_username(user.username);
-                    if (existing_with_username != null && existing_with_username != user.id) {
-                        error = "Username already exists";
-                        return false;
-                    }
-                    // Update username index
-                    remove_username_index(existing.username);
-                    set_username_index(user.username, user.id);
+        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 = get_user_id_by_email(user.email);
-                    if (existing_with_email != null && existing_with_email != user.id) {
-                        error = "Email already exists";
-                        return false;
-                    }
-                    // Update email index
-                    remove_email_index(existing.email);
-                    set_email_index(user.email, user.id);
+            // 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
-                var path = new EntityPath(@"$BASE_PATH/$USERS_CONTAINER/$(user.id)");
-                var entity = _engine.get_entity(path);
+            // Update timestamp
+            user.updated_at = new DateTime.now_utc();
 
 
-                store_user_in_document(entity, user);
+            // Store updated user
+            var path = new EntityPath(@"$BASE_PATH/$USERS_CONTAINER/$(user.id)");
+            var entity = yield _engine.get_entity_async(path);
 
 
-                return true;
-            } catch (EngineError e) {
-                error = "Storage error: %s".printf(e.message);
-                return false;
-            } catch (Error e) {
-                error = "Error: %s".printf(e.message);
-                return false;
-            }
+            yield store_user_in_document_async(entity, user);
         }
         }
 
 
         // =========================================================================
         // =========================================================================
@@ -261,40 +248,22 @@ namespace Spry.Users {
          *
          *
          * This method:
          * This method:
          * - Removes the user document
          * - Removes the user document
-         * - Removes index entries
+         * - Catalogues update automatically via hooks
          *
          *
          * @param user_id The user's unique identifier
          * @param user_id The user's unique identifier
-         * @param error Output parameter for error message on failure
-         * @return true on success, false on failure
+         * @throws Error on storage failure
          */
          */
-        public bool delete_user(string user_id, out string? error = null) {
-            error = null;
-
-            try {
-                // Get user first to remove indexes
-                var user = get_user(user_id);
-                if (user == null) {
-                    error = "User not found";
-                    return false;
-                }
-
-                // Remove indexes
-                remove_username_index(user.username);
-                remove_email_index(user.email);
-
-                // Delete user document
-                var path = new EntityPath(@"$BASE_PATH/$USERS_CONTAINER/$user_id");
-                var entity = _engine.get_entity(path);
-                entity.delete();
-
-                return true;
-            } catch (EngineError e) {
-                error = "Storage error: %s".printf(e.message);
-                return false;
-            } catch (Error e) {
-                error = "Error: %s".printf(e.message);
-                return false;
+        public async void delete_user_async(string user_id) throws Error {
+            // Get user first (optional, for logging/cleanup)
+            var user = yield get_user_async(user_id);
+            if (user == null) {
+                throw new UserError.USER_NOT_FOUND("User not found");
             }
             }
+
+            // Delete user document - catalogues will update automatically
+            var path = new EntityPath(@"$BASE_PATH/$USERS_CONTAINER/$user_id");
+            var entity = yield _engine.get_entity_async(path);
+            yield entity.delete_async();
         }
         }
 
 
         // =========================================================================
         // =========================================================================
@@ -307,43 +276,42 @@ namespace Spry.Users {
          * @param offset The number of users to skip
          * @param offset The number of users to skip
          * @param limit The maximum number of users to return
          * @param limit The maximum number of users to return
          * @return A Vector of users
          * @return A Vector of users
+         * @throws Error on storage failure
          */
          */
-        public Vector<User> list_users(int offset = 0, int limit = 100) {
+        public async Vector<User> list_users_async(int offset = 0, int limit = 100) throws Error {
             var users = new Vector<User>();
             var users = new Vector<User>();
 
 
-            try {
-                var path = new EntityPath(@"$BASE_PATH/$USERS_CONTAINER");
-                var container = _engine.get_entity_or_null(path);
+            var path = new EntityPath(@"$BASE_PATH/$USERS_CONTAINER");
+            var container = yield _engine.get_entity_or_null_async(path);
 
 
-                if (container == null) {
-                    return users;
-                }
+            if (container == null) {
+                return users;
+            }
 
 
-                int skipped = 0;
-                int taken = 0;
+            int skipped = 0;
+            int taken = 0;
 
 
-                foreach (var child in container.get_children()) {
-                    if (child.entity_type != EntityType.DOCUMENT) {
-                        continue;
-                    }
+            var children = yield container.get_children_async();
+            foreach (var child in children) {
+                // Skip catalogue entities
+                if (child.entity_type != EntityType.DOCUMENT) {
+                    continue;
+                }
 
 
-                    if (skipped < offset) {
-                        skipped++;
-                        continue;
-                    }
+                if (skipped < offset) {
+                    skipped++;
+                    continue;
+                }
 
 
-                    if (taken >= limit) {
-                        break;
-                    }
+                if (taken >= limit) {
+                    break;
+                }
 
 
-                    var user = load_user_from_document(child);
-                    if (user != null) {
-                        users.add(user);
-                        taken++;
-                    }
+                var user = yield load_user_from_document_async(child);
+                if (user != null) {
+                    users.add(user);
+                    taken++;
                 }
                 }
-            } catch (Error e) {
-                // Return empty list on error
             }
             }
 
 
             return users;
             return users;
@@ -379,22 +347,18 @@ namespace Spry.Users {
          *
          *
          * @param user The user to update
          * @param user The user to update
          * @param new_password The new plaintext password
          * @param new_password The new plaintext password
-         * @param error Output parameter for error message on failure
-         * @return true on success, false on failure
+         * @throws Error on failure
          */
          */
-        public bool set_password(User user, string new_password, out string? error = null) {
-            error = null;
-
+        public async void set_password_async(User user, string new_password) throws Error {
             var password_hash = hash_password(new_password);
             var password_hash = hash_password(new_password);
             if (password_hash == null) {
             if (password_hash == null) {
-                error = "Failed to hash password";
-                return false;
+                throw new UserError.STORAGE_ERROR("Failed to hash password");
             }
             }
 
 
             user.password_hash = (!)password_hash;
             user.password_hash = (!)password_hash;
             user.updated_at = new DateTime.now_utc();
             user.updated_at = new DateTime.now_utc();
 
 
-            return update_user(user, out error);
+            yield update_user_async(user);
         }
         }
 
 
         // =========================================================================
         // =========================================================================
@@ -412,13 +376,14 @@ namespace Spry.Users {
          * @param username_or_email The username or email address
          * @param username_or_email The username or email address
          * @param password The plaintext password
          * @param password The plaintext password
          * @return The authenticated User, or null if authentication failed
          * @return The authenticated User, or null if authentication failed
+         * @throws Error on storage failure
          */
          */
-        public User? authenticate(string username_or_email, string password) {
+        public async User? authenticate_async(string username_or_email, string password) throws Error {
             // Try to find user by username first, then by email
             // Try to find user by username first, then by email
-            User? user = get_user_by_username(username_or_email);
+            User? user = yield get_user_by_username_async(username_or_email);
 
 
             if (user == null) {
             if (user == null) {
-                user = get_user_by_email(username_or_email);
+                user = yield get_user_by_email_async(username_or_email);
             }
             }
 
 
             if (user == null) {
             if (user == null) {
@@ -426,7 +391,9 @@ namespace Spry.Users {
             }
             }
 
 
             // Verify password
             // Verify password
-            if (!verify_password(user, password)) {
+            bool password_valid = verify_password(user, password);
+            
+            if (!password_valid) {
                 return null;
                 return null;
             }
             }
 
 
@@ -442,9 +409,11 @@ namespace Spry.Users {
          *
          *
          * @param username The username to check
          * @param username The username to check
          * @return true if the username exists
          * @return true if the username exists
+         * @throws Error on storage failure
          */
          */
-        public bool username_exists(string username) {
-            return get_user_id_by_username(username) != null;
+        public async bool username_exists_async(string username) throws Error {
+            var path = new EntityPath(@"$BASE_PATH/$USERS_CONTAINER/by_username/$username");
+            return yield _engine.entity_exists_async(path);
         }
         }
 
 
         /**
         /**
@@ -452,112 +421,78 @@ namespace Spry.Users {
          *
          *
          * @param email The email to check
          * @param email The email to check
          * @return true if the email exists
          * @return true if the email exists
+         * @throws Error on storage failure
          */
          */
-        public bool email_exists(string email) {
-            return get_user_id_by_email(email) != null;
+        public async bool email_exists_async(string email) throws Error {
+            var path = new EntityPath(@"$BASE_PATH/$USERS_CONTAINER/by_email/$email");
+            return yield _engine.entity_exists_async(path);
         }
         }
 
 
         /**
         /**
          * Gets the total count of users.
          * Gets the total count of users.
          *
          *
          * @return The number of users
          * @return The number of users
+         * @throws Error on storage failure
          */
          */
-        public int user_count() {
-            try {
-                var path = new EntityPath(@"$BASE_PATH/$USERS_CONTAINER");
-                var container = _engine.get_entity_or_null(path);
+        public async int user_count_async() throws Error {
+            var path = new EntityPath(@"$BASE_PATH/$USERS_CONTAINER");
+            var container = yield _engine.get_entity_or_null_async(path);
 
 
-                if (container == null) {
-                    return 0;
-                }
+            if (container == null) {
+                return 0;
+            }
 
 
-                int count = 0;
-                foreach (var child in container.get_children()) {
-                    if (child.entity_type == EntityType.DOCUMENT) {
-                        count++;
-                    }
+            int count = 0;
+            var children = yield container.get_children_async();
+            foreach (var child in children) {
+                if (child.entity_type == EntityType.DOCUMENT) {
+                    count++;
                 }
                 }
-                return count;
-            } catch (Error e) {
-                return 0;
             }
             }
+            return count;
         }
         }
 
 
         // =========================================================================
         // =========================================================================
         // Private Helper Methods
         // Private Helper Methods
         // =========================================================================
         // =========================================================================
 
 
-        private Entity get_or_create_users_container() throws EngineError {
-            var root = _engine.get_root();
+        private async Entity get_users_container_async() throws Error {
+            var root = yield _engine.get_root_async();
 
 
             // Get or create /spry
             // Get or create /spry
-            Entity? spry = root.get_child("spry");
+            Entity? spry = yield root.get_child_async("spry");
             if (spry == null) {
             if (spry == null) {
-                spry = root.create_container("spry");
+                spry = yield root.create_container_async("spry");
             }
             }
 
 
             // Get or create /spry/users
             // Get or create /spry/users
-            Entity? users_base = spry.get_child("users");
+            Entity? users_base = yield spry.get_child_async("users");
             if (users_base == null) {
             if (users_base == null) {
-                users_base = spry.create_container("users");
+                users_base = yield spry.create_container_async("users");
             }
             }
 
 
             // Get or create /spry/users/users (where user docs are stored)
             // Get or create /spry/users/users (where user docs are stored)
-            Entity? users_container = users_base.get_child(USERS_CONTAINER);
+            Entity? users_container = yield users_base.get_child_async(USERS_CONTAINER);
             if (users_container == null) {
             if (users_container == null) {
-                users_container = users_base.create_container(USERS_CONTAINER);
+                users_container = yield users_base.create_container_async(USERS_CONTAINER);
             }
             }
 
 
             return (!)users_container;
             return (!)users_container;
         }
         }
 
 
-        private Entity? get_index_container(string container_name) {
-            try {
-                var path = new EntityPath(@"$BASE_PATH/$container_name");
-                return _engine.get_entity_or_null(path);
-            } catch (Error e) {
-                return null;
-            }
-        }
-
-        private void ensure_index_containers_exist() throws EngineError {
-            var root = _engine.get_root();
-
-            // Get or create /spry
-            Entity? spry = root.get_child("spry");
-            if (spry == null) {
-                spry = root.create_container("spry");
-            }
-
-            // Get or create /spry/users
-            Entity? users_base = spry.get_child("users");
-            if (users_base == null) {
-                users_base = spry.create_container("users");
-            }
-
-            // Get or create index containers
-            if (users_base.get_child(BY_USERNAME_CONTAINER) == null) {
-                users_base.create_container(BY_USERNAME_CONTAINER);
-            }
-
-            if (users_base.get_child(BY_EMAIL_CONTAINER) == null) {
-                users_base.create_container(BY_EMAIL_CONTAINER);
-            }
-        }
-
-        private void store_user_in_document(Entity doc, User user) throws EngineError {
+        private async void store_user_in_document_async(Entity doc, User user) throws Error {
             var props = user.to_json();
             var props = user.to_json();
 
 
             // Store each JSON member as a property
             // Store each JSON member as a property
             foreach (var member_name in props.get_members()) {
             foreach (var member_name in props.get_members()) {
                 var element = json_to_element(props.get_member(member_name));
                 var element = json_to_element(props.get_member(member_name));
-                doc.set_entity_property(member_name, element);
+                yield doc.set_entity_property_async(member_name, element);
             }
             }
         }
         }
 
 
-        private User? load_user_from_document(Entity doc) {
+        private async User? load_user_from_document_async(Entity doc) {
             try {
             try {
-                var props = doc.properties;
+                var props = yield doc.get_properties_async();
                 var json_obj = properties_to_json(props);
                 var json_obj = properties_to_json(props);
                 return User.from_json(json_obj);
                 return User.from_json(json_obj);
             } catch (Error e) {
             } catch (Error e) {
@@ -581,11 +516,10 @@ namespace Spry.Users {
                     }
                     }
                     return new NativeElement<string>(node.get_string());
                     return new NativeElement<string>(node.get_string());
                 case Json.NodeType.ARRAY:
                 case Json.NodeType.ARRAY:
-                    var arr = new Series<Element>();
-                    foreach (var element in node.get_array().get_elements()) {
-                        arr.add(json_to_element(element));
-                    }
-                    return new NativeElement<Series<Element>>(arr);
+                    // Store arrays as JSON strings to avoid NativeElement<Series<Element>> type erasure issues
+                    // The array will be stored as a JSON string and deserialized when read
+                    var json_string = Json.to_string(node, false);
+                    return new NativeElement<string>(json_string);
                 case Json.NodeType.OBJECT:
                 case Json.NodeType.OBJECT:
                     var obj = new PropertyDictionary();
                     var obj = new PropertyDictionary();
                     foreach (var member in node.get_object().get_members()) {
                     foreach (var member in node.get_object().get_members()) {
@@ -609,15 +543,31 @@ namespace Spry.Users {
 
 
         private Json.Node element_to_json(Element element) {
         private Json.Node element_to_json(Element element) {
             if (element is NullElement) {
             if (element is NullElement) {
-                return new Json.Node.alloc();
+                return new Json.Node(Json.NodeType.NULL);
             }
             }
             
             
             // Try to get as string
             // Try to get as string
             if (element.assignable_to<string>()) {
             if (element.assignable_to<string>()) {
                 try {
                 try {
                     var str_value = element.as<string>();
                     var str_value = element.as<string>();
-                    var node = new Json.Node.alloc();
-                    node.set_string(str_value);
+                    
+                    // Check if this is a JSON array stored as a string
+                    // (arrays are stored as JSON strings to avoid type erasure issues)
+                    if (str_value != null && str_value.has_prefix("[") && str_value.has_suffix("]")) {
+                        try {
+                            var parser = new Json.Parser();
+                            parser.load_from_data(str_value);
+                            var root = parser.get_root();
+                            if (root != null && root.get_node_type() == Json.NodeType.ARRAY) {
+                                return root.copy();
+                            }
+                        } catch (Error parse_error) {
+                            // Not a valid JSON array, treat as regular string
+                        }
+                    }
+                    
+                    var node = new Json.Node(Json.NodeType.VALUE);
+                    node.set_string(str_value ?? "");
                     return node;
                     return node;
                 } catch (Error e) {
                 } catch (Error e) {
                     // Fall through
                     // Fall through
@@ -628,7 +578,7 @@ namespace Spry.Users {
             if (element.assignable_to<bool>()) {
             if (element.assignable_to<bool>()) {
                 try {
                 try {
                     var bool_value = element.as<bool>();
                     var bool_value = element.as<bool>();
-                    var node = new Json.Node.alloc();
+                    var node = new Json.Node(Json.NodeType.VALUE);
                     node.set_boolean(bool_value);
                     node.set_boolean(bool_value);
                     return node;
                     return node;
                 } catch (Error e) {
                 } catch (Error e) {
@@ -641,7 +591,7 @@ namespace Spry.Users {
                 try {
                 try {
                     var int_value = element.as<int64?>();
                     var int_value = element.as<int64?>();
                     if (int_value != null) {
                     if (int_value != null) {
-                        var node = new Json.Node.alloc();
+                        var node = new Json.Node(Json.NodeType.VALUE);
                         node.set_int((!)int_value);
                         node.set_int((!)int_value);
                         return node;
                         return node;
                     }
                     }
@@ -655,7 +605,7 @@ namespace Spry.Users {
                 try {
                 try {
                     var double_value = element.as<double?>();
                     var double_value = element.as<double?>();
                     if (double_value != null) {
                     if (double_value != null) {
-                        var node = new Json.Node.alloc();
+                        var node = new Json.Node(Json.NodeType.VALUE);
                         node.set_double((!)double_value);
                         node.set_double((!)double_value);
                         return node;
                         return node;
                     }
                     }
@@ -664,113 +614,15 @@ namespace Spry.Users {
                 }
                 }
             }
             }
             
             
-            // Try as Series for arrays
-            if (element.assignable_to<Series<Element>>()) {
-                try {
-                    var series = element.as<Series<Element>>();
-                    var arr = new Json.Array();
-                    foreach (var item in series) {
-                        arr.add_element(element_to_json(item));
-                    }
-                    var node = new Json.Node.alloc();
-                    node.set_array(arr);
-                    return node;
-                } catch (Error e) {
-                    // Fall through
-                }
-            }
-            
             // Try as Properties for objects
             // Try as Properties for objects
             if (element is Properties) {
             if (element is Properties) {
-                var node = new Json.Node.alloc();
+                var node = new Json.Node(Json.NodeType.OBJECT);
                 node.set_object(properties_to_json((Properties)element));
                 node.set_object(properties_to_json((Properties)element));
                 return node;
                 return node;
             }
             }
             
             
             // Default: return null
             // Default: return null
-            return new Json.Node.alloc();
-        }
-
-        private void set_username_index(string username, string user_id) throws EngineError {
-            ensure_index_containers_exist();
-            var path = new EntityPath(@"$BASE_PATH/$BY_USERNAME_CONTAINER/$username");
-            var existing = _engine.get_entity_or_null(path);
-
-            if (existing != null) {
-                existing.delete();
-            }
-
-            var container = _engine.get_entity(new EntityPath(@"$BASE_PATH/$BY_USERNAME_CONTAINER"));
-            var index_doc = container.create_document(username, "UsernameIndex");
-            index_doc.set_entity_property("user_id", new NativeElement<string>(user_id));
-        }
-
-        private void set_email_index(string email, string user_id) throws EngineError {
-            ensure_index_containers_exist();
-            var path = new EntityPath(@"$BASE_PATH/$BY_EMAIL_CONTAINER/$email");
-            var existing = _engine.get_entity_or_null(path);
-
-            if (existing != null) {
-                existing.delete();
-            }
-
-            var container = _engine.get_entity(new EntityPath(@"$BASE_PATH/$BY_EMAIL_CONTAINER"));
-            var index_doc = container.create_document(email, "EmailIndex");
-            index_doc.set_entity_property("user_id", new NativeElement<string>(user_id));
-        }
-
-        private void remove_username_index(string username) throws EngineError {
-            var path = new EntityPath(@"$BASE_PATH/$BY_USERNAME_CONTAINER/$username");
-            var existing = _engine.get_entity_or_null(path);
-            if (existing != null) {
-                existing.delete();
-            }
-        }
-
-        private void remove_email_index(string email) throws EngineError {
-            var path = new EntityPath(@"$BASE_PATH/$BY_EMAIL_CONTAINER/$email");
-            var existing = _engine.get_entity_or_null(path);
-            if (existing != null) {
-                existing.delete();
-            }
-        }
-
-        private string? get_user_id_by_username(string username) {
-            try {
-                var path = new EntityPath(@"$BASE_PATH/$BY_USERNAME_CONTAINER/$username");
-                var entity = _engine.get_entity_or_null(path);
-
-                if (entity == null || entity.entity_type != EntityType.DOCUMENT) {
-                    return null;
-                }
-
-                var user_id_element = entity.get_entity_property("user_id");
-                if (user_id_element != null && user_id_element.assignable_to<string>()) {
-                    return user_id_element.as<string>();
-                }
-                return null;
-            } catch (Error e) {
-                return null;
-            }
-        }
-
-        private string? get_user_id_by_email(string email) {
-            try {
-                var path = new EntityPath(@"$BASE_PATH/$BY_EMAIL_CONTAINER/$email");
-                var entity = _engine.get_entity_or_null(path);
-
-                if (entity == null || entity.entity_type != EntityType.DOCUMENT) {
-                    return null;
-                }
-
-                var user_id_element = entity.get_entity_property("user_id");
-                if (user_id_element != null && user_id_element.assignable_to<string>()) {
-                    return user_id_element.as<string>();
-                }
-                return null;
-            } catch (Error e) {
-                return null;
-            }
+            return new Json.Node(Json.NodeType.NULL);
         }
         }
 
 
         private string generate_uuid() {
         private string generate_uuid() {

+ 149 - 0
src/Users/UsersMigration.vala

@@ -0,0 +1,149 @@
+using Implexus.Core;
+using Implexus.Migrations;
+
+namespace Spry.Users {
+
+    /**
+     * Abstract migration for setting up the Users system storage structure.
+     * 
+     * This migration creates:
+     * - /spry/users/users - Container for user documents
+     * - /spry/users/sessions - Container for session documents
+     * - /spry/users/users/by_username - Catalogue for username lookups
+     * - /spry/users/users/by_email - Catalogue for email lookups
+     * 
+     * The application must implement this class and provide a unique version
+     * number to integrate with the application's migration system.
+     * 
+     * Example:
+     * {{{
+     * public class MyAppUsersMigration : UsersMigration {
+     *     public override string version { owned get { return "2026031401"; } }
+     * }
+     * }}}
+     */
+    public abstract class UsersMigration : Object, Migration {
+
+        /**
+         * Unique identifier for this migration.
+         *
+         * The application must override this to provide a unique version number
+         * that fits into the application's migration ordering.
+         */
+        public abstract string version { owned get; }
+
+        /**
+         * Human-readable description of what this migration does.
+         */
+        public override string description { 
+            owned get { 
+                return "Create Users system containers and catalogues"; 
+            } 
+        }
+
+        /**
+         * Applies the migration - creates containers and catalogues.
+         *
+         * @param engine The database engine to operate on
+         * @throws MigrationError if the migration fails
+         */
+        public override async void up_async(Engine engine) throws MigrationError {
+            try {
+                // Get root
+                var root = yield engine.get_root_async();
+
+                // Create /spry container if it doesn't exist
+                Entity? spry = yield engine.get_entity_or_null_async(new EntityPath("/spry"));
+                if (spry == null) {
+                    spry = yield root.create_container_async("spry");
+                }
+
+                // Create /spry/users container if it doesn't exist
+                Entity? users_base = yield engine.get_entity_or_null_async(new EntityPath("/spry/users"));
+                if (users_base == null) {
+                    users_base = yield spry.create_container_async("users");
+                }
+
+                // Create /spry/users/users container for user documents
+                Entity? users_container = yield engine.get_entity_or_null_async(new EntityPath("/spry/users/users"));
+                if (users_container == null) {
+                    users_container = yield users_base.create_container_async("users");
+                }
+
+                // Create /spry/users/sessions container for session documents
+                Entity? sessions_container = yield engine.get_entity_or_null_async(new EntityPath("/spry/users/sessions"));
+                if (sessions_container == null) {
+                    sessions_container = yield users_base.create_container_async("sessions");
+                }
+
+                // Create catalogue for username lookups
+                // This allows looking up users by username:
+                // /spry/users/users/by_username/john -> returns user document
+                Entity? by_username = yield engine.get_entity_or_null_async(new EntityPath("/spry/users/users/by_username"));
+                if (by_username == null) {
+                    yield users_container.create_catalogue_async(
+                        "by_username",
+                        "User",
+                        "username"
+                    );
+                }
+
+                // Create catalogue for email lookups
+                // This allows looking up users by email:
+                // /spry/users/users/by_email/john@example.com -> returns user document
+                Entity? by_email = yield engine.get_entity_or_null_async(new EntityPath("/spry/users/users/by_email"));
+                if (by_email == null) {
+                    yield users_container.create_catalogue_async(
+                        "by_email",
+                        "User",
+                        "email"
+                    );
+                }
+
+            } catch (EngineError e) {
+                throw new MigrationError.EXECUTION_FAILED(
+                    "Failed to create Users system structure: %s".printf(e.message)
+                );
+            } catch (EntityError e) {
+                throw new MigrationError.EXECUTION_FAILED(
+                    "Failed to create Users system structure: %s".printf(e.message)
+                );
+            }
+        }
+
+        /**
+         * Reverses the migration - removes containers and catalogues.
+         *
+         * WARNING: This will delete all user and session data!
+         *
+         * @param engine The database engine to operate on
+         * @throws MigrationError if the rollback fails
+         */
+        public override async void down_async(Engine engine) throws MigrationError {
+            try {
+                // Get /spry/users container
+                Entity? users_base = yield engine.get_entity_or_null_async(new EntityPath("/spry"));
+                if (users_base == null) {
+                    return; // Nothing to remove
+                }
+
+                Entity? users = yield engine.get_entity_or_null_async(new EntityPath("/spry/users"));
+                if (users == null) {
+                    return; // Nothing to remove
+                }
+
+                // Delete the entire /spry/users tree (includes users, sessions, catalogues)
+                yield users.delete_async();
+
+            } catch (EngineError e) {
+                throw new MigrationError.EXECUTION_FAILED(
+                    "Failed to remove Users system structure: %s".printf(e.message)
+                );
+            } catch (EntityError e) {
+                throw new MigrationError.EXECUTION_FAILED(
+                    "Failed to remove Users system structure: %s".printf(e.message)
+                );
+            }
+        }
+    }
+}

+ 4 - 1
src/Users/meson.build

@@ -9,7 +9,8 @@ users_sources = files(
     'Components/UserListComponent.vala',
     'Components/UserListComponent.vala',
     'Components/PermissionEditorComponent.vala',
     'Components/PermissionEditorComponent.vala',
     'Components/UserFormComponent.vala',
     'Components/UserFormComponent.vala',
-    'Components/UserManagementPage.vala'
+    'Components/UserManagementPage.vala',
+    'UsersMigration.vala'
 )
 )
 
 
 libspry_users = static_library('spry-users',
 libspry_users = static_library('spry-users',
@@ -18,7 +19,9 @@ libspry_users = static_library('spry-users',
     include_directories: include_directories('..')
     include_directories: include_directories('..')
 )
 )
 
 
+spry_users_inc = include_directories('.')
 spry_users_dep = declare_dependency(
 spry_users_dep = declare_dependency(
     link_with: libspry_users,
     link_with: libspry_users,
+    include_directories: spry_users_inc,
     dependencies: [spry_dep, implexus_dep]
     dependencies: [spry_dep, implexus_dep]
 )
 )

+ 1 - 0
src/meson.build

@@ -1,5 +1,6 @@
 sources = files(
 sources = files(
     'Spry.vala',
     'Spry.vala',
+    'ResponseState.vala',
     'Component.vala',
     'Component.vala',
     'PageComponent.vala',
     'PageComponent.vala',
     'PageTemplate.vala',
     'PageTemplate.vala',