From 3311de77a90f1fcc4eb4f34a90caa9a1549c1a5f Mon Sep 17 00:00:00 2001 From: Dan Gowans Date: Wed, 23 Nov 2022 10:52:13 -0500 Subject: [PATCH] add optional ntfy notification on startup --- bin/www.js | 16 +++ bin/www.ts | 22 +++++ handlers/admin-get/ntfyStartup.d.ts | 3 + handlers/admin-get/ntfyStartup.js | 11 +++ handlers/admin-get/ntfyStartup.ts | 19 ++++ helpers/functions.config.d.ts | 1 + helpers/functions.config.ts | 1 + package-lock.json | 145 ++++++++++++++++++++++++++++ package.json | 1 + routes/admin.js | 2 + routes/admin.ts | 8 ++ types/configTypes.d.ts | 5 + types/configTypes.ts | 6 ++ views/_menu-admin.ejs | 8 ++ views/admin-ntfyStartup.ejs | 66 +++++++++++++ views/dashboard.ejs | 17 ++++ 16 files changed, 331 insertions(+) create mode 100644 handlers/admin-get/ntfyStartup.d.ts create mode 100644 handlers/admin-get/ntfyStartup.js create mode 100644 handlers/admin-get/ntfyStartup.ts create mode 100644 views/admin-ntfyStartup.ejs diff --git a/bin/www.js b/bin/www.js index 8e5b31a4..2436f2b2 100644 --- a/bin/www.js +++ b/bin/www.js @@ -2,6 +2,7 @@ import { app } from "../app.js"; import http from "node:http"; import * as configFunctions from "../helpers/functions.config.js"; import exitHook from "exit-hook"; +import ntfyPublish from "@cityssm/ntfy-publish"; import debug from "debug"; const debugWWW = debug("lot-occupancy-system:www"); let httpServer; @@ -39,6 +40,21 @@ if (httpPort) { onListening(httpServer); }); debugWWW("HTTP listening on " + httpPort.toString()); + const ntfyStartupConfig = configFunctions.getProperty("application.ntfyStartup"); + if (ntfyStartupConfig) { + const topic = ntfyStartupConfig.topic; + const server = ntfyStartupConfig.server; + const ntfyMessage = { + topic, + title: configFunctions.getProperty("application.applicationName"), + message: "Application Started", + tags: ["arrow_up"] + }; + if (server) { + ntfyMessage.server = server; + } + await ntfyPublish(ntfyMessage); + } } exitHook(() => { if (httpServer) { diff --git a/bin/www.ts b/bin/www.ts index 8d818e04..1d4f0740 100644 --- a/bin/www.ts +++ b/bin/www.ts @@ -7,6 +7,8 @@ import http from "node:http"; import * as configFunctions from "../helpers/functions.config.js"; import exitHook from "exit-hook"; +import ntfyPublish from "@cityssm/ntfy-publish"; +import type * as ntfyTypes from "@cityssm/ntfy-publish/types"; import debug from "debug"; const debugWWW = debug("lot-occupancy-system:www"); @@ -72,6 +74,26 @@ if (httpPort) { }); debugWWW("HTTP listening on " + httpPort.toString()); + + const ntfyStartupConfig = configFunctions.getProperty("application.ntfyStartup"); + + if (ntfyStartupConfig) { + const topic = ntfyStartupConfig.topic; + const server = ntfyStartupConfig.server; + + const ntfyMessage: ntfyTypes.NtfyMessageOptions = { + topic, + title: configFunctions.getProperty("application.applicationName"), + message: "Application Started", + tags: ["arrow_up"] + }; + + if (server) { + ntfyMessage.server = server; + } + + await ntfyPublish(ntfyMessage); + } } exitHook(() => { diff --git a/handlers/admin-get/ntfyStartup.d.ts b/handlers/admin-get/ntfyStartup.d.ts new file mode 100644 index 00000000..9621c611 --- /dev/null +++ b/handlers/admin-get/ntfyStartup.d.ts @@ -0,0 +1,3 @@ +import type { RequestHandler } from "express"; +export declare const handler: RequestHandler; +export default handler; diff --git a/handlers/admin-get/ntfyStartup.js b/handlers/admin-get/ntfyStartup.js new file mode 100644 index 00000000..caa13354 --- /dev/null +++ b/handlers/admin-get/ntfyStartup.js @@ -0,0 +1,11 @@ +import * as configFunctions from "../../helpers/functions.config.js"; +export const handler = (_request, response) => { + if (!configFunctions.getProperty("application.ntfyStartup")) { + return response.redirect(configFunctions.getProperty("reverseProxy.urlPrefix") + + "/dashboard/?error=ntfyNotConfigured"); + } + response.render("admin-ntfyStartup", { + headTitle: "Ntfy Startup Notification" + }); +}; +export default handler; diff --git a/handlers/admin-get/ntfyStartup.ts b/handlers/admin-get/ntfyStartup.ts new file mode 100644 index 00000000..77a5363e --- /dev/null +++ b/handlers/admin-get/ntfyStartup.ts @@ -0,0 +1,19 @@ +import type { RequestHandler } from "express"; + +import * as configFunctions from "../../helpers/functions.config.js"; + +export const handler: RequestHandler = (_request, response) => { + + if (!configFunctions.getProperty("application.ntfyStartup")) { + return response.redirect( + configFunctions.getProperty("reverseProxy.urlPrefix") + + "/dashboard/?error=ntfyNotConfigured" + ); + } + + response.render("admin-ntfyStartup", { + headTitle: "Ntfy Startup Notification" + }); +}; + +export default handler; diff --git a/helpers/functions.config.d.ts b/helpers/functions.config.d.ts index 280c7263..2d3a7bbc 100644 --- a/helpers/functions.config.d.ts +++ b/helpers/functions.config.d.ts @@ -4,6 +4,7 @@ export declare function getProperty(propertyName: "application.logoURL"): string export declare function getProperty(propertyName: "application.httpPort"): number; export declare function getProperty(propertyName: "application.userDomain"): string; export declare function getProperty(propertyName: "application.useTestDatabases"): boolean; +export declare function getProperty(propertyName: "application.ntfyStartup"): configTypes.ConfigNtfyStartup; export declare function getProperty(propertyName: "activeDirectory"): configTypes.ConfigActiveDirectory; export declare function getProperty(propertyName: "users.testing"): string[]; export declare function getProperty(propertyName: "users.canLogin"): string[]; diff --git a/helpers/functions.config.ts b/helpers/functions.config.ts index 74cd60d3..c5844d5c 100644 --- a/helpers/functions.config.ts +++ b/helpers/functions.config.ts @@ -76,6 +76,7 @@ export function getProperty(propertyName: "application.logoURL"): string; export function getProperty(propertyName: "application.httpPort"): number; export function getProperty(propertyName: "application.userDomain"): string; export function getProperty(propertyName: "application.useTestDatabases"): boolean; +export function getProperty(propertyName: "application.ntfyStartup"): configTypes.ConfigNtfyStartup; export function getProperty(propertyName: "activeDirectory"): configTypes.ConfigActiveDirectory; diff --git a/package-lock.json b/package-lock.json index b80311b4..ed33bb08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@cityssm/bulma-webapp-js": "^1.5.0", "@cityssm/date-diff": "^2.2.3", "@cityssm/expressjs-server-js": "^2.3.3", + "@cityssm/ntfy-publish": "^0.2.0", "@cityssm/pdf-puppeteer": "^2.0.0-beta.1", "@fortawesome/fontawesome-free": "^5.15.4", "@types/randomcolor": "^0.5.6", @@ -657,6 +658,31 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/@cityssm/ntfy-publish": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@cityssm/ntfy-publish/-/ntfy-publish-0.2.0.tgz", + "integrity": "sha512-flKv999vFgxCw9YeLtNXy61/4Xq3IwFB4kI5lQYm/mcs0Bge1k+jV31iVGkbtYPmzzJPreKFm8+jxQbi5x4OjA==", + "dependencies": { + "node-fetch": "^3.3.0" + } + }, + "node_modules/@cityssm/ntfy-publish/node_modules/node-fetch": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", + "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/@cityssm/pdf-puppeteer": { "version": "2.0.0-beta.1", "resolved": "https://registry.npmjs.org/@cityssm/pdf-puppeteer/-/pdf-puppeteer-2.0.0-beta.1.tgz", @@ -3418,6 +3444,14 @@ "node": ">=0.10" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", + "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==", + "engines": { + "node": ">= 12" + } + }, "node_modules/date-fns": { "version": "2.29.3", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", @@ -4961,6 +4995,28 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -5347,6 +5403,17 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -8393,6 +8460,24 @@ "integrity": "sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw==", "dev": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -11840,6 +11925,14 @@ "node": ">=0.10.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -12721,6 +12814,26 @@ "mssql": "^8.1.1" } }, + "@cityssm/ntfy-publish": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@cityssm/ntfy-publish/-/ntfy-publish-0.2.0.tgz", + "integrity": "sha512-flKv999vFgxCw9YeLtNXy61/4Xq3IwFB4kI5lQYm/mcs0Bge1k+jV31iVGkbtYPmzzJPreKFm8+jxQbi5x4OjA==", + "requires": { + "node-fetch": "^3.3.0" + }, + "dependencies": { + "node-fetch": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", + "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + } + } + }, "@cityssm/pdf-puppeteer": { "version": "2.0.0-beta.1", "resolved": "https://registry.npmjs.org/@cityssm/pdf-puppeteer/-/pdf-puppeteer-2.0.0-beta.1.tgz", @@ -14897,6 +15010,11 @@ "assert-plus": "^1.0.0" } }, + "data-uri-to-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", + "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==" + }, "date-fns": { "version": "2.29.3", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", @@ -16081,6 +16199,15 @@ "pend": "~1.2.0" } }, + "fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -16402,6 +16529,14 @@ "mime-types": "^2.1.12" } }, + "formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "requires": { + "fetch-blob": "^3.1.2" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -18732,6 +18867,11 @@ "integrity": "sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw==", "dev": true }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -21419,6 +21559,11 @@ } } }, + "web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" + }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 84d6f3e1..03159279 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@cityssm/bulma-webapp-js": "^1.5.0", "@cityssm/date-diff": "^2.2.3", "@cityssm/expressjs-server-js": "^2.3.3", + "@cityssm/ntfy-publish": "^0.2.0", "@cityssm/pdf-puppeteer": "^2.0.0-beta.1", "@fortawesome/fontawesome-free": "^5.15.4", "@types/randomcolor": "^0.5.6", diff --git a/routes/admin.js b/routes/admin.js index 80c77e35..d00d8dfc 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -55,6 +55,7 @@ import handler_doMoveLotOccupantTypeDown from "../handlers/admin-post/doMoveLotO import handler_doDeleteLotOccupantType from "../handlers/admin-post/doDeleteLotOccupantType.js"; import handler_cleanup from "../handlers/admin-get/cleanup.js"; import handler_doCleanupDatabase from "../handlers/admin-post/doCleanupDatabase.js"; +import handler_ntfyStartup from "../handlers/admin-get/ntfyStartup.js"; export const router = Router(); router.get("/fees", handler_fees); router.post("/doAddFeeCategory", handler_doAddFeeCategory); @@ -112,4 +113,5 @@ router.post("/doMoveLotOccupantTypeDown", handler_doMoveLotOccupantTypeDown); router.post("/doDeleteLotOccupantType", handler_doDeleteLotOccupantType); router.get("/cleanup", handler_cleanup); router.post("/doCleanupDatabase", handler_doCleanupDatabase); +router.get("/ntfyStartup", handler_ntfyStartup); export default router; diff --git a/routes/admin.ts b/routes/admin.ts index 0c421115..35ca876a 100644 --- a/routes/admin.ts +++ b/routes/admin.ts @@ -81,6 +81,10 @@ import handler_doDeleteLotOccupantType from "../handlers/admin-post/doDeleteLotO import handler_cleanup from "../handlers/admin-get/cleanup.js"; import handler_doCleanupDatabase from "../handlers/admin-post/doCleanupDatabase.js"; +// Ntfy Startup + +import handler_ntfyStartup from "../handlers/admin-get/ntfyStartup.js"; + export const router = Router(); /* @@ -228,4 +232,8 @@ router.get("/cleanup", handler_cleanup); router.post("/doCleanupDatabase", handler_doCleanupDatabase); +// Ntfy Startup + +router.get("/ntfyStartup", handler_ntfyStartup); + export default router; diff --git a/types/configTypes.d.ts b/types/configTypes.d.ts index 3602d810..3c8786e9 100644 --- a/types/configTypes.d.ts +++ b/types/configTypes.d.ts @@ -67,6 +67,11 @@ interface ConfigApplication { httpPort?: number; userDomain?: string; useTestDatabases?: boolean; + ntfyStartup?: ConfigNtfyStartup; +} +export interface ConfigNtfyStartup { + topic: string; + server?: string; } interface ConfigSession { cookieName?: string; diff --git a/types/configTypes.ts b/types/configTypes.ts index 2c3740e4..fddf24c3 100644 --- a/types/configTypes.ts +++ b/types/configTypes.ts @@ -68,6 +68,12 @@ interface ConfigApplication { httpPort?: number; userDomain?: string; useTestDatabases?: boolean; + ntfyStartup?: ConfigNtfyStartup; +} + +export interface ConfigNtfyStartup { + topic: string; + server?: string; } interface ConfigSession { diff --git a/views/_menu-admin.ejs b/views/_menu-admin.ejs index 127afaec..a71f5bad 100644 --- a/views/_menu-admin.ejs +++ b/views/_menu-admin.ejs @@ -33,5 +33,13 @@ Database Cleanup + <% if (configFunctions.getProperty("application.ntfyStartup")) { %> +
  • + " href="<%= urlPrefix %>/admin/ntfyStartup"> + + Ntfy Startup Notification + +
  • + <% } %> \ No newline at end of file diff --git a/views/admin-ntfyStartup.ejs b/views/admin-ntfyStartup.ejs new file mode 100644 index 00000000..3c714348 --- /dev/null +++ b/views/admin-ntfyStartup.ejs @@ -0,0 +1,66 @@ +<%- include('_header'); -%> + +
    +
    + <%- include('_menu-admin'); -%> +
    +
    + + +

    + Ntfy Startup Notification +

    + +
    +
    + What is this? +
    +
    +

    + Ntfy is a simple free service able to send push notifications to phones and computers. + Ntfy apps exist for all major phone platforms. + This application can send a notification to a ntfy server on startup. +

    +
    +
    + +
    +
    +
    +

    + Server
    + <%= configFunctions.getProperty("application.ntfyStartup.server") || "Default Server" %> +

    +
    +
    +

    + Topic
    + <%= configFunctions.getProperty("application.ntfyStartup.topic") %> +

    +
    +
    +
    +
    +
    + +<%- include('_footerA'); -%> + + + +<%- include('_footerB'); -%> diff --git a/views/dashboard.ejs b/views/dashboard.ejs index a6c6ef99..befcd419 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -378,6 +378,23 @@ + <% if (configFunctions.getProperty("application.ntfyStartup")) { %> +
    +
    +
    + +
    +
    +

    + Ntfy Startup Notification +

    +

    + Subscribe to application startup notifications on a phone or a desktop computer. +

    +
    +
    +
    + <% } %> <% } %>