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