|
|
@@ -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);
|
|
|
}
|