bump package version to 1.5.0
[squeep-authentication-module] / lib / template / login-html.js
1 'use strict';
2
3 const { TemplateHelper: th } = require('@squeep/html-template-helper');
4 const { sessionNavLinks } = require('./helpers');
5
6 /**
7 * @typedef {object} Context
8 * @property {string} clientProtocol http/https
9 */
10
11 /**
12 * @typedef {object} HtmlOptions
13 * @property {string[]=} authnEnabled list of active authn methods
14 * @property {string} indieAuthBlurb content accompanying login fields
15 * @property {boolean} secureAuthOnly do not display user logiin if insecure and not allowed
16 * @property {string} userBlurb content accompanying login fields
17 */
18
19 /**
20 * IndieAuth Profile login form.
21 * @param {Context} ctx context
22 * @param {HtmlOptions} options options
23 * @returns {string} section
24 */
25 function indieAuthSection(ctx, options) {
26 const indieAuthBlurb = (options.indieAuthBlurb || []).map((x) => '\t'.repeat(6) + x).join('\n');
27 const showIndieAuthForm = options.authnEnabled.includes('indieAuth');
28 return showIndieAuthForm ? `\t\t\t<section class="indieauth">
29 \t\t\t\t<h2>Login</h2>
30 \t\t\t\t<form method="POST">
31 \t\t\t\t\t<fieldset>
32 \t\t\t\t\t\t<legend>IndieAuth</legend>
33 \t\t\t\t\t\t<label for="me">Profile URL:</label>
34 \t\t\t\t\t\t<input id="me" name="me" type="url" size="40" placeholder="https://example.com/my_profile_url" value="" autofocus>
35 \t\t\t\t\t\t<input type="hidden" name="me_auto_scheme">
36 \t\t\t\t\t\t<button type="submit">Login</button>
37 ${indieAuthBlurb}
38 \t\t\t\t\t</fieldset>
39 \t\t\t\t</form>
40 \t\t\t</section>`
41 : '';
42 }
43
44
45 /**
46 * Default all URL inputs to https if scheme not specified,
47 * and set a flag if that happened.
48 * From https://aaronparecki.com/2019/05/13/2/https
49 * @param {Context} ctx context
50 * @param {HtmlOptions} options options
51 * @returns {string} script
52 */
53 function indieAuthURLTrySecureFirstScript(ctx, options) {
54 const showIndieAuthForm = options.authnEnabled.includes('indieAuth');
55 return showIndieAuthForm ? `
56 <script>
57 document.addEventListener('DOMContentLoaded', function() {
58 function addDefaultScheme(target) {
59 if (target.value.match(/^(?!https?:).+\\..+/)) {
60 const autoSchemeField = document.querySelector('input[name=' + target.getAttribute('name') + '_auto_scheme]');
61 let scheme;
62 if (autoSchemeField) {
63 scheme = 'https://';
64 autoSchemeField.value = '1';
65 } else {
66 scheme = 'http://';
67 }
68 target.value = scheme + target.value;
69 }
70 }
71 const elements = document.querySelectorAll('input[type=url]');
72 Array.prototype.forEach.call(elements, function (el, i) {
73 el.addEventListener('blur', function(e) {
74 addDefaultScheme(e.target);
75 });
76 el.addEventListener('keydown', function(e) {
77 if (e.keyCode === 13) {
78 addDefaultScheme(e.target);
79 }
80 });
81 });
82 });
83 </script>` : '';
84 }
85
86 /**
87 * Display user section when any of these methods are active.
88 */
89 const userAuthn = ['argon2', 'pam'];
90 /**
91 * Local identifier/credential login form.
92 * @param {Context} ctx context
93 * @param {HtmlOptions} options options
94 * @returns {string} section
95 */
96 function userSection(ctx, options) {
97 const userBlurb = (options.userBlurb || []).map((x) => '\t'.repeat(6) + x).join('\n');
98 const secure = (ctx.clientProtocol || '').toLowerCase() === 'https';
99 const showUserForm = options.authnEnabled.filter((x) => userAuthn.includes(x)).length
100 && (secure || !options.secureAuthOnly);
101 return showUserForm ? `\t\t\t<section class="user">
102 \t\t\t\t<form method="POST">
103 \t\t\t\t\t<fieldset>
104 \t\t\t\t\t\t<legend>User Account</legend>
105 \t\t\t\t\t\t<label for="identifier">Username:</label>
106 \t\t\t\t\t\t<input id="identifier" name="identifier" type="text" value="">
107 \t\t\t\t\t\t<br>
108 \t\t\t\t\t\t<label for="credential">Password:</label>
109 \t\t\t\t\t\t<input id="credential" name="credential" type="password" value="">
110 \t\t\t\t\t\t<br>
111 \t\t\t\t\t\t<button type="submit">Login</button>
112 ${userBlurb}
113 \t\t\t\t\t</fieldset>
114 \t\t\t\t</form>
115 \t\t\t</section>`
116 : '';
117 }
118
119 /**
120 * @typedef {import('../session-manager').AppTemplateCallback} AppTemplateCallback
121 */
122
123 /**
124 * Render login form for both local and profile authentication.
125 * @param {Context} ctx context
126 * @param {object} options options
127 * @param {boolean} options.authenticator.secureAuthOnly do not display user login if not secure or allowed
128 * @param {string[]=} options.authenticator.loginBlurb content included at top of page
129 * @param {string} options.authenticator.indieAuthBlurb content included with indieauth login
130 * @param {string} options.authenticator.userBlurb content included with local user login
131 * @param {object} options.manager manager options
132 * @param {string} options.manager.pageTitle page title
133 * @param {string=} options.manager.logoUrl url
134 * @param {string=} options.manager.logoAlt alt for logo
135 * @param {object} options.dingus dingus options
136 * @param {string} options.dingus.selfBaseUrl root url
137 * @param {AppTemplateCallback} appCb function to mogrify template htmlOptions
138 * @returns {string} page
139 */
140 module.exports = (ctx, options, appCb = () => {}) => {
141 const pagePathLevel = 1;
142 /**
143 * @type {HtmlOptions}
144 */
145 const htmlOptions = {
146 pageIdentifier: 'login',
147 pageTitle: options.manager.pageTitle,
148 logoUrl: options.manager.logoUrl,
149 footerEntries: options.manager.footerEntries,
150 secureAuthOnly: options.authenticator.secureAuthOnly,
151 authnEnabled: options.authenticator.authnEnabled,
152 indieAuthBlurb: options.authenticator.indieAuthBlurb,
153 userBlurb: options.authenticator.userBlurb,
154 };
155 appCb(pagePathLevel, ctx, htmlOptions);
156 sessionNavLinks(pagePathLevel, ctx, htmlOptions);
157 const mainContent = [
158 ...(options.authenticator.loginBlurb || []),
159 indieAuthURLTrySecureFirstScript(ctx, htmlOptions),
160 indieAuthSection(ctx, htmlOptions),
161 userSection(ctx, htmlOptions),
162 ];
163 return th.htmlPage(pagePathLevel, ctx, htmlOptions, mainContent);
164 };