浏览代码

refactor(auth): migrate to async database operations and token-based auth

- Replace session-based authentication with AuthorisationToken pattern
- Update database queries from where_expr() to where() with expression strings
- Convert synchronous database operations to async (insert_async, update_async)
- Add user registration fields: forename, surname, date_of_birth
- Simplify example startup with InvercargillSqlInversion integration
- Add initial database migration (M0001_Initial)
- Update example to use AuthorisationContext for authentication checks
Billy Barrow 1 月之前
父节点
当前提交
c4be1b58d7

+ 231 - 206
examples/UsersExample.vala

@@ -2,6 +2,8 @@ using Astralis;
 using Invercargill;
 using Invercargill.DataStructures;
 using InvercargillSql;
+using InvercargillSql.Migrations;
+using InvercargillSqlInversion;
 using Inversion;
 using Spry;
 using Spry.Authentication;
@@ -12,10 +14,10 @@ using Spry.Authorisation;
  * UsersExample.vala - Complete example demonstrating the Spry Authentication system
  * 
  * This example demonstrates:
- * 1. Application Migration - Extends AuthenticationMigration with a specific version
+ * 1. Application Migration - Extends UserTableMigration 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
+ * 4. User Authentication - Login flow with AuthorisationToken
  * 5. Permission Management - Setting and checking permissions
  * 6. Protected Content - Pages that require authentication
  * 7. Login Form - Using the built-in LoginFormComponent
@@ -26,7 +28,7 @@ using Spry.Authorisation;
  *   /              -> HomePage (public landing page)
  *   /register      -> RegisterPage (create new account)
  *   /login         -> LoginPage (uses LoginFormComponent)
- *   /logout        -> LogoutEndpoint (clears session and redirects)
+ *   /logout        -> LogoutEndpoint (clears cookie and redirects)
  *   /dashboard     -> DashboardPage (protected - requires authentication)
  *   /admin/users   -> UserAdminPage (protected - requires "user-management" permission, uses UserManagementComponent)
  */
@@ -56,33 +58,6 @@ public Vector<string> get_application_permissions() {
     return permissions;
 }
 
-// =============================================================================
-// DATABASE SETUP - Sets up the Authentication system SQLite database
-// =============================================================================
-
-/**
- * Database file path for the authentication database.
- */
-private const string AUTH_DB_PATH = "spry_auth.db";
-
-/**
- * Creates and returns a database connection.
- */
-private async Connection create_database_connection() throws Error {
-    var connection = new SqliteConnection(AUTH_DB_PATH);
-    yield connection.open_async();
-    return connection;
-}
-
-/**
- * Initializes the database schema using CreateAuthTables.
- */
-private async void initialize_database_schema(Connection connection) throws Error {
-    var create_tables = new CreateAuthTables(connection);
-    yield create_tables.migrate();
-    print("Database schema initialized successfully.\n");
-}
-
 // =============================================================================
 // STYLESHEETS - CSS content served as FastResources
 // =============================================================================
@@ -293,14 +268,12 @@ footer {
  * - Common <head> elements (scripts, styles)
  * - Site-wide header with navigation (changes based on auth state)
  * - Site-wide footer
+ * 
+ * Uses AuthorisationContext to check authentication status.
  */
 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; }
+    private AuthorisationContext _auth_context = inject<AuthorisationContext>();
     
     public override string markup { get {
         return """
@@ -336,12 +309,10 @@ public class MainLayoutTemplate : PageTemplate {
     }}
     
     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>";
+        // Check authentication using AuthorisationContext
+        if (!_auth_context.is_anonymous() && _auth_context.token != null) {
+            var token = _auth_context.token;
+            this["user-info"].inner_html = @"Logged in as $(token.username) | <a href=\"/logout\">Logout</a>";
         } else {
             this["user-info"].inner_html = "<a href=\"/login\">Login</a> | <a href=\"/register\">Register</a>";
         }
@@ -360,11 +331,7 @@ public class MainLayoutTemplate : PageTemplate {
  */
 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; }
+    private AuthorisationContext _auth_context = inject<AuthorisationContext>();
     
     public const string ROUTE = "/";
     
@@ -377,7 +344,7 @@ public class HomePage : PageComponent {
             <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>Authentication</strong> - Login with AuthorisationToken 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>
@@ -398,12 +365,10 @@ public class HomePage : PageComponent {
     }}
     
     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>.";
+        // Check if user is authenticated using AuthorisationContext
+        if (!_auth_context.is_anonymous() && _auth_context.token != null) {
+            var token = _auth_context.token;
+            this["status-message"].text_content = @"You are logged in as <strong>$(token.username)</strong>.";
         } else {
             this["status-message"].text_content = "You are not logged in. Register or login to access protected pages.";
         }
@@ -414,12 +379,11 @@ public class HomePage : PageComponent {
  * RegisterPage - User registration page
  * 
  * Allows new users to create an account with username, email, and password.
- * Demonstrates direct use of UserService.create_user_async().
+ * Demonstrates direct use of UserService.register_user().
  */
 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; }
@@ -451,6 +415,24 @@ public class RegisterPage : PageComponent {
                            autocomplete="email" placeholder="Enter your email"/>
                 </div>
                 
+                <div class="form-group">
+                    <label for="forename">First Name</label>
+                    <input type="text" name="forename" sid="forename-input" required 
+                           autocomplete="given-name" placeholder="Enter your first name"/>
+                </div>
+                
+                <div class="form-group">
+                    <label for="surname">Last Name</label>
+                    <input type="text" name="surname" sid="surname-input" required 
+                           autocomplete="family-name" placeholder="Enter your last name"/>
+                </div>
+                
+                <div class="form-group">
+                    <label for="date-of-birth">Date of Birth</label>
+                    <input type="date" name="date_of_birth" sid="dob-input" required 
+                           autocomplete="bday" placeholder="YYYY-MM-DD"/>
+                </div>
+                
                 <div class="form-group">
                     <label for="password">Password</label>
                     <input type="password" name="password" sid="password-input" required 
@@ -499,6 +481,9 @@ public class RegisterPage : PageComponent {
         // Get form values
         var username = (query.get_any_or_default("username") ?? "").strip();
         var email = (query.get_any_or_default("email") ?? "").strip();
+        var forename = (query.get_any_or_default("forename") ?? "").strip();
+        var surname = (query.get_any_or_default("surname") ?? "").strip();
+        var dob_string = query.get_any_or_default("date_of_birth") ?? "";
         var password = query.get_any_or_default("password") ?? "";
         var confirm_password = query.get_any_or_default("confirm_password") ?? "";
         
@@ -517,6 +502,30 @@ public class RegisterPage : PageComponent {
             return;
         }
         
+        if (forename.length == 0) {
+            error_message = "Please enter your first name";
+            return;
+        }
+        
+        if (surname.length == 0) {
+            error_message = "Please enter your last name";
+            return;
+        }
+        
+        // Parse date of birth
+        DateTime? date_of_birth = null;
+        if (dob_string.length > 0) {
+            try {
+                date_of_birth = new DateTime.from_iso8601(dob_string, new TimeZone.utc());
+            } catch (Error e) {
+                error_message = "Please enter a valid date of birth (YYYY-MM-DD)";
+                return;
+            }
+        } else {
+            error_message = "Please enter your date of birth";
+            return;
+        }
+        
         if (password.length < 6) {
             error_message = "Password must be at least 6 characters";
             return;
@@ -527,12 +536,20 @@ public class RegisterPage : PageComponent {
             return;
         }
         
-        // Attempt to create user
+        // Attempt to create user using the new register_user method
         try {
-            var user = yield _user_service.create_user_async(username, email, password);
+            var user = yield _user_service.register_user(
+                username, 
+                email, 
+                forename, 
+                surname, 
+                date_of_birth, 
+                password,
+                true  // enabled
+            );
             
             // Grant basic permissions to new users
-            yield _permission_service.set_permission_async(user, PermissionService.USER_READ);
+            yield _user_service.set_user_permission(user.id, "user.read");
             
             success_message = @"Account created successfully! You can now <a href=\"/login\">login</a>.";
             error_message = null;
@@ -541,12 +558,19 @@ public class RegisterPage : PageComponent {
             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);
+            // Handle duplicate username/email errors
+            if (e.message.contains("UNIQUE constraint failed") || e.message.contains("duplicate")) {
+                if (e.message.contains("username")) {
+                    error_message = "Username already exists. Please choose another.";
+                } else if (e.message.contains("email")) {
+                    error_message = "Email already registered. Please use another or login.";
+                } else {
+                    error_message = "A user with this information already exists.";
+                }
+            } else {
+                error_message = "Registration failed: %s".printf(e.message);
+            }
         }
     }
 }
@@ -561,9 +585,8 @@ public class RegisterPage : PageComponent {
  * 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
+ * - Authentication via UserService.authenticate_user()
+ * - Cookie management via AuthorisationService
  * - Redirect after successful login
  */
 public class LoginPage : PageComponent {
@@ -599,35 +622,24 @@ public class LoginPage : PageComponent {
 }
 
 /**
- * LogoutEndpoint - Handles logout by clearing the session
+ * LogoutEndpoint - Handles logout by clearing the authorisation cookie
  *
  * This demonstrates:
- * - Getting the current session from the cookie
- * - Deleting the session from storage
- * - Clearing the session cookie
+ * - Simply clearing the authorisation cookie
  * - Returning a redirect response
+ * 
+ * Note: In the refined authentication system, there's no server-side session
+ * to delete - authentication is stateless using signed tokens.
  */
 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);
+        // Clear the authorisation cookie by setting it to empty with an expired date
+        result.headers.add("Set-Cookie", "_spry-authorisation=; Secure; Max-Age=0");
         
         return result;
     }
@@ -641,32 +653,23 @@ public class LogoutEndpoint : Object, Endpoint {
  * DashboardPage - Protected dashboard page
  * 
  * This page demonstrates:
- * - Checking authentication in prepare()
+ * - Checking authentication using AuthorisationContext
  * - Redirecting unauthenticated users to login
- * - Accessing the current user's information
- * - Checking permissions
+ * - Accessing the current user's information from AuthorisationToken
+ * - Checking permissions using AuthorisationContext
  * - Displaying user-specific content
- * - Using AuthorisationContext for permission checking
  */
 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>();
     private AuthorisationContext _auth_context = inject<AuthorisationContext>();
     
-    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">
+            <div spry-if="this.is_anonymous" class="alert alert-error">
                 <h2>Authentication Required</h2>
                 <p>You must be logged in to view this page.</p>
                 <p style="margin-top: 1rem;">
@@ -676,7 +679,7 @@ public class DashboardPage : PageComponent {
             </div>
             
             <!-- Authenticated content -->
-            <div spry-if="this.is_authenticated">
+            <div spry-if="!this.is_anonymous">
                 <div class="welcome-section">
                     <h1 sid="welcome-heading">Dashboard</h1>
                     <p>Welcome to your dashboard! This page demonstrates protected content.</p>
@@ -700,10 +703,6 @@ public class DashboardPage : PageComponent {
                     <a href="/admin/users" style="color: #155724;">Go to User Management</a>
                 </div>
                 
-                <div spry-if="this.uses_auth_context" class="alert alert-success" style="margin-top: 1.5rem;">
-                    <strong>AuthorisationContext:</strong> Permission checking via the new Authorisation system is active.
-                </div>
-                
                 <p style="margin-top: 1.5rem;">
                     <a href="/" class="btn btn-secondary">Back to Home</a>
                 </p>
@@ -713,52 +712,69 @@ public class DashboardPage : PageComponent {
     }}
     
     // Track if user is authenticated (for template binding)
-    public bool is_authenticated { get; private set; default = false; }
+    public bool is_anonymous { get; private set; default = true; }
     
-    // Track if using AuthorisationContext for permission checking
-    public bool uses_auth_context { get; private set; default = false; }
+    // Track if user has admin permission
+    public bool is_admin { 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) {
+        // Check authentication using AuthorisationContext
+        if (_auth_context.is_anonymous()) {
             // Not authenticated - show message and link to login
-            // Note: PageComponent doesn't have redirect(), so we show a message instead
-            is_authenticated = false;
+            is_anonymous = true;
             return;
         }
         
-        is_authenticated = true;
+        is_anonymous = false;
         
-        current_user = auth_result.user;
-        permissions = _permission_service.get_permissions(current_user);
-        is_admin = _permission_service.has_permission(current_user, PermissionService.ADMIN);
+        var token = _auth_context.token;
+        if (token == null) {
+            is_anonymous = true;
+            return;
+        }
+        
+        // Check permissions using AuthorisationContext
+        is_admin = _auth_context.has_permission("admin");
+        
+        // Populate user info from the token
+        this["welcome-heading"].text_content = @"Welcome, $(token.username)!";
+        this["username"].text_content = token.username;
+        this["user-id"].text_content = token.user_identifier.to_string();
+        
+        // Get additional user data from the token's data properties
+        var user_data = token.data;
+        string? email_val = null;
+        DateTime? created_val = null;
+        
+        // Try to get email and created from properties
+        try {
+            var email_element = user_data.get("email");
+            email_val = email_element.as_string_or_null();
+        } catch (Error e) {
+            // Email not available
+        }
         
-        // Demonstrate using AuthorisationContext for permission checking
-        // This shows how the new Authorisation system can be used alongside
-        // the traditional PermissionService approach
-        if (_auth_context.is_authorised) {
-            uses_auth_context = true;
-            // The AuthorisationContext provides an alternative way to check permissions
-            // is_admin = _auth_context.has_permission(PermissionService.ADMIN);
+        try {
+            var created_element = user_data.get("created");
+            created_val = created_element.as<DateTime>();
+        } catch (Error e) {
+            // Created not available
         }
         
-        // 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");
+        this["email"].text_content = email_val ?? "N/A";
+        this["created-at"].text_content = created_val != null ? 
+            ((DateTime)created_val).format("%Y-%m-%d %H:%M:%S UTC") : "N/A";
         
-        // Populate permissions - user.permissions returns string[] now
+        // Populate permissions from the token
         var perm_text = "";
-        var user_permissions = current_user.permissions;
+        var user_permissions = token.permissions;
+        int perm_count = 0;
         foreach (var perm in user_permissions) {
-            var badge_class = perm == PermissionService.ADMIN ? "permission-badge admin" : "permission-badge";
+            var badge_class = perm == "admin" ? "permission-badge admin" : "permission-badge";
             perm_text += @"<span class=\"$badge_class\">$perm</span> ";
+            perm_count++;
         }
-        if (user_permissions.length == 0) {
+        if (perm_count == 0) {
             perm_text = "<span class=\"permission-badge\">No permissions assigned</span>";
         }
         this["permission-list"].inner_html = perm_text;
@@ -824,42 +840,77 @@ public class UserAdminPage : PageComponent {
  * 
  * This demonstrates how to:
  * - Check if users exist before creating
- * - Create users programmatically
- * - Grant permissions to users
+ * - Create users programmatically using register_user()
+ * - Grant permissions to users using set_user_permission()
  */
 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;
-        }
+    public static async void ensure_admin_exists(UserService user_service) throws Error {
+        // Try to get list of users and check if admin exists
+        var users = yield user_service.list_users(0, 100);
         
-        print("Creating admin user...\n");
+        bool admin_exists = false;
+        bool testuser_exists = false;
+        int64? admin_id = null;
+        int64? testuser_id = null;
         
-        // 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);
+        foreach (var user in users) {
+            if (user.username == "admin") {
+                admin_exists = true;
+                admin_id = user.id;
+            }
+            if (user.username == "testuser") {
+                testuser_exists = true;
+                testuser_id = user.id;
+            }
+        }
         
-        print("Admin user created with username 'admin' and password 'admin123'\n");
+        if (admin_exists) {
+            print("Admin user already exists\n");
+        } else {
+            print("Creating admin user...\n");
+            
+            // Create admin user using register_user
+            var admin_user = yield user_service.register_user(
+                "admin", 
+                "admin@example.com", 
+                "Admin", 
+                "User", 
+                new DateTime.utc(1990, 1, 1, 0, 0, 0.0),
+                "admin123",
+                true
+            );
+            admin_id = admin_user.id;
+            
+            // Grant admin permissions
+            yield user_service.set_user_permission(admin_id, "admin");
+            yield user_service.set_user_permission(admin_id, "user-management");
+            yield user_service.set_user_permission(admin_id, "user.create");
+            yield user_service.set_user_permission(admin_id, "user.read");
+            yield user_service.set_user_permission(admin_id, "user.update");
+            yield user_service.set_user_permission(admin_id, "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) {
+        if (!testuser_exists) {
             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);
+            
+            var test_user = yield user_service.register_user(
+                "testuser", 
+                "test@example.com", 
+                "Test", 
+                "User", 
+                new DateTime.utc(1995, 6, 15, 0, 0, 0.0),
+                "test123",
+                true
+            );
+            testuser_id = test_user.id;
+            
+            yield user_service.set_user_permission(testuser_id, "user.read");
             print("Test user created with username 'testuser' and password 'test123'\n");
+        } else {
+            print("Test user already exists\n");
         }
     }
 }
@@ -873,7 +924,6 @@ public class SeedData : Object {
 // =============================================================================
 
 private MainLoop main_loop;
-private Connection global_connection;
 
 public static int main(string[] args) {
     int port = args.length > 1 ? int.parse(args[1]) : 8080;
@@ -890,7 +940,7 @@ public static int main(string[] args) {
     print("═══════════════════════════════════════════════════════════════\n");
     print("  Protected Endpoints (requires login):\n");
     print("    /dashboard     - User dashboard\n");
-    print("    /logout        - Logout and clear session\n");
+    print("    /logout        - Logout and clear cookie\n");
     print("═══════════════════════════════════════════════════════════════\n");
     print("  Admin Endpoints (requires 'user-management' permission):\n");
     print("    /admin/users   - User management page\n");
@@ -904,23 +954,11 @@ public static int main(string[] args) {
     main_loop = new MainLoop();
     
     try {
-        // 1. Create the database connection FIRST
-        print("Creating database connection...\n");
-        initialize_database.begin((obj, res) => {
+        start_application.begin(port, (obj, res) => {
             try {
-                initialize_database.end(res);
-                
-                // 2. 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();
-                    }
-                });
+                start_application.end(res);
             } catch (Error e) {
-                printerr("Database initialization error: %s\n", e.message);
+                printerr("Application error: %s\n", e.message);
                 main_loop.quit();
             }
         });
@@ -935,49 +973,37 @@ public static int main(string[] args) {
 }
 
 /**
- * Initialize database connection and create schema.
- */
-private async void initialize_database() throws Error {
-    global_connection = yield create_database_connection();
-    yield initialize_database_schema(global_connection);
-    print("Database initialized successfully.\n");
-}
-
-/**
- * Start the web application after database is initialized.
+ * Start the web application.
  */
 private async void start_application(int port) throws Error {
     var application = new WebApplication(port);
     
-    // Register the database Connection in the container FIRST before any services
-    // This is critical - repositories use inject<Connection>() and need it to be available
-    application.add_singleton<Connection>(() => global_connection);
-    
-    // Register repositories (new InvercargillSql-based implementations)
-    // Register the concrete implementation and expose it via the interface
-    application.container.register_singleton<SqlUserRepository>((scope) => new SqlUserRepository(scope.resolve<Connection>()))
-        .as<UserRepository>();
-    application.container.register_singleton<SqlSessionRepository>((scope) => new SqlSessionRepository(scope.resolve<Connection>()))
-        .as<SessionRepository>();
-    
     // Enable compression
     application.use_compression();
     
     // Add Spry module for component actions
     application.add_module<SpryModule>();
     
+    // Configure the database using InvercargillSqlInversion
+    var db_config = application.container.configure_with<DatabaseConfigurator>();
+    db_config.register_connection(@"sqlite://./db.sqlite");
+    db_config.migrate_on_startup();
+    
+    // Add authentication module to register entity mappings
+    application.add_module<AuthenticationModule>();
+    
     // Register Authentication system services
-    // These now use repositories instead of Engine directly
-    application.add_singleton<UserService>();
-    application.add_singleton<SessionService>();
-    application.add_singleton<PermissionService>();
+    application.add_scoped<UserService>();
     
     // Register Authorisation system services
-    application.add_singleton<AuthorisationTokenService>();
-    application.add_singleton<AuthorisationContext>();
+    application.add_scoped<AuthorisationService>();
+    
+    // Add the authorisation pipeline component to read tokens from requests
+    // This component creates a scoped AuthorisationContext per request
+    application.add_scoped<AuthorisationPipelineComponent>()
+        .as<Astralis.PipelineComponent>();
     
     // Seed initial data (admin user, test user)
-    // Note: CryptographyProvider is already registered by SpryModule
     seed_initial_data.begin(application.container);
     
     // Register template with route prefix
@@ -1035,8 +1061,7 @@ 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);
+        yield SeedData.ensure_admin_exists(user_service);
     } catch (Error e) {
         printerr("Warning: Failed to seed initial data: %s\n", e.message);
     }

+ 1 - 0
src/Authentication/AuthenticationModule.vala

@@ -7,6 +7,7 @@ namespace Spry.Authentication {
 
         public void register_components (Inversion.Container container) throws Error {
             var sql = container.configure_with<DatabaseConfigurator>();
+            sql.add_migration<Migrations.M0001_Initial>();
             sql.add_entity<UserEntity>(UserEntity.entity_mapping);
             sql.add_entity<UserPermissionEntity>(UserPermissionEntity.entity_mapping);
             sql.add_projection<UserProjection> (UserProjection.projection_mapping);

+ 3 - 6
src/Authentication/UserTableMigration.vala → src/Authentication/Migrations/M0001_Initial.vala

@@ -2,15 +2,12 @@ using InvercargillSql.Migrations;
 
 namespace Spry.Authentication.Migrations {
 
-    public class UserTableMigration : Migration {
+    public class M0001_Initial : Migration {
 
-        private int _version;
-        public UserTableMigration(int version) {
-            _version = version;
-        }
 
         public override string name { get { return "Create Spry Authentication Tables"; } }
-        public override int version { get { return _version; } }
+        public override uint64 serial { get { return 1; } }
+        public override string migration_namespace { get { return "spry-authentication"; }}
 
         public override void up (InvercargillSql.Migrations.MigrationBuilder b) {
             b.create_table ("spry_users",  (t) => {

+ 2 - 2
src/Authentication/UserIdentityProvider.vala

@@ -12,12 +12,12 @@ namespace Spry.Authentication {
 
         public async Authorisation.Identity? get_identity_by_identifier (Element id) throws Error {
             return yield db.query<UserProjection>()
-                .where_expr(expr("id == $0", id))
+                .where(expr("id == $0", id).to_expression_string())
                 .first_async();
         }
         public async Authorisation.Identity? get_identity_by_username (string username) throws Error {
             return yield db.query<UserProjection>()
-                .where_expr(expr("username == $0", new NativeElement<string>(username)))
+                .where(expr("username == $0", new NativeElement<string>(username)).to_expression_string())
                 .first_async();
         }
         

+ 1 - 1
src/Authentication/UserProjection.vala

@@ -34,7 +34,7 @@ namespace Spry.Authentication {
         private string _username;
         private ImmutableBuffer<string> _permissions;
 
-        public static void projection_mapping(ProjectionBuilder<UserProjection> cfg) throws ProjectionError {
+        public static void projection_mapping(ProjectionBuilder<UserProjection> cfg) throws Error {
             cfg.source<UserEntity>("u")
                 .select<int64?>("id", "u.id", (o, v) => o.id = v)
                 .select<string>("username", "u.username", (o, v) => o._username = v)

+ 13 - 12
src/Authentication/UserService.vala

@@ -14,8 +14,9 @@ namespace Spry.Authentication {
 
 
         public async AuthorisationToken? authenticate_user(string username, string password) throws Error {
+            print(expr("username == $0", new NativeElement<string>(username)).to_expression_string());
             var user = yield db.query<UserProjection>()
-                .where_expr(expr("username == $0", new NativeElement<string>(username)))
+                .where(expr("username == $0", new NativeElement<string>(username)).to_expression_string())
                 .first_async();
 
             if(!Sodium.PasswordHashing.check(user.password_hash, password)){
@@ -38,23 +39,23 @@ namespace Spry.Authentication {
                 enabled = enabled,
             };
 
-            db.insert<UserEntity>(user);
+            yield yield db.insert_async<UserEntity>(user);
             return user;
         }
 
         public async void set_password(int64 user_id, string password) throws Error {
             var user = yield db.query<UserEntity>()
-                .where_expr(expr("id == $0", new NativeElement<int64?>(user_id)))
+                .where(expr("id == $0", new NativeElement<int64?>(user_id)).to_expression_string())
                 .first_async();
 
             user.password_hash = Sodium.PasswordHashing.hash(password);
             user.modified = new DateTime.now_utc();
-            db.update<UserEntity>(user);
+            yield yield db.update_async<UserEntity>(user);
         }
 
         public async UserEntity alter_user(int64 user_id, string username, string email, string forename, string surname, DateTime date_of_birth, bool enabled) throws Error {
             var user = yield db.query<UserEntity>()
-                .where_expr(expr("id == $0", new NativeElement<int64?>(user_id)))
+                .where(expr("id == $0", new NativeElement<int64?>(user_id)).to_expression_string())
                 .first_async();
 
             user.username = username;
@@ -65,19 +66,19 @@ namespace Spry.Authentication {
             user.modified = new DateTime.now_utc();
             user.enabled = enabled;
 
-            db.update<UserEntity>(user);
+            yield db.update_async<UserEntity>(user);
             return user;
         }
 
         public async void set_user_enabled(int64 user_id, bool enabled) throws Error {
             var user = yield db.query<UserEntity>()
-                .where_expr(expr("id == $0", new NativeElement<int64?>(user_id)))
+                .where(expr("id == $0", new NativeElement<int64?>(user_id)).to_expression_string())
                 .first_async();
 
             user.modified = new DateTime.now_utc();
             user.enabled = enabled;
 
-            db.update<UserEntity>(user);
+            yield db.update_async<UserEntity>(user);
         }
 
         public async ImmutableLot<UserProjection> list_users(int64 offset = 0, int64 limit = 100) throws Error {
@@ -89,7 +90,7 @@ namespace Spry.Authentication {
 
         public async void delete_user(int64 user_id) throws Error {
             var user = yield db.query<UserEntity>()
-                .where_expr(expr("id == $0", new NativeElement<int64?>(user_id)))
+                .where(expr("id == $0", new NativeElement<int64?>(user_id)).to_expression_string())
                 .first_async();
             db.delete<UserEntity>(user);
         }
@@ -99,12 +100,12 @@ namespace Spry.Authentication {
                 user_id = user_id,
                 permission = permission
             };
-            db.insert<UserPermissionEntity>(user_permission);
+            yield db.insert_async<UserPermissionEntity>(user_permission);
         }
 
         public async void clear_user_permissions(int64 user_id) throws Error {
             var permissions = yield db.query<UserPermissionEntity>()
-                .where_expr(expr("user_id == $0", new NativeElement<int64?>(user_id)))
+                .where(expr("user_id == $0", new NativeElement<int64?>(user_id)).to_expression_string())
                 .materialise_async();
 
             foreach (var permission in permissions) {
@@ -114,7 +115,7 @@ namespace Spry.Authentication {
 
         public async ImmutableLot<string> get_user_permissions(int64 user_id) throws Error {
             var permissions = yield db.query<UserPermissionEntity>()
-                .where_expr(expr("user_id == $0", new NativeElement<int64?>(user_id)))
+                .where(expr("user_id == $0", new NativeElement<int64?>(user_id)).to_expression_string())
                 .materialise_async();
 
             var result = new Vector<string>();

+ 7 - 6
src/meson.build

@@ -34,6 +34,7 @@ sources = files(
     'Authentication/UserService.vala',
     'Authentication/UserIdentityProvider.vala',
     'Authentication/AuthenticationModule.vala',
+    'Authentication/Migrations/M0001_Initial.vala',
     'Authentication/Components/LoginFormComponent.vala',
     'Authentication/Components/NewUserComponent.vala',
     'Authentication/Components/UserDetailsComponent.vala',
@@ -56,12 +57,12 @@ pkg.generate(libspry,
     version : library_version,
     name : 'spry-@0@'.format(library_version))
     
-g_ir_compiler = find_program('g-ir-compiler')
-custom_target('spry typelib', command: [g_ir_compiler, '--shared-library=libspry-@0@.so'.format(library_version), '--output', '@OUTPUT@', meson.current_build_dir() / 'spry-@0@.gir'.format(library_version)],
-              output: 'libspry-@0@.typelib'.format(library_version),
-              depends: libspry,
-              install: true,
-              install_dir: get_option('libdir') / 'girepository-1.0')
+# g_ir_compiler = find_program('g-ir-compiler')
+# custom_target('spry typelib', command: [g_ir_compiler, '--shared-library=libspry-@0@.so'.format(library_version), '--output', '@OUTPUT@', meson.current_build_dir() / 'spry-@0@.gir'.format(library_version)],
+#               output: 'libspry-@0@.typelib'.format(library_version),
+#               depends: libspry,
+#               install: true,
+#               install_dir: get_option('libdir') / 'girepository-1.0')
 
 
 spry_dep = declare_dependency(