pict-section-login 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/example_applications/custom_login/Custom-Login-Application.js +75 -0
  2. package/example_applications/custom_login/html/index.html +110 -0
  3. package/example_applications/custom_login/package.json +27 -0
  4. package/example_applications/harness_app/Harness-App-Application.js +167 -0
  5. package/example_applications/harness_app/Harness-App-Configuration.json +4 -0
  6. package/example_applications/harness_app/html/index.html +90 -0
  7. package/example_applications/harness_app/package.json +28 -0
  8. package/example_applications/harness_app/providers/PictRouter-HarnessApp.json +24 -0
  9. package/example_applications/harness_app/views/PictView-HarnessApp-Books.js +172 -0
  10. package/example_applications/harness_app/views/PictView-HarnessApp-Dashboard.js +158 -0
  11. package/example_applications/harness_app/views/PictView-HarnessApp-Layout.js +86 -0
  12. package/example_applications/harness_app/views/PictView-HarnessApp-Login.js +58 -0
  13. package/example_applications/harness_app/views/PictView-HarnessApp-TopBar.js +157 -0
  14. package/example_applications/harness_app/views/PictView-HarnessApp-Users.js +188 -0
  15. package/example_applications/oauth_login/OAuth-Login-Application.js +78 -0
  16. package/example_applications/oauth_login/html/index.html +57 -0
  17. package/example_applications/oauth_login/package.json +27 -0
  18. package/example_applications/orator_login/Orator-Login-Application.js +61 -0
  19. package/example_applications/orator_login/html/index.html +51 -0
  20. package/example_applications/orator_login/package.json +27 -0
  21. package/package.json +53 -0
  22. package/source/Pict-Section-Login-DefaultConfiguration.js +265 -0
  23. package/source/Pict-Section-Login.js +533 -0
  24. package/test/Browser_Integration_tests.js +588 -0
  25. package/test/Pict-Section-Login_tests.js +593 -0
@@ -0,0 +1,533 @@
1
+ const libPictViewClass = require('pict-view');
2
+ const _DefaultConfiguration = require('./Pict-Section-Login-DefaultConfiguration.js');
3
+
4
+ class PictSectionLogin extends libPictViewClass
5
+ {
6
+ constructor(pFable, pOptions, pServiceHash)
7
+ {
8
+ let tmpOptions = Object.assign({}, _DefaultConfiguration, pOptions);
9
+ super(pFable, tmpOptions, pServiceHash);
10
+
11
+ // --- State ---
12
+ this.authenticated = false;
13
+ this.sessionData = null;
14
+ this.oauthProviders = [];
15
+
16
+ this.initialRenderComplete = false;
17
+ }
18
+
19
+ // ===== Lifecycle Hooks =====
20
+
21
+ onBeforeInitialize()
22
+ {
23
+ return super.onBeforeInitialize();
24
+ }
25
+
26
+ onAfterRender(pRenderable)
27
+ {
28
+ // Inject all registered CSS into the DOM
29
+ this.pict.CSSMap.injectCSS();
30
+
31
+ if (!this.initialRenderComplete)
32
+ {
33
+ this.onAfterInitialRender();
34
+ this.initialRenderComplete = true;
35
+ }
36
+
37
+ return super.onAfterRender(pRenderable);
38
+ }
39
+
40
+ onAfterInitialRender()
41
+ {
42
+ // Populate the form (or status) into the wrapper placeholders
43
+ this._updateView();
44
+
45
+ if (this.options.CheckSessionOnLoad)
46
+ {
47
+ this.checkSession();
48
+ }
49
+
50
+ if (this.options.ShowOAuthProviders)
51
+ {
52
+ this.loadOAuthProviders();
53
+ }
54
+ }
55
+
56
+ // ===== Public API =====
57
+
58
+ /**
59
+ * Authenticate with username and password.
60
+ *
61
+ * @param {string} pUsername - The username / login ID
62
+ * @param {string} pPassword - The password
63
+ * @param {function} [fCallback] - Optional callback(pError, pSessionData)
64
+ */
65
+ login(pUsername, pPassword, fCallback)
66
+ {
67
+ if (typeof (fCallback) !== 'function')
68
+ {
69
+ fCallback = () => {};
70
+ }
71
+
72
+ this._clearError();
73
+
74
+ let tmpFetchOptions = {};
75
+ let tmpURL = this.options.LoginEndpoint;
76
+
77
+ if (this.options.LoginMethod === 'GET')
78
+ {
79
+ tmpURL = tmpURL + '/' + encodeURIComponent(pUsername) + '/' + encodeURIComponent(pPassword);
80
+ tmpFetchOptions.method = 'GET';
81
+ }
82
+ else
83
+ {
84
+ tmpFetchOptions.method = 'POST';
85
+ tmpFetchOptions.headers = { 'Content-Type': 'application/json' };
86
+ tmpFetchOptions.body = JSON.stringify({ UserName: pUsername, Password: pPassword });
87
+ }
88
+
89
+ fetch(tmpURL, tmpFetchOptions)
90
+ .then((pResponse) =>
91
+ {
92
+ return pResponse.json();
93
+ })
94
+ .then((pData) =>
95
+ {
96
+ if (pData && pData.LoggedIn)
97
+ {
98
+ this.authenticated = true;
99
+ this.sessionData = pData;
100
+ this._storeSessionData(pData);
101
+ this._updateView();
102
+ this.onLoginSuccess(pData);
103
+ this._solveApp();
104
+ return fCallback(null, pData);
105
+ }
106
+ else
107
+ {
108
+ let tmpError = (pData && pData.Error) ? pData.Error : 'Authentication failed.';
109
+ this._displayError(tmpError);
110
+ this.onLoginFailed(tmpError);
111
+ return fCallback(tmpError);
112
+ }
113
+ })
114
+ .catch((pError) =>
115
+ {
116
+ let tmpMessage = 'Login request failed.';
117
+ this.log.error('PictSectionLogin login error: ' + pError.message);
118
+ this._displayError(tmpMessage);
119
+ this.onLoginFailed(tmpMessage);
120
+ return fCallback(tmpMessage);
121
+ });
122
+ }
123
+
124
+ /**
125
+ * End the current session.
126
+ *
127
+ * @param {function} [fCallback] - Optional callback(pError)
128
+ */
129
+ logout(fCallback)
130
+ {
131
+ if (typeof (fCallback) !== 'function')
132
+ {
133
+ fCallback = () => {};
134
+ }
135
+
136
+ fetch(this.options.LogoutEndpoint)
137
+ .then((pResponse) =>
138
+ {
139
+ return pResponse.json();
140
+ })
141
+ .then(() =>
142
+ {
143
+ this.authenticated = false;
144
+ this.sessionData = null;
145
+ this._storeSessionData(null);
146
+ this._updateView();
147
+ this.onLogout();
148
+ this._solveApp();
149
+ return fCallback(null);
150
+ })
151
+ .catch((pError) =>
152
+ {
153
+ this.log.error('PictSectionLogin logout error: ' + pError.message);
154
+ // Clear local state even if network failed
155
+ this.authenticated = false;
156
+ this.sessionData = null;
157
+ this._storeSessionData(null);
158
+ this._updateView();
159
+ this.onLogout();
160
+ this._solveApp();
161
+ return fCallback(pError.message);
162
+ });
163
+ }
164
+
165
+ /**
166
+ * Check whether an existing session is active (e.g. from a cookie).
167
+ *
168
+ * @param {function} [fCallback] - Optional callback(pError, pSessionData)
169
+ */
170
+ checkSession(fCallback)
171
+ {
172
+ if (typeof (fCallback) !== 'function')
173
+ {
174
+ fCallback = () => {};
175
+ }
176
+
177
+ fetch(this.options.CheckSessionEndpoint)
178
+ .then((pResponse) =>
179
+ {
180
+ return pResponse.json();
181
+ })
182
+ .then((pData) =>
183
+ {
184
+ if (pData && pData.LoggedIn)
185
+ {
186
+ this.authenticated = true;
187
+ this.sessionData = pData;
188
+ this._storeSessionData(pData);
189
+ this._updateView();
190
+ }
191
+ this.onSessionChecked(pData);
192
+ this._solveApp();
193
+ return fCallback(null, pData);
194
+ })
195
+ .catch((pError) =>
196
+ {
197
+ this.log.error('PictSectionLogin checkSession error: ' + pError.message);
198
+ this.onSessionChecked(null);
199
+ return fCallback(pError.message);
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Fetch available OAuth providers and render buttons.
205
+ *
206
+ * @param {function} [fCallback] - Optional callback(pError, pProviders)
207
+ */
208
+ loadOAuthProviders(fCallback)
209
+ {
210
+ if (typeof (fCallback) !== 'function')
211
+ {
212
+ fCallback = () => {};
213
+ }
214
+
215
+ fetch(this.options.OAuthProvidersEndpoint)
216
+ .then((pResponse) =>
217
+ {
218
+ return pResponse.json();
219
+ })
220
+ .then((pData) =>
221
+ {
222
+ if (pData && Array.isArray(pData.Providers))
223
+ {
224
+ this.oauthProviders = pData.Providers;
225
+ this._renderOAuthButtons();
226
+ }
227
+ return fCallback(null, this.oauthProviders);
228
+ })
229
+ .catch((pError) =>
230
+ {
231
+ this.log.warn('PictSectionLogin loadOAuthProviders: ' + pError.message);
232
+ return fCallback(pError.message);
233
+ });
234
+ }
235
+
236
+ // ===== Overridable Hooks =====
237
+ // Developers override these for custom post-login/logout behavior.
238
+
239
+ /**
240
+ * Called after a successful login.
241
+ * @param {object} pSessionData - The session data from the server
242
+ */
243
+ onLoginSuccess(pSessionData)
244
+ {
245
+ // Override in subclass or instance
246
+ }
247
+
248
+ /**
249
+ * Called after a failed login attempt.
250
+ * @param {string} pError - The error message
251
+ */
252
+ onLoginFailed(pError)
253
+ {
254
+ // Override in subclass or instance
255
+ }
256
+
257
+ /**
258
+ * Called after a successful logout.
259
+ */
260
+ onLogout()
261
+ {
262
+ // Override in subclass or instance
263
+ }
264
+
265
+ /**
266
+ * Called after a session check completes.
267
+ * @param {object|null} pSessionData - The session data, or null on error
268
+ */
269
+ onSessionChecked(pSessionData)
270
+ {
271
+ // Override in subclass or instance
272
+ }
273
+
274
+ // ===== Internal Helpers =====
275
+
276
+ /**
277
+ * Wire up DOM event handlers on the login form and logout button.
278
+ */
279
+ _wireFormEvents()
280
+ {
281
+ let tmpFormElements = this.services.ContentAssignment.getElement('#pict-login-form');
282
+ if (tmpFormElements && tmpFormElements.length > 0)
283
+ {
284
+ let tmpForm = tmpFormElements[0];
285
+ tmpForm.addEventListener('submit', (pEvent) =>
286
+ {
287
+ pEvent.preventDefault();
288
+ let tmpUsernameInput = tmpForm.querySelector('#pict-login-username');
289
+ let tmpPasswordInput = tmpForm.querySelector('#pict-login-password');
290
+
291
+ let tmpUsername = tmpUsernameInput ? tmpUsernameInput.value : '';
292
+ let tmpPassword = tmpPasswordInput ? tmpPasswordInput.value : '';
293
+
294
+ if (!tmpUsername)
295
+ {
296
+ this._displayError('Please enter a username.');
297
+ return;
298
+ }
299
+
300
+ this.login(tmpUsername, tmpPassword);
301
+ });
302
+ }
303
+
304
+ let tmpLogoutElements = this.services.ContentAssignment.getElement('#pict-login-logout');
305
+ if (tmpLogoutElements && tmpLogoutElements.length > 0)
306
+ {
307
+ tmpLogoutElements[0].addEventListener('click', () =>
308
+ {
309
+ this.logout();
310
+ });
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Show an error message in the error area.
316
+ * @param {string} pMessage - The error text
317
+ */
318
+ _displayError(pMessage)
319
+ {
320
+ let tmpErrorElements = this.services.ContentAssignment.getElement('#pict-login-error');
321
+ if (tmpErrorElements && tmpErrorElements.length > 0)
322
+ {
323
+ let tmpErrorEl = tmpErrorElements[0];
324
+ tmpErrorEl.textContent = pMessage;
325
+ tmpErrorEl.style.display = 'block';
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Hide the error area.
331
+ */
332
+ _clearError()
333
+ {
334
+ let tmpErrorElements = this.services.ContentAssignment.getElement('#pict-login-error');
335
+ if (tmpErrorElements && tmpErrorElements.length > 0)
336
+ {
337
+ let tmpErrorEl = tmpErrorElements[0];
338
+ tmpErrorEl.textContent = '';
339
+ tmpErrorEl.style.display = 'none';
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Re-render the view to reflect current authentication state.
345
+ * Shows the login form when unauthenticated, or the status bar when authenticated.
346
+ */
347
+ _updateView()
348
+ {
349
+ let tmpFormAreaElements = this.services.ContentAssignment.getElement('#pict-login-form-area');
350
+ let tmpStatusAreaElements = this.services.ContentAssignment.getElement('#pict-login-status-area');
351
+ let tmpOAuthAreaElements = this.services.ContentAssignment.getElement('#pict-login-oauth-area');
352
+
353
+ if (this.authenticated && this.sessionData)
354
+ {
355
+ // --- Authenticated: show status, hide form ---
356
+ if (tmpFormAreaElements && tmpFormAreaElements.length > 0)
357
+ {
358
+ tmpFormAreaElements[0].style.display = 'none';
359
+ }
360
+ if (tmpOAuthAreaElements && tmpOAuthAreaElements.length > 0)
361
+ {
362
+ tmpOAuthAreaElements[0].style.display = 'none';
363
+ }
364
+ if (tmpStatusAreaElements && tmpStatusAreaElements.length > 0)
365
+ {
366
+ let tmpStatusArea = tmpStatusAreaElements[0];
367
+ tmpStatusArea.style.display = 'block';
368
+
369
+ // Render the status template
370
+ let tmpStatusHTML = this.pict.parseTemplateByHash('Pict-Login-Template-Status', {});
371
+ tmpStatusArea.innerHTML = tmpStatusHTML;
372
+
373
+ // Populate display values
374
+ let tmpDisplayName = '';
375
+ let tmpDisplayID = '';
376
+
377
+ if (this.sessionData.UserRecord)
378
+ {
379
+ tmpDisplayName = this.sessionData.UserRecord.FullName
380
+ || this.sessionData.UserRecord.LoginID
381
+ || this.sessionData.UserRecord.Email
382
+ || '';
383
+ tmpDisplayID = this.sessionData.UserRecord.IDUser || this.sessionData.UserID || '';
384
+ }
385
+ else if (this.sessionData.UserID)
386
+ {
387
+ tmpDisplayID = this.sessionData.UserID;
388
+ }
389
+
390
+ let tmpNameElements = tmpStatusArea.querySelectorAll('#pict-login-display-name');
391
+ if (tmpNameElements.length > 0)
392
+ {
393
+ tmpNameElements[0].textContent = tmpDisplayName;
394
+ }
395
+
396
+ let tmpIDElements = tmpStatusArea.querySelectorAll('#pict-login-display-id');
397
+ if (tmpIDElements.length > 0)
398
+ {
399
+ if (tmpDisplayID)
400
+ {
401
+ tmpIDElements[0].textContent = 'ID ' + tmpDisplayID;
402
+ }
403
+ else
404
+ {
405
+ tmpIDElements[0].style.display = 'none';
406
+ }
407
+ }
408
+
409
+ // Wire logout button
410
+ let tmpLogoutBtn = tmpStatusArea.querySelector('#pict-login-logout');
411
+ if (tmpLogoutBtn)
412
+ {
413
+ tmpLogoutBtn.addEventListener('click', () =>
414
+ {
415
+ this.logout();
416
+ });
417
+ }
418
+ }
419
+ this._clearError();
420
+ }
421
+ else
422
+ {
423
+ // --- Unauthenticated: show form, hide status ---
424
+ if (tmpStatusAreaElements && tmpStatusAreaElements.length > 0)
425
+ {
426
+ tmpStatusAreaElements[0].style.display = 'none';
427
+ tmpStatusAreaElements[0].innerHTML = '';
428
+ }
429
+ if (tmpFormAreaElements && tmpFormAreaElements.length > 0)
430
+ {
431
+ let tmpFormArea = tmpFormAreaElements[0];
432
+ tmpFormArea.style.display = 'block';
433
+
434
+ // Re-render form if empty
435
+ if (!tmpFormArea.querySelector('#pict-login-form'))
436
+ {
437
+ let tmpFormHTML = this.pict.parseTemplateByHash('Pict-Login-Template-Form', {});
438
+ tmpFormArea.innerHTML = tmpFormHTML;
439
+ this._wireFormEvents();
440
+ }
441
+ }
442
+ if (tmpOAuthAreaElements && tmpOAuthAreaElements.length > 0)
443
+ {
444
+ if (this.options.ShowOAuthProviders && this.oauthProviders.length > 0)
445
+ {
446
+ tmpOAuthAreaElements[0].style.display = 'block';
447
+ }
448
+ else
449
+ {
450
+ tmpOAuthAreaElements[0].style.display = 'none';
451
+ }
452
+ }
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Render OAuth provider buttons into the OAuth area.
458
+ */
459
+ _renderOAuthButtons()
460
+ {
461
+ let tmpOAuthAreaElements = this.services.ContentAssignment.getElement('#pict-login-oauth-area');
462
+ if (!tmpOAuthAreaElements || tmpOAuthAreaElements.length < 1)
463
+ {
464
+ return;
465
+ }
466
+
467
+ if (!this.oauthProviders || this.oauthProviders.length < 1)
468
+ {
469
+ tmpOAuthAreaElements[0].style.display = 'none';
470
+ return;
471
+ }
472
+
473
+ // Render the OAuth container template
474
+ let tmpOAuthHTML = this.pict.parseTemplateByHash('Pict-Login-Template-OAuthProviders', {});
475
+ tmpOAuthAreaElements[0].innerHTML = tmpOAuthHTML;
476
+ tmpOAuthAreaElements[0].style.display = 'block';
477
+
478
+ // Build individual provider buttons
479
+ let tmpButtonContainer = tmpOAuthAreaElements[0].querySelector('#pict-login-oauth-buttons');
480
+ if (!tmpButtonContainer)
481
+ {
482
+ return;
483
+ }
484
+
485
+ let tmpButtonsHTML = '';
486
+ for (let i = 0; i < this.oauthProviders.length; i++)
487
+ {
488
+ let tmpProvider = this.oauthProviders[i];
489
+ let tmpName = tmpProvider.Name || 'provider';
490
+ let tmpBeginURL = tmpProvider.BeginURL || (this.options.OAuthBeginEndpoint + '/' + tmpName);
491
+ let tmpDisplayName = tmpName.charAt(0).toUpperCase() + tmpName.slice(1);
492
+ let tmpCSSModifier = tmpName.toLowerCase();
493
+
494
+ tmpButtonsHTML += '<a class="pict-login-oauth-btn pict-login-oauth-btn--' + tmpCSSModifier + '" href="' + tmpBeginURL + '">Sign in with ' + tmpDisplayName + '</a>';
495
+ }
496
+
497
+ tmpButtonContainer.innerHTML = tmpButtonsHTML;
498
+ }
499
+
500
+ /**
501
+ * Store session data at the configured Pict address.
502
+ * @param {object|null} pData - Session data to store
503
+ */
504
+ _storeSessionData(pData)
505
+ {
506
+ if (this.options.SessionDataAddress)
507
+ {
508
+ let tmpAddressSpace =
509
+ {
510
+ Fable: this.fable,
511
+ Pict: this.fable,
512
+ AppData: this.pict.AppData,
513
+ Bundle: this.pict.Bundle
514
+ };
515
+ this.fable.manifest.setValueByHash(tmpAddressSpace, this.options.SessionDataAddress, pData);
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Trigger a solve on the PictApplication if one exists.
521
+ */
522
+ _solveApp()
523
+ {
524
+ if (this.pict && this.pict.PictApplication && typeof (this.pict.PictApplication.solve) === 'function')
525
+ {
526
+ this.pict.PictApplication.solve();
527
+ }
528
+ }
529
+ }
530
+
531
+ module.exports = PictSectionLogin;
532
+
533
+ module.exports.default_configuration = _DefaultConfiguration;