bump package version to 1.5.0
[squeep-authentication-module] / lib / template / login-html.js
index 7d059e0d505caf4d4fb503f554dcd91d9d05ccc3..c02a624e8b4a248c0e02ae6ff75c2e393e47b782 100644 (file)
@@ -1,21 +1,39 @@
 'use strict';
 
 const { TemplateHelper: th } = require('@squeep/html-template-helper');
+const { sessionNavLinks } = require('./helpers');
 
 /**
- * Login form.
+ * @typedef {object} Context
+ * @property {string} clientProtocol http/https
+ */
+
+/**
+ * @typedef {object} HtmlOptions
+ * @property {string[]=} authnEnabled list of active authn methods
+ * @property {string} indieAuthBlurb content accompanying login fields
+ * @property {boolean} secureAuthOnly do not display user logiin if insecure and not allowed
+ * @property {string} userBlurb content accompanying login fields
+ */
+
+/**
+ * IndieAuth Profile login form.
+ * @param {Context} ctx context
+ * @param {HtmlOptions} options options
+ * @returns {string} section
  */
 function indieAuthSection(ctx, options) {
   const indieAuthBlurb = (options.indieAuthBlurb || []).map((x) => '\t'.repeat(6) + x).join('\n');
   const showIndieAuthForm = options.authnEnabled.includes('indieAuth');
   return showIndieAuthForm ? `\t\t\t<section class="indieauth">
 \t\t\t\t<h2>Login</h2>
-\t\t\t\t<form action="" method="POST">
+\t\t\t\t<form method="POST">
 \t\t\t\t\t<fieldset>
 \t\t\t\t\t\t<legend>IndieAuth</legend>
 \t\t\t\t\t\t<label for="me">Profile URL:</label>
 \t\t\t\t\t\t<input id="me" name="me" type="url" size="40" placeholder="https://example.com/my_profile_url" value="" autofocus>
-\t\t\t\t\t\t<button>Login</button>
+\t\t\t\t\t\t<input type="hidden" name="me_auto_scheme">
+\t\t\t\t\t\t<button type="submit">Login</button>
 ${indieAuthBlurb}
 \t\t\t\t\t</fieldset>
 \t\t\t\t</form>
@@ -24,23 +42,73 @@ ${indieAuthBlurb}
 }
 
 
-const userAuthn = ['argon2', 'pam', 'DEBUG_ANY'];
+/**
+ * Default all URL inputs to https if scheme not specified,
+ * and set a flag if that happened.
+ * From https://aaronparecki.com/2019/05/13/2/https
+ * @param {Context} ctx context
+ * @param {HtmlOptions} options options
+ * @returns {string} script
+ */
+function indieAuthURLTrySecureFirstScript(ctx, options) {
+  const showIndieAuthForm = options.authnEnabled.includes('indieAuth');
+  return showIndieAuthForm ? `
+<script>
+document.addEventListener('DOMContentLoaded', function() {
+  function addDefaultScheme(target) {
+    if (target.value.match(/^(?!https?:).+\\..+/)) {
+      const autoSchemeField = document.querySelector('input[name=' + target.getAttribute('name') + '_auto_scheme]');
+      let scheme;
+      if (autoSchemeField) {
+        scheme = 'https://';
+        autoSchemeField.value = '1';
+      } else {
+        scheme = 'http://';
+      }
+      target.value = scheme + target.value;
+    }
+  }
+  const elements = document.querySelectorAll('input[type=url]');
+  Array.prototype.forEach.call(elements, function (el, i) {
+    el.addEventListener('blur', function(e) {
+      addDefaultScheme(e.target);
+    });
+    el.addEventListener('keydown', function(e) {
+      if (e.keyCode === 13) {
+        addDefaultScheme(e.target);
+      }
+    });
+  });
+});
+</script>` : '';
+}
+
+/**
+ * Display user section when any of these methods are active.
+ */
+const userAuthn = ['argon2', 'pam'];
+/**
+ * Local identifier/credential login form.
+ * @param {Context} ctx context
+ * @param {HtmlOptions} options options
+ * @returns {string} section
+ */
 function userSection(ctx, options) {
   const userBlurb = (options.userBlurb || []).map((x) => '\t'.repeat(6) + x).join('\n');
   const secure = (ctx.clientProtocol || '').toLowerCase() === 'https';
   const showUserForm = options.authnEnabled.filter((x) => userAuthn.includes(x)).length
     && (secure || !options.secureAuthOnly);
   return showUserForm ? `\t\t\t<section class="user">
-\t\t\t\t<form action="" method="POST">
+\t\t\t\t<form method="POST">
 \t\t\t\t\t<fieldset>
 \t\t\t\t\t\t<legend>User Account</legend>
 \t\t\t\t\t\t<label for="identifier">Username:</label>
-\t\t\t\t\t\t<input id="identifier" name="identifier" value="">
+\t\t\t\t\t\t<input id="identifier" name="identifier" type="text" value="">
 \t\t\t\t\t\t<br>
 \t\t\t\t\t\t<label for="credential">Password:</label>
 \t\t\t\t\t\t<input id="credential" name="credential" type="password" value="">
 \t\t\t\t\t\t<br>
-\t\t\t\t\t\t<button>Login</button>
+\t\t\t\t\t\t<button type="submit">Login</button>
 ${userBlurb}
 \t\t\t\t\t</fieldset>
 \t\t\t\t</form>
@@ -48,26 +116,34 @@ ${userBlurb}
     : '';
 }
 
+/**
+ * @typedef {import('../session-manager').AppTemplateCallback} AppTemplateCallback
+ */
 
 /**
  * Render login form for both local and profile authentication.
- * @param {Object} ctx
- * @param {String[]=} ctx.errors
- * @param {String} ctx.clientProtocol
- * @param {Object} options
- * @param {Boolean} options.authenticator.secureAuthOnly
- * @param {String[]=} options.authenticator.loginBlurb
- * @param {String[]=} options.authenticator.indieAuthBlurb
- * @param {String[]=} options.authenticator.userBlurb
- * @param {Object} options.manager
- * @param {String} options.manager.pageTitle
- * @param {String=} options.manager.logoUrl
- * @param {Object} options.dingus
- * @param {String} options.dingus.selfBaseUrl
- * @returns {String}
+ * @param {Context} ctx context
+ * @param {object} options options
+ * @param {boolean} options.authenticator.secureAuthOnly do not display user login if not secure or allowed
+ * @param {string[]=} options.authenticator.loginBlurb content included at top of page
+ * @param {string} options.authenticator.indieAuthBlurb content included with indieauth login
+ * @param {string} options.authenticator.userBlurb content included with local user login
+ * @param {object} options.manager manager options
+ * @param {string} options.manager.pageTitle page title
+ * @param {string=} options.manager.logoUrl url
+ * @param {string=} options.manager.logoAlt alt for logo
+ * @param {object} options.dingus dingus options
+ * @param {string} options.dingus.selfBaseUrl root url
+ * @param {AppTemplateCallback} appCb function to mogrify template htmlOptions
+ * @returns {string} page
  */
-module.exports = (ctx, options) => {
+module.exports = (ctx, options, appCb = () => {}) => {
+  const pagePathLevel = 1;
+  /**
+   * @type {HtmlOptions}
+   */
   const htmlOptions = {
+    pageIdentifier: 'login',
     pageTitle: options.manager.pageTitle,
     logoUrl: options.manager.logoUrl,
     footerEntries: options.manager.footerEntries,
@@ -76,10 +152,13 @@ module.exports = (ctx, options) => {
     indieAuthBlurb: options.authenticator.indieAuthBlurb,
     userBlurb: options.authenticator.userBlurb,
   };
+  appCb(pagePathLevel, ctx, htmlOptions);
+  sessionNavLinks(pagePathLevel, ctx, htmlOptions);
   const mainContent = [
     ...(options.authenticator.loginBlurb || []),
+    indieAuthURLTrySecureFirstScript(ctx, htmlOptions),
     indieAuthSection(ctx, htmlOptions),
     userSection(ctx, htmlOptions),
   ];
-  return th.htmlPage(2, ctx, htmlOptions, mainContent);
+  return th.htmlPage(pagePathLevel, ctx, htmlOptions, mainContent);
 };
\ No newline at end of file