JSWAI commited on
Commit
a77a436
·
verified ·
1 Parent(s): ea00e97

Update assets/js/login.js

Browse files
Files changed (1) hide show
  1. assets/js/login.js +135 -659
assets/js/login.js CHANGED
@@ -1,697 +1,173 @@
1
- /* ============================================================
2
- XENEX GLASS OS
3
- Login Controller
4
- ------------------------------------------------------------
5
- Real authentication frontend.
6
- No fake credentials.
7
- No dummy users.
8
- No hardcoded passwords.
9
- ============================================================ */
10
-
11
- "use strict";
12
-
13
- /* ============================================================
14
- CONFIGURATION
15
- ============================================================ */
16
 
17
- /*
18
- The backend URL should eventually be supplied by the real
19
- Xenex deployment configuration.
20
-
21
- Example:
22
-
23
- window.XENEX_CONFIG = {
24
- API_BASE_URL: "https://your-api.example.com"
25
- };
26
-
27
- Do NOT put AWS secrets here.
28
- */
29
-
30
- const API_BASE_URL =
31
- window.XENEX_CONFIG?.API_BASE_URL ||
32
- "";
33
-
34
-
35
- /* ============================================================
36
- DOM
37
- ============================================================ */
38
-
39
- const form = document.querySelector(".xenex-login__form");
40
-
41
- const emailInput = document.querySelector(
42
- 'input[type="email"]'
43
- );
44
-
45
- const passwordInput = document.querySelector(
46
- 'input[type="password"]'
47
- );
48
-
49
- const passwordToggle = document.querySelector(
50
- ".xenex-login__password-toggle"
51
- );
52
-
53
- const submitButton = document.querySelector(
54
- ".xenex-login__submit"
55
- );
56
-
57
- const messageElement = document.querySelector(
58
- ".xenex-login__message"
59
- );
60
-
61
- const connectionElement = document.querySelector(
62
- ".xenex-login__connection"
63
- );
64
-
65
- const forgotButton = document.querySelector(
66
- ".xenex-login__action:not(.xenex-login__action--create)"
67
- );
68
-
69
- const createAccountButton = document.querySelector(
70
- ".xenex-login__action--create"
71
- );
72
-
73
-
74
- /* ============================================================
75
- STATE
76
- ============================================================ */
77
-
78
- let loginInProgress = false;
79
-
80
-
81
- /* ============================================================
82
- HELPERS
83
- ============================================================ */
84
-
85
- function setMessage(message, state = "") {
86
- if (!messageElement) {
87
- return;
88
- }
89
-
90
- messageElement.textContent = message;
91
-
92
- if (state) {
93
- messageElement.dataset.state = state;
94
- } else {
95
- delete messageElement.dataset.state;
96
- }
97
- }
98
-
99
-
100
- function setConnectionState(state, text) {
101
- if (!connectionElement) {
102
- return;
103
- }
104
-
105
- connectionElement.dataset.state = state;
106
-
107
- const textElement =
108
- connectionElement.querySelector(
109
- "[data-connection-text]"
110
- );
111
-
112
- if (textElement && text) {
113
- textElement.textContent = text;
114
- }
115
- }
116
-
117
-
118
- function setLoading(loading) {
119
- loginInProgress = loading;
120
-
121
- if (!submitButton) {
122
- return;
123
- }
124
-
125
- submitButton.disabled = loading;
126
-
127
- const label =
128
- submitButton.querySelector("span");
129
-
130
- if (!label) {
131
- return;
132
- }
133
-
134
- if (loading) {
135
- label.textContent = "Signing in…";
136
- } else {
137
- label.textContent = "Sign in";
138
- }
139
- }
140
-
141
-
142
- function normalizeEmail(value) {
143
- return value
144
- .trim()
145
- .toLowerCase();
146
- }
147
 
 
 
 
 
148
 
149
- function isValidEmail(email) {
150
- /*
151
- Deliberately simple validation.
 
152
 
153
- The backend remains responsible for final
154
- validation and authentication.
155
- */
156
-
157
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
158
- }
159
 
 
 
160
 
161
- /* ============================================================
162
- PASSWORD VISIBILITY
163
- ============================================================ */
164
 
165
- function setupPasswordToggle() {
166
- if (!passwordToggle || !passwordInput) {
167
- return;
168
  }
169
-
170
- passwordToggle.addEventListener(
171
- "click",
172
- () => {
173
- const currentlyHidden =
174
- passwordInput.type === "password";
175
-
176
- passwordInput.type =
177
- currentlyHidden
178
- ? "text"
179
- : "password";
180
-
181
- passwordToggle.setAttribute(
182
- "aria-label",
183
- currentlyHidden
184
- ? "Hide password"
185
- : "Show password"
186
- );
187
-
188
- passwordToggle.setAttribute(
189
- "aria-pressed",
190
- String(currentlyHidden)
191
- );
192
- }
193
- );
194
  }
195
 
 
 
 
 
 
196
 
197
- /* ============================================================
198
- BACKEND REQUEST
199
- ============================================================ */
200
-
201
- async function authenticate(email, password) {
202
- if (!API_BASE_URL) {
203
- throw new Error(
204
- "Xenex backend is not configured."
205
- );
206
- }
207
-
208
- const endpoint =
209
- `${API_BASE_URL.replace(/\/$/, "")}/auth/login`;
210
-
211
- const response = await fetch(endpoint, {
212
- method: "POST",
213
-
214
- headers: {
215
- "Content-Type": "application/json",
216
- "Accept": "application/json"
217
- },
218
-
219
- /*
220
- Important for an authentication system using
221
- secure HttpOnly cookies.
222
-
223
- The backend should set the session cookie.
224
- JavaScript should NOT need to read it.
225
- */
226
-
227
- credentials: "include",
228
-
229
- body: JSON.stringify({
230
- email,
231
- password
232
- })
233
- });
234
-
235
-
236
- let data = null;
237
-
238
- try {
239
- data = await response.json();
240
- } catch {
241
- /*
242
- Some server errors may return an empty body.
243
- */
244
- }
245
-
246
-
247
- if (!response.ok) {
248
- const errorMessage =
249
- data?.message ||
250
- data?.detail ||
251
- "Unable to sign in.";
252
-
253
- const error = new Error(errorMessage);
254
-
255
- error.status = response.status;
256
-
257
- throw error;
258
- }
259
-
260
-
261
- return data;
262
- }
263
 
 
264
 
265
- /* ============================================================
266
- LOGIN
267
- ============================================================ */
268
-
269
- async function handleLogin(event) {
270
  event.preventDefault();
 
 
271
 
272
- if (loginInProgress) {
273
- return;
274
- }
275
-
276
- if (!emailInput || !passwordInput) {
277
- return;
278
- }
279
-
280
-
281
- const email =
282
- normalizeEmail(emailInput.value);
283
-
284
- const password =
285
- passwordInput.value;
286
-
287
-
288
- /* --------------------------------------------
289
- Client-side validation
290
- -------------------------------------------- */
291
-
292
- if (!email) {
293
- setMessage(
294
- "Enter your email address.",
295
- "error"
296
- );
297
-
298
- emailInput.focus();
299
-
300
- return;
301
- }
302
-
303
-
304
- if (!isValidEmail(email)) {
305
- setMessage(
306
- "Enter a valid email address.",
307
- "error"
308
- );
309
-
310
- emailInput.focus();
311
-
312
- return;
313
- }
314
 
315
-
316
- if (!password) {
317
- setMessage(
318
- "Enter your password.",
319
- "error"
320
- );
321
-
322
- passwordInput.focus();
323
-
324
- return;
325
  }
326
 
 
 
327
 
328
- /* --------------------------------------------
329
- Start authentication
330
- -------------------------------------------- */
331
-
332
- setLoading(true);
333
-
334
- setMessage(
335
- "Connecting to Xenex…"
336
- );
337
-
338
- setConnectionState(
339
- "connecting",
340
- "Connecting"
341
- );
342
-
343
-
344
- try {
345
- const result =
346
- await authenticate(
347
- email,
348
- password
349
- );
350
-
351
-
352
- /*
353
- Authentication succeeded.
354
-
355
- Ideally the backend establishes an
356
- HttpOnly secure session cookie.
357
-
358
- The frontend does not need to receive
359
- or store the password or session secret.
360
- */
361
-
362
-
363
- setMessage(
364
- "Sign in successful.",
365
- "success"
366
- );
367
-
368
- setConnectionState(
369
- "online",
370
- "Connected"
371
- );
372
-
373
-
374
- /*
375
- Give the glass interface a moment to
376
- display the successful state.
377
- */
378
-
379
- await wait(350);
380
-
381
-
382
- /*
383
- Backend may optionally provide a destination.
384
-
385
- Otherwise use the normal Xenex desktop.
386
- */
387
-
388
- const destination =
389
- result?.redirect ||
390
- result?.redirect_url ||
391
- "./desktop.html";
392
-
393
-
394
- window.location.replace(
395
- destination
396
- );
397
-
398
- } catch (error) {
399
-
400
- console.error(
401
- "Xenex authentication error:",
402
- error
403
- );
404
-
405
-
406
- /* ----------------------------------------
407
- Handle common HTTP responses
408
- ---------------------------------------- */
409
-
410
- if (error.status === 401) {
411
-
412
- setMessage(
413
- "Incorrect email or password.",
414
- "error"
415
- );
416
-
417
- } else if (error.status === 403) {
418
-
419
- setMessage(
420
- "Your account is not permitted to sign in.",
421
- "error"
422
- );
423
-
424
- } else if (error.status === 429) {
425
-
426
- setMessage(
427
- "Too many attempts. Please try again later.",
428
- "error"
429
- );
430
-
431
- } else {
432
-
433
- setMessage(
434
- error.message ||
435
- "Unable to connect to Xenex.",
436
- "error"
437
- );
438
- }
439
-
440
-
441
- setConnectionState(
442
- "offline",
443
- "Connection unavailable"
444
- );
445
-
446
- } finally {
447
-
448
- setLoading(false);
449
- }
450
  }
451
 
 
 
 
452
 
453
- /* ============================================================
454
- CONNECTION CHECK
455
- ============================================================ */
456
-
457
- async function checkBackendConnection() {
458
-
459
- if (!API_BASE_URL) {
460
-
461
- setConnectionState(
462
- "offline",
463
- "Backend not configured"
464
- );
465
-
466
- return;
467
- }
468
-
469
-
470
- try {
471
-
472
- const endpoint =
473
- `${API_BASE_URL.replace(/\/$/, "")}/health`;
474
-
475
-
476
- const response =
477
- await fetch(endpoint, {
478
- method: "GET",
479
-
480
- headers: {
481
- "Accept": "application/json"
482
- },
483
-
484
- credentials: "include",
485
-
486
- cache: "no-store"
487
- });
488
-
489
 
490
- if (!response.ok) {
491
- throw new Error(
492
- `Backend returned ${response.status}`
493
- );
494
- }
495
 
496
-
497
- setConnectionState(
498
- "online",
499
- "Xenex services online"
500
- );
501
-
502
- } catch (error) {
503
-
504
- console.warn(
505
- "Xenex backend unavailable:",
506
- error
507
- );
508
-
509
- setConnectionState(
510
- "offline",
511
- "Xenex services unavailable"
512
- );
513
- }
514
  }
515
 
516
-
517
- /* ============================================================
518
- NAVIGATION
519
- ============================================================ */
520
-
521
- function setupNavigation() {
522
-
523
- if (forgotButton) {
524
-
525
- forgotButton.addEventListener(
526
- "click",
527
- () => {
528
- window.location.href =
529
- "./forgot-password.html";
530
- }
531
- );
532
- }
533
-
534
-
535
- if (createAccountButton) {
536
-
537
- createAccountButton.addEventListener(
538
- "click",
539
- () => {
540
- window.location.href =
541
- "./create-account.html";
542
- }
543
- );
544
- }
 
545
  }
546
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
 
548
- /* ============================================================
549
- INPUT BEHAVIOR
550
- ============================================================ */
551
-
552
- function setupInputs() {
553
-
554
- if (emailInput) {
555
-
556
- emailInput.addEventListener(
557
- "input",
558
- () => {
559
-
560
- if (
561
- messageElement?.dataset.state ===
562
- "error"
563
- ) {
564
- setMessage("");
565
- }
566
- }
567
- );
568
- }
569
-
570
-
571
- if (passwordInput) {
572
 
573
- passwordInput.addEventListener(
574
- "input",
575
- () => {
576
 
577
- if (
578
- messageElement?.dataset.state ===
579
- "error"
580
- ) {
581
- setMessage("");
582
- }
583
- }
584
- );
585
- }
586
  }
587
 
588
-
589
- /* ============================================================
590
- ENTER KEY
591
- ============================================================ */
592
-
593
- function setupKeyboardBehavior() {
594
-
595
- document.addEventListener(
596
- "keydown",
597
- (event) => {
598
-
599
- if (
600
- event.key === "Enter" &&
601
- !loginInProgress
602
- ) {
603
-
604
- /*
605
- Don't interfere with normal form
606
- submission. The browser will handle
607
- Enter inside the form naturally.
608
- */
609
-
610
- if (
611
- document.activeElement ===
612
- emailInput ||
613
- document.activeElement ===
614
- passwordInput
615
- ) {
616
- return;
617
- }
618
- }
619
- }
620
- );
621
  }
622
 
623
-
624
- /* ============================================================
625
- UTILITY
626
- ============================================================ */
627
-
628
- function wait(milliseconds) {
629
- return new Promise(
630
- resolve =>
631
- setTimeout(
632
- resolve,
633
- milliseconds
634
- )
635
- );
636
  }
637
 
638
-
639
- /* ============================================================
640
- INITIALIZATION
641
- ============================================================ */
642
-
643
- function initializeLogin() {
644
-
645
- if (!form) {
646
- console.error(
647
- "Xenex login form was not found."
648
- );
649
-
650
- return;
651
- }
652
-
653
-
654
- setupPasswordToggle();
655
-
656
- setupNavigation();
657
-
658
- setupInputs();
659
-
660
- setupKeyboardBehavior();
661
-
662
-
663
- form.addEventListener(
664
- "submit",
665
- handleLogin
666
- );
667
-
668
-
669
- /*
670
- Check the backend after the page loads.
671
-
672
- This is deliberately non-blocking:
673
- the user can still interact with the
674
- login screen while the health check runs.
675
- */
676
-
677
- checkBackendConnection();
678
  }
679
-
680
-
681
- /* ============================================================
682
- START
683
- ============================================================ */
684
-
685
- if (
686
- document.readyState === "loading"
687
- ) {
688
-
689
- document.addEventListener(
690
- "DOMContentLoaded",
691
- initializeLogin
692
- );
693
-
694
- } else {
695
-
696
- initializeLogin();
697
- }
 
1
+ /* ==========================================================================
2
+ XENEX GLASS OS — Login Screen Behavior
3
+ assets/JS/login.js
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
+ This file handles UI behavior only: password visibility and client-side
6
+ form validation. It does NOT authenticate anyone. There is no backend
7
+ yet, so submitting the form cannot and does not log a user in — it only
8
+ reports that sign-in isn't wired up yet. See the comment above
9
+ handleSubmit() for where the real API call goes once it exists.
10
+ ========================================================================== */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ document.addEventListener('DOMContentLoaded', () => {
13
+ initPasswordToggle();
14
+ initFormValidation();
15
+ });
16
 
17
+ /* ---- Password visibility toggle ---- */
18
+ function initPasswordToggle() {
19
+ const toggleButton = document.querySelector('[data-password-toggle]');
20
+ const passwordInput = document.getElementById('password');
21
 
22
+ if (!toggleButton || !passwordInput) return;
 
 
 
 
 
23
 
24
+ toggleButton.addEventListener('click', () => {
25
+ const isHidden = passwordInput.type === 'password';
26
 
27
+ passwordInput.type = isHidden ? 'text' : 'password';
28
+ toggleButton.setAttribute('aria-pressed', String(isHidden));
29
+ toggleButton.setAttribute('aria-label', isHidden ? 'Hide password' : 'Show password');
30
 
31
+ const toggleText = toggleButton.querySelector('.login-field__toggle-text');
32
+ if (toggleText) {
33
+ toggleText.textContent = isHidden ? 'Hide' : 'Show';
34
  }
35
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  }
37
 
38
+ /* ---- Form validation + submit ---- */
39
+ function initFormValidation() {
40
+ const form = document.querySelector('.login-form');
41
+ const emailInput = document.getElementById('email');
42
+ const passwordInput = document.getElementById('password');
43
 
44
+ if (!form || !emailInput || !passwordInput) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
+ const statusRegion = createStatusRegion(form);
47
 
48
+ form.addEventListener('submit', (event) => {
 
 
 
 
49
  event.preventDefault();
50
+ clearFieldErrors(form);
51
+ setStatus(statusRegion, '', null);
52
 
53
+ const errors = validateFields(emailInput, passwordInput);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
+ if (errors.length > 0) {
56
+ errors.forEach(({ input, message }) => showFieldError(input, message));
57
+ errors[0].input.focus();
58
+ return;
 
 
 
 
 
 
59
  }
60
 
61
+ handleSubmit({ email: emailInput.value.trim() });
62
+ });
63
 
64
+ // Clear a field's error state as soon as the person edits it.
65
+ [emailInput, passwordInput].forEach((input) => {
66
+ input.addEventListener('input', () => clearFieldError(input));
67
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  }
69
 
70
+ function validateFields(emailInput, passwordInput) {
71
+ const errors = [];
72
+ const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
73
 
74
+ if (emailInput.value.trim() === '') {
75
+ errors.push({ input: emailInput, message: 'Enter your email address.' });
76
+ } else if (!emailPattern.test(emailInput.value.trim())) {
77
+ errors.push({ input: emailInput, message: 'Enter a valid email address.' });
78
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
+ if (passwordInput.value === '') {
81
+ errors.push({ input: passwordInput, message: 'Enter your password.' });
82
+ }
 
 
83
 
84
+ return errors;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  }
86
 
87
+ /*
88
+ * There is no backend yet. When one exists, this is where the real
89
+ * request goes, e.g.:
90
+ *
91
+ * const response = await fetch('/auth/login', {
92
+ * method: 'POST',
93
+ * headers: { 'Content-Type': 'application/json' },
94
+ * body: JSON.stringify({ email, password }),
95
+ * credentials: 'include' // secure HttpOnly session cookie, not a token in JS
96
+ * });
97
+ *
98
+ * Until that endpoint exists, do not simulate a response. Tell the person
99
+ * plainly that sign-in isn't connected yet.
100
+ */
101
+ function handleSubmit({ email }) {
102
+ const form = document.querySelector('.login-form');
103
+ const statusRegion = form.querySelector('[data-login-status]');
104
+ const submitButton = document.querySelector('[data-login-submit]');
105
+
106
+ submitButton.disabled = true;
107
+
108
+ setStatus(
109
+ statusRegion,
110
+ "Sign-in isn't connected to a server yet. This form will work once the Xenex backend is live.",
111
+ 'info'
112
+ );
113
+
114
+ console.info('[Xenex] Login form submitted with no backend configured.', { email });
115
+
116
+ submitButton.disabled = false;
117
  }
118
 
119
+ /* ---- Field error helpers ---- */
120
+ function showFieldError(input, message) {
121
+ const field = input.closest('.login-field');
122
+ if (!field) return;
123
+
124
+ field.classList.add('login-field--error');
125
+ input.setAttribute('aria-invalid', 'true');
126
+
127
+ let errorEl = field.querySelector('.login-field__error');
128
+ if (!errorEl) {
129
+ errorEl = document.createElement('p');
130
+ errorEl.className = 'login-field__error';
131
+ errorEl.id = `${input.id}-error`;
132
+ field.appendChild(errorEl);
133
+ }
134
+ errorEl.textContent = message;
135
+ input.setAttribute('aria-describedby', errorEl.id);
136
+ }
137
 
138
+ function clearFieldError(input) {
139
+ const field = input.closest('.login-field');
140
+ if (!field) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
+ field.classList.remove('login-field--error');
143
+ input.removeAttribute('aria-invalid');
144
+ input.removeAttribute('aria-describedby');
145
 
146
+ const errorEl = field.querySelector('.login-field__error');
147
+ if (errorEl) errorEl.remove();
 
 
 
 
 
 
 
148
  }
149
 
150
+ function clearFieldErrors(form) {
151
+ form.querySelectorAll('.login-field__input').forEach(clearFieldError);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  }
153
 
154
+ /* ---- Status region (created once, announced to screen readers) ---- */
155
+ function createStatusRegion(form) {
156
+ let region = form.querySelector('[data-login-status]');
157
+ if (region) return region;
158
+
159
+ region = document.createElement('p');
160
+ region.setAttribute('data-login-status', '');
161
+ region.className = 'login-status';
162
+ region.setAttribute('role', 'status');
163
+ region.setAttribute('aria-live', 'polite');
164
+ form.appendChild(region);
165
+ return region;
 
166
  }
167
 
168
+ function setStatus(region, message, kind) {
169
+ if (!region) return;
170
+ region.textContent = message;
171
+ region.classList.toggle('login-status--info', kind === 'info');
172
+ region.classList.toggle('login-status--error', kind === 'error');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  }