web-mojo 2.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +198 -0
  2. package/README.md +510 -0
  3. package/dist/admin.cjs.js +2 -0
  4. package/dist/admin.cjs.js.map +1 -0
  5. package/dist/admin.css +621 -0
  6. package/dist/admin.es.js +7973 -0
  7. package/dist/admin.es.js.map +1 -0
  8. package/dist/auth.cjs.js +2 -0
  9. package/dist/auth.cjs.js.map +1 -0
  10. package/dist/auth.css +804 -0
  11. package/dist/auth.es.js +2168 -0
  12. package/dist/auth.es.js.map +1 -0
  13. package/dist/charts.cjs.js +2 -0
  14. package/dist/charts.cjs.js.map +1 -0
  15. package/dist/charts.css +1002 -0
  16. package/dist/charts.es.js +16 -0
  17. package/dist/charts.es.js.map +1 -0
  18. package/dist/chunks/ContextMenu-BrHqj0fn.js +80 -0
  19. package/dist/chunks/ContextMenu-BrHqj0fn.js.map +1 -0
  20. package/dist/chunks/ContextMenu-gEcpSz56.js +2 -0
  21. package/dist/chunks/ContextMenu-gEcpSz56.js.map +1 -0
  22. package/dist/chunks/DataView-DPryYpEW.js +2 -0
  23. package/dist/chunks/DataView-DPryYpEW.js.map +1 -0
  24. package/dist/chunks/DataView-DjZQrpba.js +843 -0
  25. package/dist/chunks/DataView-DjZQrpba.js.map +1 -0
  26. package/dist/chunks/Dialog-BsRx4eg3.js +2 -0
  27. package/dist/chunks/Dialog-BsRx4eg3.js.map +1 -0
  28. package/dist/chunks/Dialog-DSlctbon.js +1377 -0
  29. package/dist/chunks/Dialog-DSlctbon.js.map +1 -0
  30. package/dist/chunks/FilePreviewView-BmFHzK5K.js +5868 -0
  31. package/dist/chunks/FilePreviewView-BmFHzK5K.js.map +1 -0
  32. package/dist/chunks/FilePreviewView-DcdRl_ta.js +2 -0
  33. package/dist/chunks/FilePreviewView-DcdRl_ta.js.map +1 -0
  34. package/dist/chunks/FormView-CmBuwKGD.js +2 -0
  35. package/dist/chunks/FormView-CmBuwKGD.js.map +1 -0
  36. package/dist/chunks/FormView-DqUBMPJ9.js +5054 -0
  37. package/dist/chunks/FormView-DqUBMPJ9.js.map +1 -0
  38. package/dist/chunks/MetricsChart-CM4CI6eA.js +2095 -0
  39. package/dist/chunks/MetricsChart-CM4CI6eA.js.map +1 -0
  40. package/dist/chunks/MetricsChart-CPidSMaN.js +2 -0
  41. package/dist/chunks/MetricsChart-CPidSMaN.js.map +1 -0
  42. package/dist/chunks/PDFViewer-BNQlnS83.js +2 -0
  43. package/dist/chunks/PDFViewer-BNQlnS83.js.map +1 -0
  44. package/dist/chunks/PDFViewer-Dyo-Oeyd.js +946 -0
  45. package/dist/chunks/PDFViewer-Dyo-Oeyd.js.map +1 -0
  46. package/dist/chunks/Page-B524zSQs.js +351 -0
  47. package/dist/chunks/Page-B524zSQs.js.map +1 -0
  48. package/dist/chunks/Page-BFgj0pAA.js +2 -0
  49. package/dist/chunks/Page-BFgj0pAA.js.map +1 -0
  50. package/dist/chunks/TokenManager-BXNva8Jk.js +287 -0
  51. package/dist/chunks/TokenManager-BXNva8Jk.js.map +1 -0
  52. package/dist/chunks/TokenManager-Bzn4guFm.js +2 -0
  53. package/dist/chunks/TokenManager-Bzn4guFm.js.map +1 -0
  54. package/dist/chunks/TopNav-D3I3_25f.js +371 -0
  55. package/dist/chunks/TopNav-D3I3_25f.js.map +1 -0
  56. package/dist/chunks/TopNav-MDjL4kV0.js +2 -0
  57. package/dist/chunks/TopNav-MDjL4kV0.js.map +1 -0
  58. package/dist/chunks/User-BalfYTEF.js +3 -0
  59. package/dist/chunks/User-BalfYTEF.js.map +1 -0
  60. package/dist/chunks/User-DwIT-CTQ.js +1937 -0
  61. package/dist/chunks/User-DwIT-CTQ.js.map +1 -0
  62. package/dist/chunks/WebApp-B6mgbNn2.js +4767 -0
  63. package/dist/chunks/WebApp-B6mgbNn2.js.map +1 -0
  64. package/dist/chunks/WebApp-DqDowtkl.js +2 -0
  65. package/dist/chunks/WebApp-DqDowtkl.js.map +1 -0
  66. package/dist/chunks/WebSocketClient-D6i85jl2.js +2 -0
  67. package/dist/chunks/WebSocketClient-D6i85jl2.js.map +1 -0
  68. package/dist/chunks/WebSocketClient-Dvl3AYx1.js +297 -0
  69. package/dist/chunks/WebSocketClient-Dvl3AYx1.js.map +1 -0
  70. package/dist/core.css +1181 -0
  71. package/dist/css/web-mojo.css +17 -0
  72. package/dist/css-manifest.json +6 -0
  73. package/dist/docit.cjs.js +2 -0
  74. package/dist/docit.cjs.js.map +1 -0
  75. package/dist/docit.es.js +959 -0
  76. package/dist/docit.es.js.map +1 -0
  77. package/dist/index.cjs.js +2 -0
  78. package/dist/index.cjs.js.map +1 -0
  79. package/dist/index.es.js +2681 -0
  80. package/dist/index.es.js.map +1 -0
  81. package/dist/lightbox.cjs.js +2 -0
  82. package/dist/lightbox.cjs.js.map +1 -0
  83. package/dist/lightbox.css +606 -0
  84. package/dist/lightbox.es.js +3737 -0
  85. package/dist/lightbox.es.js.map +1 -0
  86. package/dist/loader.es.js +115 -0
  87. package/dist/loader.umd.js +85 -0
  88. package/dist/portal.css +2446 -0
  89. package/dist/table.css +639 -0
  90. package/dist/toast.css +181 -0
  91. package/package.json +179 -0
@@ -0,0 +1,2168 @@
1
+ import { W as WebApp } from "./chunks/WebApp-B6mgbNn2.js";
2
+ import { B, b, a, c, e, f } from "./chunks/WebApp-B6mgbNn2.js";
3
+ import { T as TokenManager } from "./chunks/TokenManager-BXNva8Jk.js";
4
+ import { P as Page } from "./chunks/Page-B524zSQs.js";
5
+ class AuthManager {
6
+ constructor(app, config = {}) {
7
+ this.app = app;
8
+ this.config = {
9
+ autoRefresh: true,
10
+ refreshThreshold: 5,
11
+ // minutes before expiry
12
+ plugins: {},
13
+ ...config
14
+ };
15
+ this.tokenManager = new TokenManager();
16
+ this.isAuthenticated = false;
17
+ this.user = null;
18
+ this.refreshTimer = null;
19
+ this.plugins = /* @__PURE__ */ new Map();
20
+ this.initialize();
21
+ }
22
+ /**
23
+ * Initialize auth manager
24
+ */
25
+ initialize() {
26
+ this.checkAuthState();
27
+ if (this.config.autoRefresh) {
28
+ this.scheduleTokenRefresh();
29
+ }
30
+ if (this.app) {
31
+ this.app.auth = this;
32
+ }
33
+ }
34
+ /**
35
+ * Check current authentication state from stored tokens
36
+ */
37
+ checkAuthState() {
38
+ if (this.tokenManager.isValid()) {
39
+ const userInfo = this.tokenManager.getUserInfo();
40
+ if (userInfo) {
41
+ this.setAuthState(userInfo);
42
+ return true;
43
+ }
44
+ }
45
+ this.clearAuthState();
46
+ return false;
47
+ }
48
+ /**
49
+ * Login with username/email and password
50
+ * @param {string} username - Username or email
51
+ * @param {string} password - Password
52
+ * @param {boolean} rememberMe - Persist session
53
+ * @returns {Promise<object>} Login result
54
+ */
55
+ async login(username, password, rememberMe = true) {
56
+ const response = await this.app.rest.POST("/api/login", { username, password });
57
+ if (response.success && response.data.status) {
58
+ const { access_token, refresh_token, user } = response.data.data;
59
+ this.tokenManager.setTokens(access_token, refresh_token, rememberMe);
60
+ const userInfo = this.tokenManager.getUserInfo();
61
+ this.setAuthState({ ...user, ...userInfo });
62
+ if (this.config.autoRefresh) {
63
+ this.scheduleTokenRefresh();
64
+ }
65
+ this.emit("login", this.user);
66
+ return { success: true, user: this.user };
67
+ }
68
+ const message = response.data?.error || response.message || "Login failed. Please try again.";
69
+ this.emit("loginError", { message });
70
+ return { success: false, message };
71
+ }
72
+ /**
73
+ * Register new user
74
+ * @param {object} userData - Registration data
75
+ * @returns {Promise<object>} Registration result
76
+ */
77
+ async register(userData) {
78
+ const response = await this.app.rest.POST("/api/register", userData);
79
+ if (response.success && response.data.status) {
80
+ const { token, refreshToken, user } = response.data.data;
81
+ this.tokenManager.setTokens(token, refreshToken, true);
82
+ const userInfo = this.tokenManager.getUserInfo();
83
+ this.setAuthState({ ...user, ...userInfo });
84
+ if (this.config.autoRefresh) {
85
+ this.scheduleTokenRefresh();
86
+ }
87
+ this.emit("register", this.user);
88
+ return { success: true, user: this.user };
89
+ }
90
+ const message = response.data?.error || response.message || "Registration failed.";
91
+ this.emit("registerError", { message });
92
+ return { success: false, message };
93
+ }
94
+ /**
95
+ * Logout current user
96
+ */
97
+ async logout() {
98
+ try {
99
+ const token = this.tokenManager.getToken();
100
+ if (token) {
101
+ this.app.rest.POST("/api/auth/logout").catch((err) => {
102
+ console.warn("Server logout failed, proceeding with local logout.", err);
103
+ });
104
+ }
105
+ } finally {
106
+ this.clearAuthState();
107
+ this.emit("logout");
108
+ }
109
+ }
110
+ /**
111
+ * Refresh access token
112
+ * @returns {Promise<boolean>} Success status
113
+ */
114
+ async refreshToken() {
115
+ const refreshToken = this.tokenManager.getRefreshToken();
116
+ if (!refreshToken) {
117
+ this.clearAuthState();
118
+ this.emit("tokenExpired");
119
+ return false;
120
+ }
121
+ const response = await this.app.rest.POST("/api/auth/token/refresh", { refreshToken });
122
+ if (response.success && response.data.status) {
123
+ const { token, refreshToken: newRefreshToken } = response.data.data;
124
+ const isPersistent = !!localStorage.getItem(this.tokenManager.tokenKey);
125
+ this.tokenManager.setTokens(token, newRefreshToken, isPersistent);
126
+ const userInfo = this.tokenManager.getUserInfo();
127
+ if (userInfo) {
128
+ this.user = { ...this.user, ...userInfo };
129
+ }
130
+ this.scheduleTokenRefresh();
131
+ this.emit("tokenRefreshed");
132
+ return true;
133
+ }
134
+ console.error("Token refresh failed:", response.data?.error || response.message);
135
+ this.clearAuthState();
136
+ this.emit("tokenExpired");
137
+ return false;
138
+ }
139
+ /**
140
+ * Set authentication state
141
+ * @param {object} user - User data
142
+ */
143
+ setAuthState(user) {
144
+ this.isAuthenticated = true;
145
+ this.user = user;
146
+ if (this.app?.setState) {
147
+ this.app.setState("auth", {
148
+ isAuthenticated: true,
149
+ user
150
+ });
151
+ }
152
+ }
153
+ /**
154
+ * Clear authentication state
155
+ */
156
+ clearAuthState() {
157
+ this.isAuthenticated = false;
158
+ this.user = null;
159
+ this.tokenManager.clearTokens();
160
+ if (this.refreshTimer) {
161
+ clearTimeout(this.refreshTimer);
162
+ this.refreshTimer = null;
163
+ }
164
+ if (this.app?.setState) {
165
+ this.app.setState("auth", {
166
+ isAuthenticated: false,
167
+ user: null
168
+ });
169
+ }
170
+ }
171
+ /**
172
+ * Schedule automatic token refresh
173
+ */
174
+ scheduleTokenRefresh() {
175
+ if (this.refreshTimer) {
176
+ clearTimeout(this.refreshTimer);
177
+ }
178
+ if (!this.tokenManager.isValid()) {
179
+ return;
180
+ }
181
+ if (this.tokenManager.isExpiringSoon(this.config.refreshThreshold)) {
182
+ this.refreshToken();
183
+ return;
184
+ }
185
+ const token = this.tokenManager.getToken();
186
+ const payload = this.tokenManager.decode(token);
187
+ if (payload?.exp) {
188
+ const now = Math.floor(Date.now() / 1e3);
189
+ const timeUntilRefresh = (payload.exp - now - this.config.refreshThreshold * 60) * 1e3;
190
+ if (timeUntilRefresh > 0) {
191
+ this.refreshTimer = setTimeout(() => {
192
+ this.refreshToken();
193
+ }, timeUntilRefresh);
194
+ }
195
+ }
196
+ }
197
+ /**
198
+ * Register a plugin
199
+ * @param {string} name - Plugin name
200
+ * @param {object} plugin - Plugin instance
201
+ */
202
+ registerPlugin(name, plugin) {
203
+ this.plugins.set(name, plugin);
204
+ plugin.initialize(this, this.app);
205
+ }
206
+ /**
207
+ * Get a plugin by name
208
+ * @param {string} name - Plugin name
209
+ * @returns {object|null} Plugin instance
210
+ */
211
+ getPlugin(name) {
212
+ return this.plugins.get(name) || null;
213
+ }
214
+ /**
215
+ * Request password reset
216
+ * @param {string} email - User email
217
+ * @returns {Promise<object>} Request result
218
+ */
219
+ async forgotPassword(email, method = "code") {
220
+ const response = await this.app.rest.POST("/api/auth/forgot", { email, method });
221
+ if (response.success && response.data.status) {
222
+ this.emit("forgotPasswordSuccess", { email, method });
223
+ return { success: true, message: response.data.data?.message };
224
+ }
225
+ const message = response.data?.error || response.message || "Failed to process request.";
226
+ this.emit("forgotPasswordError", { message });
227
+ return { success: false, message };
228
+ }
229
+ /**
230
+ * Reset password with a token from an email link
231
+ * @param {string} token - Reset token
232
+ * @param {string} newPassword - New password
233
+ * @returns {Promise<object>} Reset result
234
+ */
235
+ async resetPasswordWithToken(token, newPassword) {
236
+ const payload = {
237
+ token,
238
+ new_password: newPassword
239
+ };
240
+ const response = await this.app.rest.POST("/api/auth/password/reset/token", payload);
241
+ if (response.success && response.data.status) {
242
+ this.emit("resetPasswordSuccess");
243
+ return { success: true, message: response.data.data?.message };
244
+ }
245
+ const message = response.data?.error || response.message || "Failed to reset password.";
246
+ this.emit("resetPasswordError", { message });
247
+ return { success: false, message };
248
+ }
249
+ /**
250
+ * Reset password with an email and code
251
+ * @param {string} email - User's email
252
+ * @param {string} code - The verification code
253
+ * @param {string} newPassword - New password
254
+ * @returns {Promise<object>} Reset result
255
+ */
256
+ async resetPasswordWithCode(email, code, newPassword) {
257
+ const payload = {
258
+ email,
259
+ code,
260
+ new_password: newPassword
261
+ };
262
+ const response = await this.app.rest.POST("/api/auth/password/reset/code", payload);
263
+ if (response.success && response.data.status) {
264
+ this.emit("resetPasswordSuccess");
265
+ return { success: true, message: response.data.data?.message };
266
+ }
267
+ const message = response.data?.error || response.message || "Failed to reset password.";
268
+ this.emit("resetPasswordError", { message });
269
+ return { success: false, message };
270
+ }
271
+ /**
272
+ * Get authorization header for API requests
273
+ * @returns {string|null} Authorization header
274
+ */
275
+ getAuthHeader() {
276
+ return this.tokenManager.getAuthHeader();
277
+ }
278
+ /**
279
+ * Emit event to app
280
+ * @param {string} event - Event name
281
+ * @param {*} data - Event data
282
+ */
283
+ emit(event, data) {
284
+ if (this.app?.events?.emit) {
285
+ this.app.events.emit(`auth:${event}`, data);
286
+ }
287
+ }
288
+ /**
289
+ * Cleanup auth manager
290
+ */
291
+ destroy() {
292
+ if (this.refreshTimer) {
293
+ clearTimeout(this.refreshTimer);
294
+ }
295
+ this.plugins.forEach((plugin) => {
296
+ if (plugin.destroy) {
297
+ plugin.destroy();
298
+ }
299
+ });
300
+ this.plugins.clear();
301
+ }
302
+ }
303
+ class LoginPage extends Page {
304
+ static pageName = "login";
305
+ static title = "Login";
306
+ static icon = "bi-box-arrow-in-right";
307
+ static route = "/login";
308
+ constructor(options = {}) {
309
+ super({
310
+ ...options,
311
+ pageName: LoginPage.pageName,
312
+ route: options.route || LoginPage.route,
313
+ pageIcon: LoginPage.icon,
314
+ template: options.template
315
+ });
316
+ this.authConfig = options.authConfig || {
317
+ ui: {
318
+ title: "My App",
319
+ logoUrl: "/assets/logo.png",
320
+ messages: {
321
+ loginTitle: "Welcome Back",
322
+ loginSubtitle: "Sign in to your account"
323
+ }
324
+ },
325
+ features: {
326
+ rememberMe: true,
327
+ forgotPassword: true,
328
+ registration: true
329
+ }
330
+ };
331
+ }
332
+ async onInit() {
333
+ await super.onInit();
334
+ this.data = {
335
+ // Form fields
336
+ username: "",
337
+ password: "",
338
+ rememberMe: true,
339
+ loginIcon: this.options.pageIcon,
340
+ // UI state
341
+ isLoading: false,
342
+ error: null,
343
+ showPassword: false,
344
+ // Feature availability
345
+ passkeySupported: this.getApp().auth?.isPasskeySupported?.() || false,
346
+ // Config data for template
347
+ ...this.authConfig.ui,
348
+ ...this.authConfig.features
349
+ };
350
+ }
351
+ async onEnter() {
352
+ await super.onEnter();
353
+ document.title = `${LoginPage.title} - ${this.authConfig.ui.title}`;
354
+ const auth = this.getApp().auth;
355
+ if (auth?.isAuthenticated) {
356
+ this.getApp().navigate("/");
357
+ return;
358
+ }
359
+ this.updateData({
360
+ username: "",
361
+ password: "",
362
+ rememberMe: true,
363
+ error: null,
364
+ isLoading: false
365
+ });
366
+ }
367
+ async onAfterRender() {
368
+ await super.onAfterRender();
369
+ const usernameInput = this.element.querySelector("#loginUsername");
370
+ if (usernameInput) {
371
+ usernameInput.focus();
372
+ }
373
+ }
374
+ /**
375
+ * Handle field updates
376
+ */
377
+ async onActionUpdateField(event, element) {
378
+ const field = element.dataset.field;
379
+ const value = element.type === "checkbox" ? element.checked : element.value;
380
+ this.updateData({
381
+ [field]: value,
382
+ error: null
383
+ // Clear error on input change
384
+ });
385
+ }
386
+ /**
387
+ * Toggle password visibility
388
+ */
389
+ async onActionTogglePassword(event) {
390
+ event.preventDefault();
391
+ this.updateData({ showPassword: !this.data.showPassword });
392
+ const passwordInput = this.element.querySelector("#loginPassword");
393
+ if (passwordInput) {
394
+ passwordInput.type = this.data.showPassword ? "text" : "password";
395
+ }
396
+ }
397
+ /**
398
+ * Handle login form submission
399
+ */
400
+ async onActionLogin(event) {
401
+ event.preventDefault();
402
+ this.data.username = this.element.querySelector("#loginUsername")?.value || "";
403
+ this.data.password = this.element.querySelector("#loginPassword")?.value || "";
404
+ await this.updateData({ error: null, isLoading: true }, true);
405
+ const auth = this.getApp().auth;
406
+ if (!auth) {
407
+ await this.updateData({ error: "Authentication system not available", isLoading: false }, true);
408
+ return;
409
+ }
410
+ if (!this.data.username || !this.data.password) {
411
+ await this.updateData({ error: "Please enter both username and password", isLoading: false }, true);
412
+ return;
413
+ }
414
+ const result = await auth.login(
415
+ this.data.username,
416
+ this.data.password,
417
+ this.data.rememberMe
418
+ );
419
+ if (!result.success) {
420
+ await this.updateData({
421
+ error: result.message,
422
+ isLoading: false
423
+ }, true);
424
+ const passwordInput = this.element.querySelector("#loginPassword");
425
+ if (passwordInput) {
426
+ passwordInput.focus();
427
+ passwordInput.select();
428
+ }
429
+ }
430
+ }
431
+ /**
432
+ * Handle passkey login
433
+ */
434
+ async onActionLoginWithPasskey(event) {
435
+ event.preventDefault();
436
+ const auth = this.getApp().auth;
437
+ if (!auth?.isPasskeySupported?.()) {
438
+ this.getApp().showError("Passkey authentication is not supported");
439
+ return;
440
+ }
441
+ this.updateData({ error: null, isLoading: true });
442
+ try {
443
+ const result = await auth.loginWithPasskey();
444
+ if (result.success) {
445
+ console.log("Passkey login successful");
446
+ }
447
+ } catch (error) {
448
+ console.error("Passkey login error:", error);
449
+ this.updateData({
450
+ error: "Passkey authentication failed. Please try another method.",
451
+ isLoading: false
452
+ });
453
+ }
454
+ }
455
+ /**
456
+ * Navigate to registration page
457
+ */
458
+ async onActionRegister(event) {
459
+ event.preventDefault();
460
+ this.getApp().navigate("/register");
461
+ }
462
+ /**
463
+ * Navigate to forgot password page
464
+ */
465
+ async onActionForgotPassword(event) {
466
+ event.preventDefault();
467
+ this.getApp().navigate("/forgot-password");
468
+ }
469
+ /**
470
+ * Handle Enter key in form fields
471
+ */
472
+ async onActionHandleKeyPress(event, element) {
473
+ if (event.key === "Enter") {
474
+ event.preventDefault();
475
+ if (element.id === "loginUsername") {
476
+ const passwordInput = this.element.querySelector("#loginPassword");
477
+ if (passwordInput) {
478
+ passwordInput.focus();
479
+ }
480
+ } else if (element.id === "loginPassword") {
481
+ await this.onActionLogin(event);
482
+ }
483
+ }
484
+ }
485
+ /**
486
+ * Get view data for template rendering
487
+ */
488
+ async getViewData() {
489
+ return {
490
+ ...this.data
491
+ };
492
+ }
493
+ }
494
+ class RegisterPage extends Page {
495
+ static pageName = "auth-register";
496
+ static title = "Register";
497
+ static icon = "bi-person-plus";
498
+ static route = "register";
499
+ constructor(options = {}) {
500
+ super({
501
+ ...options,
502
+ pageName: RegisterPage.pageName,
503
+ route: options.route || RegisterPage.route,
504
+ pageIcon: RegisterPage.icon,
505
+ template: options.template
506
+ });
507
+ this.authConfig = options.authConfig || {
508
+ ui: {
509
+ title: "My App",
510
+ logoUrl: "/assets/logo.png",
511
+ messages: {
512
+ registerTitle: "Create Account",
513
+ registerSubtitle: "Join us today"
514
+ }
515
+ },
516
+ features: {
517
+ registration: true
518
+ }
519
+ };
520
+ }
521
+ async onInit() {
522
+ await super.onInit();
523
+ this.data = {
524
+ // Config data for template
525
+ ...this.authConfig.ui,
526
+ ...this.authConfig.features,
527
+ // Form fields
528
+ name: "",
529
+ email: "",
530
+ password: "",
531
+ confirmPassword: "",
532
+ acceptTerms: false,
533
+ // UI state
534
+ isLoading: false,
535
+ error: null,
536
+ showPassword: false,
537
+ showConfirmPassword: false,
538
+ // Validation state
539
+ passwordStrength: null,
540
+ passwordMatch: true
541
+ };
542
+ }
543
+ async onEnter() {
544
+ await super.onEnter();
545
+ document.title = `${RegisterPage.title} - ${this.authConfig.ui.title}`;
546
+ const auth = this.getApp().auth;
547
+ if (auth?.isAuthenticated) {
548
+ this.getApp().navigate("/");
549
+ return;
550
+ }
551
+ this.updateData({
552
+ name: "",
553
+ email: "",
554
+ password: "",
555
+ confirmPassword: "",
556
+ acceptTerms: false,
557
+ error: null,
558
+ isLoading: false,
559
+ passwordStrength: null,
560
+ passwordMatch: true
561
+ });
562
+ }
563
+ async onAfterRender() {
564
+ await super.onAfterRender();
565
+ const nameInput = this.element.querySelector("#registerName");
566
+ if (nameInput) {
567
+ nameInput.focus();
568
+ }
569
+ }
570
+ /**
571
+ * Handle field updates
572
+ */
573
+ async onActionUpdateField(event, element) {
574
+ const field = element.dataset.field;
575
+ const value = element.type === "checkbox" ? element.checked : element.value;
576
+ this.updateData({ [field]: value });
577
+ if (field === "password") {
578
+ this.checkPasswordStrength(value);
579
+ }
580
+ if (field === "password" || field === "confirmPassword") {
581
+ this.checkPasswordMatch();
582
+ }
583
+ if (this.data.error) {
584
+ this.updateData({ error: null });
585
+ }
586
+ }
587
+ /**
588
+ * Check password strength
589
+ */
590
+ checkPasswordStrength(password) {
591
+ let strength = null;
592
+ if (password.length === 0) {
593
+ strength = null;
594
+ } else if (password.length < 6) {
595
+ strength = "weak";
596
+ } else if (password.length < 8) {
597
+ strength = "fair";
598
+ } else if (/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/.test(password)) {
599
+ strength = "strong";
600
+ } else if (/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(password)) {
601
+ strength = "good";
602
+ } else {
603
+ strength = "fair";
604
+ }
605
+ this.updateData({ passwordStrength: strength }, true);
606
+ }
607
+ /**
608
+ * Check if passwords match
609
+ */
610
+ checkPasswordMatch() {
611
+ const match = !this.data.confirmPassword || this.data.password === this.data.confirmPassword;
612
+ this.updateData({ passwordMatch: match }, true);
613
+ }
614
+ /**
615
+ * Toggle password visibility
616
+ */
617
+ async onActionTogglePassword(event, element) {
618
+ event.preventDefault();
619
+ const field = element.dataset.passwordField;
620
+ if (field === "password") {
621
+ this.updateData({ showPassword: !this.data.showPassword });
622
+ const input = this.element.querySelector("#registerPassword");
623
+ if (input) {
624
+ input.type = this.data.showPassword ? "text" : "password";
625
+ }
626
+ } else if (field === "confirmPassword") {
627
+ this.updateData({ showConfirmPassword: !this.data.showConfirmPassword });
628
+ const input = this.element.querySelector("#registerConfirmPassword");
629
+ if (input) {
630
+ input.type = this.data.showConfirmPassword ? "text" : "password";
631
+ }
632
+ }
633
+ }
634
+ /**
635
+ * Handle registration form submission
636
+ */
637
+ async onActionRegister(event) {
638
+ event.preventDefault();
639
+ await this.updateData({ error: null, isLoading: true }, true);
640
+ if (!this.data.name || !this.data.email || !this.data.password || !this.data.confirmPassword) {
641
+ await this.updateData({ error: "Please fill in all required fields", isLoading: false }, true);
642
+ return;
643
+ }
644
+ if (this.data.name.trim().length < 2) {
645
+ await this.updateData({ error: "Name must be at least 2 characters long", isLoading: false }, true);
646
+ return;
647
+ }
648
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
649
+ if (!emailRegex.test(this.data.email)) {
650
+ await this.updateData({ error: "Please enter a valid email address", isLoading: false }, true);
651
+ return;
652
+ }
653
+ if (this.data.password.length < 6) {
654
+ await this.updateData({ error: "Password must be at least 6 characters long", isLoading: false }, true);
655
+ return;
656
+ }
657
+ if (this.data.password !== this.data.confirmPassword) {
658
+ await this.updateData({ error: "Passwords do not match", isLoading: false }, true);
659
+ return;
660
+ }
661
+ const auth = this.getApp().auth;
662
+ if (!auth) {
663
+ await this.updateData({ error: "Authentication system not available", isLoading: false }, true);
664
+ return;
665
+ }
666
+ const registrationData = {
667
+ name: this.data.name.trim(),
668
+ email: this.data.email.toLowerCase().trim(),
669
+ password: this.data.password,
670
+ acceptedTerms: this.data.acceptTerms
671
+ };
672
+ const result = await auth.register(registrationData);
673
+ if (!result.success) {
674
+ await this.updateData({
675
+ error: result.message || "Registration failed. Please try again.",
676
+ isLoading: false
677
+ }, true);
678
+ }
679
+ }
680
+ /**
681
+ * Navigate to login page
682
+ */
683
+ async onActionLogin(event) {
684
+ event.preventDefault();
685
+ this.getApp().navigate("/login");
686
+ }
687
+ /**
688
+ * Handle Enter key in form fields
689
+ */
690
+ async onActionHandleKeyPress(event, element) {
691
+ if (event.key === "Enter") {
692
+ event.preventDefault();
693
+ const fieldOrder = ["registerName", "registerEmail", "registerPassword", "registerConfirmPassword"];
694
+ const currentIndex = fieldOrder.indexOf(element.id);
695
+ if (currentIndex >= 0 && currentIndex < fieldOrder.length - 1) {
696
+ const nextField = this.element.querySelector(`#${fieldOrder[currentIndex + 1]}`);
697
+ if (nextField) {
698
+ nextField.focus();
699
+ }
700
+ } else if (currentIndex === fieldOrder.length - 1) {
701
+ await this.onActionRegister(event);
702
+ }
703
+ }
704
+ }
705
+ /**
706
+ * Get view data for template rendering
707
+ */
708
+ async getViewData() {
709
+ return {
710
+ ...this.data
711
+ };
712
+ }
713
+ }
714
+ class ForgotPasswordPage extends Page {
715
+ static pageName = "auth-forgot-password";
716
+ static title = "Forgot Password";
717
+ static icon = "bi-key";
718
+ static route = "forgot-password";
719
+ constructor(options = {}) {
720
+ super({ ...options, template: options.template });
721
+ this.authConfig = options.authConfig || {
722
+ passwordResetMethod: "code",
723
+ ui: { title: "My App" },
724
+ features: {}
725
+ };
726
+ }
727
+ async onInit() {
728
+ this.data = {
729
+ ...this.authConfig.ui,
730
+ ...this.authConfig.features,
731
+ passwordResetMethod: this.authConfig.passwordResetMethod,
732
+ step: "email",
733
+ // 'email', 'code', 'link_sent', 'success'
734
+ isLoading: false,
735
+ error: null,
736
+ email: ""
737
+ // Store email across steps
738
+ };
739
+ }
740
+ async onEnter() {
741
+ document.title = `${ForgotPasswordPage.title} - ${this.authConfig.ui.title}`;
742
+ this.updateData({
743
+ step: "email",
744
+ isLoading: false,
745
+ error: null,
746
+ email: ""
747
+ });
748
+ }
749
+ /**
750
+ * Gets data from the currently visible form.
751
+ * @param {string} formSelector - The CSS selector for the form.
752
+ * @returns {object} An object containing the form data.
753
+ */
754
+ getFormData(formSelector) {
755
+ const form = this.element.querySelector(formSelector);
756
+ if (!form) return {};
757
+ const formData = new FormData(form);
758
+ return Object.fromEntries(formData.entries());
759
+ }
760
+ /**
761
+ * Handles the initial request to reset a password.
762
+ */
763
+ async onActionRequestReset() {
764
+ const { email } = this.getFormData("#form-request-reset");
765
+ await this.updateData({ isLoading: true, error: null, email }, true);
766
+ if (!email) {
767
+ return this.updateData({ error: "Please enter your email address", isLoading: false }, true);
768
+ }
769
+ const auth = this.getApp().auth;
770
+ const resetMethod = this.authConfig.passwordResetMethod || "code";
771
+ const response = await auth.forgotPassword(email, resetMethod);
772
+ if (resetMethod === "link") {
773
+ await this.updateData({ step: "link_sent", isLoading: false }, true);
774
+ if (!response.success) console.error("Forgot password (link) error:", response.message);
775
+ } else {
776
+ if (response.success) {
777
+ await this.updateData({ step: "code", isLoading: false }, true);
778
+ } else {
779
+ await this.updateData({ error: response.message, isLoading: false }, true);
780
+ }
781
+ }
782
+ }
783
+ /**
784
+ * Handles the final password reset using a verification code.
785
+ */
786
+ async onActionResetWithCode() {
787
+ const { code, new_password, confirm_password } = this.getFormData("#form-reset-with-code");
788
+ await this.updateData({ isLoading: true, error: null }, true);
789
+ if (!code || !new_password) {
790
+ return this.updateData({ error: "Please enter the code and your new password", isLoading: false }, true);
791
+ }
792
+ if (new_password !== confirm_password) {
793
+ return this.updateData({ error: "Passwords do not match", isLoading: false }, true);
794
+ }
795
+ const auth = this.getApp().auth;
796
+ const response = await auth.resetPasswordWithCode(this.data.email, code, new_password);
797
+ if (response.success) {
798
+ await this.updateData({ step: "success", isLoading: false }, true);
799
+ setTimeout(() => this.getApp().navigate("/login"), 3e3);
800
+ } else {
801
+ await this.updateData({ error: response.message, isLoading: false }, true);
802
+ }
803
+ }
804
+ async onActionBackToLogin() {
805
+ this.getApp().navigate("/login");
806
+ }
807
+ // --- Template Getters for State ---
808
+ get isStepEmail() {
809
+ return this.data.step === "email";
810
+ }
811
+ get isStepCode() {
812
+ return this.data.step === "code";
813
+ }
814
+ get isStepLinkSent() {
815
+ return this.data.step === "link_sent";
816
+ }
817
+ get isStepSuccess() {
818
+ return this.data.step === "success";
819
+ }
820
+ }
821
+ class ResetPasswordPage extends Page {
822
+ static pageName = "auth-reset-password";
823
+ static title = "Reset Password";
824
+ static icon = "bi-key-fill";
825
+ static route = "reset-password";
826
+ constructor(options = {}) {
827
+ super({
828
+ ...options,
829
+ pageName: ResetPasswordPage.pageName,
830
+ route: options.route || ResetPasswordPage.route,
831
+ pageIcon: ResetPasswordPage.icon,
832
+ template: "auth/pages/ResetPasswordPage.mst"
833
+ });
834
+ this.authConfig = options.authConfig || {
835
+ ui: {
836
+ title: "My App",
837
+ logoUrl: "/assets/logo.png",
838
+ messages: {
839
+ resetTitle: "Set New Password",
840
+ resetSubtitle: "Choose a strong password"
841
+ }
842
+ },
843
+ features: {
844
+ registration: true
845
+ }
846
+ };
847
+ this.resetToken = null;
848
+ }
849
+ async onInit() {
850
+ await super.onInit();
851
+ this.data = {
852
+ // Config data for template
853
+ ...this.authConfig.ui,
854
+ ...this.authConfig.features,
855
+ // Form fields
856
+ password: "",
857
+ confirmPassword: "",
858
+ resetToken: "",
859
+ // UI state
860
+ isLoading: false,
861
+ error: null,
862
+ success: false,
863
+ successMessage: null,
864
+ showPassword: false,
865
+ showConfirmPassword: false,
866
+ // Validation state
867
+ passwordStrength: null,
868
+ passwordMatch: true,
869
+ tokenValid: false
870
+ };
871
+ }
872
+ async onEnter() {
873
+ await super.onEnter();
874
+ document.title = `${ResetPasswordPage.title} - ${this.authConfig.ui.title}`;
875
+ const urlParams = new URLSearchParams(window.location.search);
876
+ this.resetToken = urlParams.get("token") || "";
877
+ if (!this.resetToken) {
878
+ this.updateData({
879
+ error: "Invalid or missing reset token. Please request a new password reset.",
880
+ tokenValid: false
881
+ });
882
+ return;
883
+ }
884
+ this.updateData({
885
+ resetToken: this.resetToken,
886
+ tokenValid: true,
887
+ error: null,
888
+ success: false
889
+ });
890
+ }
891
+ async onAfterRender() {
892
+ await super.onAfterRender();
893
+ if (this.data.tokenValid) {
894
+ const passwordInput = this.element.querySelector("#resetPassword");
895
+ if (passwordInput) {
896
+ passwordInput.focus();
897
+ }
898
+ }
899
+ }
900
+ /**
901
+ * Handle field updates
902
+ */
903
+ async onActionUpdateField(event, element) {
904
+ const field = element.dataset.field;
905
+ const value = element.value;
906
+ this.updateData({
907
+ [field]: value,
908
+ error: null
909
+ // Clear error on input change
910
+ });
911
+ if (field === "password") {
912
+ this.checkPasswordStrength(value);
913
+ }
914
+ if (field === "password" || field === "confirmPassword") {
915
+ this.checkPasswordMatch();
916
+ }
917
+ }
918
+ /**
919
+ * Check password strength
920
+ */
921
+ checkPasswordStrength(password) {
922
+ let strength = null;
923
+ if (password.length === 0) {
924
+ strength = null;
925
+ } else if (password.length < 6) {
926
+ strength = "weak";
927
+ } else if (password.length < 8) {
928
+ strength = "fair";
929
+ } else if (/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/.test(password)) {
930
+ strength = "strong";
931
+ } else if (/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(password)) {
932
+ strength = "good";
933
+ } else {
934
+ strength = "fair";
935
+ }
936
+ this.updateData({ passwordStrength: strength }, true);
937
+ }
938
+ /**
939
+ * Check if passwords match
940
+ */
941
+ checkPasswordMatch() {
942
+ const match = !this.data.confirmPassword || this.data.password === this.data.confirmPassword;
943
+ this.updateData({ passwordMatch: match }, true);
944
+ }
945
+ /**
946
+ * Toggle password visibility
947
+ */
948
+ async onActionTogglePassword(event, element) {
949
+ event.preventDefault();
950
+ const field = element.dataset.passwordField;
951
+ if (field === "password") {
952
+ this.updateData({ showPassword: !this.data.showPassword });
953
+ const input = this.element.querySelector("#resetPassword");
954
+ if (input) {
955
+ input.type = this.data.showPassword ? "text" : "password";
956
+ }
957
+ } else if (field === "confirmPassword") {
958
+ this.updateData({ showConfirmPassword: !this.data.showConfirmPassword });
959
+ const input = this.element.querySelector("#resetConfirmPassword");
960
+ if (input) {
961
+ input.type = this.data.showConfirmPassword ? "text" : "password";
962
+ }
963
+ }
964
+ }
965
+ /**
966
+ * Handle password reset submission
967
+ */
968
+ async onActionResetPassword(event) {
969
+ event.preventDefault();
970
+ await this.updateData({ error: null, isLoading: true }, true);
971
+ if (!this.data.password || !this.data.confirmPassword) {
972
+ await this.updateData({ error: "Please enter and confirm your new password", isLoading: false }, true);
973
+ return;
974
+ }
975
+ if (this.data.password.length < 6) {
976
+ await this.updateData({ error: "Password must be at least 6 characters long", isLoading: false }, true);
977
+ return;
978
+ }
979
+ if (this.data.password !== this.data.confirmPassword) {
980
+ await this.updateData({ error: "Passwords do not match", isLoading: false }, true);
981
+ return;
982
+ }
983
+ const auth = this.getApp().auth;
984
+ if (!auth) {
985
+ await this.updateData({ error: "Authentication system not available", isLoading: false }, true);
986
+ return;
987
+ }
988
+ const response = await auth.resetPasswordWithToken(this.resetToken, this.data.password);
989
+ if (response.success) {
990
+ await this.updateData({
991
+ success: true,
992
+ successMessage: response.message || "Password reset successful! You can now log in.",
993
+ isLoading: false
994
+ }, true);
995
+ setTimeout(() => {
996
+ this.getApp().showSuccess("Password reset complete. Please log in.");
997
+ this.getApp().navigate("/login");
998
+ }, 3e3);
999
+ } else {
1000
+ await this.updateData({
1001
+ error: response.message || "Password reset failed. Please try again.",
1002
+ isLoading: false
1003
+ }, true);
1004
+ }
1005
+ }
1006
+ /**
1007
+ * Navigate to login page
1008
+ */
1009
+ async onActionBackToLogin(event) {
1010
+ event.preventDefault();
1011
+ this.getApp().navigate("/login");
1012
+ }
1013
+ /**
1014
+ * Navigate to registration page
1015
+ */
1016
+ async onActionRegister(event) {
1017
+ event.preventDefault();
1018
+ this.getApp().navigate("/register");
1019
+ }
1020
+ /**
1021
+ * Request new reset email
1022
+ */
1023
+ async onActionRequestNew(event) {
1024
+ event.preventDefault();
1025
+ this.getApp().navigate("/forgot-password");
1026
+ }
1027
+ /**
1028
+ * Handle Enter key in form fields
1029
+ */
1030
+ async onActionHandleKeyPress(event, element) {
1031
+ if (event.key === "Enter") {
1032
+ event.preventDefault();
1033
+ const fieldOrder = ["resetPassword", "resetConfirmPassword"];
1034
+ const currentIndex = fieldOrder.indexOf(element.id);
1035
+ if (currentIndex >= 0 && currentIndex < fieldOrder.length - 1) {
1036
+ const nextField = this.element.querySelector(`#${fieldOrder[currentIndex + 1]}`);
1037
+ if (nextField) {
1038
+ nextField.focus();
1039
+ }
1040
+ } else if (currentIndex === fieldOrder.length - 1) {
1041
+ await this.onActionResetPassword(event);
1042
+ }
1043
+ }
1044
+ }
1045
+ /**
1046
+ * Get view data for template rendering
1047
+ */
1048
+ async getViewData() {
1049
+ return {
1050
+ ...this.data
1051
+ };
1052
+ }
1053
+ }
1054
+ const templates = {};
1055
+ templates["auth/pages/ForgotPasswordPage.mst"] = `<div class="auth-page forgot-password-page min-vh-100 d-flex align-items-center py-4">
1056
+ <div class="container">
1057
+ <div class="row justify-content-center">
1058
+ <div class="col-sm-8 col-md-8 col-lg-6 col-xl-5">
1059
+ <div class="card shadow-lg border-0">
1060
+ <div class="card-body p-4 p-md-5">
1061
+ <!-- Header -->
1062
+ <div class="text-center mb-4">
1063
+ {{#logoUrl}}<img src="{{logoUrl}}" alt="{{title}}" class="mb-3" style="max-height: 60px;">{{/logoUrl}}
1064
+ <h2 class="h3 mb-2">{{messages.forgotTitle}}</h2>
1065
+ <p class="text-muted">{{messages.forgotSubtitle}}</p>
1066
+ </div>
1067
+
1068
+ <!-- Error Alert -->
1069
+ {{#error}}
1070
+ <div class="alert alert-danger d-flex align-items-center alert-dismissible fade show" role="alert">
1071
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
1072
+ <div>{{error}}</div>
1073
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
1074
+ </div>
1075
+ {{/error}}
1076
+
1077
+ <!-- Step 1: Email Form -->
1078
+ {{#isStepEmail}}
1079
+ <form id="form-request-reset" novalidate>
1080
+ <div class="mb-3">
1081
+ <label for="forgotEmail" class="form-label"><i class="bi bi-envelope me-1"></i>Email Address</label>
1082
+ <input type="email" class="form-control form-control-lg" id="forgotEmail" name="email" placeholder="Enter your registered email" required autofocus>
1083
+ <div class="form-text">We'll send you instructions to reset your password.</div>
1084
+ </div>
1085
+ <button type="button" class="btn btn-primary btn-lg w-100 mb-3" data-action="requestReset" {{#isLoading}}disabled{{/isLoading}}>
1086
+ {{#isLoading}}<span class="spinner-border spinner-border-sm me-2"></span>Sending...{{/isLoading}}
1087
+ {{^isLoading}}<i class="bi bi-send me-2"></i>Send Instructions{{/isLoading}}
1088
+ </button>
1089
+ <button type="button" class="btn btn-outline-secondary btn-lg w-100" data-action="backToLogin" {{#isLoading}}disabled{{/isLoading}}>
1090
+ <i class="bi bi-arrow-left me-2"></i>Back to Login
1091
+ </button>
1092
+ </form>
1093
+ <div class="text-center mt-4">
1094
+ <p class="text-muted">Remember your password? <a href="#" class="text-decoration-none fw-semibold" data-action="backToLogin">Sign in</a></p>
1095
+ </div>
1096
+ {{/isStepEmail}}
1097
+
1098
+ <!-- Step 2 (Link Method): Confirmation -->
1099
+ {{#isStepLinkSent}}
1100
+ <div class="text-center">
1101
+ <div class="mb-4"><i class="bi bi-envelope-check text-success" style="font-size: 4rem;"></i></div>
1102
+ <h3 class="h4 mb-3">Check your email</h3>
1103
+ <p class="text-muted">If an account exists for <strong>{{data.email}}</strong>, we have sent instructions for resetting your password.</p>
1104
+ <div class="d-grid gap-2 mt-4">
1105
+ <button type="button" class="btn btn-primary btn-lg" data-action="backToLogin"><i class="bi bi-arrow-left me-2"></i>Back to Login</button>
1106
+ </div>
1107
+ </div>
1108
+ {{/isStepLinkSent}}
1109
+
1110
+ <!-- Step 2 (Code Method): Code Entry Form -->
1111
+ {{#isStepCode}}
1112
+ <p class="text-muted text-center mb-3">A verification code has been sent to <strong>{{data.email}}</strong>. Please enter it below.</p>
1113
+ <form id="form-reset-with-code" novalidate>
1114
+ <div class="mb-3">
1115
+ <label for="resetCode" class="form-label"><i class="bi bi-shield-lock me-1"></i>Verification Code</label>
1116
+ <input type="text" class="form-control form-control-lg" id="resetCode" name="code" placeholder="Enter code" required>
1117
+ </div>
1118
+ <div class="mb-3">
1119
+ <label for="resetPassword" class="form-label"><i class="bi bi-lock me-1"></i>New Password</label>
1120
+ <input type="password" class="form-control form-control-lg" id="resetPassword" name="new_password" placeholder="Enter new password" required autocomplete="new-password">
1121
+ </div>
1122
+ <div class="mb-3">
1123
+ <label for="confirmPassword" class="form-label"><i class="bi bi-lock-fill me-1"></i>Confirm New Password</label>
1124
+ <input type="password" class="form-control form-control-lg" id="confirmPassword" name="confirm_password" placeholder="Confirm new password" required autocomplete="new-password">
1125
+ </div>
1126
+ <button type="button" class="btn btn-primary btn-lg w-100" data-action="resetWithCode" {{#isLoading}}disabled{{/isLoading}}>
1127
+ {{#isLoading}}<span class="spinner-border spinner-border-sm me-2"></span>Resetting...{{/isLoading}}
1128
+ {{^isLoading}}<i class="bi bi-key me-2"></i>Reset Password{{/isLoading}}
1129
+ </button>
1130
+ </form>
1131
+ {{/isStepCode}}
1132
+
1133
+ <!-- Step 3 (Code Method): Success -->
1134
+ {{#isStepSuccess}}
1135
+ <div class="text-center">
1136
+ <div class="mb-4"><i class="bi bi-check-circle text-success" style="font-size: 4rem;"></i></div>
1137
+ <h3 class="h4 mb-3">Password Reset!</h3>
1138
+ <p class="text-muted">Your password has been changed successfully. You will be redirected to the login page shortly.</p>
1139
+ </div>
1140
+ {{/isStepSuccess}}
1141
+
1142
+ </div>
1143
+ </div>
1144
+ </div>
1145
+ </div>
1146
+ </div>
1147
+ </div>
1148
+ `;
1149
+ templates["auth/pages/LoginPage.mst"] = `<div class="auth-page login-page min-vh-100 d-flex align-items-center py-4">
1150
+ <div class="container">
1151
+ <div class="row justify-content-center">
1152
+ <div class="col-sm-10 col-md-8 col-lg-6 col-xl-5">
1153
+ <div class="card shadow-lg border-0">
1154
+ <div class="card-body p-4 p-md-5">
1155
+ <!-- Logo and Header -->
1156
+ <div class="text-center mb-4">
1157
+ {{#data.logoUrl}}
1158
+ <img src="{{data.logoUrl}}" alt="{{data.title}}" class="mb-3" style="max-height: 60px;">
1159
+ {{/data.logoUrl}}
1160
+ <h2 class="h3 mb-2">{{#data.loginIcon}}<i class="{{data.loginIcon}}"></i> {{/data.loginIcon}}{{data.messages.loginTitle}}</h2>
1161
+ <p class="text-muted">{{data.messages.loginSubtitle}}</p>
1162
+ </div>
1163
+
1164
+ <!-- Error Alert -->
1165
+ {{#data.error}}
1166
+ <div class="alert alert-danger d-flex align-items-center alert-dismissible fade show" role="alert">
1167
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
1168
+ <div>{{data.error}}</div>
1169
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
1170
+ </div>
1171
+ {{/data.error}}
1172
+
1173
+ <!-- Login Form -->
1174
+ <form novalidate>
1175
+ <!-- Username/Email Field -->
1176
+ <div class="mb-3">
1177
+ <label for="loginUsername" class="form-label">
1178
+ <i class="bi bi-person me-1"></i>Username or Email
1179
+ </label>
1180
+ <input
1181
+ type="text"
1182
+ class="form-control form-control-lg"
1183
+ id="loginUsername"
1184
+ placeholder="Enter your username or email"
1185
+ value="{{username}}"
1186
+ data-field="username"
1187
+ data-action-keydown="handleKeyPress"
1188
+ autocomplete="username"
1189
+ required
1190
+ autofocus
1191
+ {{#isLoading}}disabled{{/isLoading}}>
1192
+ </div>
1193
+
1194
+ <!-- Password Field -->
1195
+ <div class="mb-3">
1196
+ <label for="loginPassword" class="form-label">
1197
+ <i class="bi bi-lock me-1"></i>Password
1198
+ </label>
1199
+ <div class="input-group">
1200
+ <input
1201
+ type="{{#showPassword}}text{{/showPassword}}{{^showPassword}}password{{/showPassword}}"
1202
+ class="form-control form-control-lg"
1203
+ id="loginPassword"
1204
+ placeholder="Enter your password"
1205
+ value="{{password}}"
1206
+ data-field="password"
1207
+ data-action-keydown="handleKeyPress"
1208
+ autocomplete="current-password"
1209
+ required
1210
+ {{#isLoading}}disabled{{/isLoading}}>
1211
+ <button
1212
+ class="btn btn-outline-secondary"
1213
+ type="button"
1214
+ data-action="togglePassword"
1215
+ {{#isLoading}}disabled{{/isLoading}}>
1216
+ <i class="bi bi-eye{{#data.showPassword}}-slash{{/data.showPassword}}"></i>
1217
+ </button>
1218
+ </div>
1219
+ </div>
1220
+
1221
+ <!-- Remember Me & Forgot Password -->
1222
+ <div class="d-flex justify-content-between align-items-center mb-4">
1223
+ {{#data.rememberMe}}
1224
+ <div class="form-check">
1225
+ <input
1226
+ class="form-check-input"
1227
+ type="checkbox"
1228
+ id="rememberMe"
1229
+ data-field="rememberMe"
1230
+ data-change-action="updateField"
1231
+ autocomplete="off"
1232
+ {{#rememberMe}}checked{{/rememberMe}}
1233
+ {{#isLoading}}disabled{{/isLoading}}>
1234
+ <label class="form-check-label" for="rememberMe">
1235
+ Remember me
1236
+ </label>
1237
+ </div>
1238
+ {{/data.rememberMe}}
1239
+ {{^data.rememberMe}}<div></div>{{/data.rememberMe}}
1240
+
1241
+ {{#data.forgotPassword}}
1242
+ <a href="?page=forgot-password" class="text-decoration-none" data-action="forgotPassword">
1243
+ Forgot password?
1244
+ </a>
1245
+ {{/data.forgotPassword}}
1246
+ </div>
1247
+
1248
+ <!-- Login Button -->
1249
+ <button
1250
+ type="button"
1251
+ class="btn btn-primary btn-lg w-100 mb-3"
1252
+ data-action="login"
1253
+ {{#data.isLoading}}disabled{{/data.isLoading}}>
1254
+ {{#data.isLoading}}
1255
+ <span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
1256
+ Signing in...
1257
+ {{/data.isLoading}}
1258
+ {{^data.isLoading}}
1259
+ <i class="bi bi-box-arrow-in-right me-2"></i>Sign In
1260
+ {{/data.isLoading}}
1261
+ </button>
1262
+
1263
+ <!-- Alternative Login Methods -->
1264
+ {{#data.passkeySupported}}
1265
+ <div class="position-relative my-3">
1266
+ <hr class="text-muted">
1267
+ <span class="position-absolute top-50 start-50 translate-middle bg-white px-3 text-muted small">
1268
+ OR
1269
+ </span>
1270
+ </div>
1271
+
1272
+ <button
1273
+ type="button"
1274
+ class="btn btn-outline-primary btn-lg w-100 mb-2"
1275
+ data-action="loginWithPasskey"
1276
+ {{#data.isLoading}}disabled{{/data.isLoading}}>
1277
+ <i class="bi bi-fingerprint me-2"></i>Sign in with Passkey
1278
+ </button>
1279
+ {{/data.passkeySupported}}
1280
+ </form>
1281
+
1282
+ <!-- Register Link -->
1283
+ {{#data.registration}}
1284
+ <div class="text-center mt-4">
1285
+ <p class="mb-0">
1286
+ Don't have an account?
1287
+ <a href="#" class="text-decoration-none fw-semibold" data-action="register">
1288
+ Sign up
1289
+ </a>
1290
+ </p>
1291
+ </div>
1292
+ {{/data.registration}}
1293
+ </div>
1294
+ </div>
1295
+
1296
+ <!-- Security Notice -->
1297
+ <div class="text-center mt-3">
1298
+ <small class="text-muted">
1299
+ <i class="bi bi-shield-check me-1"></i>Secure Login with 256-bit Encryption
1300
+ </small>
1301
+ </div>
1302
+ </div>
1303
+ </div>
1304
+ </div>
1305
+ </div>
1306
+ `;
1307
+ templates["auth/pages/RegisterPage.mst"] = `<div class="auth-page register-page min-vh-100 d-flex align-items-center py-4">
1308
+ <div class="container">
1309
+ <div class="row justify-content-center">
1310
+ <div class="col-sm-10 col-md-8 col-lg-6 col-xl-5">
1311
+ <div class="card shadow-lg border-0">
1312
+ <div class="card-body p-4 p-md-5">
1313
+ <!-- Logo and Header -->
1314
+ <div class="text-center mb-4">
1315
+ {{#logoUrl}}
1316
+ <img src="{{logoUrl}}" alt="{{title}}" class="mb-3" style="max-height: 60px;">
1317
+ {{/logoUrl}}
1318
+ <h2 class="h3 mb-2">{{messages.registerTitle}}</h2>
1319
+ <p class="text-muted">{{messages.registerSubtitle}}</p>
1320
+ </div>
1321
+
1322
+ <!-- Error Alert -->
1323
+ {{#error}}
1324
+ <div class="alert alert-danger d-flex align-items-center alert-dismissible fade show" role="alert">
1325
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
1326
+ <div>{{error}}</div>
1327
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
1328
+ </div>
1329
+ {{/error}}
1330
+
1331
+ <!-- Registration Form -->
1332
+ <form data-action="register" novalidate>
1333
+ <!-- Name Field -->
1334
+ <div class="mb-3">
1335
+ <label for="registerName" class="form-label">
1336
+ <i class="bi bi-person me-1"></i>Full Name
1337
+ </label>
1338
+ <input
1339
+ type="text"
1340
+ class="form-control form-control-lg"
1341
+ id="registerName"
1342
+ placeholder="Enter your full name"
1343
+ value="{{name}}"
1344
+ data-field="name"
1345
+ data-change-action="updateField"
1346
+ data-filter="live-search"
1347
+ data-action-keydown="handleKeyPress"
1348
+ autocomplete="name"
1349
+ required
1350
+ autofocus
1351
+ {{#isLoading}}disabled{{/isLoading}}>
1352
+ </div>
1353
+
1354
+ <!-- Email Field -->
1355
+ <div class="mb-3">
1356
+ <label for="registerEmail" class="form-label">
1357
+ <i class="bi bi-envelope me-1"></i>Email Address
1358
+ </label>
1359
+ <input
1360
+ type="email"
1361
+ class="form-control form-control-lg"
1362
+ id="registerEmail"
1363
+ placeholder="name@example.com"
1364
+ value="{{email}}"
1365
+ data-field="email"
1366
+ data-change-action="updateField"
1367
+ data-filter="live-search"
1368
+ data-action-keydown="handleKeyPress"
1369
+ autocomplete="email"
1370
+ required
1371
+ {{#isLoading}}disabled{{/isLoading}}>
1372
+ </div>
1373
+
1374
+ <!-- Password Field -->
1375
+ <div class="mb-3">
1376
+ <label for="registerPassword" class="form-label">
1377
+ <i class="bi bi-lock me-1"></i>Password
1378
+ </label>
1379
+ <div class="input-group">
1380
+ <input
1381
+ type="{{#showPassword}}text{{/showPassword}}{{^showPassword}}password{{/showPassword}}"
1382
+ class="form-control form-control-lg"
1383
+ id="registerPassword"
1384
+ placeholder="Create a strong password"
1385
+ value="{{password}}"
1386
+ data-field="password"
1387
+ data-change-action="updateField"
1388
+ data-filter="live-search"
1389
+ data-action-keydown="handleKeyPress"
1390
+ autocomplete="new-password"
1391
+ required
1392
+ {{#isLoading}}disabled{{/isLoading}}>
1393
+ <button
1394
+ class="btn btn-outline-secondary"
1395
+ type="button"
1396
+ data-password-field="password"
1397
+ data-action="togglePassword"
1398
+ {{#isLoading}}disabled{{/isLoading}}>
1399
+ <i class="bi bi-eye{{#showPassword}}-slash{{/showPassword}}"></i>
1400
+ </button>
1401
+ </div>
1402
+
1403
+ <!-- Password Strength Indicator -->
1404
+ {{#passwordStrength}}
1405
+ <div class="mt-2">
1406
+ <div class="progress" style="height: 4px;">
1407
+ <div class="progress-bar
1408
+ {{#passwordStrength.weak}}bg-danger{{/passwordStrength.weak}}
1409
+ {{#passwordStrength.fair}}bg-warning{{/passwordStrength.fair}}
1410
+ {{#passwordStrength.good}}bg-info{{/passwordStrength.good}}
1411
+ {{#passwordStrength.strong}}bg-success{{/passwordStrength.strong}}"
1412
+ role="progressbar"
1413
+ style="width:
1414
+ {{#passwordStrength.weak}}25%{{/passwordStrength.weak}}
1415
+ {{#passwordStrength.fair}}50%{{/passwordStrength.fair}}
1416
+ {{#passwordStrength.good}}75%{{/passwordStrength.good}}
1417
+ {{#passwordStrength.strong}}100%{{/passwordStrength.strong}}">
1418
+ </div>
1419
+ </div>
1420
+ <small class="text-muted mt-1">
1421
+ Password strength: {{passwordStrength}}
1422
+ </small>
1423
+ </div>
1424
+ {{/passwordStrength}}
1425
+ </div>
1426
+
1427
+ <!-- Confirm Password Field -->
1428
+ <div class="mb-3">
1429
+ <label for="registerConfirmPassword" class="form-label">
1430
+ <i class="bi bi-lock-fill me-1"></i>Confirm Password
1431
+ </label>
1432
+ <div class="input-group">
1433
+ <input
1434
+ type="{{#showConfirmPassword}}text{{/showConfirmPassword}}{{^showConfirmPassword}}password{{/showConfirmPassword}}"
1435
+ class="form-control form-control-lg {{^passwordMatch}}is-invalid{{/passwordMatch}}"
1436
+ id="registerConfirmPassword"
1437
+ placeholder="Re-enter your password"
1438
+ value="{{confirmPassword}}"
1439
+ data-field="confirmPassword"
1440
+ data-change-action="updateField"
1441
+ data-filter="live-search"
1442
+ data-action-keydown="handleKeyPress"
1443
+ autocomplete="new-password"
1444
+ required
1445
+ {{#isLoading}}disabled{{/isLoading}}>
1446
+ <button
1447
+ class="btn btn-outline-secondary"
1448
+ type="button"
1449
+ data-password-field="confirmPassword"
1450
+ data-action="togglePassword"
1451
+ {{#isLoading}}disabled{{/isLoading}}>
1452
+ <i class="bi bi-eye{{#showConfirmPassword}}-slash{{/showConfirmPassword}}"></i>
1453
+ </button>
1454
+ </div>
1455
+ {{^passwordMatch}}
1456
+ <div class="invalid-feedback">
1457
+ Passwords do not match
1458
+ </div>
1459
+ {{/passwordMatch}}
1460
+ </div>
1461
+
1462
+ <!-- Register Button -->
1463
+ <button
1464
+ type="submit"
1465
+ class="btn btn-primary btn-lg w-100 mb-3"
1466
+ {{#isLoading}}disabled{{/isLoading}}>
1467
+ {{#isLoading}}
1468
+ <span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
1469
+ Creating account...
1470
+ {{/isLoading}}
1471
+ {{^isLoading}}
1472
+ <i class="bi bi-person-plus me-2"></i>Create Account
1473
+ {{/isLoading}}
1474
+ </button>
1475
+ </form>
1476
+
1477
+ <!-- Login Link -->
1478
+ <div class="text-center mt-4">
1479
+ <p class="mb-0">
1480
+ Already have an account?
1481
+ <a href="#" class="text-decoration-none fw-semibold" data-action="login">
1482
+ Sign in
1483
+ </a>
1484
+ </p>
1485
+ </div>
1486
+ </div>
1487
+ </div>
1488
+
1489
+ <!-- Security Notice -->
1490
+ <div class="text-center mt-3">
1491
+ <small class="text-muted">
1492
+ <i class="bi bi-shield-check me-1"></i>Your information is secure and encrypted
1493
+ </small>
1494
+ </div>
1495
+ </div>
1496
+ </div>
1497
+ </div>
1498
+ </div>
1499
+ `;
1500
+ templates["auth/pages/ResetPasswordPage.mst"] = `<div class="auth-page reset-password-page min-vh-100 d-flex align-items-center py-4">
1501
+ <div class="container">
1502
+ <div class="row justify-content-center">
1503
+ <div class="col-sm-10 col-md-8 col-lg-6 col-xl-5">
1504
+ <div class="card shadow-lg border-0">
1505
+ <div class="card-body p-4 p-md-5">
1506
+ <!-- Logo and Header -->
1507
+ <div class="text-center mb-4">
1508
+ {{#logoUrl}}
1509
+ <img src="{{logoUrl}}" alt="{{title}}" class="mb-3" style="max-height: 60px;">
1510
+ {{/logoUrl}}
1511
+ <h2 class="h3 mb-2">{{messages.resetTitle}}</h2>
1512
+ <p class="text-muted">{{messages.resetSubtitle}}</p>
1513
+ </div>
1514
+
1515
+ <!-- Error Alert -->
1516
+ {{#error}}
1517
+ <div class="alert alert-danger d-flex align-items-center alert-dismissible fade show" role="alert">
1518
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
1519
+ <div>{{error}}</div>
1520
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
1521
+ </div>
1522
+ {{/error}}
1523
+
1524
+ <!-- Success State -->
1525
+ {{#success}}
1526
+ <div class="alert alert-success d-flex align-items-center" role="alert">
1527
+ <i class="bi bi-check-circle-fill me-2"></i>
1528
+ <div>
1529
+ <strong>Password Reset Complete!</strong><br>
1530
+ {{#successMessage}}{{successMessage}}{{/successMessage}}
1531
+ {{^successMessage}}Your password has been reset successfully. You can now log in with your new password.{{/successMessage}}
1532
+ </div>
1533
+ </div>
1534
+
1535
+ <div class="text-center">
1536
+ <p class="mb-3">
1537
+ <i class="bi bi-shield-check text-success" style="font-size: 3rem;"></i>
1538
+ </p>
1539
+ <p class="text-muted">
1540
+ Redirecting you to the login page...
1541
+ </p>
1542
+ <div class="d-grid gap-2 mt-4">
1543
+ <button
1544
+ class="btn btn-primary btn-lg"
1545
+ data-action="backToLogin">
1546
+ <i class="bi bi-box-arrow-in-right me-2"></i>Continue to Login
1547
+ </button>
1548
+ </div>
1549
+ </div>
1550
+ {{/success}}
1551
+
1552
+ <!-- Reset Form -->
1553
+ {{^success}}
1554
+ {{#tokenValid}}
1555
+ <form data-action="resetPassword" novalidate>
1556
+ <!-- New Password Field -->
1557
+ <div class="mb-3">
1558
+ <label for="resetPassword" class="form-label">
1559
+ <i class="bi bi-lock me-1"></i>New Password
1560
+ </label>
1561
+ <div class="input-group">
1562
+ <input
1563
+ type="{{#showPassword}}text{{/showPassword}}{{^showPassword}}password{{/showPassword}}"
1564
+ class="form-control form-control-lg"
1565
+ id="resetPassword"
1566
+ placeholder="Enter your new password"
1567
+ value="{{password}}"
1568
+ data-field="password"
1569
+ data-change-action="updateField"
1570
+ data-filter="live-search"
1571
+ data-action-keydown="handleKeyPress"
1572
+ autocomplete="new-password"
1573
+ required
1574
+ autofocus
1575
+ {{#isLoading}}disabled{{/isLoading}}>
1576
+ <button
1577
+ class="btn btn-outline-secondary"
1578
+ type="button"
1579
+ data-password-field="password"
1580
+ data-action="togglePassword"
1581
+ {{#isLoading}}disabled{{/isLoading}}>
1582
+ <i class="bi bi-eye{{#showPassword}}-slash{{/showPassword}}"></i>
1583
+ </button>
1584
+ </div>
1585
+
1586
+ <!-- Password Strength Indicator -->
1587
+ {{#passwordStrength}}
1588
+ <div class="mt-2">
1589
+ <div class="progress" style="height: 4px;">
1590
+ <div class="progress-bar
1591
+ {{#passwordStrength.weak}}bg-danger{{/passwordStrength.weak}}
1592
+ {{#passwordStrength.fair}}bg-warning{{/passwordStrength.fair}}
1593
+ {{#passwordStrength.good}}bg-info{{/passwordStrength.good}}
1594
+ {{#passwordStrength.strong}}bg-success{{/passwordStrength.strong}}"
1595
+ role="progressbar"
1596
+ style="width:
1597
+ {{#passwordStrength.weak}}25%{{/passwordStrength.weak}}
1598
+ {{#passwordStrength.fair}}50%{{/passwordStrength.fair}}
1599
+ {{#passwordStrength.good}}75%{{/passwordStrength.good}}
1600
+ {{#passwordStrength.strong}}100%{{/passwordStrength.strong}}">
1601
+ </div>
1602
+ </div>
1603
+ <small class="text-muted mt-1">
1604
+ Password strength: {{passwordStrength}}
1605
+ </small>
1606
+ </div>
1607
+ {{/passwordStrength}}
1608
+ </div>
1609
+
1610
+ <!-- Confirm Password Field -->
1611
+ <div class="mb-4">
1612
+ <label for="resetConfirmPassword" class="form-label">
1613
+ <i class="bi bi-lock-fill me-1"></i>Confirm New Password
1614
+ </label>
1615
+ <div class="input-group">
1616
+ <input
1617
+ type="{{#showConfirmPassword}}text{{/showConfirmPassword}}{{^showConfirmPassword}}password{{/showConfirmPassword}}"
1618
+ class="form-control form-control-lg {{^passwordMatch}}is-invalid{{/passwordMatch}}"
1619
+ id="resetConfirmPassword"
1620
+ placeholder="Re-enter your new password"
1621
+ value="{{confirmPassword}}"
1622
+ data-field="confirmPassword"
1623
+ data-change-action="updateField"
1624
+ data-filter="live-search"
1625
+ data-action-keydown="handleKeyPress"
1626
+ autocomplete="new-password"
1627
+ required
1628
+ {{#isLoading}}disabled{{/isLoading}}>
1629
+ <button
1630
+ class="btn btn-outline-secondary"
1631
+ type="button"
1632
+ data-password-field="confirmPassword"
1633
+ data-action="togglePassword"
1634
+ {{#isLoading}}disabled{{/isLoading}}>
1635
+ <i class="bi bi-eye{{#showConfirmPassword}}-slash{{/showConfirmPassword}}"></i>
1636
+ </button>
1637
+ </div>
1638
+ {{^passwordMatch}}
1639
+ <div class="invalid-feedback">
1640
+ Passwords do not match
1641
+ </div>
1642
+ {{/passwordMatch}}
1643
+ </div>
1644
+
1645
+ <!-- Reset Button -->
1646
+ <button
1647
+ type="submit"
1648
+ class="btn btn-primary btn-lg w-100 mb-3"
1649
+ {{#isLoading}}disabled{{/isLoading}}>
1650
+ {{#isLoading}}
1651
+ <span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
1652
+ Resetting password...
1653
+ {{/isLoading}}
1654
+ {{^isLoading}}
1655
+ <i class="bi bi-key me-2"></i>Reset Password
1656
+ {{/isLoading}}
1657
+ </button>
1658
+
1659
+ <!-- Back to Login -->
1660
+ <button
1661
+ type="button"
1662
+ class="btn btn-outline-secondary btn-lg w-100"
1663
+ data-action="backToLogin"
1664
+ {{#isLoading}}disabled{{/isLoading}}>
1665
+ <i class="bi bi-arrow-left me-2"></i>Back to Login
1666
+ </button>
1667
+ </form>
1668
+ {{/tokenValid}}
1669
+
1670
+ <!-- Invalid Token State -->
1671
+ {{^tokenValid}}
1672
+ <div class="text-center">
1673
+ <div class="mb-4">
1674
+ <i class="bi bi-exclamation-triangle text-warning" style="font-size: 4rem;"></i>
1675
+ </div>
1676
+ <h4 class="text-warning mb-3">Invalid Reset Link</h4>
1677
+ <p class="text-muted mb-4">
1678
+ This password reset link is invalid or has expired.
1679
+ Please request a new password reset.
1680
+ </p>
1681
+ <div class="d-grid gap-2">
1682
+ <button
1683
+ class="btn btn-primary btn-lg"
1684
+ data-action="requestNew">
1685
+ <i class="bi bi-envelope me-2"></i>Request New Reset
1686
+ </button>
1687
+ <button
1688
+ class="btn btn-outline-secondary btn-lg"
1689
+ data-action="backToLogin">
1690
+ <i class="bi bi-arrow-left me-2"></i>Back to Login
1691
+ </button>
1692
+ </div>
1693
+ </div>
1694
+ {{/tokenValid}}
1695
+
1696
+ <!-- Additional Links -->
1697
+ {{#tokenValid}}{{^success}}
1698
+ <div class="text-center mt-4">
1699
+ <p class="text-muted mb-2">
1700
+ Remember your password?
1701
+ <a href="#" class="text-decoration-none fw-semibold" data-action="backToLogin">
1702
+ Sign in
1703
+ </a>
1704
+ </p>
1705
+ {{#registration}}
1706
+ <p class="text-muted mb-0">
1707
+ Don't have an account?
1708
+ <a href="#" class="text-decoration-none fw-semibold" data-action="register">
1709
+ Sign up
1710
+ </a>
1711
+ </p>
1712
+ {{/registration}}
1713
+ </div>
1714
+ {{/success}}{{/tokenValid}}
1715
+ {{/success}}
1716
+ </div>
1717
+ </div>
1718
+
1719
+ <!-- Security Notice -->
1720
+ <div class="text-center mt-3">
1721
+ <small class="text-muted">
1722
+ <i class="bi bi-shield-lock me-1"></i>
1723
+ Secure password reset with email verification
1724
+ </small>
1725
+ </div>
1726
+ </div>
1727
+ </div>
1728
+ </div>
1729
+ </div>
1730
+ `;
1731
+ const auth_pages_ForgotPasswordPage_mst = templates["auth/pages/ForgotPasswordPage.mst"];
1732
+ const auth_pages_LoginPage_mst = templates["auth/pages/LoginPage.mst"];
1733
+ const auth_pages_RegisterPage_mst = templates["auth/pages/RegisterPage.mst"];
1734
+ class AuthApp extends WebApp {
1735
+ constructor(config = {}) {
1736
+ const authConfig = {
1737
+ // Default auth routes
1738
+ routes: {
1739
+ login: "/login",
1740
+ register: "/register",
1741
+ forgot: "/forgot-password",
1742
+ reset: "/reset-password",
1743
+ ...config.routes
1744
+ },
1745
+ // Default navigation redirects
1746
+ loginRedirect: config.loginRedirect || "/",
1747
+ logoutRedirect: config.logoutRedirect || "/login",
1748
+ // Default features
1749
+ features: {
1750
+ forgotPassword: true,
1751
+ registration: true,
1752
+ rememberMe: true,
1753
+ ...config.features
1754
+ },
1755
+ // Default password reset method
1756
+ passwordResetMethod: config.passwordResetMethod || "code",
1757
+ // Default UI messages and branding
1758
+ ui: {
1759
+ title: config.name || "My App",
1760
+ logoUrl: config.logoUrl || null,
1761
+ messages: {
1762
+ loginTitle: "Welcome Back",
1763
+ loginSubtitle: "Sign in to your account",
1764
+ registerTitle: "Create Account",
1765
+ registerSubtitle: "Join us today",
1766
+ forgotTitle: "Reset Password",
1767
+ forgotSubtitle: "We'll send you reset instructions",
1768
+ ...config.ui?.messages || {}
1769
+ }
1770
+ },
1771
+ ...config
1772
+ };
1773
+ super(authConfig);
1774
+ this.auth = new AuthManager(this, authConfig);
1775
+ this.authConfig = authConfig;
1776
+ this.registerAuthPages();
1777
+ this.setupAuthIntegration();
1778
+ this.setupAuthGuards();
1779
+ }
1780
+ /**
1781
+ * Registers all the standard authentication pages with the application.
1782
+ */
1783
+ registerAuthPages() {
1784
+ const cfg = this.authConfig;
1785
+ this.registerPage("login", LoginPage, {
1786
+ route: cfg.routes.login,
1787
+ title: "Login",
1788
+ authConfig: cfg,
1789
+ template: auth_pages_LoginPage_mst
1790
+ });
1791
+ if (cfg.features.registration) {
1792
+ this.registerPage("register", RegisterPage, {
1793
+ route: cfg.routes.register,
1794
+ title: "Register",
1795
+ authConfig: cfg,
1796
+ template: auth_pages_RegisterPage_mst
1797
+ });
1798
+ }
1799
+ if (cfg.features.forgotPassword) {
1800
+ this.registerPage("forgot-password", ForgotPasswordPage, {
1801
+ route: cfg.routes.forgot,
1802
+ title: "Reset Password",
1803
+ authConfig: cfg,
1804
+ template: auth_pages_ForgotPasswordPage_mst
1805
+ });
1806
+ this.registerPage("reset-password", ResetPasswordPage, {
1807
+ route: cfg.routes.reset,
1808
+ title: "Set New Password",
1809
+ authConfig: cfg
1810
+ });
1811
+ }
1812
+ }
1813
+ /**
1814
+ * Sets up global event listeners to integrate AuthManager state with the app.
1815
+ */
1816
+ setupAuthIntegration() {
1817
+ this.events.on("auth:login", (user) => {
1818
+ this.showSuccess(`Welcome back, ${user.name || user.email}!`);
1819
+ this.navigateAfterLogin();
1820
+ });
1821
+ this.events.on("auth:logout", () => {
1822
+ this.showInfo("You have been logged out.");
1823
+ this.navigate(this.authConfig.logoutRedirect);
1824
+ });
1825
+ this.events.on("auth:register", (user) => {
1826
+ this.showSuccess(`Welcome, ${user.name || user.email}! Your account is ready.`);
1827
+ this.navigate(this.authConfig.loginRedirect);
1828
+ });
1829
+ this.events.on("auth:tokenExpired", () => {
1830
+ this.showWarning("Your session has expired. Please login again.");
1831
+ this.navigate(this.authConfig.logoutRedirect);
1832
+ });
1833
+ }
1834
+ /**
1835
+ * Sets up route guards to protect pages.
1836
+ */
1837
+ setupAuthGuards() {
1838
+ this.events.on("route:changed", ({ pageName, path }) => {
1839
+ const page = this.getOrCreatePage(pageName);
1840
+ if (!page) return;
1841
+ const PageClass = page.constructor;
1842
+ const isAuthenticated = this.auth.isAuthenticated;
1843
+ const isAuthPage = ["login", "register", "forgot-password", "reset-password"].includes(pageName);
1844
+ if (PageClass.requiresAuth && !isAuthenticated) {
1845
+ sessionStorage.setItem("auth_redirect", path);
1846
+ this.navigate(this.authConfig.routes.login);
1847
+ this.showWarning("Please login to access this page.");
1848
+ return;
1849
+ }
1850
+ if (isAuthenticated && isAuthPage) {
1851
+ this.navigate(this.authConfig.loginRedirect);
1852
+ }
1853
+ });
1854
+ }
1855
+ /**
1856
+ * Navigates to the intended page after a successful login.
1857
+ */
1858
+ navigateAfterLogin() {
1859
+ const redirectPath = sessionStorage.getItem("auth_redirect");
1860
+ if (redirectPath) {
1861
+ sessionStorage.removeItem("auth_redirect");
1862
+ this.navigate(redirectPath);
1863
+ } else {
1864
+ this.navigate(this.authConfig.loginRedirect);
1865
+ }
1866
+ }
1867
+ /**
1868
+ * Helper to protect a Page class.
1869
+ * @param {Page} PageClass - The class to protect.
1870
+ * @returns {Page} The protected class.
1871
+ */
1872
+ static requireAuth(PageClass) {
1873
+ PageClass.requiresAuth = true;
1874
+ return PageClass;
1875
+ }
1876
+ }
1877
+ class PasskeyPlugin {
1878
+ constructor(config = {}) {
1879
+ this.name = "passkey";
1880
+ this.config = {
1881
+ rpName: "MOJO App",
1882
+ rpId: window?.location?.hostname || "localhost",
1883
+ timeout: 6e4,
1884
+ userVerification: "preferred",
1885
+ authenticatorAttachment: "platform",
1886
+ // 'platform', 'cross-platform', or undefined
1887
+ ...config
1888
+ };
1889
+ this.authManager = null;
1890
+ this.app = null;
1891
+ this.authService = null;
1892
+ }
1893
+ /**
1894
+ * Initialize plugin with AuthManager and WebApp
1895
+ * @param {AuthManager} authManager - Auth manager instance
1896
+ * @param {WebApp} app - WebApp instance
1897
+ */
1898
+ async initialize(authManager, app) {
1899
+ this.authManager = authManager;
1900
+ this.app = app;
1901
+ if (!this.isSupported()) {
1902
+ console.warn("Passkey authentication is not supported in this browser");
1903
+ return;
1904
+ }
1905
+ this.authManager.loginWithPasskey = this.loginWithPasskey.bind(this);
1906
+ this.authManager.setupPasskey = this.setupPasskey.bind(this);
1907
+ this.authManager.isPasskeySupported = this.isSupported.bind(this);
1908
+ console.log("PasskeyPlugin initialized successfully");
1909
+ }
1910
+ /**
1911
+ * Check if WebAuthn is supported in this browser
1912
+ * @returns {boolean} True if supported
1913
+ */
1914
+ isSupported() {
1915
+ return window.PublicKeyCredential !== void 0 && navigator.credentials !== void 0 && typeof navigator.credentials.create === "function" && typeof navigator.credentials.get === "function";
1916
+ }
1917
+ /**
1918
+ * Login with passkey
1919
+ * @returns {Promise<object>} Login result with user data and tokens
1920
+ */
1921
+ async loginWithPasskey() {
1922
+ if (!this.isSupported()) {
1923
+ throw new Error("Passkey authentication is not supported in this browser");
1924
+ }
1925
+ try {
1926
+ const challengeResponse = await this.app.rest.POST("/api/auth/passkey/challenge");
1927
+ if (!challengeResponse.success || !challengeResponse.data.data.challenge) {
1928
+ throw new Error("No authentication challenge received from server");
1929
+ }
1930
+ const challengeData = challengeResponse.data.data;
1931
+ const credentialRequestOptions = {
1932
+ publicKey: {
1933
+ challenge: this.base64ToArrayBuffer(challengeData.challenge),
1934
+ timeout: this.config.timeout,
1935
+ userVerification: this.config.userVerification,
1936
+ rpId: this.config.rpId
1937
+ }
1938
+ };
1939
+ const credential = await navigator.credentials.get(credentialRequestOptions);
1940
+ if (!credential) {
1941
+ throw new Error("No credential received from authenticator");
1942
+ }
1943
+ const credentialData = {
1944
+ id: credential.id,
1945
+ rawId: this.arrayBufferToBase64(credential.rawId),
1946
+ type: credential.type,
1947
+ response: {
1948
+ authenticatorData: this.arrayBufferToBase64(credential.response.authenticatorData),
1949
+ clientDataJSON: this.arrayBufferToBase64(credential.response.clientDataJSON),
1950
+ signature: this.arrayBufferToBase64(credential.response.signature),
1951
+ userHandle: credential.response.userHandle ? this.arrayBufferToBase64(credential.response.userHandle) : null
1952
+ }
1953
+ };
1954
+ const loginResponse = await this.app.rest.POST("/api/auth/passkey/verify", {
1955
+ credential: credentialData,
1956
+ challengeId: challengeData.challengeId
1957
+ });
1958
+ if (!loginResponse.success || !loginResponse.data.status) {
1959
+ throw new Error(loginResponse.data.error || "Passkey verification failed");
1960
+ }
1961
+ const { token, refreshToken, user } = loginResponse.data.data;
1962
+ this.authManager.tokenManager.setTokens(token, refreshToken, true);
1963
+ const userInfo = this.authManager.tokenManager.getUserInfo();
1964
+ this.authManager.setAuthState({ ...user, ...userInfo });
1965
+ if (this.authManager.config.autoRefresh) {
1966
+ this.authManager.scheduleTokenRefresh();
1967
+ }
1968
+ this.authManager.emit("login", this.authManager.user);
1969
+ return {
1970
+ success: true,
1971
+ user: this.authManager.user
1972
+ };
1973
+ } catch (error) {
1974
+ console.error("Passkey login error:", error);
1975
+ this.authManager.emit("loginError", error);
1976
+ throw new Error(error.message || "Passkey authentication failed");
1977
+ }
1978
+ }
1979
+ /**
1980
+ * Setup passkey for current authenticated user
1981
+ * @returns {Promise<object>} Setup result
1982
+ */
1983
+ async setupPasskey() {
1984
+ if (!this.isSupported()) {
1985
+ throw new Error("Passkey authentication is not supported in this browser");
1986
+ }
1987
+ if (!this.authManager.isAuthenticated) {
1988
+ throw new Error("User must be authenticated to setup passkey");
1989
+ }
1990
+ try {
1991
+ const optionsResponse = await this.app.rest.POST("/api/auth/passkey/register-options");
1992
+ if (!optionsResponse.success || !optionsResponse.data.data.options) {
1993
+ throw new Error("No registration options received from server");
1994
+ }
1995
+ const optionsData = optionsResponse.data.data;
1996
+ const options = optionsData.options;
1997
+ const credentialCreationOptions = {
1998
+ publicKey: {
1999
+ challenge: this.base64ToArrayBuffer(options.challenge),
2000
+ rp: {
2001
+ name: this.config.rpName,
2002
+ id: this.config.rpId
2003
+ },
2004
+ user: {
2005
+ id: this.base64ToArrayBuffer(options.userId),
2006
+ name: options.userName,
2007
+ displayName: options.userDisplayName
2008
+ },
2009
+ pubKeyCredParams: [
2010
+ { alg: -7, type: "public-key" },
2011
+ // ES256
2012
+ { alg: -257, type: "public-key" }
2013
+ // RS256
2014
+ ],
2015
+ authenticatorSelection: {
2016
+ userVerification: this.config.userVerification
2017
+ },
2018
+ timeout: this.config.timeout,
2019
+ attestation: "none"
2020
+ }
2021
+ };
2022
+ if (this.config.authenticatorAttachment) {
2023
+ credentialCreationOptions.publicKey.authenticatorSelection.authenticatorAttachment = this.config.authenticatorAttachment;
2024
+ }
2025
+ const credential = await navigator.credentials.create(credentialCreationOptions);
2026
+ if (!credential) {
2027
+ throw new Error("Failed to create credential");
2028
+ }
2029
+ const credentialData = {
2030
+ id: credential.id,
2031
+ rawId: this.arrayBufferToBase64(credential.rawId),
2032
+ type: credential.type,
2033
+ response: {
2034
+ attestationObject: this.arrayBufferToBase64(credential.response.attestationObject),
2035
+ clientDataJSON: this.arrayBufferToBase64(credential.response.clientDataJSON)
2036
+ }
2037
+ };
2038
+ const registrationResponse = await this.app.rest.POST("/api/auth/passkey/register", {
2039
+ credential: credentialData,
2040
+ optionsId: optionsData.optionsId
2041
+ });
2042
+ if (!registrationResponse.success || !registrationResponse.data.status) {
2043
+ throw new Error(registrationResponse.data.error || "Failed to register passkey");
2044
+ }
2045
+ this.authManager.emit("passkeySetupSuccess", registrationResponse.data.data);
2046
+ return {
2047
+ success: true,
2048
+ data: registrationResponse.data.data
2049
+ };
2050
+ } catch (error) {
2051
+ console.error("Passkey setup error:", error);
2052
+ this.authManager.emit("passkeySetupError", error);
2053
+ throw new Error(error.message || "Failed to setup passkey");
2054
+ }
2055
+ }
2056
+ /**
2057
+ * Check if user has passkeys registered
2058
+ * @returns {Promise<object>} Result with passkey availability
2059
+ */
2060
+ async hasPasskeys() {
2061
+ if (!this.authManager.isAuthenticated) {
2062
+ return { success: false, hasPasskeys: false };
2063
+ }
2064
+ try {
2065
+ const response = await this.app.rest.GET("/api/auth/passkey/list");
2066
+ return {
2067
+ success: response.success,
2068
+ hasPasskeys: response.data.data?.passkeys && response.data.data.passkeys.length > 0,
2069
+ count: response.data.data?.passkeys ? response.data.data.passkeys.length : 0
2070
+ };
2071
+ } catch (error) {
2072
+ console.error("Error checking passkeys:", error);
2073
+ return { success: false, hasPasskeys: false };
2074
+ }
2075
+ }
2076
+ /**
2077
+ * Remove/revoke a specific passkey
2078
+ * @param {string} credentialId - ID of credential to remove
2079
+ * @returns {Promise<object>} Result of removal
2080
+ */
2081
+ async removePasskey(credentialId) {
2082
+ if (!this.authManager.isAuthenticated) {
2083
+ throw new Error("User must be authenticated to remove passkey");
2084
+ }
2085
+ try {
2086
+ const response = await this.app.rest.DELETE("/api/auth/passkey/remove", { credentialId });
2087
+ if (!response.success || !response.data.status) {
2088
+ throw new Error(response.data.error || "Failed to remove passkey");
2089
+ }
2090
+ this.authManager.emit("passkeyRemoved", { credentialId });
2091
+ return {
2092
+ success: true,
2093
+ data: response.data.data
2094
+ };
2095
+ } catch (error) {
2096
+ console.error("Error removing passkey:", error);
2097
+ throw new Error(error.message || "Failed to remove passkey");
2098
+ }
2099
+ }
2100
+ /**
2101
+ * Convert base64 string to ArrayBuffer
2102
+ * @param {string} base64 - Base64 string
2103
+ * @returns {ArrayBuffer} ArrayBuffer
2104
+ */
2105
+ base64ToArrayBuffer(base64) {
2106
+ const binaryString = atob(base64);
2107
+ const bytes = new Uint8Array(binaryString.length);
2108
+ for (let i = 0; i < binaryString.length; i++) {
2109
+ bytes[i] = binaryString.charCodeAt(i);
2110
+ }
2111
+ return bytes.buffer;
2112
+ }
2113
+ /**
2114
+ * Convert ArrayBuffer to base64 string
2115
+ * @param {ArrayBuffer} buffer - ArrayBuffer
2116
+ * @returns {string} Base64 string
2117
+ */
2118
+ arrayBufferToBase64(buffer) {
2119
+ const bytes = new Uint8Array(buffer);
2120
+ let binary = "";
2121
+ for (let i = 0; i < bytes.byteLength; i++) {
2122
+ binary += String.fromCharCode(bytes[i]);
2123
+ }
2124
+ return btoa(binary);
2125
+ }
2126
+ /**
2127
+ * Get browser compatibility info
2128
+ * @returns {object} Compatibility information
2129
+ */
2130
+ getBrowserCompatibility() {
2131
+ return {
2132
+ webAuthnSupported: !!window.PublicKeyCredential,
2133
+ credentialsSupported: !!navigator.credentials,
2134
+ platformSupported: this.config.authenticatorAttachment === "platform" ? window.PublicKeyCredential?.isUserVerifyingPlatformAuthenticatorAvailable?.() : true,
2135
+ conditionalMediationSupported: window.PublicKeyCredential?.isConditionalMediationAvailable?.()
2136
+ };
2137
+ }
2138
+ /**
2139
+ * Plugin cleanup
2140
+ */
2141
+ destroy() {
2142
+ if (this.authManager) {
2143
+ delete this.authManager.loginWithPasskey;
2144
+ delete this.authManager.setupPasskey;
2145
+ delete this.authManager.isPasskeySupported;
2146
+ }
2147
+ this.authManager = null;
2148
+ this.app = null;
2149
+ this.authService = null;
2150
+ }
2151
+ }
2152
+ export {
2153
+ AuthApp,
2154
+ AuthManager,
2155
+ B as BUILD_TIME,
2156
+ ForgotPasswordPage,
2157
+ LoginPage,
2158
+ PasskeyPlugin,
2159
+ RegisterPage,
2160
+ ResetPasswordPage,
2161
+ b as VERSION,
2162
+ a as VERSION_INFO,
2163
+ c as VERSION_MAJOR,
2164
+ e as VERSION_MINOR,
2165
+ f as VERSION_REVISION,
2166
+ WebApp
2167
+ };
2168
+ //# sourceMappingURL=auth.es.js.map