UsersExample.vala 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043
  1. using Astralis;
  2. using Invercargill;
  3. using Invercargill.DataStructures;
  4. using InvercargillSql;
  5. using Inversion;
  6. using Spry;
  7. using Spry.Authentication;
  8. using Spry.Authentication.Components;
  9. using Spry.Authorisation;
  10. /**
  11. * UsersExample.vala - Complete example demonstrating the Spry Authentication system
  12. *
  13. * This example demonstrates:
  14. * 1. Application Migration - Extends AuthenticationMigration with a specific version
  15. * 2. Service Setup - Using Inversion's inject<T>() pattern
  16. * 3. User Registration - Creating a new user with username/email/password
  17. * 4. User Authentication - Login flow with session creation
  18. * 5. Permission Management - Setting and checking permissions
  19. * 6. Protected Content - Pages that require authentication
  20. * 7. Login Form - Using the built-in LoginFormComponent
  21. * 8. User Management - Using UserManagementComponent (for users with permission)
  22. * 9. Authorisation Context - Using AuthorisationContext for permission checking
  23. *
  24. * Route structure:
  25. * / -> HomePage (public landing page)
  26. * /register -> RegisterPage (create new account)
  27. * /login -> LoginPage (uses LoginFormComponent)
  28. * /logout -> LogoutEndpoint (clears session and redirects)
  29. * /dashboard -> DashboardPage (protected - requires authentication)
  30. * /admin/users -> UserAdminPage (protected - requires "user-management" permission, uses UserManagementComponent)
  31. */
  32. // =============================================================================
  33. // APPLICATION PERMISSIONS - Define permissions available in this application
  34. // =============================================================================
  35. /**
  36. * Creates a Vector of permissions for the UsersExample application.
  37. *
  38. * These permissions are passed to the authentication components so they can
  39. * display appropriate checkboxes. The components themselves do NOT hardcode
  40. * any permissions - they must be provided by the application.
  41. *
  42. * Returns a Vector<string> which is required for proper property binding
  43. * in the authentication components.
  44. */
  45. public Vector<string> get_application_permissions() {
  46. var permissions = new Vector<string>();
  47. permissions.add("user-management");
  48. permissions.add("user.create");
  49. permissions.add("user.read");
  50. permissions.add("user.update");
  51. permissions.add("user.delete");
  52. permissions.add("admin");
  53. return permissions;
  54. }
  55. // =============================================================================
  56. // DATABASE SETUP - Sets up the Authentication system SQLite database
  57. // =============================================================================
  58. /**
  59. * Database file path for the authentication database.
  60. */
  61. private const string AUTH_DB_PATH = "spry_auth.db";
  62. /**
  63. * Creates and returns a database connection.
  64. */
  65. private async Connection create_database_connection() throws Error {
  66. var connection = new SqliteConnection(AUTH_DB_PATH);
  67. yield connection.open_async();
  68. return connection;
  69. }
  70. /**
  71. * Initializes the database schema using CreateAuthTables.
  72. */
  73. private async void initialize_database_schema(Connection connection) throws Error {
  74. var create_tables = new CreateAuthTables(connection);
  75. yield create_tables.migrate();
  76. print("Database schema initialized successfully.\n");
  77. }
  78. // =============================================================================
  79. // STYLESHEETS - CSS content served as FastResources
  80. // =============================================================================
  81. private const string MAIN_CSS = """
  82. /* Base Reset & Layout */
  83. * { box-sizing: border-box; margin: 0; padding: 0; }
  84. html, body {
  85. height: 100%;
  86. }
  87. body {
  88. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  89. line-height: 1.6;
  90. background: #f5f7fa;
  91. display: flex;
  92. flex-direction: column;
  93. min-height: 100vh;
  94. }
  95. /* Header */
  96. header {
  97. background: #2c3e50;
  98. color: white;
  99. padding: 1rem 2rem;
  100. display: flex;
  101. justify-content: space-between;
  102. align-items: center;
  103. flex-shrink: 0;
  104. }
  105. header nav a {
  106. color: white;
  107. margin-right: 1.5rem;
  108. text-decoration: none;
  109. font-weight: 500;
  110. transition: opacity 0.2s;
  111. }
  112. header nav a:hover { opacity: 0.8; }
  113. header nav a:last-child { margin-right: 0; }
  114. header .user-info {
  115. font-size: 0.9rem;
  116. }
  117. header .user-info a {
  118. margin-left: 1rem;
  119. color: #3498db;
  120. }
  121. /* Main Content */
  122. main.container {
  123. flex: 1;
  124. max-width: 800px;
  125. width: 100%;
  126. margin: 0 auto;
  127. padding: 2rem 1rem;
  128. }
  129. /* Cards */
  130. .card {
  131. background: white;
  132. border-radius: 12px;
  133. padding: 2rem;
  134. margin-bottom: 1.5rem;
  135. box-shadow: 0 2px 8px rgba(0,0,0,0.08);
  136. }
  137. /* Typography */
  138. h1 { color: #2c3e50; margin-bottom: 1rem; }
  139. h2 { color: #34495e; margin-bottom: 0.75rem; margin-top: 1.5rem; }
  140. p { color: #555; margin-bottom: 1rem; }
  141. ul { margin-left: 1.5rem; margin-bottom: 1rem; }
  142. li { margin-bottom: 0.5rem; color: #555; }
  143. /* Links */
  144. a { color: #3498db; text-decoration: none; transition: color 0.2s; }
  145. a:hover { color: #2980b9; text-decoration: underline; }
  146. /* Forms */
  147. .form-group {
  148. margin-bottom: 1.25rem;
  149. }
  150. .form-group label {
  151. display: block;
  152. margin-bottom: 0.5rem;
  153. font-weight: 500;
  154. color: #34495e;
  155. }
  156. .form-group input {
  157. width: 100%;
  158. padding: 0.75rem;
  159. border: 2px solid #e0e0e0;
  160. border-radius: 6px;
  161. font-size: 1rem;
  162. transition: border-color 0.2s;
  163. }
  164. .form-group input:focus {
  165. outline: none;
  166. border-color: #3498db;
  167. }
  168. .form-group-checkbox {
  169. display: flex;
  170. align-items: center;
  171. gap: 0.5rem;
  172. }
  173. .form-group-checkbox input {
  174. width: auto;
  175. }
  176. .form-group-checkbox label {
  177. margin: 0;
  178. font-weight: normal;
  179. }
  180. /* Buttons */
  181. button, .btn {
  182. background: #3498db;
  183. color: white;
  184. border: none;
  185. padding: 0.75rem 1.5rem;
  186. border-radius: 6px;
  187. cursor: pointer;
  188. font-size: 1rem;
  189. font-weight: 500;
  190. transition: all 0.2s;
  191. text-decoration: none;
  192. display: inline-block;
  193. }
  194. button:hover, .btn:hover {
  195. background: #2980b9;
  196. text-decoration: none;
  197. }
  198. .btn-secondary {
  199. background: #6c757d;
  200. }
  201. .btn-secondary:hover {
  202. background: #545b62;
  203. }
  204. /* Alerts */
  205. .alert {
  206. padding: 1rem;
  207. border-radius: 6px;
  208. margin-bottom: 1rem;
  209. }
  210. .alert-success {
  211. background: #d4edda;
  212. color: #155724;
  213. border: 1px solid #c3e6cb;
  214. }
  215. .alert-error {
  216. background: #f8d7da;
  217. color: #721c24;
  218. border: 1px solid #f5c6cb;
  219. }
  220. .error-message {
  221. color: #dc3545;
  222. font-size: 0.9rem;
  223. margin-top: 0.5rem;
  224. }
  225. /* Login Form */
  226. .spry-login-form {
  227. max-width: 400px;
  228. }
  229. .login-btn {
  230. width: 100%;
  231. margin-top: 1rem;
  232. }
  233. /* Footer */
  234. footer {
  235. background: #2c3e50;
  236. color: rgba(255,255,255,0.7);
  237. padding: 1.5rem;
  238. text-align: center;
  239. flex-shrink: 0;
  240. }
  241. /* Dashboard */
  242. .welcome-section {
  243. margin-bottom: 2rem;
  244. }
  245. .permission-list {
  246. display: flex;
  247. flex-wrap: wrap;
  248. gap: 0.5rem;
  249. margin-top: 0.5rem;
  250. }
  251. .permission-badge {
  252. background: #e9ecef;
  253. color: #495057;
  254. padding: 0.25rem 0.75rem;
  255. border-radius: 20px;
  256. font-size: 0.85rem;
  257. }
  258. .permission-badge.admin {
  259. background: #ffc107;
  260. color: #856404;
  261. }
  262. """;
  263. // =============================================================================
  264. // PAGE TEMPLATE - Provides consistent layout for all pages
  265. // =============================================================================
  266. /**
  267. * MainLayoutTemplate - Base template for all pages
  268. *
  269. * Provides:
  270. * - HTML document structure
  271. * - Common <head> elements (scripts, styles)
  272. * - Site-wide header with navigation (changes based on auth state)
  273. * - Site-wide footer
  274. */
  275. public class MainLayoutTemplate : PageTemplate {
  276. private SessionService _session_service = inject<SessionService>();
  277. private UserService _user_service = inject<UserService>();
  278. private HttpContext _http_context = inject<HttpContext>();
  279. public User? current_user { get; private set; }
  280. public override string markup { get {
  281. return """
  282. <!DOCTYPE html>
  283. <html lang="en">
  284. <head>
  285. <meta charset="UTF-8">
  286. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  287. <title>Spry Authentication Example</title>
  288. <link rel="stylesheet" href="/styles/main.css">
  289. <script spry-res="htmx.js"></script>
  290. </head>
  291. <body>
  292. <header>
  293. <nav>
  294. <a href="/">Home</a>
  295. <a href="/dashboard">Dashboard</a>
  296. <a href="/admin/users">User Admin</a>
  297. </nav>
  298. <div class="user-info" sid="user-info">
  299. <!-- Will be populated based on auth state -->
  300. </div>
  301. </header>
  302. <main class="container">
  303. <spry-template-outlet />
  304. </main>
  305. <footer>
  306. <p>Built with Spry Framework - Authentication Example</p>
  307. </footer>
  308. </body>
  309. </html>
  310. """;
  311. }}
  312. public override async void prepare() throws Error {
  313. // Try to authenticate the request
  314. var auth_result = yield _session_service.authenticate_request_async(_http_context, _user_service);
  315. if (auth_result.is_authenticated && auth_result.user != null) {
  316. current_user = auth_result.user;
  317. this["user-info"].inner_html = @"Logged in as $(auth_result.user.username) | <a href=\"/logout\">Logout</a>";
  318. } else {
  319. this["user-info"].inner_html = "<a href=\"/login\">Login</a> | <a href=\"/register\">Register</a>";
  320. }
  321. }
  322. }
  323. // =============================================================================
  324. // PAGE COMPONENTS - Public pages
  325. // =============================================================================
  326. /**
  327. * HomePage - Public landing page
  328. *
  329. * This page is accessible to everyone, authenticated or not.
  330. * It provides an overview of the Authentication system features.
  331. */
  332. public class HomePage : PageComponent {
  333. private SessionService _session_service = inject<SessionService>();
  334. private UserService _user_service = inject<UserService>();
  335. private HttpContext _http_context = inject<HttpContext>();
  336. public User? current_user { get; private set; }
  337. public const string ROUTE = "/";
  338. public override string markup { get {
  339. return """
  340. <div class="card">
  341. <h1>Welcome to the Spry Authentication Example</h1>
  342. <p>This example demonstrates the complete Spry Authentication system including:</p>
  343. <h2>Features</h2>
  344. <ul>
  345. <li><strong>User Registration</strong> - Create new accounts with username/email/password</li>
  346. <li><strong>Authentication</strong> - Login with session management</li>
  347. <li><strong>Permissions</strong> - Granular permission system with wildcard support</li>
  348. <li><strong>Protected Pages</strong> - Pages that require authentication</li>
  349. <li><strong>User Management</strong> - Admin interface for managing users</li>
  350. <li><strong>Authorisation Context</strong> - Request-scoped permission checking</li>
  351. </ul>
  352. <h2>Try It Out</h2>
  353. <p sid="status-message"></p>
  354. <ul>
  355. <li><a href="/register">Register</a> - Create a new account</li>
  356. <li><a href="/login">Login</a> - Sign in to your account</li>
  357. <li><a href="/dashboard">Dashboard</a> - Protected page (requires login)</li>
  358. <li><a href="/admin/users">User Admin</a> - Admin page (requires permission)</li>
  359. </ul>
  360. </div>
  361. """;
  362. }}
  363. public override async void prepare() throws Error {
  364. // Check if user is authenticated
  365. var auth_result = yield _session_service.authenticate_request_async(_http_context, _user_service);
  366. if (auth_result.is_authenticated && auth_result.user != null) {
  367. current_user = auth_result.user;
  368. this["status-message"].text_content = @"You are logged in as <strong>$(auth_result.user.username)</strong>.";
  369. } else {
  370. this["status-message"].text_content = "You are not logged in. Register or login to access protected pages.";
  371. }
  372. }
  373. }
  374. /**
  375. * RegisterPage - User registration page
  376. *
  377. * Allows new users to create an account with username, email, and password.
  378. * Demonstrates direct use of UserService.create_user_async().
  379. */
  380. public class RegisterPage : PageComponent {
  381. private UserService _user_service = inject<UserService>();
  382. private PermissionService _permission_service = inject<PermissionService>();
  383. private HttpContext _http_context = inject<HttpContext>();
  384. public string? error_message { get; private set; default = null; }
  385. public string? success_message { get; private set; default = null; }
  386. public string preserved_username { get; private set; default = ""; }
  387. public string preserved_email { get; private set; default = ""; }
  388. public const string ROUTE = "/register";
  389. public override string markup { get {
  390. return """
  391. <div class="card" sid="register-card" hx-swap="outerHTML">
  392. <h1>Create Account</h1>
  393. <div spry-if="this.success_message != null" class="alert alert-success" sid="success-alert">
  394. <span content-expr="this.success_message"></span>
  395. </div>
  396. <form sid="register-form" spry-action=":Register" spry-target="register-card">
  397. <div class="form-group">
  398. <label for="username">Username</label>
  399. <input type="text" name="username" sid="username-input" required
  400. autocomplete="username" placeholder="Choose a username"/>
  401. </div>
  402. <div class="form-group">
  403. <label for="email">Email</label>
  404. <input type="email" name="email" sid="email-input" required
  405. autocomplete="email" placeholder="Enter your email"/>
  406. </div>
  407. <div class="form-group">
  408. <label for="password">Password</label>
  409. <input type="password" name="password" sid="password-input" required
  410. autocomplete="new-password" placeholder="Choose a password"/>
  411. </div>
  412. <div class="form-group">
  413. <label for="confirm-password">Confirm Password</label>
  414. <input type="password" name="confirm_password" sid="confirm-password-input" required
  415. autocomplete="new-password" placeholder="Confirm your password"/>
  416. </div>
  417. <div spry-if="this.error_message != null" class="error-message" sid="error-container">
  418. <span content-expr="this.error_message"></span>
  419. </div>
  420. <button type="submit">Create Account</button>
  421. </form>
  422. <p style="margin-top: 1.5rem;">
  423. Already have an account? <a href="/login">Login here</a>
  424. </p>
  425. </div>
  426. """;
  427. }}
  428. public override async void prepare() throws Error {
  429. // Preserve form values after failed submission
  430. if (preserved_username.length > 0) {
  431. this["username-input"].set_attribute("value", preserved_username);
  432. }
  433. if (preserved_email.length > 0) {
  434. this["email-input"].set_attribute("value", preserved_email);
  435. }
  436. }
  437. public async override void handle_action(string action) throws Error {
  438. if (action == "Register") {
  439. yield handle_register_async();
  440. }
  441. }
  442. private async void handle_register_async() throws Error {
  443. var query = _http_context.request.query_params;
  444. // Get form values
  445. var username = (query.get_any_or_default("username") ?? "").strip();
  446. var email = (query.get_any_or_default("email") ?? "").strip();
  447. var password = query.get_any_or_default("password") ?? "";
  448. var confirm_password = query.get_any_or_default("confirm_password") ?? "";
  449. // Preserve values for re-display
  450. preserved_username = username;
  451. preserved_email = email;
  452. // Validate inputs
  453. if (username.length < 3) {
  454. error_message = "Username must be at least 3 characters";
  455. return;
  456. }
  457. if (!email.contains("@") || !email.contains(".")) {
  458. error_message = "Please enter a valid email address";
  459. return;
  460. }
  461. if (password.length < 6) {
  462. error_message = "Password must be at least 6 characters";
  463. return;
  464. }
  465. if (password != confirm_password) {
  466. error_message = "Passwords do not match";
  467. return;
  468. }
  469. // Attempt to create user
  470. try {
  471. var user = yield _user_service.create_user_async(username, email, password);
  472. // Grant basic permissions to new users
  473. yield _permission_service.set_permission_async(user, PermissionService.USER_READ);
  474. success_message = @"Account created successfully! You can now <a href=\"/login\">login</a>.";
  475. error_message = null;
  476. // Clear preserved values on success
  477. preserved_username = "";
  478. preserved_email = "";
  479. } catch (UserError.DUPLICATE_USERNAME e) {
  480. error_message = "Username already exists. Please choose another.";
  481. } catch (UserError.DUPLICATE_EMAIL e) {
  482. error_message = "Email already registered. Please use another or login.";
  483. } catch (Error e) {
  484. error_message = "Registration failed: %s".printf(e.message);
  485. }
  486. }
  487. }
  488. // =============================================================================
  489. // PAGE COMPONENTS - Authentication pages
  490. // =============================================================================
  491. /**
  492. * LoginPage - Login page using the built-in LoginFormComponent
  493. *
  494. * This page demonstrates how to use the LoginFormComponent for authentication.
  495. * The component handles:
  496. * - Form display and validation
  497. * - Authentication via UserService
  498. * - Session creation via SessionService
  499. * - Cookie management
  500. * - Redirect after successful login
  501. */
  502. public class LoginPage : PageComponent {
  503. private ComponentFactory _factory = inject<ComponentFactory>();
  504. private LoginFormComponent _login_form;
  505. public const string ROUTE = "/login";
  506. public override string markup { get {
  507. return """
  508. <div class="card">
  509. <h1>Login</h1>
  510. <spry-outlet sid="login-form-outlet"/>
  511. <p style="margin-top: 1.5rem;">
  512. Don't have an account? <a href="/register">Register here</a>
  513. </p>
  514. </div>
  515. """;
  516. }}
  517. public override async void prepare() throws Error {
  518. // Create and configure the login form component
  519. _login_form = _factory.create<LoginFormComponent>();
  520. _login_form.redirect_url = "/dashboard"; // Redirect here after successful login
  521. // Add the form to our outlet
  522. add_outlet_child("login-form-outlet", _login_form);
  523. // Share globals with the login form (required for action handling)
  524. add_globals_from(_login_form);
  525. }
  526. }
  527. /**
  528. * LogoutEndpoint - Handles logout by clearing the session
  529. *
  530. * This demonstrates:
  531. * - Getting the current session from the cookie
  532. * - Deleting the session from storage
  533. * - Clearing the session cookie
  534. * - Returning a redirect response
  535. */
  536. public class LogoutEndpoint : Object, Endpoint {
  537. private SessionService _session_service = inject<SessionService>();
  538. private UserService _user_service = inject<UserService>();
  539. public async HttpResult handle_request(HttpContext http_context, RouteContext route_context) throws Error {
  540. // Try to get the current session
  541. var auth_result = yield _session_service.authenticate_request_async(http_context, _user_service);
  542. if (auth_result.is_authenticated && auth_result.session != null) {
  543. // Delete the session from storage
  544. yield _session_service.delete_session_async(auth_result.session.id);
  545. }
  546. // Create redirect result with Location header
  547. // Note: We use 302 (FOUND) redirect - StatusCode enum may not have FOUND, so we use the numeric value
  548. var result = new HttpStringResult("Redirecting to home page...", 302);
  549. result.set_header("Location", "/");
  550. // Clear the session cookie
  551. _session_service.clear_session_cookie(result);
  552. return result;
  553. }
  554. }
  555. // =============================================================================
  556. // PAGE COMPONENTS - Protected pages
  557. // =============================================================================
  558. /**
  559. * DashboardPage - Protected dashboard page
  560. *
  561. * This page demonstrates:
  562. * - Checking authentication in prepare()
  563. * - Redirecting unauthenticated users to login
  564. * - Accessing the current user's information
  565. * - Checking permissions
  566. * - Displaying user-specific content
  567. * - Using AuthorisationContext for permission checking
  568. */
  569. public class DashboardPage : PageComponent {
  570. private SessionService _session_service = inject<SessionService>();
  571. private UserService _user_service = inject<UserService>();
  572. private PermissionService _permission_service = inject<PermissionService>();
  573. private HttpContext _http_context = inject<HttpContext>();
  574. private AuthorisationContext _auth_context = inject<AuthorisationContext>();
  575. public User? current_user { get; private set; }
  576. public bool is_admin { get; private set; default = false; }
  577. public Vector<string> permissions { get; private set; }
  578. public const string ROUTE = "/dashboard";
  579. public override string markup { get {
  580. return """
  581. <div class="card">
  582. <!-- Not authenticated message -->
  583. <div spry-if="!this.is_authenticated" class="alert alert-error">
  584. <h2>Authentication Required</h2>
  585. <p>You must be logged in to view this page.</p>
  586. <p style="margin-top: 1rem;">
  587. <a href="/login" class="btn">Login</a>
  588. <a href="/register" class="btn btn-secondary" style="margin-left: 0.5rem;">Register</a>
  589. </p>
  590. </div>
  591. <!-- Authenticated content -->
  592. <div spry-if="this.is_authenticated">
  593. <div class="welcome-section">
  594. <h1 sid="welcome-heading">Dashboard</h1>
  595. <p>Welcome to your dashboard! This page demonstrates protected content.</p>
  596. </div>
  597. <h2>Your Profile</h2>
  598. <ul>
  599. <li><strong>Username:</strong> <span sid="username"></span></li>
  600. <li><strong>Email:</strong> <span sid="email"></span></li>
  601. <li><strong>User ID:</strong> <span sid="user-id"></span></li>
  602. <li><strong>Account Created:</strong> <span sid="created-at"></span></li>
  603. </ul>
  604. <h2>Your Permissions</h2>
  605. <div class="permission-list" sid="permission-list">
  606. <!-- Permissions will be listed here -->
  607. </div>
  608. <div spry-if="this.is_admin" class="alert alert-success" style="margin-top: 1.5rem;">
  609. <strong>Admin Access:</strong> You have admin privileges.
  610. <a href="/admin/users" style="color: #155724;">Go to User Management</a>
  611. </div>
  612. <div spry-if="this.uses_auth_context" class="alert alert-success" style="margin-top: 1.5rem;">
  613. <strong>AuthorisationContext:</strong> Permission checking via the new Authorisation system is active.
  614. </div>
  615. <p style="margin-top: 1.5rem;">
  616. <a href="/" class="btn btn-secondary">Back to Home</a>
  617. </p>
  618. </div>
  619. </div>
  620. """;
  621. }}
  622. // Track if user is authenticated (for template binding)
  623. public bool is_authenticated { get; private set; default = false; }
  624. // Track if using AuthorisationContext for permission checking
  625. public bool uses_auth_context { get; private set; default = false; }
  626. public override async void prepare() throws Error {
  627. // Authenticate the request
  628. var auth_result = yield _session_service.authenticate_request_async(_http_context, _user_service);
  629. if (!auth_result.is_authenticated || auth_result.user == null) {
  630. // Not authenticated - show message and link to login
  631. // Note: PageComponent doesn't have redirect(), so we show a message instead
  632. is_authenticated = false;
  633. return;
  634. }
  635. is_authenticated = true;
  636. current_user = auth_result.user;
  637. permissions = _permission_service.get_permissions(current_user);
  638. is_admin = _permission_service.has_permission(current_user, PermissionService.ADMIN);
  639. // Demonstrate using AuthorisationContext for permission checking
  640. // This shows how the new Authorisation system can be used alongside
  641. // the traditional PermissionService approach
  642. if (_auth_context.is_authorised) {
  643. uses_auth_context = true;
  644. // The AuthorisationContext provides an alternative way to check permissions
  645. // is_admin = _auth_context.has_permission(PermissionService.ADMIN);
  646. }
  647. // Populate user info
  648. this["welcome-heading"].text_content = @"Welcome, $(current_user.username)!";
  649. this["username"].text_content = current_user.username;
  650. this["email"].text_content = current_user.email;
  651. this["user-id"].text_content = current_user.id;
  652. this["created-at"].text_content = current_user.created_at.format("%Y-%m-%d %H:%M:%S UTC");
  653. // Populate permissions - user.permissions returns string[] now
  654. var perm_text = "";
  655. var user_permissions = current_user.permissions;
  656. foreach (var perm in user_permissions) {
  657. var badge_class = perm == PermissionService.ADMIN ? "permission-badge admin" : "permission-badge";
  658. perm_text += @"<span class=\"$badge_class\">$perm</span> ";
  659. }
  660. if (user_permissions.length == 0) {
  661. perm_text = "<span class=\"permission-badge\">No permissions assigned</span>";
  662. }
  663. this["permission-list"].inner_html = perm_text;
  664. }
  665. }
  666. // =============================================================================
  667. // PAGE COMPONENTS - Admin pages
  668. // =============================================================================
  669. /**
  670. * UserAdminPage - Admin page for user management
  671. *
  672. * This page demonstrates how to use UserManagementComponent within your own
  673. * page layout. Unlike the old UserManagementPage (which was a PageComponent
  674. * that generated a full HTML document), UserManagementComponent is a regular
  675. * Component that can be placed anywhere.
  676. *
  677. * Applications now have full control over:
  678. * - The page layout and navigation
  679. * - CSS styling via their own stylesheets
  680. * - Where the user management component appears
  681. */
  682. public class UserAdminPage : PageComponent {
  683. private ComponentFactory _factory = inject<ComponentFactory>();
  684. private UserManagementComponent _user_management;
  685. public const string ROUTE = "/admin/users";
  686. public override string markup { get {
  687. return """
  688. <div class="card">
  689. <h1>User Administration</h1>
  690. <p>Manage user accounts, permissions, and access control.</p>
  691. <spry-outlet sid="user-management-outlet"/>
  692. </div>
  693. """;
  694. }}
  695. public override async void prepare() throws Error {
  696. // Create the user management component
  697. _user_management = _factory.create<UserManagementComponent>();
  698. // Pass the application-defined permissions to the component
  699. // This allows the component to display appropriate checkboxes
  700. _user_management.available_permissions = get_application_permissions();
  701. // Add it to our outlet
  702. add_outlet_child("user-management-outlet", _user_management);
  703. // Share globals with the component (required for action handling)
  704. add_globals_from(_user_management);
  705. }
  706. }
  707. // =============================================================================
  708. // SEED DATA - Creates initial admin user
  709. // =============================================================================
  710. /**
  711. * SeedData - Creates initial users for testing
  712. *
  713. * This demonstrates how to:
  714. * - Check if users exist before creating
  715. * - Create users programmatically
  716. * - Grant permissions to users
  717. */
  718. public class SeedData : Object {
  719. public static async void ensure_admin_exists(UserService user_service, PermissionService permission_service) throws Error {
  720. // Check if admin user exists
  721. var admin_user = yield user_service.get_user_by_username_async("admin");
  722. if (admin_user != null) {
  723. print("Admin user already exists\n");
  724. return;
  725. }
  726. print("Creating admin user...\n");
  727. // Create admin user
  728. admin_user = yield user_service.create_user_async("admin", "admin@example.com", "admin123");
  729. // Grant admin permissions
  730. yield permission_service.set_permission_async(admin_user, PermissionService.ADMIN);
  731. yield permission_service.set_permission_async(admin_user, PermissionService.USER_MANAGEMENT);
  732. yield permission_service.set_permission_async(admin_user, PermissionService.USER_CREATE);
  733. yield permission_service.set_permission_async(admin_user, PermissionService.USER_READ);
  734. yield permission_service.set_permission_async(admin_user, PermissionService.USER_UPDATE);
  735. yield permission_service.set_permission_async(admin_user, PermissionService.USER_DELETE);
  736. print("Admin user created with username 'admin' and password 'admin123'\n");
  737. // Create a regular test user
  738. var test_user = yield user_service.get_user_by_username_async("testuser");
  739. if (test_user == null) {
  740. print("Creating test user...\n");
  741. test_user = yield user_service.create_user_async("testuser", "test@example.com", "test123");
  742. yield permission_service.set_permission_async(test_user, PermissionService.USER_READ);
  743. print("Test user created with username 'testuser' and password 'test123'\n");
  744. }
  745. }
  746. }
  747. // =============================================================================
  748. // APPLICATION SETUP
  749. // =============================================================================
  750. // =============================================================================
  751. // ASYNC MAIN LOOP - Required for async initialization
  752. // =============================================================================
  753. private MainLoop main_loop;
  754. private Connection global_connection;
  755. public static int main(string[] args) {
  756. int port = args.length > 1 ? int.parse(args[1]) : 8080;
  757. print("═══════════════════════════════════════════════════════════════\n");
  758. print(" Spry Authentication Example - Complete Demo\n");
  759. print("═══════════════════════════════════════════════════════════════\n");
  760. print(" Port: %d\n", port);
  761. print("═══════════════════════════════════════════════════════════════\n");
  762. print(" Public Endpoints:\n");
  763. print(" / - Home page (public)\n");
  764. print(" /register - Create new account\n");
  765. print(" /login - Login page\n");
  766. print("═══════════════════════════════════════════════════════════════\n");
  767. print(" Protected Endpoints (requires login):\n");
  768. print(" /dashboard - User dashboard\n");
  769. print(" /logout - Logout and clear session\n");
  770. print("═══════════════════════════════════════════════════════════════\n");
  771. print(" Admin Endpoints (requires 'user-management' permission):\n");
  772. print(" /admin/users - User management page\n");
  773. print("═══════════════════════════════════════════════════════════════\n");
  774. print(" Default Users:\n");
  775. print(" admin / admin123 - Has all permissions\n");
  776. print(" testuser / test123 - Regular user\n");
  777. print("═══════════════════════════════════════════════════════════════\n");
  778. print("\nPress Ctrl+C to stop the server\n\n");
  779. main_loop = new MainLoop();
  780. try {
  781. // 1. Create the database connection FIRST
  782. print("Creating database connection...\n");
  783. initialize_database.begin((obj, res) => {
  784. try {
  785. initialize_database.end(res);
  786. // 2. Now start the application
  787. start_application.begin(port, (obj, res) => {
  788. try {
  789. start_application.end(res);
  790. } catch (Error e) {
  791. printerr("Application error: %s\n", e.message);
  792. main_loop.quit();
  793. }
  794. });
  795. } catch (Error e) {
  796. printerr("Database initialization error: %s\n", e.message);
  797. main_loop.quit();
  798. }
  799. });
  800. main_loop.run();
  801. return 0;
  802. } catch (Error e) {
  803. printerr("Error: %s\n", e.message);
  804. return 1;
  805. }
  806. }
  807. /**
  808. * Initialize database connection and create schema.
  809. */
  810. private async void initialize_database() throws Error {
  811. global_connection = yield create_database_connection();
  812. yield initialize_database_schema(global_connection);
  813. print("Database initialized successfully.\n");
  814. }
  815. /**
  816. * Start the web application after database is initialized.
  817. */
  818. private async void start_application(int port) throws Error {
  819. var application = new WebApplication(port);
  820. // Register the database Connection in the container FIRST before any services
  821. // This is critical - repositories use inject<Connection>() and need it to be available
  822. application.add_singleton<Connection>(() => global_connection);
  823. // Register repositories (new InvercargillSql-based implementations)
  824. // Register the concrete implementation and expose it via the interface
  825. application.container.register_singleton<SqlUserRepository>((scope) => new SqlUserRepository(scope.resolve<Connection>()))
  826. .as<UserRepository>();
  827. application.container.register_singleton<SqlSessionRepository>((scope) => new SqlSessionRepository(scope.resolve<Connection>()))
  828. .as<SessionRepository>();
  829. // Enable compression
  830. application.use_compression();
  831. // Add Spry module for component actions
  832. application.add_module<SpryModule>();
  833. // Register Authentication system services
  834. // These now use repositories instead of Engine directly
  835. application.add_singleton<UserService>();
  836. application.add_singleton<SessionService>();
  837. application.add_singleton<PermissionService>();
  838. // Register Authorisation system services
  839. application.add_singleton<AuthorisationTokenService>();
  840. application.add_singleton<AuthorisationContext>();
  841. // Seed initial data (admin user, test user)
  842. // Note: CryptographyProvider is already registered by SpryModule
  843. seed_initial_data.begin(application.container);
  844. // Register template with route prefix
  845. // MainLayoutTemplate applies to ALL routes (empty prefix)
  846. var spry_cfg = application.configure_with<SpryConfigurator>();
  847. spry_cfg.add_template<MainLayoutTemplate>("");
  848. // Register page components as endpoints
  849. application.add_transient<HomePage>();
  850. application.add_endpoint<HomePage>(new EndpointRoute(HomePage.ROUTE));
  851. application.add_transient<RegisterPage>();
  852. application.add_endpoint<RegisterPage>(new EndpointRoute(RegisterPage.ROUTE));
  853. application.add_transient<LoginPage>();
  854. application.add_endpoint<LoginPage>(new EndpointRoute(LoginPage.ROUTE));
  855. application.add_transient<DashboardPage>();
  856. application.add_endpoint<DashboardPage>(new EndpointRoute(DashboardPage.ROUTE));
  857. // Register logout endpoint
  858. application.add_endpoint<LogoutEndpoint>(new EndpointRoute("/logout"));
  859. // Register UserAdminPage (admin page wrapping UserManagementComponent)
  860. application.add_transient<UserAdminPage>();
  861. application.add_endpoint<UserAdminPage>(new EndpointRoute(UserAdminPage.ROUTE));
  862. // Register LoginFormComponent (used by LoginPage)
  863. application.add_transient<LoginFormComponent>();
  864. // Register new user management components
  865. application.add_transient<UserManagementComponent>();
  866. application.add_transient<UserDetailsComponent>();
  867. application.add_transient<NewUserComponent>();
  868. // Register CSS as FastResource
  869. application.add_startup_endpoint<FastResource>(new EndpointRoute("/styles/main.css"), () => {
  870. try {
  871. return new FastResource.from_string(MAIN_CSS)
  872. .with_content_type("text/css; charset=utf-8")
  873. .with_default_compressors();
  874. } catch (Error e) {
  875. error("Failed to create main CSS resource: %s", e.message);
  876. }
  877. });
  878. print("Starting web server on port %d...\n\n", port);
  879. application.run();
  880. }
  881. /**
  882. * Seed initial data (admin and test users).
  883. */
  884. private async void seed_initial_data(Container container) {
  885. try {
  886. var scope = container.create_scope();
  887. var user_service = scope.resolve<UserService>();
  888. var permission_service = scope.resolve<PermissionService>();
  889. yield SeedData.ensure_admin_exists(user_service, permission_service);
  890. } catch (Error e) {
  891. printerr("Warning: Failed to seed initial data: %s\n", e.message);
  892. }
  893. }