refactor of authentication and html-templates into separate modules
authorJustin Wind <justin.wind+git@gmail.com>
Thu, 30 Dec 2021 22:41:18 +0000 (14:41 -0800)
committerJustin Wind <justin.wind+git@gmail.com>
Thu, 30 Dec 2021 22:41:18 +0000 (14:41 -0800)
25 files changed:
config/default.js
package-lock.json
package.json
src/authenticator.js [deleted file]
src/service.js
src/session-manager.js [deleted file]
src/template/admin-ia-html.js [deleted file]
src/template/admin-login-html.js [deleted file]
src/template/admin-overview-html.js
src/template/admin-topic-details-html.js
src/template/index.js
src/template/root-html.js
src/template/template-helper.js
static/custom.css [new file with mode: 0644]
static/theme.css
test/lint-html.js [new file with mode: 0644]
test/src/authenticator.js [deleted file]
test/src/service.js
test/src/session-manager.js [deleted file]
test/src/template/admin-ia-html.js [deleted file]
test/src/template/admin-login-html.js [deleted file]
test/src/template/admin-overview-html.js
test/src/template/admin-topic-details-html.js
test/src/template/root-html.js
test/src/template/template-helper.js

index bbc66fcd2fcf6f4249deeb483136abf630d3ea01..9781ba6d62050a621eb722f4302e4a652d7623d1 100644 (file)
@@ -2,8 +2,9 @@
 
 // Provide default values for all configuration.
 
-const packageName = require('../package.json').name;
+const { name: packageName, version: packageVersion } = require('../package.json');
 const common = require('../src/common');
+const Enum = require('../src/enum');
 
 const defaultOptions = {
   // Uniquely identify this instance, used to tag work-in-progress.
@@ -47,6 +48,7 @@ const defaultOptions = {
 
   manager: {
     pageTitle: packageName, // title on html pages
+    logoUrl: '/static/logo.svg', // image to go with title
     footerEntries: [ // common footers on all html pages
       '<a href="https://git.squeep.com/?p=websub-hub;a=tree">Development Repository</a> / <a href="https://github.com/thylacine/websub-hub/">GitHub mirror</a>',
       '<span class="copyright">&copy;<time datetime="2021">&#8559;&#8559;&#8553;&#8553;&#8544;</time></span>',
@@ -62,17 +64,19 @@ const defaultOptions = {
     claimTimeoutSeconds: 600, // how long until an in-progress task is deemed abandoned
   },
 
-  // Outgoing request UA header. Comments are defaults in code.
+  // Outgoing request UA header.
+  // These values are the same as the defaults in the code, but we are setting
+  // them here so they also apply to UA of other modules, e.g. @squeep/indieauth-helper
   userAgent: {
-    // product: packageName,
-    // version: packageVersion,
-    // implementation: Enum.Specification,
+    product: packageName,
+    version: packageVersion,
+    implementation: Enum.Specification,
   },
 
   authenticator: {
     basicRealm: packageName, // Realm prompt for login on administration pages
     secureAuthOnly: true, // Require secure transport for authentication.
-    authnEnabled: ['argon2', 'pam'],
+    authnEnabled: ['indieAuth', 'argon2', 'pam'],
     forbiddenPAMIdentifiers: ['root'],
   },
 
@@ -84,4 +88,4 @@ const defaultOptions = {
 
 };
 
-module.exports = defaultOptions;
\ No newline at end of file
+module.exports = defaultOptions;
index d63e97d25231435e7ea646d51b95164cd194ceaa..bb90f2de7d577fb757fa0a84d25523d01a8a1707 100644 (file)
       "dev": true
     },
     "@mapbox/node-pre-gyp": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz",
-      "integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==",
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.8.tgz",
+      "integrity": "sha512-CMGKi28CF+qlbXh26hDe6NxCd7amqeAzEqnS6IHeO6LoaKyM/n+Xw3HT1COdq8cuioOdlKdqn/hCmqPUOMOywg==",
       "optional": true,
       "requires": {
         "detect-libc": "^1.0.3",
         "https-proxy-agent": "^5.0.0",
         "make-dir": "^3.1.0",
-        "node-fetch": "^2.6.1",
+        "node-fetch": "^2.6.5",
         "nopt": "^5.0.0",
-        "npmlog": "^4.1.2",
+        "npmlog": "^5.0.1",
         "rimraf": "^3.0.2",
-        "semver": "^7.3.4",
-        "tar": "^6.1.0"
+        "semver": "^7.3.5",
+        "tar": "^6.1.11"
       }
     },
     "@phc/format": {
       "dev": true
     },
     "@squeep/api-dingus": {
-      "version": "git+https://git.squeep.com/squeep-api-dingus/#47f38ca4c67e902ccef0b7114a0d144f476258bd",
-      "from": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.2",
+      "version": "git+https://git.squeep.com/squeep-api-dingus/#a1b8b75e64c862276563e874c11a0228ac681946",
+      "from": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.3",
       "requires": {
-        "mime-db": "^1.50.0",
+        "mime-db": "^1.51.0",
         "uuid": "^8.3.2"
       },
       "dependencies": {
         }
       }
     },
+    "@squeep/authentication-module": {
+      "version": "git+https://git.squeep.com/squeep-authentication-module/#ecf9c52f00dae3b9eee8316a1cffbc496f0fbd67",
+      "from": "git+https://git.squeep.com/squeep-authentication-module/#v1.1.0",
+      "requires": {
+        "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.3",
+        "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.1",
+        "@squeep/indieauth-helper": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.1",
+        "@squeep/mystery-box": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.2",
+        "argon2": "^0.28.3",
+        "node-linux-pam": "^0.2.1"
+      }
+    },
+    "@squeep/html-template-helper": {
+      "version": "git+https://git.squeep.com/squeep-html-template-helper#d3f76b9e76b8f133e8158c1087bb01b32c38d9bb",
+      "from": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.1"
+    },
     "@squeep/indieauth-helper": {
-      "version": "git+https://git.squeep.com/squeep-indieauth-helper/#ea59a4bf7a596b502aacf06baee26518d86fd312",
-      "from": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.0",
+      "version": "git+https://git.squeep.com/squeep-indieauth-helper/#f8f7fc8165426471f3d290f10cda2ea2cdb7f643",
+      "from": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.1",
       "requires": {
-        "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.0",
-        "axios": "^0.23.0",
+        "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.2",
+        "axios": "^0.24.0",
         "iconv": "^3.0.1",
         "microformats-parser": "^1.4.0"
-      },
-      "dependencies": {
-        "@squeep/web-linking": {
-          "version": "git+https://git.squeep.com/squeep-web-linking/#3436c07c25324507228f3d538d345ea35751c623",
-          "from": "git+https://git.squeep.com/squeep-web-linking/#v1.0.0"
-        },
-        "axios": {
-          "version": "0.23.0",
-          "resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz",
-          "integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==",
-          "requires": {
-            "follow-redirects": "^1.14.4"
-          }
-        }
       }
     },
     "@squeep/mystery-box": {
-      "version": "git+https://git.squeep.com/squeep-mystery-box/#8df7723f7bbd9ad239bd5a3f66d6e9a8cd3c8100",
-      "from": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.1"
+      "version": "git+https://git.squeep.com/squeep-mystery-box/#c6559eefb2fa776d6e4eedb876470790a53339c1",
+      "from": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.2"
     },
     "@squeep/web-linking": {
-      "version": "git+https://git.squeep.com/squeep-web-linking/#0728e2b9347826ed0be7f9a7939bf5d61e181204",
-      "from": "git+https://git.squeep.com/squeep-web-linking/#v1.0.1"
+      "version": "git+https://git.squeep.com/squeep-web-linking/#e0d9ba95109a2bbb61dbdce7a582f5579aa77c5c",
+      "from": "git+https://git.squeep.com/squeep-web-linking/#v1.0.2"
     },
     "@ungap/promise-all-settled": {
       "version": "1.1.2",
       }
     },
     "aproba": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
-      "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
+      "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
       "optional": true
     },
     "archy": {
       "dev": true
     },
     "are-we-there-yet": {
-      "version": "1.1.5",
-      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
-      "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
+      "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
       "optional": true,
       "requires": {
         "delegates": "^1.0.0",
-        "readable-stream": "^2.0.6"
-      }
-    },
-    "argon2": {
-      "version": "0.28.3",
-      "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.28.3.tgz",
-      "integrity": "sha512-NkEJOImg+T7nnkx6/Fy8EbjZsF20hbBBKdVP/YUxujuLTAjIODmrFeY4vVpekKwGAGDm6roXxluFQ+CIaoVrbg==",
-      "optional": true,
-      "requires": {
-        "@mapbox/node-pre-gyp": "^1.0.7",
-        "@phc/format": "^1.0.0",
-        "node-addon-api": "^4.2.0",
-        "opencollective-postinstall": "^2.0.3"
+        "readable-stream": "^3.6.0"
       },
       "dependencies": {
-        "@mapbox/node-pre-gyp": {
-          "version": "1.0.7",
-          "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.7.tgz",
-          "integrity": "sha512-PplSvl4pJ5N3BkVjAdDzpPhVUPdC73JgttkR+LnBx2OORC1GCQsBjUeEuipf9uOaAM1SbxcdZFfR3KDTKm2S0A==",
-          "optional": true,
-          "requires": {
-            "detect-libc": "^1.0.3",
-            "https-proxy-agent": "^5.0.0",
-            "make-dir": "^3.1.0",
-            "node-fetch": "^2.6.5",
-            "nopt": "^5.0.0",
-            "npmlog": "^6.0.0",
-            "rimraf": "^3.0.2",
-            "semver": "^7.3.5",
-            "tar": "^6.1.11"
-          }
-        },
-        "are-we-there-yet": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
-          "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
-          "optional": true,
-          "requires": {
-            "delegates": "^1.0.0",
-            "readable-stream": "^3.6.0"
-          }
-        },
-        "gauge": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.0.tgz",
-          "integrity": "sha512-F8sU45yQpjQjxKkm1UOAhf0U/O0aFt//Fl7hsrNVto+patMHjs7dPI9mFOGUKbhrgKm0S3EjW3scMFuQmWSROw==",
-          "optional": true,
-          "requires": {
-            "ansi-regex": "^5.0.1",
-            "aproba": "^1.0.3 || ^2.0.0",
-            "color-support": "^1.1.2",
-            "console-control-strings": "^1.0.0",
-            "has-unicode": "^2.0.1",
-            "signal-exit": "^3.0.0",
-            "string-width": "^4.2.3",
-            "strip-ansi": "^6.0.1",
-            "wide-align": "^1.1.2"
-          }
-        },
-        "node-fetch": {
-          "version": "2.6.6",
-          "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
-          "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
-          "optional": true,
-          "requires": {
-            "whatwg-url": "^5.0.0"
-          }
-        },
-        "npmlog": {
-          "version": "6.0.0",
-          "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.0.tgz",
-          "integrity": "sha512-03ppFRGlsyUaQFbGC2C8QWJN/C/K7PsfyD9aQdhVKAQIH4sQBc8WASqFBP7O+Ut4d2oo5LoeoboB3cGdBZSp6Q==",
-          "optional": true,
-          "requires": {
-            "are-we-there-yet": "^2.0.0",
-            "console-control-strings": "^1.1.0",
-            "gauge": "^4.0.0",
-            "set-blocking": "^2.0.0"
-          }
-        },
         "readable-stream": {
           "version": "3.6.0",
           "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
             "string_decoder": "^1.1.1",
             "util-deprecate": "^1.0.1"
           }
-        },
-        "string-width": {
-          "version": "4.2.3",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
-          "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-          "optional": true,
-          "requires": {
-            "emoji-regex": "^8.0.0",
-            "is-fullwidth-code-point": "^3.0.0",
-            "strip-ansi": "^6.0.1"
-          }
-        },
-        "strip-ansi": {
-          "version": "6.0.1",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
-          "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-          "optional": true,
-          "requires": {
-            "ansi-regex": "^5.0.1"
-          }
         }
       }
     },
+    "argon2": {
+      "version": "0.28.3",
+      "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.28.3.tgz",
+      "integrity": "sha512-NkEJOImg+T7nnkx6/Fy8EbjZsF20hbBBKdVP/YUxujuLTAjIODmrFeY4vVpekKwGAGDm6roXxluFQ+CIaoVrbg==",
+      "optional": true,
+      "requires": {
+        "@mapbox/node-pre-gyp": "^1.0.7",
+        "@phc/format": "^1.0.0",
+        "node-addon-api": "^4.2.0",
+        "opencollective-postinstall": "^2.0.3"
+      }
+    },
     "argparse": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
       "optional": true
     },
     "better-sqlite3": {
-      "version": "7.4.5",
-      "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-7.4.5.tgz",
-      "integrity": "sha512-mybC3dgrtJeHkIRGP36tST7wjBlIMgTRAXhhO4bMpPZ17EG23FZxZeFcwKWy6o8mV1SKQFnQNyeAZlQpGrgheQ==",
+      "version": "7.4.6",
+      "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-7.4.6.tgz",
+      "integrity": "sha512-LB/UxnMhcJY12bRCDXl2jTk0lsbXHCHOLn3cPjGhy3GCcVPGq45sCGJPUdfBZnfXGN14tYTJyq0ztUI3lGng8A==",
       "optional": true,
       "requires": {
         "bindings": "^1.5.0",
       "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
       "dev": true
     },
+    "camel-case": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
+      "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
+      "dev": true,
+      "requires": {
+        "no-case": "^2.2.0",
+        "upper-case": "^1.1.1"
+      }
+    },
     "camelcase": {
       "version": "6.2.0",
       "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz",
       }
     },
     "chownr": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
-      "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+      "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
       "optional": true
     },
+    "clean-css": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz",
+      "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==",
+      "dev": true,
+      "requires": {
+        "source-map": "~0.6.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
     "clean-stack": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
       "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
       "dev": true
     },
+    "commander": {
+      "version": "2.17.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
+      "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
+      "dev": true
+    },
     "commondir": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
       "dev": true
     },
     "gauge": {
-      "version": "2.7.4",
-      "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
-      "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
+      "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
       "optional": true,
       "requires": {
-        "aproba": "^1.0.3",
+        "aproba": "^1.0.3 || ^2.0.0",
+        "color-support": "^1.1.2",
         "console-control-strings": "^1.0.0",
-        "has-unicode": "^2.0.0",
-        "object-assign": "^4.1.0",
+        "has-unicode": "^2.0.1",
+        "object-assign": "^4.1.1",
         "signal-exit": "^3.0.0",
-        "string-width": "^1.0.1",
-        "strip-ansi": "^3.0.1",
-        "wide-align": "^1.1.0"
+        "string-width": "^4.2.3",
+        "strip-ansi": "^6.0.1",
+        "wide-align": "^1.1.2"
       },
       "dependencies": {
-        "ansi-regex": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
-          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
-          "optional": true
-        },
-        "is-fullwidth-code-point": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
-          "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
-          "optional": true,
-          "requires": {
-            "number-is-nan": "^1.0.0"
-          }
-        },
         "string-width": {
-          "version": "1.0.2",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
-          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "version": "4.2.3",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+          "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
           "optional": true,
           "requires": {
-            "code-point-at": "^1.0.0",
-            "is-fullwidth-code-point": "^1.0.0",
-            "strip-ansi": "^3.0.0"
+            "emoji-regex": "^8.0.0",
+            "is-fullwidth-code-point": "^3.0.0",
+            "strip-ansi": "^6.0.1"
           }
         },
         "strip-ansi": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
-          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "version": "6.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+          "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
           "optional": true,
           "requires": {
-            "ansi-regex": "^2.0.0"
+            "ansi-regex": "^5.0.1"
           }
         }
       }
       "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
       "dev": true
     },
+    "html-minifier": {
+      "version": "3.5.21",
+      "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz",
+      "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==",
+      "dev": true,
+      "requires": {
+        "camel-case": "3.0.x",
+        "clean-css": "4.2.x",
+        "commander": "2.17.x",
+        "he": "1.2.x",
+        "param-case": "2.1.x",
+        "relateurl": "0.2.x",
+        "uglify-js": "3.4.x"
+      }
+    },
+    "html-minifier-lint": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/html-minifier-lint/-/html-minifier-lint-2.0.0.tgz",
+      "integrity": "sha1-i0vuyTHaiNsEyHc+5WgYjfO111g=",
+      "dev": true,
+      "requires": {
+        "html-minifier": "3.x"
+      }
+    },
     "htmlparser2": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz",
         "is-unicode-supported": "^0.1.0"
       }
     },
+    "lower-case": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
+      "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=",
+      "dev": true
+    },
     "lru-cache": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
       }
     },
     "mime-db": {
-      "version": "1.50.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz",
-      "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A=="
+      "version": "1.51.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
+      "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
     },
     "mimic-response": {
       "version": "3.1.0",
       "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
     },
     "minipass": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz",
-      "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==",
+      "version": "3.1.6",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz",
+      "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==",
       "optional": true,
       "requires": {
         "yallist": "^4.0.0"
         }
       }
     },
+    "no-case": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
+      "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
+      "dev": true,
+      "requires": {
+        "lower-case": "^1.1.1"
+      }
+    },
     "node-abi": {
       "version": "3.5.0",
       "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.5.0.tgz",
       "optional": true
     },
     "node-fetch": {
-      "version": "2.6.1",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
-      "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
-      "optional": true
+      "version": "2.6.6",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
+      "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
+      "optional": true,
+      "requires": {
+        "whatwg-url": "^5.0.0"
+      }
     },
     "node-linux-pam": {
       "version": "0.2.1",
         "yargs": "15.4.1"
       },
       "dependencies": {
+        "@mapbox/node-pre-gyp": {
+          "version": "1.0.5",
+          "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz",
+          "integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==",
+          "optional": true,
+          "requires": {
+            "detect-libc": "^1.0.3",
+            "https-proxy-agent": "^5.0.0",
+            "make-dir": "^3.1.0",
+            "node-fetch": "^2.6.1",
+            "nopt": "^5.0.0",
+            "npmlog": "^4.1.2",
+            "rimraf": "^3.0.2",
+            "semver": "^7.3.4",
+            "tar": "^6.1.0"
+          }
+        },
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "optional": true
+        },
         "ansi-styles": {
           "version": "4.3.0",
           "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
             "color-convert": "^2.0.1"
           }
         },
+        "aproba": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+          "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
+          "optional": true
+        },
+        "are-we-there-yet": {
+          "version": "1.1.7",
+          "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz",
+          "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==",
+          "optional": true,
+          "requires": {
+            "delegates": "^1.0.0",
+            "readable-stream": "^2.0.6"
+          }
+        },
         "camelcase": {
           "version": "5.3.1",
           "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
             "string-width": "^4.2.0",
             "strip-ansi": "^6.0.0",
             "wrap-ansi": "^6.2.0"
+          },
+          "dependencies": {
+            "ansi-regex": {
+              "version": "5.0.1",
+              "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+              "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+              "optional": true
+            },
+            "is-fullwidth-code-point": {
+              "version": "3.0.0",
+              "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+              "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+              "optional": true
+            },
+            "string-width": {
+              "version": "4.2.3",
+              "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+              "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+              "optional": true,
+              "requires": {
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
+              }
+            },
+            "strip-ansi": {
+              "version": "6.0.1",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+              "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+              "optional": true,
+              "requires": {
+                "ansi-regex": "^5.0.1"
+              }
+            }
           }
         },
         "color-convert": {
             "path-exists": "^4.0.0"
           }
         },
+        "gauge": {
+          "version": "2.7.4",
+          "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+          "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+          "optional": true,
+          "requires": {
+            "aproba": "^1.0.3",
+            "console-control-strings": "^1.0.0",
+            "has-unicode": "^2.0.0",
+            "object-assign": "^4.1.0",
+            "signal-exit": "^3.0.0",
+            "string-width": "^1.0.1",
+            "strip-ansi": "^3.0.1",
+            "wide-align": "^1.1.0"
+          }
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+          "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+          "optional": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
         "locate-path": {
           "version": "5.0.0",
           "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
           "integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==",
           "optional": true
         },
+        "npmlog": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+          "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+          "optional": true,
+          "requires": {
+            "are-we-there-yet": "~1.1.2",
+            "console-control-strings": "~1.1.0",
+            "gauge": "~2.7.3",
+            "set-blocking": "~2.0.0"
+          }
+        },
         "p-limit": {
           "version": "2.3.0",
           "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
             "p-limit": "^2.2.0"
           }
         },
+        "string-width": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "optional": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "optional": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
         "wrap-ansi": {
           "version": "6.2.0",
           "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
             "ansi-styles": "^4.0.0",
             "string-width": "^4.1.0",
             "strip-ansi": "^6.0.0"
+          },
+          "dependencies": {
+            "ansi-regex": {
+              "version": "5.0.1",
+              "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+              "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+              "optional": true
+            },
+            "is-fullwidth-code-point": {
+              "version": "3.0.0",
+              "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+              "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+              "optional": true
+            },
+            "string-width": {
+              "version": "4.2.3",
+              "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+              "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+              "optional": true,
+              "requires": {
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
+              }
+            },
+            "strip-ansi": {
+              "version": "6.0.1",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+              "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+              "optional": true,
+              "requires": {
+                "ansi-regex": "^5.0.1"
+              }
+            }
           }
         },
         "y18n": {
             "which-module": "^2.0.0",
             "y18n": "^4.0.0",
             "yargs-parser": "^18.1.2"
+          },
+          "dependencies": {
+            "ansi-regex": {
+              "version": "5.0.1",
+              "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+              "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+              "optional": true
+            },
+            "is-fullwidth-code-point": {
+              "version": "3.0.0",
+              "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+              "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+              "optional": true
+            },
+            "string-width": {
+              "version": "4.2.3",
+              "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+              "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+              "optional": true,
+              "requires": {
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
+              }
+            },
+            "strip-ansi": {
+              "version": "6.0.1",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+              "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+              "optional": true,
+              "requires": {
+                "ansi-regex": "^5.0.1"
+              }
+            }
           }
         },
         "yargs-parser": {
       "dev": true
     },
     "npmlog": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
-      "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
+      "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
       "optional": true,
       "requires": {
-        "are-we-there-yet": "~1.1.2",
-        "console-control-strings": "~1.1.0",
-        "gauge": "~2.7.3",
-        "set-blocking": "~2.0.0"
+        "are-we-there-yet": "^2.0.0",
+        "console-control-strings": "^1.1.0",
+        "gauge": "^3.0.0",
+        "set-blocking": "^2.0.0"
       }
     },
     "number-is-nan": {
       "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==",
       "optional": true
     },
+    "param-case": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
+      "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
+      "dev": true,
+      "requires": {
+        "no-case": "^2.2.0"
+      }
+    },
     "parent-module": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
         "simple-get": "^4.0.0",
         "tar-fs": "^2.0.0",
         "tunnel-agent": "^0.6.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "optional": true
+        },
+        "aproba": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+          "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
+          "optional": true
+        },
+        "are-we-there-yet": {
+          "version": "1.1.7",
+          "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz",
+          "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==",
+          "optional": true,
+          "requires": {
+            "delegates": "^1.0.0",
+            "readable-stream": "^2.0.6"
+          }
+        },
+        "gauge": {
+          "version": "2.7.4",
+          "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+          "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+          "optional": true,
+          "requires": {
+            "aproba": "^1.0.3",
+            "console-control-strings": "^1.0.0",
+            "has-unicode": "^2.0.0",
+            "object-assign": "^4.1.0",
+            "signal-exit": "^3.0.0",
+            "string-width": "^1.0.1",
+            "strip-ansi": "^3.0.1",
+            "wide-align": "^1.1.0"
+          }
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+          "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+          "optional": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
+        "npmlog": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+          "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+          "optional": true,
+          "requires": {
+            "are-we-there-yet": "~1.1.2",
+            "console-control-strings": "~1.1.0",
+            "gauge": "~2.7.3",
+            "set-blocking": "~2.0.0"
+          }
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "optional": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "optional": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        }
       }
     },
     "prelude-ls": {
       "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
       "dev": true
     },
+    "relateurl": {
+      "version": "0.2.7",
+      "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
+      "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
+      "dev": true
+    },
     "release-zalgo": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz",
         "minizlib": "^2.1.1",
         "mkdirp": "^1.0.3",
         "yallist": "^4.0.0"
-      },
-      "dependencies": {
-        "chownr": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
-          "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
-          "optional": true
-        }
       }
     },
     "tar-fs": {
         "mkdirp-classic": "^0.5.2",
         "pump": "^3.0.0",
         "tar-stream": "^2.1.4"
+      },
+      "dependencies": {
+        "chownr": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+          "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+          "optional": true
+        }
       }
     },
     "tar-stream": {
         "is-typedarray": "^1.0.0"
       }
     },
+    "uglify-js": {
+      "version": "3.4.10",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz",
+      "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==",
+      "dev": true,
+      "requires": {
+        "commander": "~2.19.0",
+        "source-map": "~0.6.1"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.19.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
+          "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "upper-case": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
+      "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
+      "dev": true
+    },
     "uri-js": {
       "version": "4.4.1",
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
       "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
     },
     "wide-align": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
-      "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+      "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
       "optional": true,
       "requires": {
-        "string-width": "^1.0.2 || 2"
-      },
-      "dependencies": {
-        "ansi-regex": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
-          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
-          "optional": true
-        },
-        "is-fullwidth-code-point": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
-          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
-          "optional": true
-        },
-        "string-width": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
-          "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
-          "optional": true,
-          "requires": {
-            "is-fullwidth-code-point": "^2.0.0",
-            "strip-ansi": "^4.0.0"
-          }
-        },
-        "strip-ansi": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
-          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
-          "optional": true,
-          "requires": {
-            "ansi-regex": "^3.0.0"
-          }
-        }
+        "string-width": "^1.0.2 || 2 || 3 || 4"
       }
     },
     "word-wrap": {
index 4ae8aac7de172e587b6c9c753bca5168180cfa4b..99af66e6a4210e8eec623016ae3f3ebd0d0c1a83 100644 (file)
     "coverage-check"
   ],
   "dependencies": {
-    "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.2",
-    "@squeep/indieauth-helper": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.0",
-    "@squeep/mystery-box": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.1",
-    "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.1",
+    "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.3",
+    "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.1.0",
+    "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.1",
+    "@squeep/indieauth-helper": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.1",
+    "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.2",
     "axios": "^0.24.0",
     "feedparser": "^2.2.10",
     "htmlparser2": "^7.2.0",
     "iconv": "^3.0.1"
   },
   "optionalDependencies": {
-    "argon2": "^0.28.3",
-    "better-sqlite3": "^7.4.5",
-    "node-linux-pam": "^0.2.1",
+    "better-sqlite3": "^7.4.6",
     "pg-promise": "^10.11.1"
   },
   "devDependencies": {
@@ -52,6 +51,7 @@
     "eslint-plugin-node": "^11.1.0",
     "eslint-plugin-security": "^1.4.0",
     "eslint-plugin-sonarjs": "^0.11.0",
+    "html-minifier-lint": "^2.0.0",
     "mocha": "^9.1.3",
     "mocha-steps": "^1.3.0",
     "nyc": "^15.1.0",
diff --git a/src/authenticator.js b/src/authenticator.js
deleted file mode 100644 (file)
index 3a196fc..0000000
+++ /dev/null
@@ -1,276 +0,0 @@
-'use strict';
-
-const common = require('./common');
-const Enum = require('./enum');
-const Errors = require('./errors');
-const { MysteryBox } = require('@squeep/mystery-box');
-
-const _fileScope = common.fileScope(__filename);
-
-class Authenticator {
-  /**
-   * @param {Console} logger
-   * @param {*} db
-   * @param {Object} options
-   * @param {Object} options.authenticator
-   * @param {String} options.authenticator.basicRealm
-   * @param {Boolean} options.authenticator.secureAuthOnly
-   * @param {String[]} options.authenticator.forbiddenPAMIdentifiers
-   * @param {String[]} options.authenticator.authnEnabled
-   */
-  constructor(logger, db, options) {
-    this.logger = logger;
-    this.db = db;
-    this.basicRealm = options.authenticator.basicRealm;
-    this.secureAuthOnly = options.authenticator.secureAuthOnly;
-
-    this.authn = {
-      DEBUG_ANY: {},
-    };
-    try {
-      this.authn.argon2 = require('argon2');
-    } catch (e) { /**/ }
-    try {
-      this.authn.pam = require('node-linux-pam');
-      this.forbiddenPAMIdentifiers = options.authenticator.forbiddenPAMIdentifiers;
-    } catch (e) { /**/ }
-
-    this.authnEnabled = Object.keys(this.authn).filter((auth) => options.authenticator.authnEnabled.includes(auth));
-    this.logger.debug(_fileScope('constructor'), 'available mechanisms', { authn: this.authnEnabled });
-
-    if (this.authnEnabled.length === 0) {
-      throw new Error('no authentication mechanisms available');
-    }
-
-    this.mysteryBox = new MysteryBox(logger, options);
-  }
-
-
-  /**
-   * Check for valid Basic auth, updates ctx with identifier if valid.
-   * @param {String} credentials
-   * @param {Object} ctx
-   * @returns {Boolean}
-   */
-  async isValidBasic(credentials, ctx) {
-    const _scope = _fileScope('isValidBasic');
-    this.logger.debug(_scope, 'called', { ctx });
-
-    const [identifier, credential] = common.splitFirst(credentials, ':', '');
-
-    return this.isValidIdentifierCredential(identifier, credential, ctx);
-  }
-
-
-  /**
-   * Check local auth entries.
-   * @param {String} identifier
-   * @param {String} credential
-   * @param {Object} ctx
-   */
-  async isValidIdentifierCredential(identifier, credential, ctx) {
-    const _scope = _fileScope('isValidIdentifierCredential');
-    this.logger.debug(_scope, 'called', { identifier, credential: '*'.repeat(credential.length), ctx });
-
-    let isValid = false;
-
-    await this.db.context(async (dbCtx) => {
-      const authData = await this.db.authenticationGet(dbCtx, identifier);
-      if (!authData) {
-        this.logger.debug(_scope, 'failed, invalid identifier', { ctx, identifier });
-      } else {
-        if (authData.credential.startsWith('$argon2')
-        &&  this.authnEnabled.includes('argon2')) {
-          isValid = await this.authn.argon2.verify(authData.credential, credential);
-        } else if (authData.credential.startsWith('$PAM$')
-        &&         this.authnEnabled.includes('pam')) {
-          isValid = this._isValidPAMIdentifier(identifier, credential);
-        } else {
-          this.logger.error(_scope, 'failed, unknown type of stored credential', { identifier, ctx });
-        }
-      }
-
-      if (this.authnEnabled.includes('DEBUG_ANY')) {
-        isValid = true;
-      }
-
-      if (isValid) {
-        ctx.authenticationId = identifier;
-        await this.db.authenticationSuccess(dbCtx, identifier);
-      }
-    }); // dbCtx
-
-    return isValid;
-  }
-
-
-  /**
-   * Check system PAM.
-   * @param {String} identifier
-   * @param {String} credential
-   * @returns {Boolean}
-   */
-  async _isValidPAMIdentifier(identifier, credential) {
-    const _scope = _fileScope('_isValidPAMIdentifier');
-    let isValid = false;
-    if (this.forbiddenPAMIdentifiers.includes(identifier)) {
-      return false;
-    }
-    try {
-      await this.authn.pam.pamAuthenticatePromise({ username: identifier, password: credential });
-      isValid = true;
-    } catch (e) {
-      this.logger.debug(_scope, 'failed', { error: e });
-      if (!(e instanceof this.authn.pam.PamError)) {
-        throw e;
-      }
-    }
-    return isValid;
-  }
-
-
-  /**
-   * Determine which Authorization header is available, and if it is valid.
-   * @param {String} authorizationHeader
-   * @param {Object} ctx
-   */
-  async isValidAuthorization(authorizationHeader, ctx) {
-    const _scope = _fileScope('isValidAuthorization');
-    this.logger.debug(_scope, 'called', { authorizationHeader, ctx });
-
-    const [authMethod, authString] = common.splitFirst(authorizationHeader, ' ', '').map((x) => x.trim());
-    // eslint-disable-next-line sonarjs/no-small-switch
-    switch (authMethod.toLowerCase()) {
-      case 'basic': {
-        const credentials = Buffer.from(authString, 'base64').toString('utf-8');
-        return this.isValidBasic(credentials, ctx);
-      }
-
-      default:
-        this.logger.debug(_scope, 'unknown authorization scheme', { ctx });
-        return false;
-    }
-  }
-
-
-  /**
-   * Send a response requesting basic auth.
-   * @param {http.ServerResponse} res
-   */
-  requestBasic(res) {
-    res.setHeader(Enum.Header.WWWAuthenticate, `Basic realm="${this.basicRealm}", charset="UTF-8"`);
-    throw new Errors.ResponseError(Enum.ErrorResponse.Unauthorized);
-  }
-
-
-  /**
-   * Attempt to parse a session cookie, and determine if it
-   * contains authenticated user.
-   * Restores ctx.session from cookie data.
-   * @param {Object} ctx
-   * @param {String} cookieHeader
-   * @returns {Boolean}
-   */
-  async isValidCookieAuth(ctx, cookieHeader) {
-    const _scope = _fileScope('isValidCookieAuth');
-    this.logger.debug(_scope, 'called', { ctx, cookieHeader });
-
-    const [ cookieName, cookieValue ] = common.splitFirst(cookieHeader, '=', '');
-    if (cookieName !== 'WSHas') {
-      return false;
-    }
-    try {
-      ctx.session = await this.mysteryBox.unpack(cookieValue);
-      this.logger.debug(_scope, 'unpacked cookie', { ctx });
-      return !!ctx.session.authenticatedProfile || !! ctx.session.authenticatedIdentifier;
-    } catch (e) {
-      this.logger.debug(_scope, 'could not unpack cookie', { error:e, ctx });
-      return false;
-    }
-  }
-
-
-  /**
-   * Require that a request has valid auth over secure channel, requests if missing.
-   * @param {http.ClientRequest} req
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
-   * @param {String} loginPath
-   */
-  async required(req, res, ctx, loginPath) {
-    const _scope = _fileScope('required');
-    this.logger.debug(_scope, 'called', { ctx });
-
-    if (this.secureAuthOnly && ctx.clientProtocol.toLowerCase() !== 'https') {
-      this.logger.debug(_scope, 'rejecting insecure auth', ctx);
-      throw new Errors.ResponseError(Enum.ErrorResponse.Forbidden, 'authentication required, but connection is insecure; cannot continue');
-    }
-
-    const sessionCookie = req.getHeader(Enum.Header.Cookie);
-    if (sessionCookie && await this.isValidCookieAuth(ctx, sessionCookie)) {
-      return true;
-    }
-
-    const authData = req.getHeader(Enum.Header.Authorization);
-    if (authData) {
-      if (await this.isValidAuthorization(authData, ctx)) {
-        return true;
-      }
-      // If they came in trying header auth, let them try again.
-      return this.requestBasic(res);
-    }
-
-    // Otherwise redirect to login.
-    res.statusCode = 302;
-    res.setHeader(Enum.Header.Location, loginPath);
-    res.end();
-
-    return false;
-  }
-
-
-  /**
-   * Require that a request has valid local auth over secure channel, requests if missing.
-   * @param {http.ClientRequest} req
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
-   * @param {String} loginPath
-   */
-  async requiredLocal(req, res, ctx, loginPath) {
-    const _scope = _fileScope('requiredLocal');
-    this.logger.debug(_scope, 'called', { ctx });
-
-    if (this.secureAuthOnly && ctx.clientProtocol.toLowerCase() !== 'https') {
-      this.logger.debug(_scope, 'rejecting insecure auth', ctx);
-      throw new Errors.ResponseError(Enum.ErrorResponse.Forbidden, 'authentication required, but connection is insecure; cannot continue');
-    }
-
-    // Only accept identifier sessions.
-    const sessionCookie = req.getHeader(Enum.Header.Cookie);
-    if (sessionCookie
-    &&  await this.isValidCookieAuth(ctx, sessionCookie)
-    &&  ctx.session.authenticatedIdentifier) {
-      return true;
-    }
-
-    // Allow header auth
-    const authData = req.getHeader(Enum.Header.Authorization);
-    if (authData) {
-      if (await this.isValidAuthorization(authData, ctx)) {
-        return true;
-      }
-      // If they came in trying header auth, let them try again.
-      return this.requestBasic(res);
-    }
-
-    // Otherwise redirect to login.
-    res.statusCode = 302;
-    res.setHeader(Enum.Header.Location, loginPath);
-    res.end();
-
-    return false;
-  }
-
-}
-
-module.exports = Authenticator;
\ No newline at end of file
index a0043f7d24caa49a71c85341dc4fadca6ea563d2..6dcde1ec201b1a212beb704d99c61edebcd75be9 100644 (file)
@@ -9,8 +9,7 @@ const { Dingus } = require('@squeep/api-dingus');
 const common = require('./common');
 const Enum = require('./enum');
 const Manager = require('./manager');
-const SessionManager = require('./session-manager');
-const Authenticator = require('./authenticator');
+const { Authenticator, SessionManager } = require('@squeep/authentication-module');
 const path = require('path');
 
 const _fileScope = common.fileScope(__filename);
@@ -155,7 +154,7 @@ class Service extends Dingus {
 
     this.setResponseType(this.responseTypes, req, res, ctx);
 
-    await this.authenticator.required(req, res, ctx, this.loginPath);
+    await this.authenticator.sessionRequired(req, res, ctx, this.loginPath);
 
     await this.manager.getAdminOverview(res, ctx);
   }
@@ -174,7 +173,7 @@ class Service extends Dingus {
 
     this.setResponseType(this.responseTypes, req, res, ctx);
 
-    await this.authenticator.required(req, res, ctx, this.loginPath);
+    await this.authenticator.sessionRequired(req, res, ctx, this.loginPath);
 
     await this.manager.getTopicDetails(res, ctx);
   }
@@ -209,7 +208,7 @@ class Service extends Dingus {
 
     this.setResponseType(this.responseTypes, req, res, ctx);
 
-    await this.authenticator.requiredLocal(req, res, ctx, this.loginPath);
+    await this.authenticator.apiRequiredLocal(req, res, ctx);
 
     await this.maybeIngestBody(req, res, ctx);
     ctx.method = req.method;
@@ -228,7 +227,7 @@ class Service extends Dingus {
 
     this.setResponseType(this.responseTypes, req, res, ctx);
 
-    await this.authenticator.requiredLocal(req, res, ctx, this.loginPath);
+    await this.authenticator.apiRequiredLocal(req, res, ctx);
 
     await this.maybeIngestBody(req, res, ctx);
     ctx.method = req.method;
@@ -247,13 +246,14 @@ class Service extends Dingus {
 
     this.setResponseType(this.responseTypes, req, res, ctx);
 
-    await this.authenticator.requiredLocal(req, res, ctx, this.loginPath);
+    await this.authenticator.apiRequiredLocal(req, res, ctx);
 
     await this.manager.processTasks(res, ctx);
   }
 
 
   /**
+   * Delegate login to authentication module.
    * @param {http.ClientRequest} req
    * @param {http.ServerResponse} res
    * @param {Object} ctx
@@ -271,6 +271,7 @@ class Service extends Dingus {
 
 
   /**
+   * Delegate login to authentication module.
    * @param {http.ClientRequest} req
    * @param {http.ServerResponse} res
    * @param {Object} ctx
@@ -281,6 +282,8 @@ class Service extends Dingus {
 
     this.setResponseType(this.responseTypes, req, res, ctx);
 
+    await this.authenticator.sessionOptionalLocal(req, res, ctx);
+
     await this.maybeIngestBody(req, res, ctx);
 
     await this.sessionManager.postAdminLogin(res, ctx);
@@ -288,6 +291,7 @@ class Service extends Dingus {
 
 
   /**
+   * Delegate login to authentication module.
    * @param {http.ClientRequest} req
    * @param {http.ServerResponse} res
    * @param {Object} ctx
@@ -298,11 +302,14 @@ class Service extends Dingus {
 
     this.setResponseType(this.responseTypes, req, res, ctx);
 
+    await this.authenticator.sessionOptionalLocal(req, res, ctx);
+
     await this.sessionManager.getAdminLogout(res, ctx);
   }
 
 
   /**
+   * Delegate login to authentication module.
    * @param {http.ClientRequest} req
    * @param {http.ServerResponse} res
    * @param {Object} ctx
diff --git a/src/session-manager.js b/src/session-manager.js
deleted file mode 100644 (file)
index 5caff34..0000000
+++ /dev/null
@@ -1,279 +0,0 @@
-'use strict';
-
-/**
- * Here we process activities which support login sessions.
- */
-
-const { Communication: IndieAuthCommunication } = require('@squeep/indieauth-helper');
-const { MysteryBox } = require('@squeep/mystery-box');
-const common = require('./common');
-const Enum = require('./enum');
-const Template = require('./template');
-
-const _fileScope = common.fileScope(__filename);
-
-class SessionManager {
-  constructor(logger, authenticator, options) {
-    this.logger = logger;
-    this.authenticator = authenticator;
-    this.options = options;
-    this.indieAuthCommunication = new IndieAuthCommunication(logger, options);
-    this.mysteryBox = new MysteryBox(logger, options);
-
-    this.secureCookie = options.authenticator.secureAuthOnly ? ' Secure;' : '';
-    this.cookieLifespan = 60 * 60 * 24 * 32;
-  }
-
-
-  /**
-   * Set or update our session cookie.
-   * @param {http.ServerResponse} res
-   * @param {Object} session
-   * @param {Number} maxAge
-   */
-  async _sessionCookieSet(res, session, maxAge) {
-    const cookieName = 'WSHas';
-    const secureSession = session && await this.mysteryBox.pack(session) || '';
-    const cookie = [
-      `${cookieName}=${secureSession}`,
-      'HttpOnly',
-      this.secureCookie,
-      `Max-Age: ${maxAge}`,
-    ].join('; ');
-    res.setHeader(Enum.Header.SetCookie, cookie);
-  }
-
-
-  /**
-   * GET request for establishing admin session.
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
-   */
-  async getAdminLogin(res, ctx) {
-    const _scope = _fileScope('getAdminLogin');
-    this.logger.debug(_scope, 'called', { ctx });
-
-    res.end(Template.adminLoginHTML(ctx, this.options));
-    this.logger.info(_scope, 'finished', { ctx })
-  }
-
-
-  /**
-   * POST request for taking form data to establish admin session.
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
-   */
-  async postAdminLogin(res, ctx) {
-    const _scope = _fileScope('postAdminLogin');
-    this.logger.debug(_scope, 'called', { ctx });
-
-    ctx.errors = [];
-
-    // Only attempt user login if no IndieAuth profile is set
-    if (!ctx.parsedBody['me']) {
-      this.logger.debug(_scope, 'no indieauth profile, trying identifier', { ctx });
-
-      const identifier = ctx.parsedBody['identifier'];
-      const credential = ctx.parsedBody['credential'];
-
-      const isValidLocalIdentifier = await this.authenticator.isValidIdentifierCredential(identifier, credential, ctx);
-      if (!isValidLocalIdentifier) {
-        ctx.errors.push('Invalid username or password');
-      }
-
-      if (ctx.errors.length) {
-        res.end(Template.adminLoginHTML(ctx, this.options));
-        return;
-      }
-
-      // Valid auth, persist the authenticated session
-      ctx.session = {
-        authenticatedIdentifier: ctx.authenticationId,
-      };
-      await this._sessionCookieSet(res, ctx.session, this.cookieLifespan);
-      res.statusCode = 302;
-      res.setHeader(Enum.Header.Location, './');
-      res.end();
-      this.logger.info(_scope, 'finished local', { ctx });
-      return;
-    }
-
-    let me, session, authorizationEndpoint;
-    try {
-      me = new URL(ctx.parsedBody['me']);
-    } catch (e) {
-      this.logger.debug(_scope, 'failed to parse supplied profile url', { ctx });
-      ctx.errors.push(`Unable to understand '${ctx.parsedBody['me']}' as a profile URL.`);
-    }
-
-    if (me) {
-      const profile = await this.indieAuthCommunication.fetchProfile(me);
-      if (!profile || !profile.authorizationEndpoint) {
-        this.logger.debug(_scope, 'failed to find any profile information at url', { ctx });
-        ctx.errors.push(`No profile information was found at '${me}'.`);
-      } else {
-        // fetch and parse me for 'authorization_endpoint' relation links
-        try {
-          authorizationEndpoint = new URL(profile.authorizationEndpoint);
-        } catch (e) {
-          ctx.errors.push(`Unable to understand the authorization endpoint ('${profile.authorizationEndpoint}') indicated by that profile ('${me}') as a URL.`);
-        }
-      }
-
-      if (authorizationEndpoint) {
-        const pkce = await IndieAuthCommunication.generatePKCE();
-        session = {
-          authorizationEndpoint: authorizationEndpoint.href,
-          state: ctx.requestId,
-          codeVerifier: pkce.codeVerifier,
-          me,
-        };
-
-        Object.entries({
-          'response_type': 'code',
-          'client_id': this.options.dingus.selfBaseUrl,
-          'redirect_uri': `${this.options.dingus.selfBaseUrl}admin/_ia`,
-          'state': session.state,
-          'code_challenge': pkce.codeChallenge,
-          'code_challenge_method': pkce.codeChallengeMethod,
-          'me': me,
-        }).forEach(([name, value]) => authorizationEndpoint.searchParams.set(name, value));
-      }
-    }
-
-    if (ctx.errors.length) {
-      res.end(Template.adminLoginHTML(ctx, this.options));
-      return;
-    }
-
-    await this._sessionCookieSet(res, session, this.cookieLifespan);
-    res.setHeader(Enum.Header.Location, authorizationEndpoint.href);
-    res.statusCode = 302; // Found
-    res.end();
-
-    this.logger.info(_scope, 'finished indieauth', { ctx })
-  }
-
-
-  /**
-   * GET request to remove current credentials.
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
-   */
-  async getAdminLogout(res, ctx) {
-    const _scope = _fileScope('getAdminLogout');
-    this.logger.debug(_scope, 'called', { ctx });
-
-    this._sessionCookieSet(res, '', 0);
-    res.statusCode = 302;
-    res.setHeader(Enum.Header.Location, './');
-    res.end();
-
-    this.logger.info(_scope, 'finished', { ctx });
-  }
-
-
-  /**
-   * GET request for returning IndieAuth redirect.
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
-   */
-  async getAdminIA(res, ctx) {
-    const _scope = _fileScope('getAdminIA');
-    this.logger.debug(_scope, 'called', { ctx });
-
-    ctx.errors = [];
-    ctx.session = {};
-
-    // Unpack cookie to restore session data
-
-    const [ cookieName, cookieValue ] = common.splitFirst((ctx.cookie || ''), '=', '');
-    if (cookieName !== 'WSHas') {
-      this.logger.debug(_scope, 'no cookie', { ctx });
-      ctx.errors.push('missing required cookie');
-    } else {
-      try {
-        ctx.session = await this.mysteryBox.unpack(cookieValue);
-        this.logger.debug(_scope, 'restored session from cookie', { ctx });
-      } catch (e) {
-        this.logger.debug(_scope, 'could not unpack cookie');
-        ctx.errors.push('invalid cookie');
-      }
-    }
-
-    // Validate unpacked session values
-
-    // Add any auth errors
-    if (ctx.queryParams['error']) {
-      ctx.errors.push(ctx.queryParams['error']);
-      if (ctx.queryParams['error_description']) {
-        ctx.errors.push(ctx.queryParams['error_description']);
-      }
-    }
-
-    // check stuff
-    if (ctx.queryParams['state'] !== ctx.session.state) {
-      this.logger.debug(_scope, 'state mismatch', { ctx });
-      ctx.errors.push('invalid state');
-    }
-
-    const code = ctx.queryParams['code'];
-    if (!code) {
-      this.logger.debug(_scope, 'missing code', { ctx });
-      ctx.errors.push('invalid code');
-    }
-
-    let redeemProfileUrl;
-    try {
-      redeemProfileUrl = new URL(ctx.session.authorizationEndpoint);
-    } catch (e) {
-      this.logger.debug(_scope, 'failed to parse restored session authorization endpoint as url', { ctx });
-      ctx.errors.push('invalid cookie');
-    }
-    let profile;
-    if (redeemProfileUrl) {
-      profile = await this.indieAuthCommunication.redeemProfileCode(redeemProfileUrl, code, ctx.session.codeVerifier, this.options.dingus.selfBaseUrl, `${this.options.dingus.selfBaseUrl}admin/_ia`);
-      if (!profile) {
-        this.logger.debug(_scope, 'no profile from code redemption', { ctx });
-        ctx.errors.push('did not get a profile response from authorization endpoint code redemption');
-      } else if (!profile.me) {
-        this.logger.debug(_scope, 'no profile me identifier from code redemption', { ctx });
-        ctx.errors.push('did not get \'me\' value from authorization endpoint code redemption');
-      } else if (profile.me !== ctx.session.me) {
-        this.logger.debug(_scope, 'mis-matched canonical me from redeemed profile', { ctx, profile });
-        const newProfileUrl = new URL(profile.me);
-        // Rediscover auth endpoint for the new returned profile.
-        const newProfile = await this.indieAuthCommunication.fetchProfile(newProfileUrl);
-        if (newProfile.authorizationEndpoint !== ctx.session.authorizationEndpoint) {
-          this.logger.debug(_scope, 'mis-matched auth endpoints between provided me and canonical me', { ctx, profile, newProfile });
-          ctx.errors.push('canonical profile url provided by authorization endpoint is not handled by that endpoint, cannot continue');
-        } else {
-          // The endpoints match, all is okay, update our records.
-          ctx.session.me = profile.me;
-        }
-      }
-    }
-
-    if (ctx.errors.length) {
-      await this._sessionCookieSet(res, '', 0);
-      res.end(Template.adminIAHTML(ctx, this.options));
-      return;
-    }
-
-    // set cookie as auth valid, redirect to admin
-    ctx.session = {
-      authenticatedProfile: ctx.session.me,
-    };
-
-    await this._sessionCookieSet(res, ctx.session, this.cookieLifespan);
-    res.statusCode = 302;
-    res.setHeader(Enum.Header.Location, './');
-    res.end();
-
-    this.logger.info(_scope, 'finished', { ctx })
-  }
-
-
-}
-
-module.exports = SessionManager;
\ No newline at end of file
diff --git a/src/template/admin-ia-html.js b/src/template/admin-ia-html.js
deleted file mode 100644 (file)
index 103582d..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-'use strict';
-
-const th = require('./template-helper');
-
-function errorsSection(ctx) {
-  return (ctx.errors && ctx.errors.length) ? `        <section class="errors">
-          <h2>Troubles</h2>
-          <p>Problems were encountered while trying to authenticate your profile URL.</p>
-          <ul>` +
-          ctx.errors.map((error) => `<li>${error}</li>`).join('\n') + `
-          </ul>
-        </section>
-        <div>
-          <a href="./login">Try Again?</a>
-        </div>`
-    : '';
-}
-
-/**
- * Render any errors from attempting IndieAuth.
- * @param {Object} ctx
- * @param {String[]} ctx.errors
- * @param {Object} options
- * @param {Object} options.manager
- * @param {String} options.manager.pageTitle
- * @param {Object} options.dingus
- * @param {String} options.dingus.selfBaseUrl
- * @returns {String}
- */
-module.exports = (ctx, options) => {
-  const pageTitle = options.manager.pageTitle;
-  const footerEntries = options.manager.footerEntries;
-  const headElements = [];
-  const navLinks = [];
-  const mainContent = [
-    errorsSection(ctx),
-  ];
-  return th.htmlTemplate(ctx, 2, pageTitle, headElements, navLinks, mainContent, footerEntries);
-};
\ No newline at end of file
diff --git a/src/template/admin-login-html.js b/src/template/admin-login-html.js
deleted file mode 100644 (file)
index a87f930..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-'use strict';
-
-const th = require('./template-helper');
-
-
-/**
- * Login form.
- */
-function indieAuthSection() {
-  return `      <section class="indieauth">
-        <h2>Login</h2>
-        <form action="" method="POST">
-          <fieldset>
-            <legend>IndieAuth</legend>
-            <label for="me">Profile URL:</label>
-            <input id="me" name="me" type="url" size="40" placeholder="https://example.com/my_profile_url" value="" autofocus>
-            <button>Login</button>
-            <br>
-            <div>
-              Logging in with an <a class="external" href="https://indieweb.org/IndieAuth">IndieAuth</a> profile will allow you to view details of any topics on this hub which are related to that profile's domain.
-            </div>
-          </fieldset>
-        </form>
-      </section>`;
-}
-
-
-function userSection(ctx, options) {
-  const secure = (ctx.clientProtocol || '').toLowerCase() === 'https';
-  const showUserForm = secure || !options.authenticator.secureAuthOnly;
-  return showUserForm ? `      <section class="user">
-        <form action="" method="POST">
-          <fieldset>
-            <legend>User Account</legend>
-            <label for="identifier">Username:</label>
-            <input id="identifier" name="identifier" value="">
-            <br>
-            <label for="credential">Password:</label>
-            <input id="credential" name="credential" type="password" value="">
-            <br>
-            <button>Login</button>
-            <br>
-          </fieldset>
-        </form>
-      </section>`
-    : '';
-}
-
-
-function errorsSection(ctx) {
-  return (ctx.errors && ctx.errors.length) ? `        <section class="errors">
-          <h2>Troubles</h2>
-          <p>Problems were encountered while trying to authenticate you.</p>
-          <ul>` +
-          ctx.errors.map((error) => `<li>${error}</li>`).join('\n') + `
-          </ul>
-        </section>`
-    : '';
-}
-
-
-/**
- * Render login form for both local and profile authentication.
- * @param {Object} ctx
- * @param {Object} options
- * @param {Object} options.manager
- * @param {String} options.manager.pageTitle
- * @param {Object} options.dingus
- * @param {String} options.dingus.selfBaseUrl
- * @returns {String}
- */
-module.exports = (ctx, options) => {
-  const pageTitle = options.manager.pageTitle;
-  const footerEntries = options.manager.footerEntries;
-  const headElements = [];
-  const navLinks = [];
-  const mainContent = [
-    errorsSection(ctx),
-    indieAuthSection(),
-    userSection(ctx, options),
-  ];
-  return th.htmlTemplate(ctx, 2, pageTitle, headElements, navLinks, mainContent, footerEntries);
-};
\ No newline at end of file
index a8efef3bb5c7d77cbe9cdd070e0a645008297414..525337dc5ad6e76b3591d6d6297717009cc0d1e4 100644 (file)
@@ -13,13 +13,20 @@ const th = require('./template-helper');
  */
 module.exports = (ctx, options) => {
   const pageTitle = `${options.manager.pageTitle} - Topics`;
-  const headElements = [];
-  const navLinks = [];
+  const logoUrl = options.manager.logoUrl;
   const footerEntries = options.manager.footerEntries;
   if (!ctx.topics) {
     ctx.topics = [];
   }
-  return th.htmlTemplate(ctx, 1, pageTitle, headElements, navLinks, [
+
+  const htmlOptions = {
+    pageTitle,
+    logoUrl,
+    footerEntries,
+  };
+
+  const content = [
+    '<script>0</script>', // This fixes a layout rendering flash on load in FF; do not know why this works but it does.
     `      <section class="topics">
         <p>${ctx.topics.length ? ctx.topics.length : 'no'} topic${(ctx.topics.length === 1) ? '' : 's'}</p>
         <table>
@@ -31,5 +38,7 @@ module.exports = (ctx, options) => {
     `        </tbody>
         </table>
       </section>`,
-  ], footerEntries);
+  ];
+
+  return th.htmlPage(1, ctx, htmlOptions, content);
 };
\ No newline at end of file
index e03918910d935f98f9c70f6cd1720d5d545b8f59..0c281e48086ef4514ea75996ec62577b0823d66d 100644 (file)
@@ -14,7 +14,7 @@ const th = require('./template-helper');
  */
 module.exports = (ctx, options) => {
   const pageTitle = `${options.manager.pageTitle} - Topic Details`;
-  const headElements = [];
+  const logoUrl = options.manager.logoUrl;
   const navLinks = [
     {
       href: '..',
@@ -25,7 +25,16 @@ module.exports = (ctx, options) => {
   if (!ctx.subscriptions) {
     ctx.subscriptions = [];
   }
-  return th.htmlTemplate(ctx, 2, pageTitle, headElements, navLinks, [
+
+  const htmlOptions = {
+    pageTitle,
+    logoUrl,
+    navLinks,
+    footerEntries,
+  };
+
+  const content = [
+    '<script>0</script>', // This fixes a layout rendering flash on load in FF; do not know why this works but it does.
     `      <section class="topics">
         <table>
           <thead>`,
@@ -47,5 +56,7 @@ module.exports = (ctx, options) => {
     `          </tbody>
         </table>
       </section>`,
-  ], footerEntries);
+  ];
+
+  return th.htmlPage(2, htmlOptions, content);
 };
\ No newline at end of file
index ced94bfbef72f5873be0920d68c82282fc3b9929..e177788079d180ccf598fbdef9a04b400fce5784 100644 (file)
@@ -1,8 +1,6 @@
 'use strict';
 
 module.exports = {
-  adminLoginHTML: require('./admin-login-html'),
-  adminIAHTML: require('./admin-ia-html'),
   adminOverviewHTML: require('./admin-overview-html'),
   adminTopicDetailsHTML: require('./admin-topic-details-html'),
   badgeSVG: require('./badge-svg'),
index fc655c01200b2f94cacec37d82d7847d044ca108..14b9d4773c7e8dfcb5e5659a1b6b2cf6ef471b73 100644 (file)
@@ -2,10 +2,10 @@
 
 const th = require('./template-helper');
 
-function hAppSection(pageTitle) {
+function hAppSection(pageTitle, logoUrl) {
   return `      <section class="h-app hidden">
         <h2>h-app Information for IndieAuth Logins</h2>
-        <img src="static/favicon.ico" class="u-logo">
+        <img src="${logoUrl}" class="u-logo">
         <a href="" class="u-url p-name">${pageTitle}</a>
         <p class="p-summary">
           This is a WebSub Hub service, facilitating content distribution.
@@ -149,17 +149,22 @@ module.exports = (ctx, options) => {
   const contactHTML = options.adminContactHTML;
   const footerEntries = options.manager.footerEntries;
   const hubURL = options.dingus.selfBaseUrl || '<s>https://hub.example.com/</s>';
-  const headElements = [];
   const navLinks = [{
     href: 'admin/',
     text: 'Admin',
   }];
-  const mainContent = [
+  const htmlOptions = {
+    pageTitle,
+    logoUrl: options.manager.logoUrl,
+    footerEntries,
+    navLinks,
+  };
+  const content = [
+    '<script>0</script>', // This fixes a layout rendering flash on load in FF; do not know why this works but it does.
     aboutSection(),
     usageSection(isPublicHub, hubURL),
     contactSection(contactHTML),
-    hAppSection(pageTitle),
+    hAppSection(pageTitle, options.manager.logoUrl),
   ];
-  return th.htmlTemplate(ctx, 0, pageTitle, headElements, navLinks, mainContent, footerEntries,
-  );
+  return th.htmlPage(1, ctx, htmlOptions, content);
 };
\ No newline at end of file
index 0ff90772ec7ebd2479344979b08e7d4c075fdb01..bb4dad1e3f574f2332683c9e4bc990c9bc98b3a2 100644 (file)
@@ -1,56 +1,6 @@
 'use strict';
 
-/**
- * A bunch of shorthand to put together common parts of an HTML page. 
- */
-
-/**
- * Some fields may have values outside normal dates, handle them here.
- * @param {Date} date
- * @param {String} otherwise
- */
-const dateOrNot = (date, otherwise) => {
-  if (!date) {
-    return otherwise;
-  }
-  if (typeof date === 'number') {
-    date = new Date(date);
-  }
-  const dateMs = date.getTime();
-  if (!Number.isFinite(dateMs)
-  ||  dateMs == 0) {
-    return otherwise;
-  }
-  return date.toString();
-};
-
-
-/**
- * Render a duration.
- * @param {Number} seconds
- * @returns {String}
- */
-const secondsToPeriod = (seconds) => {
-  let value = seconds;
-  const result = [];
-
-  const nextResult = (factor, label) => {
-    const r = factor ? value % factor : value;
-    if (r) {
-      result.push(`${r} ${label}${r != 1 ? 's' : ''}`);
-    }
-    value = factor ? Math.floor(value / factor) : value;
-  }
-
-  nextResult(60, 'second');
-  nextResult(60, 'minute');
-  nextResult(24, 'hour');
-  nextResult(30, 'day');
-  nextResult(undefined, 'month');
-
-  result.reverse();
-  return result.join(' ');
-};
+const { TemplateHelper } = require('@squeep/html-template-helper');
 
 
 /**
@@ -69,17 +19,17 @@ function renderTopicRow(topic, subscribers, detailsLink = true) {
   return `<tr>
   <th scope="row">${detailsLink ? '<a href="topic/' + topic.id + '">' : ''}${topic.url}${detailsLink ? '</a>' : ''}</th>
   <td>${subscribers.length}</td>
-  <td>${dateOrNot(topic.created, 'Unknown')}</td>
-  <td>${secondsToPeriod(topic.leaseSecondsPreferred)}</td>
-  <td>${secondsToPeriod(topic.leaseSecondsMin)}</td>
-  <td>${secondsToPeriod(topic.leaseSecondsMax)}</td>
+  <td>${TemplateHelper.dateOrNot(topic.created, 'Unknown')}</td>
+  <td>${TemplateHelper.secondsToPeriod(topic.leaseSecondsPreferred)}</td>
+  <td>${TemplateHelper.secondsToPeriod(topic.leaseSecondsMin)}</td>
+  <td>${TemplateHelper.secondsToPeriod(topic.leaseSecondsMax)}</td>
   <td>${topic.publisherValidationUrl ? topic.publisherValidationUrl : 'None'}</td>
   <td>${topic.isActive}</td>
   <td>${topic.isDeleted}</td>
-  <td>${dateOrNot(topic.lastPublish, 'Never')}</td>
-  <td>${dateOrNot(topic.contentFetchNextAttempt, 'Next Publish')}</td>
+  <td>${TemplateHelper.dateOrNot(topic.lastPublish, 'Never')}</td>
+  <td>${TemplateHelper.dateOrNot(topic.contentFetchNextAttempt, 'Next Publish')}</td>
   <td>${topic.contentFetchAttemptsSinceSuccess}</td>
-  <td>${dateOrNot(topic.contentUpdated, 'Never')}</td>
+  <td>${TemplateHelper.dateOrNot(topic.contentUpdated, 'Never')}</td>
   <td>${topic.contentType}</td>
   <td>${topic.id}</td>
 </tr>`;
@@ -124,16 +74,16 @@ function renderSubscriptionRow(subscription) {
   }
   return `<tr>
   <td scope="row">${subscription.callback}</td>
-  <td>${dateOrNot(subscription.created, 'Unknown')}</td>
-  <td>${dateOrNot(subscription.verified, 'Never')}</td>
-  <td>${dateOrNot(subscription.expires, 'Never')}</td>
+  <td>${TemplateHelper.dateOrNot(subscription.created, 'Unknown')}</td>
+  <td>${TemplateHelper.dateOrNot(subscription.verified, 'Never')}</td>
+  <td>${TemplateHelper.dateOrNot(subscription.expires, 'Never')}</td>
   <td>${!!subscription.secret}</td>
   <td>${subscription.signatureAlgorithm}</td>
   <td>${subscription.httpRemoteAddr}</td>
   <td>${subscription.httpFrom}</td>
-  <td>${dateOrNot(subscription.contentDelivered, 'Never')}</td>
+  <td>${TemplateHelper.dateOrNot(subscription.contentDelivered, 'Never')}</td>
   <td>${subscription.deliveryAttemptsSinceSuccess}</td>
-  <td>${dateOrNot(subscription.deliveryNextAttempt, 'Next Publish')}</td>
+  <td>${TemplateHelper.dateOrNot(subscription.deliveryNextAttempt, 'Next Publish')}</td>
   <td>${subscription.id}</td>
 </tr>`;
 }
@@ -162,135 +112,9 @@ function renderSubscriptionRowHeader() {
 }
 
 
-/**
- * Render the preamble for an HTML page, up through body.
- * @param {Number} pagePathLevel number of paths below root this page is
- * @param {String} pageTitle
- * @param {String[]} headElements
- * @returns 
- */
-function htmlHead(pagePathLevel, pageTitle, headElements = []) {
-  const rootPathPfx = '../'.repeat(pagePathLevel);
-  return `<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8">` +
-  headElements.map((e) => `${'  '.repeat(2)}${e}`).join('\n') + `
-    <title>${pageTitle}</title>
-    <link rel="stylesheet" href="${rootPathPfx}static/theme.css">
-  </head>
-  <body>`;
-}
-
-
-/**
- * Closes remainder of HTML page body.
- * @returns {String}
- */
-function htmlTail() {
-  return `  </body>
-</html>`;
-}
-
-
-/**
- * Render a navigation link for the header section.
- * @param {Object} nav
- * @param {String} nav.href
- * @param {String} nav.class
- * @param {String} nav.text
- * @returns {String}
- */
-function renderNavLink(nav) {
-  return `<li>
-  <a href="${nav.href}"${nav.class ? (' class="' + nav.class + '"') : ''}>${nav.text}</a>
-</li>`;
-}
-
-
-/**
- * Render the navigation header, and open the main section.
- * @param {String} pageTitle
- * @param {Object[]} navLinks
- * @returns {String}
- */
-function htmlHeader(pageTitle, navLinks = []) {
-  return `    <header>
-      <h1>${pageTitle}</h1>
-      <nav>` +
-    (navLinks.length ? `
-        <ol>
-          ${navLinks.map((l) => renderNavLink(l)).join('\n')}
-        </ol>`
-      : '') + `
-      </nav>
-    </header>
-    <main>`;
-}
-
-
-/**
- * Close the main section and finish off with boilerplate.
- * @param {String[]} footerEntries
- * @returns {String}
- */
-function htmlFooter(footerEntries = []) {
-  return `    </main>
-    <footer>` +
-    (footerEntries.length ? `
-      <ol>` + footerEntries.map((f) => `        <li>${f}</li>`).join('\n') + `
-      </ol>`
-      : '') + `
-    </footer>`;
-}
-
-
-/**
- * Render all parts of an HTML page. Adds user logout nav link automatically.
- * @param {Object} ctx
- * @param {Number} pagePathLevel
- * @param {String} pageTitle
- * @param {String[]} headElements
- * @param {Object[]} navLinks
- * @param {String[]} main
- * @param {String[]} footerEntries
- * @returns {String}
- */
-function htmlTemplate(ctx, pagePathLevel, pageTitle, headElements = [], navLinks = [], main = [], footerEntries = []) {
-  const user = (ctx && ctx.session && ctx.session.authenticatedProfile) || (ctx && ctx.session && ctx.session.authenticatedIdentifier);
-  if (user) {
-    let logoutPath;
-    if (pagePathLevel > 0) {
-      logoutPath = `${'../'.repeat(pagePathLevel - 1)}`;
-    } else {
-      logoutPath = 'admin/';
-    }
-    navLinks.push({
-      text: `Logout (${user})`,
-      href: `${logoutPath}logout`,
-    });
-  }
-  return [
-    htmlHead(pagePathLevel, pageTitle, headElements),
-    htmlHeader(pageTitle, navLinks),
-    ...main,
-    htmlFooter(footerEntries),
-    htmlTail(),
-  ].join('\n');
-}
-
-
-module.exports = {
-  dateOrNot,
-  secondsToPeriod,
-  htmlHeader,
-  htmlFooter,
-  htmlHead,
-  htmlTail,
-  renderNavLink,
+module.exports = Object.assign(Object.create(TemplateHelper), {
   renderTopicRowHeader,
   renderTopicRow,
   renderSubscriptionRowHeader,
   renderSubscriptionRow,
-  htmlTemplate,
-};
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/static/custom.css b/static/custom.css
new file mode 100644 (file)
index 0000000..e69de29
index 7417e57c080fe03e69977cc8294be143f2c32ef1..fb85b52b1834bd91ae5ed426d585fd0eb65780a9 100644 (file)
@@ -58,6 +58,10 @@ section {}
 section + section {
        margin-top: 2em;
 }
+.logo {
+       vertical-align: middle;
+       height: 2em;
+}
 .about {}
 .usage {}
 .copyright {
diff --git a/test/lint-html.js b/test/lint-html.js
new file mode 100644 (file)
index 0000000..4b5cd47
--- /dev/null
@@ -0,0 +1,13 @@
+'use strict';
+
+const assert = require('assert');
+const stubLogger = require('./stub-logger');
+const { lint } = require('html-minifier-lint'); // eslint-disable-line node/no-unpublished-require
+
+function lintHtml(html) {
+  const result = lint(html);
+  stubLogger.debug('lintHtml', '', { result, html });
+  assert(!result);
+}
+
+module.exports = lintHtml;
diff --git a/test/src/authenticator.js b/test/src/authenticator.js
deleted file mode 100644 (file)
index 47313d8..0000000
+++ /dev/null
@@ -1,344 +0,0 @@
-/* eslint-env mocha */
-'use strict';
-
-const assert = require('assert');
-const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
-const Authenticator = require('../../src/authenticator');
-const stubLogger = require('../stub-logger');
-const stubDb = require('../stub-db');
-const Errors = require('../../src/errors');
-const Enum = require('../../src/enum');
-const Config = require('../../config');
-
-const noExpectedException = 'did not receive expected exception';
-
-describe('Authenticator', function () {
-  let authenticator, credential, ctx, identifier, password, options;
-  function _authMechanismRequired(a, m) {
-    if (!a.authn[m]) { // eslint-disable-line security/detect-object-injection
-      this.skip();
-    }
-  };
-
-  beforeEach(function () {
-    options = Config('test');
-    authenticator = new Authenticator(stubLogger, stubDb, options);
-    identifier = 'username';
-    credential = '$argon2id$v=19$m=4096,t=3,p=1$1a6zRlX4BI4$sZGcQ72BTpDOlxUI/j3DmE1PMcu+Cs5liZ/D6kk79Ew';
-    ctx = {};
-    password = 'badPassword';
-    stubDb._reset();
-  });
-  afterEach(function () {
-    sinon.restore();
-  });
-
-  it('covers no auth mechanisms', function () {
-    options.authenticator.authnEnabled = [];
-    try {
-      authenticator = new Authenticator(stubLogger, stubDb, options);
-      assert.fail(noExpectedException);
-    } catch (e) {
-      assert.strictEqual(e.message, 'no authentication mechanisms available');
-    }
-  });
-
-  describe('isValidBasic', function () {
-    it('succeeds', async function () {
-      _authMechanismRequired(authenticator, 'argon2');
-      authenticator.db.authenticationGet.resolves({
-        identifier,
-        credential,
-      });
-      const authString = `${identifier}:${password}`;
-      const result = await authenticator.isValidBasic(authString, ctx);
-      assert.strictEqual(result, true);
-      assert.strictEqual(ctx.authenticationId, identifier);
-    });
-    it('fails', async function () {
-      _authMechanismRequired(authenticator, 'argon2');
-      authenticator.db.authenticationGet.resolves({
-        identifier,
-        credential,
-      });
-      const authString = `${identifier}:wrongPassword}`;
-      const result = await authenticator.isValidBasic(authString, ctx);
-      assert.strictEqual(result, false);
-      assert.strictEqual(ctx.authenticationId, undefined);
-    });
-    it('covers no entry', async function() {
-      authenticator.db.authenticationGet.resolves();
-      const authString = `${identifier}:wrongPassword}`;
-      const result = await authenticator.isValidBasic(authString, ctx);
-      assert.strictEqual(result, false);
-      assert.strictEqual(ctx.authenticationId, undefined);
-    });
-    it('covers unknown password hash', async function () {
-      authenticator.db.authenticationGet.resolves({
-        identifier,
-        credential: '$other$kind_of_credential',
-      });
-      const authString = `${identifier}:wrongPassword}`;
-      const result = await authenticator.isValidBasic(authString, ctx);
-      assert.strictEqual(result, false);
-      assert.strictEqual(ctx.authenticationId, undefined);
-    });
-  }); // isValidBasic
-
-  describe('isValidIdentifierCredential', function () {
-    it('succeeds', async function () {
-      _authMechanismRequired(authenticator, 'argon2');
-      authenticator.db.authenticationGet.resolves({
-        identifier,
-        credential,
-      });
-      const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
-      assert.strictEqual(result, true);
-      assert.strictEqual(ctx.authenticationId, identifier);
-    });
-    it('fails', async function () {
-      _authMechanismRequired(authenticator, 'argon2');
-      authenticator.db.authenticationGet.resolves({
-        identifier,
-        credential,
-      });
-      const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
-      assert.strictEqual(result, false);
-      assert.strictEqual(ctx.authenticationId, undefined);
-    });
-    it('covers no entry', async function() {
-      authenticator.db.authenticationGet.resolves();
-      const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
-      assert.strictEqual(result, false);
-      assert.strictEqual(ctx.authenticationId, undefined);
-    });
-    it('covers unknown password hash', async function () {
-      authenticator.db.authenticationGet.resolves({
-        identifier,
-        credential: '$other$kind_of_credential',
-      });
-      const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
-      assert.strictEqual(result, false);
-      assert.strictEqual(ctx.authenticationId, undefined);
-    });
-    it('covers PAM', async function () {
-      _authMechanismRequired(authenticator, 'pam');
-      sinon.stub(authenticator, '_isValidPAMIdentifier').resolves(true);
-      authenticator.db.authenticationGet.resolves({
-        identifier,
-        credential: '$PAM$',
-      });
-      const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
-      assert.strictEqual(result, true);
-      assert.strictEqual(ctx.authenticationId, identifier);
-    });
-    it('covers debug', async function () {
-      authenticator.authnEnabled = ['DEBUG_ANY'];
-      const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
-      assert.strictEqual(result, true);
-      assert.strictEqual(ctx.authenticationId, identifier);
-    });
-  }); // isValidIdentifierCredential
-
-  describe('_isValidPAMIdentifier', function () {
-    beforeEach(function () {
-      _authMechanismRequired(authenticator, 'pam');
-      sinon.stub(authenticator.authn.pam, 'pamAuthenticatePromise');
-    });
-    it('covers success', async function () {
-      authenticator.authn.pam.pamAuthenticatePromise.resolves(true);
-      const result = await authenticator._isValidPAMIdentifier(identifier, credential);
-      assert.strictEqual(result, true);
-    });
-    it('covers failure', async function () {
-      _authMechanismRequired(authenticator, 'pam');
-      authenticator.authn.pam.pamAuthenticatePromise.rejects(new authenticator.authn.pam.PamError());
-      const result = await authenticator._isValidPAMIdentifier(identifier, credential);
-      assert.strictEqual(result, false);
-    });
-    it('covers error', async function () {
-      _authMechanismRequired(authenticator, 'pam');
-      const expected = new Error('blah');
-      authenticator.authn.pam.pamAuthenticatePromise.rejects(expected);
-      try {
-        await authenticator._isValidPAMIdentifier(identifier, credential);
-        assert.fail(noExpectedException);
-      } catch (e) {
-        assert.deepStrictEqual(e, expected);
-      }
-    });
-    it('covers forbidden', async function () {
-      identifier = 'root';
-      const result = await authenticator._isValidPAMIdentifier(identifier, credential);
-      assert.strictEqual(result, false);
-    });
-  }); // _isValidPAMIdentifier
-
-  describe('isValidAuthorization', function () {
-    it('handles basic', async function () {
-      const expected = true;
-      const authorizationHeader = 'basic Zm9vOmJhcg==';
-      sinon.stub(authenticator, 'isValidBasic').resolves(expected);
-      const result = await authenticator.isValidAuthorization(authorizationHeader, ctx);
-      assert.strictEqual(result, expected);
-    });
-    it('handles other', async function () {
-      const expected = false;
-      const authorizationHeader = 'bearer Zm9vOmJhcg==';
-      const result = await authenticator.isValidAuthorization(authorizationHeader, ctx);
-      assert.strictEqual(result, expected);
-    });
-  }); // isValidAuthorization
-
-  describe('requestBasic', function () {
-    it('covers', function () {
-      try {
-        const res = {
-          setHeader: () => {},
-        };
-        authenticator.requestBasic(res);
-        assert.fail(noExpectedException);
-      } catch (e) {
-        assert(e instanceof Errors.ResponseError);
-        assert.strictEqual(e.statusCode, Enum.ErrorResponse.Unauthorized.statusCode);
-      }
-    });
-  }); // requestBasic
-
-  describe('isValidCookieAuth', function () {
-    beforeEach(function () {
-      sinon.stub(authenticator.mysteryBox, 'unpack');
-    });
-    it('covers identifier success', async function () {
-      authenticator.mysteryBox.unpack.resolves({
-        authenticatedIdentifier: 'identifier',
-      });
-      const result = await authenticator.isValidCookieAuth(ctx, 'WSHas=dummy');
-      assert.strictEqual(result, true);
-    });
-    it('covers profile success', async function () {
-      authenticator.mysteryBox.unpack.resolves({
-        authenticatedProfile: 'profile',
-      });
-      const result = await authenticator.isValidCookieAuth(ctx, 'WSHas=dummy');
-      assert.strictEqual(result, true);
-    });
-    it('covers missing cookie', async function () {
-      const result = await authenticator.isValidCookieAuth(ctx, 'wrongCookie');
-      assert.strictEqual(result, false);
-    });
-    it('covers bad cookie', async function () {
-      authenticator.mysteryBox.unpack.rejects();
-      const result = await authenticator.isValidCookieAuth(ctx, 'WSHas=dummy');
-      assert.strictEqual(result, false);
-    });
-  }); // isValidCookieAuth
-
-  describe('required', function () {
-    let req, res;
-    beforeEach(function () {
-      ctx.clientProtocol = 'https';
-      req = {
-        getHeader: sinon.stub(),
-      };
-      res = {
-        end: sinon.stub(),
-        setHeader: sinon.stub(),
-      }
-    });
-    it('succeeds', async function() {
-      req.getHeader.returns('auth header');
-      sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
-      const result = await authenticator.required(req, res, ctx);
-      assert.strictEqual(result, true);
-    });
-    it('covers valid cookie session', async function () {
-      req.getHeader.returns('WSHas=sessionCookie');
-      sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
-      const result = await authenticator.required(req, res, ctx);
-      assert.strictEqual(result, true);
-    });
-    it('rejects insecure connection', async function () {
-      ctx.clientProtocol = 'http';
-      try {
-        await authenticator.required(req, res, ctx);
-        assert.fail(noExpectedException);
-      } catch (e) {
-        assert(e instanceof Errors.ResponseError);
-        assert.strictEqual(e.statusCode, Enum.ErrorResponse.Forbidden.statusCode);
-      }
-    });
-    it('rejects invalid auth', async function () {
-      try {
-        req.getHeader.returns('auth header');
-        sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
-        await authenticator.required(req, res, ctx);
-        assert.fail(noExpectedException);
-      } catch (e) {
-        assert(e instanceof Errors.ResponseError);
-        assert.strictEqual(e.statusCode, Enum.ErrorResponse.Unauthorized.statusCode);
-      }
-    });
-    it('redirects without any auth', async function () {
-      await authenticator.required(req, res, ctx);
-      assert(res.end.called);
-      assert(res.setHeader.called);
-    });
-  }); // required
-
-  describe('requiredLocal', function () {
-    let req, res;
-    beforeEach(function () {
-      ctx.clientProtocol = 'https';
-      req = {
-        getHeader: sinon.stub(),
-      };
-      res = {
-        end: sinon.stub(),
-        setHeader: sinon.stub(),
-      }
-    });
-    it('succeeds', async function() {
-      req.getHeader.returns('auth header');
-      sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
-      const result = await authenticator.requiredLocal(req, res, ctx);
-      assert.strictEqual(result, true);
-    });
-    it('covers valid cookie session', async function () {
-      req.getHeader.returns('WSHas=sessionCookie');
-      sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
-      ctx.session = {
-        authenticatedIdentifier: identifier,
-      };
-      const result = await authenticator.requiredLocal(req, res, ctx);
-      assert.strictEqual(result, true);
-    });
-    it('rejects insecure connection', async function () {
-      ctx.clientProtocol = 'http';
-      try {
-        await authenticator.requiredLocal(req, res, ctx);
-        assert.fail(noExpectedException);
-      } catch (e) {
-        assert(e instanceof Errors.ResponseError);
-        assert.strictEqual(e.statusCode, Enum.ErrorResponse.Forbidden.statusCode);
-      }
-    });
-    it('rejects invalid auth', async function () {
-      try {
-        req.getHeader.returns('auth header');
-        sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
-        await authenticator.requiredLocal(req, res, ctx);
-        assert.fail(noExpectedException);
-      } catch (e) {
-        assert(e instanceof Errors.ResponseError);
-        assert.strictEqual(e.statusCode, Enum.ErrorResponse.Unauthorized.statusCode);
-      }
-    });
-    it('redirects without any auth', async function () {
-      await authenticator.requiredLocal(req, res, ctx);
-      assert(res.end.called);
-      assert(res.setHeader.called);
-    });
-  }); // requiredLocal
-
-}); // Authenticator
index bb93d2e7cc463efcc7b9193ba14306e9ba2ddd99..23b50e15eb0a970e30a7f8bc00bd558d3d64e150 100644 (file)
@@ -103,7 +103,7 @@ describe('Service', function () {
   describe('handlerGetAdminOverview', function () {
     it('covers', async function () {
       await service.handlerGetAdminOverview(req, res, ctx);
-      assert(service.authenticator.required.called);
+      assert(service.authenticator.sessionRequired.called);
       assert(service.manager.getAdminOverview.called);
     })
   }); // handlerGetAdminOverview
@@ -111,7 +111,7 @@ describe('Service', function () {
   describe('handlerGetAdminTopicDetails', function () {
     it('covers', async function () {
       await service.handlerGetAdminTopicDetails(req, res, ctx);
-      assert(service.authenticator.required.called);
+      assert(service.authenticator.sessionRequired.called);
       assert(service.manager.getTopicDetails.called);
     })
   }); // handlerGetAdminTopicDetails
@@ -120,7 +120,7 @@ describe('Service', function () {
     it('covers', async function () {
       service.serveFile.resolves();
       await service.handlerPostAdminProcess(req, res, ctx);
-      assert(service.authenticator.requiredLocal.called);
+      assert(service.authenticator.apiRequiredLocal.called);
       assert(service.manager.processTasks.called);
     });
   }); // handlerPostAdminProcess
@@ -129,7 +129,7 @@ describe('Service', function () {
     it('covers', async function () {
       sinon.stub(service, 'bodyData').resolves();
       await service.handlerUpdateTopic(req, res, ctx);
-      assert(service.authenticator.requiredLocal.called);
+      assert(service.authenticator.apiRequiredLocal.called);
       assert(service.manager.updateTopic.called);
     });
   }); // handlerUpdateTopic
@@ -138,7 +138,7 @@ describe('Service', function () {
     it('covers', async function () {
       sinon.stub(service, 'bodyData').resolves();
       await service.handlerUpdateSubscription(req, res, ctx);
-      assert(service.authenticator.requiredLocal.called);
+      assert(service.authenticator.apiRequiredLocal.called);
       assert(service.manager.updateSubscription.called);
     });
   }); // handlerUpdateSubscription
diff --git a/test/src/session-manager.js b/test/src/session-manager.js
deleted file mode 100644 (file)
index 0d77ba0..0000000
+++ /dev/null
@@ -1,240 +0,0 @@
-/* eslint-env mocha */
-/* eslint-disable capitalized-comments, sonarjs/no-duplicate-string, sonarjs/no-identical-functions */
-
-'use strict';
-
-const assert = require('assert');
-const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
-
-const SessionManager = require('../../src/session-manager');
-const Config = require('../../config');
-const stubLogger = require('../stub-logger');
-
-describe('SessionManager', function () {
-  let manager, options, stubAuthenticator;
-  let res, ctx;
-
-  beforeEach(function () {
-    options = new Config('test');
-    res = {
-      end: sinon.stub(),
-      setHeader: sinon.stub(),
-    };
-    ctx = {
-      cookie: '',
-      params: {},
-      queryParams: {},
-      parsedBody: {},
-    };
-    stubAuthenticator = {
-      isValidIdentifierCredential: sinon.stub(),
-    };
-    manager = new SessionManager(stubLogger, stubAuthenticator, options);
-    sinon.stub(manager.indieAuthCommunication);
-    stubLogger._reset();
-  });
-  afterEach(function () {
-    sinon.restore();
-  });
-
-  describe('_sessionCookieSet', function () {
-    let session, maxAge;
-    beforeEach(function () {
-      session = {};
-      maxAge = 86400;
-    });
-    it('covers', async function () {
-      await manager._sessionCookieSet(res, session, maxAge);
-      assert(res.setHeader.called);
-    });
-    it('covers reset', async function () {
-      session = undefined;
-      maxAge = 0;
-      await manager._sessionCookieSet(res, session, maxAge);
-      assert(res.setHeader.called);
-    });
-  }); // _sessionCookieSet
-
-  describe('getAdminLogin', function () {
-    it('covers', async function () {
-      await manager.getAdminLogin(res, ctx);
-    });
-  }); // getAdminLogin
-
-  describe('postAdminLogin', function () {
-    it('covers valid local', async function () {
-      ctx.parsedBody.identifier = 'user';
-      ctx.parsedBody.credential = 'password';
-      manager.authenticator.isValidIdentifierCredential.resolves(true);
-      await manager.postAdminLogin(res, ctx);
-      assert.strictEqual(res.statusCode, 302);
-    });
-    it('covers invalid local', async function () {
-      ctx.parsedBody.identifier = 'user';
-      ctx.parsedBody.credential = 'password';
-      manager.authenticator.isValidIdentifierCredential.resolves(false);
-      await manager.postAdminLogin(res, ctx);
-      assert(!res.setHeader.called);
-    });
-    it('covers valid profile', async function () {
-      ctx.parsedBody.me = 'https://example.com/profile';
-      manager.indieAuthCommunication.fetchProfile.resolves({
-        authorizationEndpoint: 'https://example.com/auth',
-      });
-      await manager.postAdminLogin(res, ctx);
-      assert.strictEqual(res.statusCode, 302);
-    });
-    it('covers invalid profile', async function () {
-      ctx.parsedBody.me = 'not a profile';
-      manager.indieAuthCommunication.fetchProfile.resolves();
-      await manager.postAdminLogin(res, ctx);
-      assert(!res.setHeader.called);
-    });
-    it('covers invalid profile response', async function () {
-      ctx.parsedBody.me = 'https://example.com/profile';
-      manager.indieAuthCommunication.fetchProfile.resolves();
-      await manager.postAdminLogin(res, ctx);
-      assert(!res.setHeader.called);
-    });
-    it('covers invalid profile response endpoint', async function () {
-      ctx.parsedBody.me = 'https://example.com/profile';
-      manager.indieAuthCommunication.fetchProfile.resolves({
-        authorizationEndpoint: 'not an auth endpoint',
-      });
-      await manager.postAdminLogin(res, ctx);
-      assert(!res.setHeader.called);
-    });
-  }); // postAdminLogin
-
-  describe('getAdminLogout', function () {
-    it('covers', async function () {
-      await manager.getAdminLogout(res, ctx);
-    });
-  }); // getAdminLogout
-
-  describe('getAdminIA', function () {
-    let state, me, authorizationEndpoint;
-    beforeEach(function () {
-      state = '4ea7e936-3427-11ec-9f4b-0025905f714a';
-      me = 'https://example.com/profile';
-      authorizationEndpoint = 'https://example.com/auth'
-      ctx.cookie = 'WSHas=sessionCookie';
-      manager.indieAuthCommunication.redeemProfileCode.resolves({
-        me,
-      });
-      manager.indieAuthCommunication.fetchProfile.resolves({
-        authorizationEndpoint,
-      });
-      sinon.stub(manager.mysteryBox, 'unpack').resolves({
-        authorizationEndpoint,
-        state,
-        me,
-      });
-    });
-    it('covers valid', async function () {
-      ctx.queryParams['state'] = state;
-      ctx.queryParams['code'] = 'codeCodeCode';
-
-      await manager.getAdminIA(res, ctx);
-
-      assert.strictEqual(res.statusCode, 302);
-    });
-    it('covers missing cookie', async function () {
-      delete ctx.cookie;
-
-      await manager.getAdminIA(res, ctx);
-
-      assert(ctx.errors.length);
-    });
-    it('covers invalid cookie', async function () {
-      manager.mysteryBox.unpack.restore();
-      sinon.stub(manager.mysteryBox, 'unpack').rejects();
-
-      await manager.getAdminIA(res, ctx);
-
-      assert(ctx.errors.length);
-    });
-    it('covers mis-matched state', async function () {
-      ctx.queryParams['state'] = 'incorrect-state';
-      ctx.queryParams['code'] = 'codeCodeCode';
-
-      await manager.getAdminIA(res, ctx);
-
-      assert(ctx.errors.length);
-    });
-    it('relays auth endpoint errors', async function () {
-      ctx.queryParams['state'] = state;
-      ctx.queryParams['code'] = 'codeCodeCode';
-      ctx.queryParams['error'] = 'error_code';
-      ctx.queryParams['error_description'] = 'something went wrong';
-
-      await manager.getAdminIA(res, ctx);
-
-      assert(ctx.errors.length);
-    });
-    it('covers invalid restored session', async function () {
-      manager.mysteryBox.unpack.restore();
-      sinon.stub(manager.mysteryBox, 'unpack').resolves({
-        authorizationEndpoint: 'not a url',
-        state,
-        me,
-      });
-      ctx.queryParams['state'] = state;
-      ctx.queryParams['code'] = 'codeCodeCode';
-
-      await manager.getAdminIA(res, ctx);
-
-      assert(ctx.errors.length);
-    });
-    it('covers empty profile redemption response', async function () {
-      ctx.queryParams['state'] = state;
-      ctx.queryParams['code'] = 'codeCodeCode';
-      manager.indieAuthCommunication.redeemProfileCode.restore();
-      sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves();
-
-      await manager.getAdminIA(res, ctx);
-
-      assert(ctx.errors.length);
-    });
-    it('covers missing profile in redemption response', async function () {
-      ctx.queryParams['state'] = state;
-      ctx.queryParams['code'] = 'codeCodeCode';
-      manager.indieAuthCommunication.redeemProfileCode.restore();
-      sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves({
-      });
-
-      await manager.getAdminIA(res, ctx);
-
-      assert(ctx.errors.length);
-    });
-    it('covers different canonical profile response', async function () {
-      ctx.queryParams['state'] = state;
-      ctx.queryParams['code'] = 'codeCodeCode';
-      manager.indieAuthCommunication.redeemProfileCode.restore();
-      sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves({
-        me: 'https://different.example.com/profile',
-      });
-
-      await manager.getAdminIA(res, ctx);
-
-      assert.strictEqual(res.statusCode, 302);
-    });
-    it('covers different canonical profile response mis-matched endpoint', async function () {
-      ctx.queryParams['state'] = state;
-      ctx.queryParams['code'] = 'codeCodeCode';
-      manager.indieAuthCommunication.redeemProfileCode.restore();
-      sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves({
-        me: 'https://different.example.com/profile',
-      });
-      manager.indieAuthCommunication.fetchProfile.restore();
-      sinon.stub(manager.indieAuthCommunication, 'fetchProfile').resolves({
-        authorizationEndpoint: 'https://elsewhere.example.com/auth',
-      });
-
-      await manager.getAdminIA(res, ctx);
-
-      assert(ctx.errors.length);
-    });
-  }); // getAdminIA
-
-}); // SessionManager
\ No newline at end of file
diff --git a/test/src/template/admin-ia-html.js b/test/src/template/admin-ia-html.js
deleted file mode 100644 (file)
index d909389..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-/* eslint-env mocha */
-'use strict';
-
-const assert = require('assert');
-const template = require('../../../src/template/admin-ia-html');
-const Config = require('../../../config');
-const config = new Config('test');
-
-describe('Admin Login HTML Template', function () {
-  let ctx;
-
-  beforeEach(function () {
-    ctx = {};
-  });
-
-  it('covers', function () {
-    ctx.errors = ['bad'];
-    const result = template(ctx, config);
-    assert(result);
-  });
-  it('covers empty', function () {
-    const result = template(ctx, config);
-    assert(result);
-  });
-});
diff --git a/test/src/template/admin-login-html.js b/test/src/template/admin-login-html.js
deleted file mode 100644 (file)
index 661efd9..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-/* eslint-env mocha */
-'use strict';
-
-const assert = require('assert');
-const template = require('../../../src/template/admin-login-html');
-const Config = require('../../../config');
-const config = new Config('test');
-
-describe('Admin Login HTML Template', function () {
-  let ctx;
-
-  beforeEach(function () {
-    ctx = {};
-  });
-
-  it('covers', function () {
-    ctx.errors = ['bad'];
-    ctx.clientProtocol = 'https';
-    const result = template(ctx, config);
-    assert(result);
-  });
-  it('covers empty', function () {
-    const result = template(ctx, config);
-    assert(result);
-  });
-});
index 5fa0a0e1bb903d303052e2de4ceecf846b32b9bf..1e715d28dd686630bfdcd7de2797661dd91adacc 100644 (file)
@@ -4,27 +4,31 @@
 const assert = require('assert');
 const template = require('../../../src/template/admin-overview-html');
 const Config = require('../../../config');
-const config = new Config('test');
+const lintHtml = require('../../lint-html');
 
 describe('Admin Overview HTML Template', function () {
-  let ctx;
+  let ctx, config;
 
   beforeEach(function () {
     ctx = {};
+    config = new Config('test');
   });
 
   it('covers missing topics', function () {
     const result = template(ctx, config);
+    lintHtml(result);
     assert(result);
   });
   it('covers single topic', function () {
     ctx.topics = [{}];
     const result = template(ctx, config);
+    lintHtml(result);
     assert(result);
   });
   it('covers plural topics', function () {
     ctx.topics = [{}, {}, {}];
     const result = template(ctx, config);
+    lintHtml(result);
     assert(result);
   });
 });
index 4b8c0dd832ef9cd28d3e996848ffae3c1746ca60..823b439d695ecbe41566b40d91a38382fae433b4 100644 (file)
@@ -4,10 +4,10 @@
 const assert = require('assert');
 const template = require('../../../src/template/admin-topic-details-html');
 const Config = require('../../../config');
-const config = new Config('test');
+const lintHtml = require('../../lint-html');
 
 describe('Admin Topic Details HTML Template', function () {
-  let ctx;
+  let ctx, config;
 
   beforeEach(function () {
     ctx = {
@@ -16,26 +16,31 @@ describe('Admin Topic Details HTML Template', function () {
         {},
       ],
     };
+    config = new Config('test');
   });
 
   it('renders', function () {
     const result = template(ctx, config);
+    lintHtml(result);
     assert(result);
   });
   it('covers null topic', function () {
     ctx.topic = null;
     ctx.subscriptions = null;
     const result = template(ctx, config);
+    lintHtml(result);
     assert(result);
   });
   it('covers missing subscriptions', function () {
     delete ctx.subscriptions;
     const result = template(ctx, config);
+    lintHtml(result);
     assert(result);
   });
   it('covers plural subscriptions', function () {
     ctx.subscriptions = [{}, {}, {}];
     const result = template(ctx, config);
+    lintHtml(result);
     assert(result);
   });
 });
index f48cb0828dcb16488aaf36ed2f02f32d089eda75..36c7b44a5cacdcd4f1f7c2fccff7d8bfd8e3d847 100644 (file)
@@ -4,6 +4,7 @@
 const assert = require('assert');
 const template = require('../../../src/template/root-html');
 const Config = require('../../../config');
+const lintHtml = require('../../lint-html');
 
 describe('Root HTML Template', function () {
   let ctx, config;
@@ -15,12 +16,14 @@ describe('Root HTML Template', function () {
 
   it('renders', function () {
     const result = template(ctx, config);
+    lintHtml(result);
     assert(result);
   });
 
   it('covers options', function () {
     delete config.dingus.selfBaseUrl;
     const result = template(ctx, config);
+    lintHtml(result);
     assert(result);
   });
 
@@ -28,6 +31,7 @@ describe('Root HTML Template', function () {
     config.adminContactHTML = '<div>support</div>';
     config.manager.publicHub = false;
     const result = template(ctx, config);
+    lintHtml(result);
     assert(result);
   });
 
index 3ad7f83812a3c3c8c5f46b7e32360837c059f338..8c8579d1cc5a83b3d65b8acd2c232805d61097a8 100644 (file)
@@ -3,67 +3,8 @@
 
 const assert = require('assert');
 const th = require('../../../src/template/template-helper');
-const Config = require('../../../config');
-const config = new Config('test');
 
 describe('Template Helper', function () {
-  let ctx;
-
-  beforeEach(function () {
-    ctx = {};
-  });
-
-  describe('dateOrNot', function () {
-    let date, otherwise;
-    beforeEach(function () {
-      date = new Date();
-      otherwise = 'otherwise';
-    });
-    it('covers', function () {
-      const result = th.dateOrNot(date, otherwise);
-      assert.strictEqual(result, date.toString());
-    });
-    it('covers no date', function () {
-      date = undefined;
-      const result = th.dateOrNot(date, otherwise);
-      assert.strictEqual(result, otherwise);
-    });
-    it('covers ms', function () {
-      const result = th.dateOrNot(date.getTime(), otherwise);
-      assert.strictEqual(result, date.toString());
-    });
-    it('covers naught', function () {
-      const result = th.dateOrNot(0, otherwise);
-      assert.strictEqual(result, otherwise);
-    });
-    it('covers the infinite', function () {
-      const result = th.dateOrNot(-Infinity, otherwise);
-      assert.strictEqual(result, otherwise);
-    });
-  }); // dateOrNot
-
-  describe('secondsToPeriod', function () {
-    it('covers seconds', function () {
-      const result = th.secondsToPeriod(45);
-      assert.strictEqual(result, '45 seconds');
-    });
-    it('covers minutes', function () {
-      const result = th.secondsToPeriod(105);
-      assert.strictEqual(result, '1 minute 45 seconds');
-    });
-    it('covers hours', function () {
-      const result = th.secondsToPeriod(3705);
-      assert.strictEqual(result, '1 hour 1 minute 45 seconds');
-    });
-    it('covers days', function () {
-      const result = th.secondsToPeriod(90105);
-      assert.strictEqual(result, '1 day 1 hour 1 minute 45 seconds');
-    });
-    it('covers months', function () {
-      const result = th.secondsToPeriod(5274105);
-      assert.strictEqual(result, '2 months 1 day 1 hour 1 minute 45 seconds');
-    });
-  }); // secondsToPeriod
 
   describe('renderTopicRow', function () {
     let topic, subscribers;
@@ -122,115 +63,4 @@ describe('Template Helper', function () {
     });
   }); // renderSubscriptionRowHeader
 
-  describe('htmlHead', function () {
-    let pagePathLevel, pageTitle, headElements;
-    beforeEach(function () {
-      pagePathLevel = 2;
-      pageTitle = 'title';
-    });
-    it('covers', function () {
-      const result = th.htmlHead(pagePathLevel, pageTitle, headElements);
-      assert(result);
-    });
-    it('covers elements', function () {
-      headElements = [ '<div>foop</div>', '<div>poof</div>' ];
-      const result = th.htmlHead(pagePathLevel, pageTitle, headElements);
-      assert(result);
-    });
-  }); // htmlHead
-
-  describe('htmlTail', function () {
-    it('covers', function () {
-      const result = th.htmlTail();
-      assert(result);
-    });
-  }); // htmlTail
-
-  describe('renderNavLink', function () {
-    let nav;
-    beforeEach(function () {
-      nav = {
-        href: 'https://example.com/',
-        text: 'example',
-      };
-    });
-    it('covers no class', function () {
-      const result = th.renderNavLink(nav);
-      assert(result);
-    });
-    it('covers class', function () {
-      nav.class = 'foo bar';
-      const result = th.renderNavLink(nav);
-      assert(result);
-    });
-  }); // renderNavLink
-
-  describe('htmlHeader', function () {
-    let pageTitle, navLinks;
-    beforeEach(function () {
-      pageTitle = 'title';
-      navLinks = [];
-    });
-    it('covers no links', function () {
-      const result = th.htmlHeader(pageTitle);
-      assert(result);
-    });
-    it('covers links', function () {
-      navLinks = [
-        {
-          href: 'https://exmaple.com/',
-          text: 'example',
-        },
-      ];
-      const result = th.htmlHeader(pageTitle, navLinks);
-      assert(result);
-    });
-  }); // htmlHeader
-
-  describe('htmlFooter', function () {
-    it('covers', function () {
-      const result = th.htmlFooter(['foo', 'bar']);
-      assert(result);
-    });
-    it('covers default', function () {
-      const result = th.htmlFooter();
-      assert(result);
-    });
-  }); // htmlFooter
-
-  describe('htmlTemplate', function () {
-    let pagePathLevel, pageTitle, headElements, navLinks, main;
-    beforeEach(function () {
-      ctx = {};
-      pagePathLevel = 1;
-      pageTitle = 'title';
-      headElements = [];
-      navLinks = [];
-      main = [];
-    });
-    it('covers', function () {
-      const result = th.htmlTemplate(ctx, pagePathLevel, pageTitle, headElements, navLinks, main);
-      assert(result);
-    });
-    it('covers defaults', function () {
-      const result = th.htmlTemplate(ctx, pagePathLevel, pageTitle);
-      assert(result);
-    });
-    it('covers user', function () {
-      ctx.session = {
-        authenticatedProfile: 'user',
-      };
-      const result = th.htmlTemplate(ctx, pagePathLevel, pageTitle);
-      assert(result);
-    });
-    it('covers user at root path', function () {
-      ctx.session = {
-        authenticatedIdentifier: 'user',
-      };
-      pagePathLevel = 0;
-      const result = th.htmlTemplate(ctx, pagePathLevel, pageTitle);
-      assert(result);
-    });
-  }); // htmlTemplate
-
 });