web-mojo 2.1.1043 → 2.1.1044

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