push notification implemented
need testing
This commit is contained in:
1 parent
26a17b5a4c
commit
0bbf2ab56f
19 files changed
+907
-84
No files matched your search
Generated
+101
-31
@@ -31,7 +31,8 @@
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"typeorm": "^0.3.28"
|
||||
"typeorm": "^0.3.28",
|
||||
"web-push": "^3.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
@@ -43,6 +44,7 @@
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
@@ -234,7 +236,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -2160,7 +2161,6 @@
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -2331,7 +2331,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.13.tgz",
|
||||
"integrity": "sha512-ieqWtipT+VlyDWLz5Rvz0f3E5rXcVAnaAi+D53DEHLjc1kmFxCgZ62qVfTX2vwkywwqNkTNXvBgGR72hYqV//Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"file-type": "21.3.0",
|
||||
"iterare": "1.2.1",
|
||||
@@ -2379,7 +2378,6 @@
|
||||
"integrity": "sha512-Tq9EIKiC30EBL8hLK93tNqaToy0hzbuVGYt29V8NhkVJUsDzlmiVf6c3hSPtzx2krIUVbTgQ2KFeaxr72rEyzQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@nuxt/opencollective": "0.4.1",
|
||||
"fast-safe-stringify": "2.1.1",
|
||||
@@ -2456,7 +2454,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.13.tgz",
|
||||
"integrity": "sha512-LYmi43BrAs1n74kLCUfXcHag7s1CmGETcFbf9IVyA/KWXAuAH95G3wEaZZiyabOLFNwq4ifnRGnIwUwW7cz3+w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cors": "2.8.6",
|
||||
"express": "5.2.1",
|
||||
@@ -2904,7 +2901,6 @@
|
||||
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "*",
|
||||
"@types/json-schema": "*"
|
||||
@@ -3030,7 +3026,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.8.tgz",
|
||||
"integrity": "sha512-ebO/Yl+EAvVe8DnMfi+iaAyIqYdK0q/q0y0rw82INWEKJOBe6b/P3YWE8NW7oOlF/nXFNrHwhARrN/hdgDkraA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -3143,6 +3138,16 @@
|
||||
"integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/web-push": {
|
||||
"version": "3.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.6.4.tgz",
|
||||
"integrity": "sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||
@@ -3205,7 +3210,6 @@
|
||||
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
@@ -3894,7 +3898,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3984,7 +3987,6 @@
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -4199,6 +4201,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asn1.js": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
|
||||
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bn.js": "^4.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -4401,6 +4415,12 @@
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||
@@ -4469,7 +4489,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -4789,7 +4808,6 @@
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
@@ -4846,15 +4864,13 @@
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-validator": {
|
||||
"version": "0.14.3",
|
||||
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz",
|
||||
"integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/validator": "^13.15.3",
|
||||
"libphonenumber-js": "^1.11.1",
|
||||
@@ -5662,7 +5678,6 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -5723,7 +5738,6 @@
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
@@ -6208,6 +6222,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
@@ -6744,6 +6759,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/http_ece": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz",
|
||||
"integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||
@@ -7221,7 +7245,6 @@
|
||||
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "30.2.0",
|
||||
"@jest/types": "30.2.0",
|
||||
@@ -8544,6 +8567,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -9292,7 +9321,6 @@
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
|
||||
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"passport-strategy": "1.x.x",
|
||||
"pause": "0.0.1",
|
||||
@@ -9421,7 +9449,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
|
||||
"integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.11.0",
|
||||
"pg-pool": "^3.11.0",
|
||||
@@ -9705,7 +9732,6 @@
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -9795,7 +9821,8 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.3",
|
||||
@@ -9946,8 +9973,7 @@
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
@@ -10092,7 +10118,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@@ -10980,7 +11005,6 @@
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -11322,7 +11346,6 @@
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
@@ -11495,7 +11518,6 @@
|
||||
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz",
|
||||
"integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@sqltools/formatter": "^1.2.5",
|
||||
"ansis": "^4.2.0",
|
||||
@@ -11702,7 +11724,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -11996,6 +12017,47 @@
|
||||
"defaults": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/web-push": {
|
||||
"version": "3.6.7",
|
||||
"resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
|
||||
"integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"asn1.js": "^5.3.0",
|
||||
"http_ece": "1.2.0",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"jws": "^4.0.0",
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"bin": {
|
||||
"web-push": "src/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/web-push/node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/web-push/node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.105.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz",
|
||||
@@ -12090,6 +12152,7 @@
|
||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
@@ -12108,6 +12171,7 @@
|
||||
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3"
|
||||
},
|
||||
@@ -12121,6 +12185,7 @@
|
||||
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esrecurse": "^4.3.0",
|
||||
"estraverse": "^4.1.1"
|
||||
@@ -12135,6 +12200,7 @@
|
||||
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
@@ -12144,7 +12210,8 @@
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/webpack/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
@@ -12152,6 +12219,7 @@
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -12162,6 +12230,7 @@
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
@@ -12175,6 +12244,7 @@
|
||||
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"ajv": "^8.9.0",
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"dev:local": "cp -f .env.local .env && nest start --watch",
|
||||
"dev:reset-env": "cp -f .env.docker .env",
|
||||
"seed:admin": "ts-node scripts/seed-admin.ts",
|
||||
"seed:categories": "ts-node scripts/seed-categories.ts"
|
||||
"seed:categories": "ts-node scripts/seed-categories.ts",
|
||||
"generate-vapid": "ts-node scripts/generate-vapid-keys.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
@@ -49,7 +50,8 @@
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"typeorm": "^0.3.28"
|
||||
"typeorm": "^0.3.28",
|
||||
"web-push": "^3.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
@@ -61,6 +63,7 @@
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as webpush from 'web-push';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const envPath = path.join(__dirname, '..', '.env');
|
||||
|
||||
function generateVapidKeys(): void {
|
||||
console.log('Generating VAPID keys...');
|
||||
|
||||
const vapidKeys = webpush.generateVAPIDKeys();
|
||||
|
||||
let envContent = '';
|
||||
|
||||
if (fs.existsSync(envPath)) {
|
||||
envContent = fs.readFileSync(envPath, 'utf-8');
|
||||
}
|
||||
|
||||
const lines = envContent.split('\n');
|
||||
|
||||
const vapidSubject = `VAPID_SUBJECT=mailto:contact@placebo.mk`;
|
||||
const vapidPublicKey = `VAPID_PUBLIC_KEY=${vapidKeys.publicKey}`;
|
||||
const vapidPrivateKey = `VAPID_PRIVATE_KEY=${vapidKeys.privateKey}`;
|
||||
|
||||
const updatedLines = lines.filter(
|
||||
(line) =>
|
||||
!line.startsWith('VAPID_SUBJECT=') &&
|
||||
!line.startsWith('VAPID_PUBLIC_KEY=') &&
|
||||
!line.startsWith('VAPID_PRIVATE_KEY='),
|
||||
);
|
||||
|
||||
const nonEmptyLines = updatedLines.filter((line) => line.trim() !== '');
|
||||
|
||||
const newEnvContent =
|
||||
nonEmptyLines.join('\n') +
|
||||
(nonEmptyLines.length > 0 ? '\n' : '') +
|
||||
`${vapidSubject}\n${vapidPublicKey}\n${vapidPrivateKey}\n`;
|
||||
|
||||
fs.writeFileSync(envPath, newEnvContent);
|
||||
|
||||
console.log('VAPID keys generated and added to .env file:');
|
||||
console.log(` Public Key: ${vapidKeys.publicKey}`);
|
||||
console.log(` Private Key: ${vapidKeys.privateKey}`);
|
||||
console.log('\nKeep your private key secret!');
|
||||
}
|
||||
|
||||
generateVapidKeys();
|
||||
@@ -10,6 +10,7 @@ import { UserModule } from './modules/users/user.module';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { CommentModule } from './modules/comment/comment.module';
|
||||
import { AnalyticsModule } from './modules/analytics/analytics.module';
|
||||
import { PushModule } from './modules/push/push.module';
|
||||
import {
|
||||
Article,
|
||||
Author,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
Reaction,
|
||||
} from './modules/entities';
|
||||
import { ShareEvent } from './modules/analytics/analytics.entity';
|
||||
import { PushSubscriptionEntity } from './modules/push/push-subscription.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -44,6 +46,7 @@ import { ShareEvent } from './modules/analytics/analytics.entity';
|
||||
Comment,
|
||||
Reaction,
|
||||
ShareEvent,
|
||||
PushSubscriptionEntity,
|
||||
],
|
||||
synchronize: process.env.DATABASE_SYNCHRONIZE === 'true',
|
||||
logging: process.env.DATABASE_LOGGING === 'true',
|
||||
@@ -55,6 +58,7 @@ import { ShareEvent } from './modules/analytics/analytics.entity';
|
||||
AuthModule,
|
||||
CommentModule,
|
||||
AnalyticsModule,
|
||||
PushModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
||||
@@ -4,11 +4,13 @@ import { ArticlesService } from './articles.service';
|
||||
import { ArticlesController } from './articles.controller';
|
||||
import { Article, Author, Category } from './entities';
|
||||
import { StrapiModule } from './strapi.module';
|
||||
import { PushModule } from './push/push.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Article, Author, Category]),
|
||||
forwardRef(() => StrapiModule),
|
||||
forwardRef(() => PushModule),
|
||||
],
|
||||
controllers: [ArticlesController],
|
||||
providers: [ArticlesService],
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Logger,
|
||||
Inject,
|
||||
forwardRef,
|
||||
Optional,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
UpdateArticleDto,
|
||||
FindArticlesDto,
|
||||
} from './articles.dto';
|
||||
import { PushService } from './push/push.service';
|
||||
|
||||
@Injectable()
|
||||
export class ArticlesService {
|
||||
@@ -24,6 +26,9 @@ export class ArticlesService {
|
||||
private readonly articleRepository: Repository<Article>,
|
||||
@Inject(forwardRef(() => StrapiService))
|
||||
private readonly strapiService: StrapiService,
|
||||
@Optional()
|
||||
@Inject(forwardRef(() => PushService))
|
||||
private readonly pushService?: PushService,
|
||||
) {}
|
||||
|
||||
async create(dto: CreateArticleDto): Promise<Article> {
|
||||
@@ -183,6 +188,7 @@ export class ArticlesService {
|
||||
status: ArticleStatus = ArticleStatus.PUBLISHED,
|
||||
): Promise<Article> {
|
||||
const article = await this.findOne(id);
|
||||
const wasDraft = article.status === ArticleStatus.DRAFT;
|
||||
article.status = status;
|
||||
const savedArticle = await this.articleRepository.save(article);
|
||||
|
||||
@@ -202,6 +208,21 @@ export class ArticlesService {
|
||||
}
|
||||
}
|
||||
|
||||
// Send push notification for newly published articles
|
||||
if (wasDraft && status === ArticleStatus.PUBLISHED && this.pushService) {
|
||||
try {
|
||||
await this.pushService.notifyNewArticle(
|
||||
savedArticle.title,
|
||||
savedArticle.slug,
|
||||
);
|
||||
this.logger.log(
|
||||
`Push notification sent for article: ${savedArticle.title}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to send push notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return savedArticle;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,14 @@ import { LiveBlogService } from './live-blog.service';
|
||||
import { LiveBlogController } from './live-blog.controller';
|
||||
import { LiveBlog, LiveBlogUpdate, Author, Category } from './entities';
|
||||
import { StrapiModule } from './strapi.module';
|
||||
import { PushModule } from './push/push.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([LiveBlog, LiveBlogUpdate, Author, Category]),
|
||||
EventEmitterModule.forRoot(),
|
||||
forwardRef(() => StrapiModule),
|
||||
forwardRef(() => PushModule),
|
||||
],
|
||||
controllers: [LiveBlogController],
|
||||
providers: [LiveBlogService],
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
OnModuleInit,
|
||||
Inject,
|
||||
forwardRef,
|
||||
Optional,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, In } from 'typeorm';
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
CreateLiveBlogUpdateDto,
|
||||
UpdateLiveBlogUpdateDto,
|
||||
} from './articles.dto';
|
||||
import { PushService } from './push/push.service';
|
||||
|
||||
interface SseClient {
|
||||
id: string;
|
||||
@@ -55,6 +57,9 @@ export class LiveBlogService implements OnModuleInit {
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
@Inject(forwardRef(() => StrapiService))
|
||||
private readonly strapiService: StrapiService,
|
||||
@Optional()
|
||||
@Inject(forwardRef(() => PushService))
|
||||
private readonly pushService?: PushService,
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
@@ -364,6 +369,22 @@ export class LiveBlogService implements OnModuleInit {
|
||||
update: savedUpdate,
|
||||
});
|
||||
|
||||
// Send push notification for live blog updates (only for live blogs)
|
||||
if (liveBlogEntity.status === LiveBlogStatus.LIVE && this.pushService) {
|
||||
try {
|
||||
await this.pushService.notifyLiveBlogUpdate(
|
||||
liveBlogEntity.title,
|
||||
liveBlogEntity.slug,
|
||||
savedUpdate.content,
|
||||
);
|
||||
this.logger.debug(
|
||||
`Push notification sent for live blog update: ${liveBlogEntity.title}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to send push notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return savedUpdate;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('push_subscriptions')
|
||||
export class PushSubscriptionEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'text', unique: true })
|
||||
endpoint: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
p256dh: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
auth: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
userId: string | null;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Body,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { PushService } from './push.service';
|
||||
import { SubscribeDto, UnsubscribeDto } from './push.dto';
|
||||
import { Public } from '../auth/public.decorator';
|
||||
|
||||
@Controller('push')
|
||||
export class PushController {
|
||||
constructor(private readonly pushService: PushService) {}
|
||||
|
||||
@Public()
|
||||
@Get('vapid-public-key')
|
||||
getVapidPublicKey(): { publicKey: string | null } {
|
||||
return { publicKey: this.pushService.getPublicKey() };
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('subscribe')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
async subscribe(@Body() dto: SubscribeDto): Promise<{ success: boolean }> {
|
||||
await this.pushService.subscribe(dto);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Delete('unsubscribe')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async unsubscribe(@Body() dto: UnsubscribeDto): Promise<void> {
|
||||
await this.pushService.unsubscribe(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { IsString, IsNotEmpty, IsOptional, IsUrl } from 'class-validator';
|
||||
|
||||
export class SubscribeDto {
|
||||
@IsUrl()
|
||||
@IsNotEmpty()
|
||||
endpoint: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
p256dh: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
auth: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export class UnsubscribeDto {
|
||||
@IsUrl()
|
||||
@IsNotEmpty()
|
||||
endpoint: string;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PushController } from './push.controller';
|
||||
import { PushService } from './push.service';
|
||||
import { PushSubscriptionEntity } from './push-subscription.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([PushSubscriptionEntity])],
|
||||
controllers: [PushController],
|
||||
providers: [PushService],
|
||||
exports: [PushService],
|
||||
})
|
||||
export class PushModule {}
|
||||
@@ -0,0 +1,173 @@
|
||||
import * as webpush from 'web-push';
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PushSubscriptionEntity } from './push-subscription.entity';
|
||||
import { SubscribeDto, UnsubscribeDto } from './push.dto';
|
||||
|
||||
export interface PushPayload {
|
||||
title: string;
|
||||
body: string;
|
||||
icon?: string;
|
||||
badge?: string;
|
||||
url?: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PushService implements OnModuleInit {
|
||||
private readonly logger = new Logger(PushService.name);
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
@InjectRepository(PushSubscriptionEntity)
|
||||
private subscriptionRepo: Repository<PushSubscriptionEntity>,
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
const publicKey = this.configService.get<string>('VAPID_PUBLIC_KEY');
|
||||
const privateKey = this.configService.get<string>('VAPID_PRIVATE_KEY');
|
||||
const subject = this.configService.get<string>(
|
||||
'VAPID_SUBJECT',
|
||||
'mailto:contact@placebo.mk',
|
||||
);
|
||||
|
||||
if (!publicKey || !privateKey) {
|
||||
this.logger.warn(
|
||||
'VAPID keys not configured. Push notifications will not work. Run: npm run generate-vapid',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
webpush.setVapidDetails(subject, publicKey, privateKey);
|
||||
this.logger.log('VAPID keys configured successfully');
|
||||
}
|
||||
|
||||
getPublicKey(): string | null {
|
||||
return this.configService.get<string>('VAPID_PUBLIC_KEY') ?? null;
|
||||
}
|
||||
|
||||
async subscribe(dto: SubscribeDto): Promise<PushSubscriptionEntity> {
|
||||
const existing = await this.subscriptionRepo.findOne({
|
||||
where: { endpoint: dto.endpoint },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
if (dto.userId && existing.userId !== dto.userId) {
|
||||
existing.userId = dto.userId;
|
||||
return this.subscriptionRepo.save(existing);
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
const subscription = this.subscriptionRepo.create({
|
||||
endpoint: dto.endpoint,
|
||||
p256dh: dto.p256dh,
|
||||
auth: dto.auth,
|
||||
userId: dto.userId ?? null,
|
||||
});
|
||||
|
||||
return this.subscriptionRepo.save(subscription);
|
||||
}
|
||||
|
||||
async unsubscribe(dto: UnsubscribeDto): Promise<void> {
|
||||
await this.subscriptionRepo.delete({ endpoint: dto.endpoint });
|
||||
}
|
||||
|
||||
async sendToAll(
|
||||
payload: PushPayload,
|
||||
): Promise<{ sent: number; failed: number }> {
|
||||
const subscriptions = await this.subscriptionRepo.find();
|
||||
|
||||
if (subscriptions.length === 0) {
|
||||
return { sent: 0, failed: 0 };
|
||||
}
|
||||
|
||||
const notification = JSON.stringify({
|
||||
title: payload.title,
|
||||
body: payload.body,
|
||||
icon: payload.icon ?? '/icons/icon-192.png',
|
||||
badge: payload.badge ?? '/icons/badge-72.png',
|
||||
url: payload.url ?? '/',
|
||||
tag: payload.tag,
|
||||
});
|
||||
|
||||
let sent = 0;
|
||||
let failed = 0;
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
subscriptions.map(async (sub) => {
|
||||
try {
|
||||
await webpush.sendNotification(
|
||||
{
|
||||
endpoint: sub.endpoint,
|
||||
keys: {
|
||||
p256dh: sub.p256dh,
|
||||
auth: sub.auth,
|
||||
},
|
||||
},
|
||||
notification,
|
||||
{
|
||||
TTL: 86400,
|
||||
},
|
||||
);
|
||||
return { success: true, id: sub.id };
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
this.logger.warn(`Push failed for ${sub.id}: ${error.message}`);
|
||||
}
|
||||
if (
|
||||
error instanceof webpush.WebPushError &&
|
||||
(error.statusCode === 410 || error.statusCode === 404)
|
||||
) {
|
||||
await this.subscriptionRepo.delete({ id: sub.id });
|
||||
this.logger.log(`Removed invalid subscription: ${sub.id}`);
|
||||
}
|
||||
return { success: false, id: sub.id };
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled' && result.value.success) {
|
||||
sent++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`Push sent: ${sent}, failed: ${failed}`);
|
||||
return { sent, failed };
|
||||
}
|
||||
|
||||
async notifyNewArticle(
|
||||
articleTitle: string,
|
||||
articleSlug: string,
|
||||
): Promise<void> {
|
||||
await this.sendToAll({
|
||||
title: 'Нови вести! 📰',
|
||||
body: articleTitle,
|
||||
url: `/article/${articleSlug}`,
|
||||
tag: 'new-article',
|
||||
});
|
||||
}
|
||||
|
||||
async notifyLiveBlogUpdate(
|
||||
blogTitle: string,
|
||||
blogSlug: string,
|
||||
updatePreview: string,
|
||||
): Promise<void> {
|
||||
const body =
|
||||
updatePreview.length > 100
|
||||
? updatePreview.substring(0, 97) + '...'
|
||||
: updatePreview;
|
||||
|
||||
await this.sendToAll({
|
||||
title: `${blogTitle} 📡`,
|
||||
body,
|
||||
url: `/live-blog/${blogSlug}`,
|
||||
tag: `live-blog-${blogSlug}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user