using Astralis; using Invercargill; using Invercargill.DataStructures; using InvercargillSql; using Inversion; using Spry; using Spry.Authentication; using Spry.Authentication.Components; using Spry.Authorisation; /** * UsersExample.vala - Complete example demonstrating the Spry Authentication system * * This example demonstrates: * 1. Application Migration - Extends AuthenticationMigration with a specific version * 2. Service Setup - Using Inversion's inject() 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 UserManagementComponent (for users with permission) * 9. Authorisation Context - Using AuthorisationContext for permission checking * * 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 -> UserAdminPage (protected - requires "user-management" permission, uses UserManagementComponent) */ // ============================================================================= // APPLICATION PERMISSIONS - Define permissions available in this application // ============================================================================= /** * Creates a Vector of permissions for the UsersExample application. * * These permissions are passed to the authentication components so they can * display appropriate checkboxes. The components themselves do NOT hardcode * any permissions - they must be provided by the application. * * Returns a Vector which is required for proper property binding * in the authentication components. */ public Vector get_application_permissions() { var permissions = new Vector(); permissions.add("user-management"); permissions.add("user.create"); permissions.add("user.read"); permissions.add("user.update"); permissions.add("user.delete"); permissions.add("admin"); 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 // ============================================================================= 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 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(); private UserService _user_service = inject(); private HttpContext _http_context = inject(); public User? current_user { get; private set; } public override string markup { get { return """ Spry Authentication Example

Built with Spry Framework - Authentication Example

"""; }} 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) | Logout"; } else { this["user-info"].inner_html = "Login | Register"; } } } // ============================================================================= // PAGE COMPONENTS - Public pages // ============================================================================= /** * HomePage - Public landing page * * This page is accessible to everyone, authenticated or not. * It provides an overview of the Authentication system features. */ public class HomePage : PageComponent { private SessionService _session_service = inject(); private UserService _user_service = inject(); private HttpContext _http_context = inject(); public User? current_user { get; private set; } public const string ROUTE = "/"; public override string markup { get { return """

Welcome to the Spry Authentication Example

This example demonstrates the complete Spry Authentication system including:

Features

  • User Registration - Create new accounts with username/email/password
  • Authentication - Login with session management
  • Permissions - Granular permission system with wildcard support
  • Protected Pages - Pages that require authentication
  • User Management - Admin interface for managing users
  • Authorisation Context - Request-scoped permission checking

Try It Out

"""; }} 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 $(auth_result.user.username)."; } 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(); private PermissionService _permission_service = inject(); private HttpContext _http_context = inject(); 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 """

Create Account

Already have an account? Login here

"""; }} 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 login."; 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(); private LoginFormComponent _login_form; public const string ROUTE = "/login"; public override string markup { get { return """

Login

Don't have an account? Register here

"""; }} public override async void prepare() throws Error { // Create and configure the login form component _login_form = _factory.create(); _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(); private UserService _user_service = inject(); 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 * - Using AuthorisationContext for permission checking */ public class DashboardPage : PageComponent { private SessionService _session_service = inject(); private UserService _user_service = inject(); private PermissionService _permission_service = inject(); private HttpContext _http_context = inject(); private AuthorisationContext _auth_context = inject(); public User? current_user { get; private set; } public bool is_admin { get; private set; default = false; } public Vector permissions { get; private set; } public const string ROUTE = "/dashboard"; public override string markup { get { return """

Authentication Required

You must be logged in to view this page.

Login Register

Dashboard

Welcome to your dashboard! This page demonstrates protected content.

Your Profile

  • Username:
  • Email:
  • User ID:
  • Account Created:

Your Permissions

Admin Access: You have admin privileges. Go to User Management
AuthorisationContext: Permission checking via the new Authorisation system is active.

Back to Home

"""; }} // Track if user is authenticated (for template binding) public bool is_authenticated { get; private set; default = false; } // Track if using AuthorisationContext for permission checking public bool uses_auth_context { 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); // 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); } // 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 - user.permissions returns string[] now var perm_text = ""; var user_permissions = current_user.permissions; foreach (var perm in user_permissions) { var badge_class = perm == PermissionService.ADMIN ? "permission-badge admin" : "permission-badge"; perm_text += @"$perm "; } if (user_permissions.length == 0) { perm_text = "No permissions assigned"; } this["permission-list"].inner_html = perm_text; } } // ============================================================================= // PAGE COMPONENTS - Admin pages // ============================================================================= /** * UserAdminPage - Admin page for user management * * This page demonstrates how to use UserManagementComponent within your own * page layout. Unlike the old UserManagementPage (which was a PageComponent * that generated a full HTML document), UserManagementComponent is a regular * Component that can be placed anywhere. * * Applications now have full control over: * - The page layout and navigation * - CSS styling via their own stylesheets * - Where the user management component appears */ public class UserAdminPage : PageComponent { private ComponentFactory _factory = inject(); private UserManagementComponent _user_management; public const string ROUTE = "/admin/users"; public override string markup { get { return """

User Administration

Manage user accounts, permissions, and access control.

"""; }} public override async void prepare() throws Error { // Create the user management component _user_management = _factory.create(); // Pass the application-defined permissions to the component // This allows the component to display appropriate checkboxes _user_management.available_permissions = get_application_permissions(); // Add it to our outlet add_outlet_child("user-management-outlet", _user_management); // Share globals with the component (required for action handling) add_globals_from(_user_management); } } // ============================================================================= // 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 Connection global_connection; public static int main(string[] args) { int port = args.length > 1 ? int.parse(args[1]) : 8080; print("═══════════════════════════════════════════════════════════════\n"); print(" Spry Authentication 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 database connection FIRST print("Creating database connection...\n"); initialize_database.begin((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(); } }); } catch (Error e) { printerr("Database initialization error: %s\n", e.message); main_loop.quit(); } }); main_loop.run(); return 0; } catch (Error e) { printerr("Error: %s\n", e.message); return 1; } } /** * 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. */ 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() and need it to be available application.add_singleton(() => global_connection); // Register repositories (new InvercargillSql-based implementations) // Register the concrete implementation and expose it via the interface application.container.register_singleton((scope) => new SqlUserRepository(scope.resolve())) .as(); application.container.register_singleton((scope) => new SqlSessionRepository(scope.resolve())) .as(); // Enable compression application.use_compression(); // Add Spry module for component actions application.add_module(); // Register Authentication system services // These now use repositories instead of Engine directly application.add_singleton(); application.add_singleton(); application.add_singleton(); // Register Authorisation system services application.add_singleton(); application.add_singleton(); // Seed initial data (admin user, test user) application.add_singleton(); seed_initial_data.begin(application.container); // Register template with route prefix // MainLayoutTemplate applies to ALL routes (empty prefix) var spry_cfg = application.configure_with(); spry_cfg.add_template(""); // Register page components as endpoints application.add_transient(); application.add_endpoint(new EndpointRoute(HomePage.ROUTE)); application.add_transient(); application.add_endpoint(new EndpointRoute(RegisterPage.ROUTE)); application.add_transient(); application.add_endpoint(new EndpointRoute(LoginPage.ROUTE)); application.add_transient(); application.add_endpoint(new EndpointRoute(DashboardPage.ROUTE)); // Register logout endpoint application.add_endpoint(new EndpointRoute("/logout")); // Register UserAdminPage (admin page wrapping UserManagementComponent) application.add_transient(); application.add_endpoint(new EndpointRoute(UserAdminPage.ROUTE)); // Register LoginFormComponent (used by LoginPage) application.add_transient(); // Register new user management components application.add_transient(); application.add_transient(); application.add_transient(); // Register CSS as FastResource application.add_startup_endpoint(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(); var permission_service = scope.resolve(); yield SeedData.ensure_admin_exists(user_service, permission_service); } catch (Error e) { printerr("Warning: Failed to seed initial data: %s\n", e.message); } }