| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043 |
- 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<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 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<string> which is required for proper property binding
- * in the authentication components.
- */
- public Vector<string> get_application_permissions() {
- var permissions = new Vector<string>();
- 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 <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 Authentication 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 - Authentication 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 Authentication 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 Authentication Example</h1>
- <p>This example demonstrates the complete Spry Authentication 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>
- <li><strong>Authorisation Context</strong> - Request-scoped permission checking</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
- * - 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">
- <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>
-
- <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>
- </div>
- </div>
- """;
- }}
-
- // 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 += @"<span class=\"$badge_class\">$perm</span> ";
- }
- if (user_permissions.length == 0) {
- perm_text = "<span class=\"permission-badge\">No permissions assigned</span>";
- }
- 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<ComponentFactory>();
- private UserManagementComponent _user_management;
-
- public const string ROUTE = "/admin/users";
-
- public override string markup { get {
- return """
- <div class="card">
- <h1>User Administration</h1>
- <p>Manage user accounts, permissions, and access control.</p>
- <spry-outlet sid="user-management-outlet"/>
- </div>
- """;
- }}
-
- public override async void prepare() throws Error {
- // Create the user management component
- _user_management = _factory.create<UserManagementComponent>();
-
- // 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<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>();
-
- // Register Authentication system services
- // These now use repositories instead of Engine directly
- application.add_singleton<UserService>();
- application.add_singleton<SessionService>();
- application.add_singleton<PermissionService>();
-
- // Register Authorisation system services
- application.add_singleton<AuthorisationTokenService>();
- application.add_singleton<AuthorisationContext>();
-
- // 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
- // 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 UserAdminPage (admin page wrapping UserManagementComponent)
- application.add_transient<UserAdminPage>();
- application.add_endpoint<UserAdminPage>(new EndpointRoute(UserAdminPage.ROUTE));
-
- // Register LoginFormComponent (used by LoginPage)
- application.add_transient<LoginFormComponent>();
-
- // Register new user management components
- application.add_transient<UserManagementComponent>();
- application.add_transient<UserDetailsComponent>();
- application.add_transient<NewUserComponent>();
-
- // 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);
- }
- }
|