UsersExample.vala 34 KB

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