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