Compare commits
8 Commits
b9ec9a2e38
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 41ef684461 | |||
| 13fb846277 | |||
| 56a8c1a66a | |||
| 4b81150636 | |||
| 5988df7d03 | |||
| c198bfb0aa | |||
| 53ff28c6f7 | |||
| a72c7a0c1a |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,3 +1,3 @@
|
|||||||
dist/*
|
dist/
|
||||||
node_modules/*
|
node_modules/
|
||||||
.env
|
*.env
|
||||||
34
data/oauth.ts
Normal file
34
data/oauth.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export const OAuthData: { [service: string]: { type: string, endpoint: string, grantType: string, scopes: string[], redirect: string } } = {
|
||||||
|
'nightbot': {
|
||||||
|
type: 'nightbot',
|
||||||
|
endpoint: 'https://api.nightbot.tv/oauth2/authorize',
|
||||||
|
grantType: 'token',
|
||||||
|
scopes: ['song_requests', 'song_requests_queue', 'song_requests_playlist'],
|
||||||
|
redirect: process.env.WEB_HOST + '/connections/callback'
|
||||||
|
},
|
||||||
|
'twitch': {
|
||||||
|
type: 'twitch',
|
||||||
|
endpoint: 'https://id.twitch.tv/oauth2/authorize',
|
||||||
|
grantType: 'token',
|
||||||
|
scopes: [
|
||||||
|
'chat:read',
|
||||||
|
'bits:read',
|
||||||
|
'channel:read:polls',
|
||||||
|
'channel:read:predictions',
|
||||||
|
'channel:read:subscriptions',
|
||||||
|
'channel:read:vips',
|
||||||
|
'moderator:read:blocked_terms',
|
||||||
|
'chat:read',
|
||||||
|
'channel:moderate',
|
||||||
|
'channel:read:redemptions',
|
||||||
|
'channel:manage:redemptions',
|
||||||
|
'channel:manage:predictions',
|
||||||
|
'user:read:chat',
|
||||||
|
'channel:bot',
|
||||||
|
'moderator:read:followers',
|
||||||
|
'channel:read:ads',
|
||||||
|
'moderator:read:chatters',
|
||||||
|
],
|
||||||
|
redirect: process.env.WEB_HOST + '/connections/callback'
|
||||||
|
},
|
||||||
|
};
|
||||||
395
package-lock.json
generated
395
package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"helmet": "^8.0.0",
|
"helmet": "^8.0.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
@@ -136,9 +137,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/express-serve-static-core": {
|
"node_modules/@types/express-serve-static-core": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.4.tgz",
|
||||||
"integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==",
|
"integrity": "sha512-5kz9ScmzBdzTgB/3susoCgfqNDzBjvLL4taparufgSvlwjdLy6UyUy9T/tCpYd2GIdIilCatC4iSQS0QSYHt0w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -163,19 +164,19 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.7.5",
|
"version": "22.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
|
||||||
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
|
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.20.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.9.16",
|
"version": "6.9.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
|
||||||
"integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==",
|
"integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -223,9 +224,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.13.0",
|
"version": "8.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||||
"integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==",
|
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -393,17 +394,27 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/call-bind": {
|
"node_modules/call-bind-apply-helpers": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
|
||||||
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-define-property": "^1.0.0",
|
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"function-bind": "^1.1.2",
|
"function-bind": "^1.1.2"
|
||||||
"get-intrinsic": "^1.2.4",
|
},
|
||||||
"set-function-length": "^1.2.1"
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"get-intrinsic": "^1.2.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -510,9 +521,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/concurrently": {
|
"node_modules/concurrently": {
|
||||||
"version": "9.0.1",
|
"version": "9.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz",
|
||||||
"integrity": "sha512-wYKvCd/f54sTXJMSfV6Ln/B8UrfLBKOYa+lzc6CHay3Qek+LorVSBdMVfyewFhRbH0Rbabsk4D+3PL/VjQ5gzg==",
|
"integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -587,23 +598,6 @@
|
|||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/define-data-property": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"es-define-property": "^1.0.0",
|
|
||||||
"es-errors": "^1.3.0",
|
|
||||||
"gopd": "^1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -644,9 +638,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.4.5",
|
"version": "16.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||||
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
|
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -655,6 +649,20 @@
|
|||||||
"url": "https://dotenvx.com"
|
"url": "https://dotenvx.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ecdsa-sig-formatter": {
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
@@ -687,13 +695,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"get-intrinsic": "^1.2.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
@@ -707,6 +712,18 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
@@ -733,9 +750,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.1",
|
"version": "4.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||||
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
@@ -757,7 +774,7 @@
|
|||||||
"methods": "~1.1.2",
|
"methods": "~1.1.2",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"path-to-regexp": "0.1.10",
|
"path-to-regexp": "0.1.12",
|
||||||
"proxy-addr": "~2.0.7",
|
"proxy-addr": "~2.0.7",
|
||||||
"qs": "6.13.0",
|
"qs": "6.13.0",
|
||||||
"range-parser": "~1.2.1",
|
"range-parser": "~1.2.1",
|
||||||
@@ -772,12 +789,16 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express-rate-limit": {
|
"node_modules/express-rate-limit": {
|
||||||
"version": "7.4.1",
|
"version": "7.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
|
||||||
"integrity": "sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg==",
|
"integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
@@ -786,7 +807,7 @@
|
|||||||
"url": "https://github.com/sponsors/express-rate-limit"
|
"url": "https://github.com/sponsors/express-rate-limit"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"express": "4 || 5 || ^5.0.0-beta.1"
|
"express": "^4.11 || 5 || ^5.0.0-beta.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express-session": {
|
"node_modules/express-session": {
|
||||||
@@ -907,16 +928,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
|
||||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.0.0",
|
||||||
"function-bind": "^1.1.2",
|
"function-bind": "^1.1.2",
|
||||||
"has-proto": "^1.0.1",
|
"get-proto": "^1.0.0",
|
||||||
"has-symbols": "^1.0.3",
|
"gopd": "^1.2.0",
|
||||||
"hasown": "^2.0.0"
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -925,6 +951,19 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
@@ -939,12 +978,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.0.1",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"engines": {
|
||||||
"get-intrinsic": "^1.1.3"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@@ -960,34 +999,10 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/has-property-descriptors": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"es-define-property": "^1.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-proto": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-symbols": {
|
"node_modules/has-symbols": {
|
||||||
"version": "1.0.3",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -1234,6 +1249,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/media-typer": {
|
"node_modules/media-typer": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
@@ -1313,6 +1337,15 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/moment": {
|
||||||
|
"version": "2.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||||
|
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
@@ -1329,9 +1362,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nodemon": {
|
"node_modules/nodemon": {
|
||||||
"version": "3.1.7",
|
"version": "3.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
|
||||||
"integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==",
|
"integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1358,9 +1391,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nodemon/node_modules/debug": {
|
"node_modules/nodemon/node_modules/debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1422,9 +1455,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.2",
|
"version": "1.13.3",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
|
||||||
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
|
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -1528,9 +1561,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.10",
|
"version": "0.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/pause": {
|
"node_modules/pause": {
|
||||||
@@ -1539,9 +1572,9 @@
|
|||||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||||
},
|
},
|
||||||
"node_modules/pg": {
|
"node_modules/pg": {
|
||||||
"version": "8.13.0",
|
"version": "8.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz",
|
||||||
"integrity": "sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw==",
|
"integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pg-connection-string": "^2.7.0",
|
"pg-connection-string": "^2.7.0",
|
||||||
@@ -1579,9 +1612,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/pg-cursor": {
|
"node_modules/pg-cursor": {
|
||||||
"version": "2.12.0",
|
"version": "2.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.12.1.tgz",
|
||||||
"integrity": "sha512-rppw54OnuYZfMUjiJI2zJMwAjjt2V9EtLUb+t7V5tqwSE5Jxod+7vA7Y0FI6Nq976jNLciA0hoVkwvjjB8qzEw==",
|
"integrity": "sha512-V13tEaA9Oq1w+V6Q3UBIB/blxJrwbbr35/dY54r/86soBJ7xkP236bXaORUTVXUPt9B6Ql2BQu+uwQiuMfRVgg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -1616,13 +1649,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pg-promise": {
|
"node_modules/pg-promise": {
|
||||||
"version": "11.10.1",
|
"version": "11.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-11.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-11.10.2.tgz",
|
||||||
"integrity": "sha512-TceugkypE+VkHTjlklMmLYLN5hUDbM9dIhKZQq2onxN9F//6X6Q5czQ7Ms5sCi0JB5pkbt8fgoC7OLHM2EVI7Q==",
|
"integrity": "sha512-wK4yjxZdfxBmAMcs40q6IsC1SOzdLilc1yNvJqlbOjtm2syayqLDCt1JQ9lhS6yNSgVlGOQZT88yb/SADJmEBw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"assert-options": "0.8.2",
|
"assert-options": "0.8.2",
|
||||||
"pg": "8.13.0",
|
"pg": "8.13.1",
|
||||||
"pg-minify": "1.6.5",
|
"pg-minify": "1.6.5",
|
||||||
"spex": "3.4.0"
|
"spex": "3.4.0"
|
||||||
},
|
},
|
||||||
@@ -1630,7 +1663,7 @@
|
|||||||
"node": ">=14.0"
|
"node": ">=14.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"pg-query-stream": "4.7.0"
|
"pg-query-stream": "4.7.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pg-protocol": {
|
"node_modules/pg-protocol": {
|
||||||
@@ -1640,13 +1673,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/pg-query-stream": {
|
"node_modules/pg-query-stream": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/pg-query-stream/-/pg-query-stream-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg-query-stream/-/pg-query-stream-4.7.1.tgz",
|
||||||
"integrity": "sha512-aQpK8yfFTvOzvPmhXEzWfkwM24lv2Y3TfFY0HJYwx0YM/2fL4DhqpBhLni2Kd+l9p/XoDEi+HFvEvOCm7oqaLg==",
|
"integrity": "sha512-UMgsgn/pOIYsIifRySp59vwlpTpLADMK9HWJtq5ff0Z3MxBnPMGnCQeaQl5VuL+7ov4F96mSzIRIcz+Duo6OiQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pg-cursor": "^2.12.0"
|
"pg-cursor": "^2.12.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"pg": "^8"
|
"pg": "^8"
|
||||||
@@ -1922,23 +1955,6 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/set-function-length": {
|
|
||||||
"version": "1.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
|
||||||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"define-data-property": "^1.1.4",
|
|
||||||
"es-errors": "^1.3.0",
|
|
||||||
"function-bind": "^1.1.2",
|
|
||||||
"get-intrinsic": "^1.2.4",
|
|
||||||
"gopd": "^1.0.1",
|
|
||||||
"has-property-descriptors": "^1.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/setprototypeof": {
|
"node_modules/setprototypeof": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
@@ -1946,25 +1962,82 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/shell-quote": {
|
"node_modules/shell-quote": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
|
||||||
"integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
|
"integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.0.6",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind": "^1.0.7",
|
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"get-intrinsic": "^1.2.4",
|
"object-inspect": "^1.13.3",
|
||||||
"object-inspect": "^1.13.1"
|
"side-channel-list": "^1.0.0",
|
||||||
|
"side-channel-map": "^1.0.1",
|
||||||
|
"side-channel-weakmap": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-list": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-map": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-weakmap": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-map": "^1.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -2144,9 +2217,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.0",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
@@ -2189,9 +2262,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.6.3",
|
"version": "5.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -2228,9 +2301,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.19.8",
|
"version": "6.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -2253,9 +2326,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "11.0.2",
|
"version": "11.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.4.tgz",
|
||||||
"integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==",
|
"integrity": "sha512-IzL6VtTTYcAhA/oghbFJ1Dkmqev+FpQWnCBaKq/gUluLxliWvO8DPFWfIviRmYbtaavtSQe4WBL++rFjdcGWEg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
"https://github.com/sponsors/broofa",
|
"https://github.com/sponsors/broofa",
|
||||||
"https://github.com/sponsors/ctavan"
|
"https://github.com/sponsors/ctavan"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npx tsc",
|
"build": "npx tsc",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/src/index.js",
|
||||||
"dev": "nodemon src/index.ts"
|
"dev": "nodemon src/index.ts"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"helmet": "^8.0.0",
|
"helmet": "^8.0.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
|||||||
35
src/controllers/admin.controller.ts
Normal file
35
src/controllers/admin.controller.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { database } from "../services/database";
|
||||||
|
|
||||||
|
export async function getUsers(req: any, res: any, next: any) {
|
||||||
|
const users = await database.manyOrNone('SELECT id, name FROM "User"');
|
||||||
|
res.send(users);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function updateImpersonation(req: any, res: any, next: any) {
|
||||||
|
if (!req.body.impersonation) {
|
||||||
|
res.status(400).send('Invalid user.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await database.oneOrNone('SELECT "targetId" FROM "Impersonation" where "sourceId" = $1', req.user.id);
|
||||||
|
if (!data?.targetId) {
|
||||||
|
await database.none('INSERT INTO "Impersonation" ("sourceId", "targetId") VALUES ($1, $2)', [req.user.id, req.body.impersonation]);
|
||||||
|
} else {
|
||||||
|
await database.none('UPDATE "Impersonation" SET "targetId" = $2 WHERE "sourceId" = $1', [req.user.id, req.body.impersonation]);
|
||||||
|
}
|
||||||
|
res.send({
|
||||||
|
data: {
|
||||||
|
impersonation: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function deleteImpersonation(req: any, res: any, next: any) {
|
||||||
|
if (req.user.role != 'ADMIN') {
|
||||||
|
res.status(403).send('You do not have the permissions for this.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await database.oneOrNone('DELETE FROM "Impersonation" where "sourceId" = $1', req.user.id);
|
||||||
|
res.send({ data });
|
||||||
|
};
|
||||||
41
src/controllers/api-keys.controller.ts
Normal file
41
src/controllers/api-keys.controller.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { database } from "../services/database";
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
export async function getApiKeys(req: any, res: any, next: any) {
|
||||||
|
const userId = req.user.impersonation?.id ?? req.user.id;
|
||||||
|
const data = await database.manyOrNone('SELECT id, label FROM "ApiKey" WHERE "userId" = $1', userId);
|
||||||
|
res.send(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createApiKey(req: any, res: any, next: any) {
|
||||||
|
const userId = req.user.impersonation?.id ?? req.user.id;
|
||||||
|
const keys = await database.one('SELECT count(*) FROM "ApiKey" WHERE "userId" = $1', userId);
|
||||||
|
if (keys.count > 10) {
|
||||||
|
res.status(403).send('Too many keys');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const label = req.body.label;
|
||||||
|
if (!label) {
|
||||||
|
res.status(400).send('No label is attached.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = uuidv4();
|
||||||
|
await database.none('INSERT INTO "ApiKey" (id, label, "userId") VALUES ($1, $2, $3);', [key, label, userId]);
|
||||||
|
res.send({ label, key });
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function deleteApiKey(req: any, res: any, next: any) {
|
||||||
|
if (!req.body.key) {
|
||||||
|
res.status(400).send('key has not been provided.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const key = await database.one('SELECT EXISTS(SELECT 1 FROM "ApiKey" WHERE id = $1)', req.body.key);
|
||||||
|
if (!key.exists) {
|
||||||
|
res.status(400).send('key does not exist.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await database.none('DELETE FROM "ApiKey" WHERE id = $1', req.body.key);
|
||||||
|
res.send({ key: req.body.key });
|
||||||
|
};
|
||||||
121
src/controllers/auth.controller.ts
Normal file
121
src/controllers/auth.controller.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { database } from "../services/database";
|
||||||
|
import * as http from 'typed-rest-client/HttpClient';
|
||||||
|
import { jwt } from "../services/passport";
|
||||||
|
import { OAuthData } from "../../data/oauth";
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import moment from "moment";
|
||||||
|
const pino = require('pino-http')();
|
||||||
|
|
||||||
|
|
||||||
|
export async function connectionPreparation(req: Request, res: Response) {
|
||||||
|
const state = req.query['state'];
|
||||||
|
const access_token = req.query['token'];
|
||||||
|
let expires_in = req.query['expires_in'];
|
||||||
|
if (!state || !access_token) {
|
||||||
|
res.status(400).send({ error: 'Missing fields in the body.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = await database.oneOrNone('SELECT "name", "type", "clientId", "grantType", "userId" FROM "ConnectionState" WHERE "state" = $1', [state]);
|
||||||
|
if (!connection) {
|
||||||
|
res.status(400).send({ error: 'Invalid combination.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connection.type == 'twitch') {
|
||||||
|
const rest = new http.HttpClient(null);
|
||||||
|
const response = await rest.get('https://id.twitch.tv/oauth2/validate', {
|
||||||
|
'Authorization': 'OAuth ' + access_token,
|
||||||
|
});
|
||||||
|
const body = await response.readBody();
|
||||||
|
const json = JSON.parse(body);
|
||||||
|
expires_in = json.expires_in;
|
||||||
|
|
||||||
|
if (!expires_in) {
|
||||||
|
res.status(400).send({ error: 'Twitch API is not working correctly.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expiration = expires_in ? parseInt(expires_in.toString()) : null;
|
||||||
|
if (!expiration || isNaN(expiration) || !isFinite(expiration) || expiration < 0) {
|
||||||
|
res.status(400).send({ error: 'Could not determine the expiration of the token.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expires_at = moment().add(expiration - 300, 's');
|
||||||
|
res.send({
|
||||||
|
data: { connection, expires_at },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function connectionCallback(req: any, res: Response) {
|
||||||
|
const name = req.body.name;
|
||||||
|
const type = req.body.type?.toLowerCase();
|
||||||
|
const client_id = req.body.client_id;
|
||||||
|
const grant_type = req.body.grant_type?.toLowerCase();
|
||||||
|
if (!name || !type || !client_id || !grant_type) {
|
||||||
|
res.status(400).send({ error: 'Missing fields in the body.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = OAuthData[type].endpoint;
|
||||||
|
const redirect = OAuthData[type].redirect;
|
||||||
|
const scopes = OAuthData[type].scopes.join(' ');
|
||||||
|
const nounce = uuidv4();
|
||||||
|
await database.none('INSERT INTO "ConnectionState" ("name", "type", "clientId", "grantType", "state", "userId") VALUES ($1, $2, $3, $4, $5, $6)'
|
||||||
|
+ ' ON CONFLICT ("userId", "name") DO UPDATE SET "type" = $2, "clientId" = $3, "grantType" = $4, "state" = $5;',
|
||||||
|
[name, type, client_id, grant_type, nounce, req.user.id]);
|
||||||
|
|
||||||
|
const redirect_uri = url + '?client_id=' + client_id + '&force_verify=true&redirect_uri=' + redirect + '&response_type=token&scope=' + scopes + '&state=' + nounce;
|
||||||
|
res.send({ success: true, error: null, data: redirect_uri });
|
||||||
|
};
|
||||||
|
|
||||||
|
// login/register
|
||||||
|
export async function twitchCallback(req: any, res: any) {
|
||||||
|
const query = `client_id=${process.env.AUTH_CLIENT_ID}&client_secret=${process.env.AUTH_CLIENT_SECRET}&code=${req.body.code}&grant_type=authorization_code&redirect_uri=${process.env.AUTH_REDIRECT_URI}`
|
||||||
|
const rest = new http.HttpClient(null);
|
||||||
|
const response = await rest.post('https://id.twitch.tv/oauth2/token', query, {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
});
|
||||||
|
const body = await response.readBody();
|
||||||
|
const codeData = JSON.parse(body);
|
||||||
|
if (!codeData || codeData.message) {
|
||||||
|
console.log('Failed to validate Twitch code authentication:', codeData);
|
||||||
|
res.send({ authenticated: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Validated Twitch code authentication:', codeData);
|
||||||
|
|
||||||
|
const resp = await rest.get('https://api.twitch.tv/helix/users', {
|
||||||
|
'Authorization': 'Bearer ' + codeData.access_token,
|
||||||
|
'Client-Id': process.env.AUTH_CLIENT_ID
|
||||||
|
});
|
||||||
|
const b = await resp.readBody();
|
||||||
|
const userData = JSON.parse(b);
|
||||||
|
if (!userData?.data) {
|
||||||
|
console.log('Failed to fetch twitch data:', codeData, userData?.data);
|
||||||
|
res.send({ authenticated: false, error: 'Twitch API is not working correctly.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Fetched Twitch user data:', userData);
|
||||||
|
|
||||||
|
const account: any = await database.oneOrNone('SELECT "userId" FROM "Account" WHERE "providerAccountId" = $1', userData.data[0].id);
|
||||||
|
if (account != null) {
|
||||||
|
const user: any = await database.one('SELECT id FROM "User" WHERE id = $1', account.userId);
|
||||||
|
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '30d' });
|
||||||
|
res.send({ authenticated: true, token: token });
|
||||||
|
|
||||||
|
var now = Date.now();
|
||||||
|
const expires_at = ((now / 1000) | 0) + codeData.expires_in;
|
||||||
|
await database.none('UPDATE "Account" SET refresh_token = COALESCE($1, refresh_token), access_token = $2, id_token = COALESCE($3, id_token), expires_at = $4, scope = $5 WHERE "userId" = $6', [codeData.refresh_token, codeData.access_token, codeData.id_token, expires_at, codeData.scope.join(' '), account.userId]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({ authenticated: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function validate(req: any, res: Response, next: () => void) {
|
||||||
|
res.send({ authenticated: req?.user != null, user: req?.user });
|
||||||
|
}
|
||||||
48
src/controllers/twitch.controller.ts
Normal file
48
src/controllers/twitch.controller.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { database } from "../services/database";
|
||||||
|
import * as http from 'typed-rest-client/HttpClient';
|
||||||
|
|
||||||
|
export async function getTwitchRedemptions(req: any, res: any, next: any) {
|
||||||
|
const userId = req.user.impersonation?.id ?? req.user.id;
|
||||||
|
const account: any = await database.one('SELECT "providerAccountId" FROM "Account" WHERE "userId" = $1', userId);
|
||||||
|
const connection: any = await database.oneOrNone('SELECT "clientId", "accessToken" FROM "Connection" WHERE "userId" = $1 AND "default" = true AND "type" = \'twitch\'', userId);
|
||||||
|
const rest = new http.HttpClient(null);
|
||||||
|
const resp = await rest.get('https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=' + account.providerAccountId, {
|
||||||
|
'Authorization': 'Bearer ' + connection.accessToken,
|
||||||
|
'Client-Id': connection.clientId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const twitch = JSON.parse(await resp.readBody());
|
||||||
|
if (!twitch?.data) {
|
||||||
|
console.log('Failed to fetch twitch data:', account, twitch.data);
|
||||||
|
res.status(401).send({ error: 'Could not fetch Twitch channel redemption data.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(twitch.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getTwitchUsers(req: any, res: any) {
|
||||||
|
const username = req.query.login?.toLowerCase();
|
||||||
|
if (!username) {
|
||||||
|
res.status(400).send({ user: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rest = new http.HttpClient(null);
|
||||||
|
const userId = req.user.impersonation?.id ?? req.user.id;
|
||||||
|
|
||||||
|
const connection: any = await database.oneOrNone('SELECT "clientId", "accessToken" FROM "Connection" WHERE "userId" = $1 AND "default" = true AND "type" = \'twitch\'', userId);
|
||||||
|
|
||||||
|
const resp = await rest.get('https://api.twitch.tv/helix/users?login=' + username, {
|
||||||
|
'Authorization': 'Bearer ' + connection.accessToken,
|
||||||
|
'Client-Id': connection.clientId,
|
||||||
|
});
|
||||||
|
const twitch = JSON.parse(await resp.readBody());
|
||||||
|
if (!twitch?.data) {
|
||||||
|
res.status(403).send({ user: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = twitch.data.find((u: any) => u.login == username);
|
||||||
|
res.send({ user });
|
||||||
|
};
|
||||||
244
src/database.postgres.sql
Normal file
244
src/database.postgres.sql
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
DELETE TYPE IF EXISTS "UserRole";
|
||||||
|
|
||||||
|
DELETE TYPE IF EXISTS "ActionType";
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "actions";
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "chatters";
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "chatter_groups";
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "redemptions";
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "permissions";
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "policies";
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "groups";
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "connections";
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "accounts";
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "api_keys";
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "users";
|
||||||
|
|
||||||
|
CREATE TYPE "UserRole" AS ENUM ('USER', 'ADMIN');
|
||||||
|
|
||||||
|
CREATE TYPE "ActionType" AS ENUM (
|
||||||
|
'WRITE_TO_FILE',
|
||||||
|
'APPEND_TO_FILE',
|
||||||
|
'AUDIO_FILE',
|
||||||
|
'OBS_TRANSFORM',
|
||||||
|
'RANDOM_TTS_VOICE',
|
||||||
|
'SPECIFIC_TTS_VOICE',
|
||||||
|
'TTS_MESSAGE',
|
||||||
|
'TOGGLE_OBS_VISIBILITY',
|
||||||
|
'SPECIFIC_OBS_VISIBILITY',
|
||||||
|
'SPECIFIC_OBS_INDEX',
|
||||||
|
'SLEEP',
|
||||||
|
'OAUTH',
|
||||||
|
'NIGHTBOT_PLAY',
|
||||||
|
'NIGHTBOT_PAUSE',
|
||||||
|
'NIGHTBOT_SKIP',
|
||||||
|
'TWITCH_OAUTH',
|
||||||
|
'NIGHTBOT_CLEAR_PLAYLIST',
|
||||||
|
'NIGHTBOT_CLEAR_QUEUE',
|
||||||
|
'VEADOTUBE_SET_STATE',
|
||||||
|
'VEADOTUBE_PUSH_STATE',
|
||||||
|
'VEADOTUBE_POP_STATE'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
users (
|
||||||
|
user_id uuid DEFAULT gen_random_uuid (),
|
||||||
|
name text NOT NULL,
|
||||||
|
email text,
|
||||||
|
role "UserRole" NOT NULL DEFAULT USER,
|
||||||
|
image text,
|
||||||
|
tts_default_voice text NOT NULL,
|
||||||
|
created_at timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at timestamp(3) NOT NULL,
|
||||||
|
CONSTRAINT "users_pkey" PRIMARY KEY ("user_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
accounts (
|
||||||
|
account_id uuid DEFAULT gen_random_uuid (),
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
type text NOT NULL,
|
||||||
|
provider text NOT NULL,
|
||||||
|
providerAccountId text NOT NULL,
|
||||||
|
access_token text NOT NULL,
|
||||||
|
refresh_token text NOT NULL,
|
||||||
|
expires_at integer NOT NULL,
|
||||||
|
token_type text NOT NULL,
|
||||||
|
scope text NOT NULL,
|
||||||
|
created_at timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "accounts_pkey" PRIMARY KEY ("account_id"),
|
||||||
|
CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
actions (
|
||||||
|
user_id uuid DEFAULT gen_random_uuid (),
|
||||||
|
name text NOT NULL,
|
||||||
|
type "ActionType" NOT NULL,
|
||||||
|
data jsonb NOT NULL,
|
||||||
|
has_message boolean NOT NULL DEFAULT false,
|
||||||
|
CONSTRAINT "Action_pkey" PRIMARY KEY ("user_id", "name"),
|
||||||
|
CONSTRAINT "s_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
api_keys (
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
key text NOT NULL,
|
||||||
|
label text NOT NULL,
|
||||||
|
CONSTRAINT "api_keys_pkey" PRIMARY KEY ("key") CONSTRAINT "api_keys_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
chatters (chatter_id text NOT NULL, name text NOT NULL);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
chatter_groups (
|
||||||
|
chatter_id text DEFAULT gen_random_uuid (),
|
||||||
|
group_id uuid DEFAULT gen_random_uuid (),
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
chatter_label text NOT NULL,
|
||||||
|
CONSTRAINT "chatter_groups_pkey" PRIMARY KEY ("user_id", "group_id", "chatter_id") CONSTRAINT "chatter_groups_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
connections (
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
provider text NOT NULL,
|
||||||
|
grant_type text NOT NULL,
|
||||||
|
client_id text NOT NULL,
|
||||||
|
access_token NOT NULL,
|
||||||
|
scope text NOT NULL,
|
||||||
|
expires_at timestamp(3) NOT NULL,
|
||||||
|
default boolean NOT NULL DEFAULT false,
|
||||||
|
CONSTRAINT "connections_pkey" PRIMARY KEY ("user_id", "name") CONSTRAINT "connections_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
connection_previews (
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
provider text NOT NULL,
|
||||||
|
grant_type text NOT NULL,
|
||||||
|
client_id text NOT NULL,
|
||||||
|
state text NOT NULL,
|
||||||
|
CONSTRAINT "connection_previews_pkey" PRIMARY KEY ("user_id", "name") CONSTRAINT "connection_previews_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
emotes (
|
||||||
|
emote_id text NOT NULL,
|
||||||
|
label text NOT NULL,
|
||||||
|
CONSTRAINT "emotes_pkey" PRIMARY KEY ("emote_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
emote_usages (
|
||||||
|
emote_id text NOT NULL,
|
||||||
|
chatter_id text NOT NULL,
|
||||||
|
broadcaster_id text NOT NULL,
|
||||||
|
timestamp timestamp(3) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
groups (
|
||||||
|
group_id uuid DEFAULT gen_random_uuid (),
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
priority integer NOT NULL,
|
||||||
|
CONSTRAINT "groups_pkey" PRIMARY KEY ("group_id"),
|
||||||
|
CONSTRAINT "groups_user_id_name_unique" UNIQUE ("user_id", "name") CONSTRAINT "groups_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "groups_userId_idx" ON "groups" USING btree ("user_id");
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
permissions (
|
||||||
|
permission_id uuid DEFAULT gen_random_uuid (),
|
||||||
|
group_id uuid NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
path text NOT NULL,
|
||||||
|
allow boolean,
|
||||||
|
CONSTRAINT "permissions" PRIMARY KEY ("permission_id") CONSTRAINT "permissions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "permissions_user_id_idx" ON "permissions" USING btree ("user_id");
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
policies (
|
||||||
|
policy_id uuid DEFAULT gen_random_uuid (),
|
||||||
|
group_id uuid NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
path text NOT NULL,
|
||||||
|
count integer NOT NULL,
|
||||||
|
span integer NOT NULL,
|
||||||
|
CONSTRAINT "policies_pkey" PRIMARY KEY ("policy_id") CONSTRAINT "policies_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "policies_user_id_idx" ON "policies" USING btree ("user_id");
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
impersonations (
|
||||||
|
source_id uuid NOT NULL,
|
||||||
|
target_id uuid NOT NULL,
|
||||||
|
CONSTRAINT "impersonations_pkey" PRIMARY KEY ("source_id") CONSTRAINT "impersonations_source_id_fkey" FOREIGN KEY ("source_id") REFERENCES users ("user_id") ON DELETE CASCADE CONSTRAINT "impersonations_target_id_fkey" FOREIGN KEY ("target_id") REFERENCES users ("user_id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
redemption_actions (
|
||||||
|
redemption_actions_id uuid DEFAULT gen_random_uuid (),
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
provider_redemption_id text,
|
||||||
|
action_name text NOT NULL,
|
||||||
|
order integer NOT NULL,
|
||||||
|
state boolean NOT NULL,
|
||||||
|
CONSTRAINT "redemption_actions_pkey" PRIMARY KEY ("redemption_actions_id"),
|
||||||
|
CONSTRAINT "redemptions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
tts_chatter_voices (
|
||||||
|
chatter_id text NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
tts_voice_id uuid NOT NULL,
|
||||||
|
CONSTRAINT "tts_chatter_voices" PRIMARY KEY ("user_id", "chatter_id") CONSTRAINT "tts_chatter_voices_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
tts_voices (
|
||||||
|
tts_voice_id uuid DEFAULT gen_random_uuid (),
|
||||||
|
name uuid NOT NULL,
|
||||||
|
gender text,
|
||||||
|
base_language text CONSTRAINT "tts_voices_pkey" PRIMARY KEY ("tts_voice_id"),
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
tts_voice_states (
|
||||||
|
tts_voice_id uuid NOT NULL,
|
||||||
|
user_id NOT NULL,
|
||||||
|
state boolean NOT NULL DEFAULT true,
|
||||||
|
CONSTRAINT "tts_voice_states_pkey" PRIMARY KEY ("tts_voice_id", "user_id"),
|
||||||
|
CONSTRAINT "tts_voice_states_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
tts_word_filters (
|
||||||
|
tts_word_filter_id uuid DEFAULT gen_random_uuid (),
|
||||||
|
user_id NOT NULL,
|
||||||
|
search text NOT NULL,
|
||||||
|
replace text NOT NULL,
|
||||||
|
flag integer NOT NULL,
|
||||||
|
CONSTRAINT "tts_word_filters_pkey" PRIMARY KEY ("tts_word_filter_id"),
|
||||||
|
CONSTRAINT "tts_word_filters_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
281
src/index.ts
281
src/index.ts
@@ -1,25 +1,19 @@
|
|||||||
import express, { Express, Request, Response } from "express";
|
import express, { Express, Request } from "express";
|
||||||
import pgPromise from "pg-promise";
|
|
||||||
import rateLimit from "express-rate-limit";
|
import rateLimit from "express-rate-limit";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
|
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import * as httpm from 'typed-rest-client/HttpClient';
|
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
if (!process.env.CONNECTION_STRING) {
|
if (!process.env.CONNECTION_STRING) {
|
||||||
throw new Error("Cannot find connection string.");
|
throw new Error("Cannot find connection string.");
|
||||||
}
|
}
|
||||||
|
import { passport } from "./services/passport";
|
||||||
const pgp = pgPromise({});
|
|
||||||
const db = pgp(process.env.CONNECTION_STRING as string);
|
|
||||||
|
|
||||||
const limiter = rateLimit({
|
const limiter = rateLimit({
|
||||||
legacyHeaders: true,
|
legacyHeaders: true,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 1000,
|
||||||
limit: 200,
|
limit: 8,
|
||||||
max: 2,
|
max: 2,
|
||||||
message: "Too many requests, please try again later.",
|
message: "Too many requests, please try again later.",
|
||||||
keyGenerator: (req: Request) => req.ip as string,
|
keyGenerator: (req: Request) => req.ip as string,
|
||||||
@@ -28,265 +22,32 @@ const limiter = rateLimit({
|
|||||||
const app: Express = express();
|
const app: Express = express();
|
||||||
const port = process.env.PORT || 3000;
|
const port = process.env.PORT || 3000;
|
||||||
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(express.urlencoded());
|
|
||||||
|
|
||||||
var jwt = require('jsonwebtoken');
|
|
||||||
const passport = require('passport');
|
|
||||||
const JwtStrat = require('passport-jwt').Strategy;
|
|
||||||
const ExtractJwt = require('passport-jwt').ExtractJwt;
|
|
||||||
passport.use(new JwtStrat({
|
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
||||||
secretOrKey: process.env.JWT_SECRET,
|
|
||||||
}, async (jwt_payload: any, done: any) => {
|
|
||||||
console.log('jwt payload', jwt_payload);
|
|
||||||
const user = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', jwt_payload.id);
|
|
||||||
|
|
||||||
console.log('jwt user', user);
|
|
||||||
if (user) {
|
|
||||||
const impersonationId = await db.oneOrNone('SELECT "targetId" FROM "Impersonation" WHERE "sourceId" = $1', jwt_payload.id);
|
|
||||||
if (impersonationId) {
|
|
||||||
const impersonation = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', impersonationId.targetId);
|
|
||||||
if (impersonation) {
|
|
||||||
user.impersonation = impersonation;
|
|
||||||
console.log('found impersonation via jwt');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
done(null, user);
|
|
||||||
} else {
|
|
||||||
done(null, false);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
const OpenIDConnectStrategy = require('passport-openidconnect');
|
|
||||||
app.use(session({
|
app.use(session({
|
||||||
key: 'passport',
|
secret: process.env.SESSION_SECRET,
|
||||||
secret: process.env.AUTH_SECRET,
|
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
|
cookie: {
|
||||||
|
maxAge: 7 * 24 * 60 * 60 * 1000,
|
||||||
|
secure: true,
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
app.use(passport.initialize());
|
app.use(passport.initialize());
|
||||||
app.use(passport.session());
|
app.use(passport.session());
|
||||||
app.set('trust proxy', true);
|
|
||||||
|
|
||||||
passport.use(new OpenIDConnectStrategy({
|
|
||||||
issuer: 'https://id.twitch.tv/oauth2',
|
|
||||||
authorizationURL: 'https://id.twitch.tv/oauth2/authorize',
|
|
||||||
tokenURL: 'https://id.twitch.tv/oauth2/token',
|
|
||||||
clientID: process.env.AUTH_CLIENT_ID,
|
|
||||||
clientSecret: process.env.AUTH_CLIENT_SECRET,
|
|
||||||
callbackURL: process.env.AUTH_REDIRECT_URI,
|
|
||||||
scope: 'user_read'
|
|
||||||
}, async (url: any, profile: any, something: any, done: any) => {
|
|
||||||
console.log('login', 'pus:', profile, url, something);
|
|
||||||
const account: any = await db.oneOrNone('SELECT "userId" FROM "Account" WHERE "providerAccountId" = $1', profile.id);
|
|
||||||
if (account != null) {
|
|
||||||
const user: any = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', account.userId);
|
|
||||||
if (user.name != profile.username) {
|
|
||||||
db.none('UPDATE "User" SET name = $1 WHERE id = $2', [profile.username, profile.id]);
|
|
||||||
user.name = profile.username;
|
|
||||||
}
|
|
||||||
const impersonationId = await db.oneOrNone('SELECT "targetId" FROM "Impersonation" WHERE "sourceId" = $1', profile.id);
|
|
||||||
if (impersonationId) {
|
|
||||||
const impersonation = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', impersonationId.targetId);
|
|
||||||
if (impersonation) {
|
|
||||||
user.impersonation = impersonation;
|
|
||||||
console.log('found impersonation via open id');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return done(null, user);
|
|
||||||
}
|
|
||||||
return done(new Error('Account does not exist.'), null);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
passport.serializeUser((user: any, done: any) => {
|
|
||||||
if (!user)
|
|
||||||
return done(new Error('user is null'), null);
|
|
||||||
return done(null, user);
|
|
||||||
});
|
|
||||||
|
|
||||||
passport.deserializeUser((user: any, done: any) => {
|
|
||||||
done(null, user);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/auth', passport.authenticate("openidconnect", { failureRedirect: '/login' }), (req: Request, res: Response) => {
|
|
||||||
res.send('');
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/auth/validate', [isApiKeyAuthenticated, isJWTAuthenticated], (req: any, res: Response, next: () => void) => {
|
|
||||||
const user = req?.user;
|
|
||||||
res.send({ authenticated: user != null, user: user });
|
|
||||||
});
|
|
||||||
|
|
||||||
async function isApiKeyAuthenticated(req: any, res: any, next: any) {
|
|
||||||
const key = req.get('x-api-key');
|
|
||||||
if (key && !req.user) {
|
|
||||||
const data = await db.oneOrNone('SELECT "userId" from "ApiKey" WHERE id = $1', key);
|
|
||||||
if (data) {
|
|
||||||
const user = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', data.userId);
|
|
||||||
if (user.role == "ADMIN") {
|
|
||||||
const impersonationId = await db.oneOrNone('SELECT "targetId" FROM "Impersonation" WHERE "sourceId" = $1', data.userId);
|
|
||||||
if (impersonationId) {
|
|
||||||
const impersonation = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', impersonationId.targetId);
|
|
||||||
if (impersonation) {
|
|
||||||
user.impersonation = impersonation;
|
|
||||||
console.log('found impersonation via api key');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
req.user = user
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWebAuthenticated(req: any, res: any, next: () => void) {
|
|
||||||
console.log('web authentication', req.user, req.sessionID, req.session);
|
|
||||||
if (req.user) {
|
|
||||||
next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
res.status(401).send({ message: 'User is not authenticated.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
function isJWTAuthenticated(req: any, res: any, next: () => void) {
|
|
||||||
if (req.user) {
|
|
||||||
next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const check = passport.authenticate('jwt', { session: false });
|
|
||||||
check(req, res, next);
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiMiddlewares = [isApiKeyAuthenticated, isJWTAuthenticated, isWebAuthenticated]
|
|
||||||
|
|
||||||
app.get('/api/admin/users', apiMiddlewares, async (req: any, res: any, next: any) => {
|
|
||||||
if (req.user.role != 'ADMIN') {
|
|
||||||
res.status(403).send('You do not have the permissions for this.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await db.manyOrNone('SELECT id, name FROM "User"');
|
|
||||||
res.send(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.put('/api/admin/impersonate', apiMiddlewares, async (req: any, res: any, next: any) => {
|
|
||||||
if (req.user.role != 'ADMIN') {
|
|
||||||
res.status(403).send('You do not have the permissions for this.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.body.impersonation) {
|
|
||||||
res.status(400).send('Invalid user.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const impersonation = await db.one('SELECT EXISTS (SELECT 1 FROM "User" WHERE id = $1)', req.body.impersonation);
|
|
||||||
if (!impersonation) {
|
|
||||||
res.status(400).send('Invalid user.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await db.oneOrNone('SELECT "targetId" FROM "Impersonation" where "sourceId" = $1', req.user.id);
|
|
||||||
if (!data?.targetId) {
|
|
||||||
const insert = await db.none('INSERT INTO "Impersonation" ("sourceId", "targetId") VALUES ($1, $2)', [req.user.id, req.body.impersonation]);
|
|
||||||
res.send(insert);
|
|
||||||
} else {
|
|
||||||
const update = await db.none('UPDATE "Impersonation" SET "targetId" = $2 WHERE "sourceId" = $1', [req.user.id, req.body.impersonation]);
|
|
||||||
res.send(update);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.delete('/api/admin/impersonate', apiMiddlewares, async (req: any, res: any, next: any) => {
|
|
||||||
if (req.user.role != 'ADMIN') {
|
|
||||||
res.status(403).send('You do not have the permissions for this.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await db.oneOrNone('DELETE FROM "Impersonation" where "sourceId" = $1', req.user.id);
|
|
||||||
res.send(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/keys', apiMiddlewares, async (req: any, res: any, next: any) => {
|
|
||||||
const userId = req.user.impersonation?.id ?? req.user.id;
|
|
||||||
const data = await db.manyOrNone('SELECT id, label FROM "ApiKey" WHERE "userId" = $1', userId);
|
|
||||||
res.send(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/keys', apiMiddlewares, async (req: any, res: any, next: any) => {
|
|
||||||
const userId = req.user.impersonation?.id ?? req.user.id;
|
|
||||||
const keys = await db.one('SELECT count(*) FROM "ApiKey" WHERE "userId" = $1', userId);
|
|
||||||
if (keys.count > 10) {
|
|
||||||
res.status(400).send('too many keys');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const label = req.body.label;
|
|
||||||
if (!label) {
|
|
||||||
res.status(400).send('no label is attached.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const key = uuidv4();
|
|
||||||
await db.none('INSERT INTO "ApiKey" (id, label, "userId") VALUES ($1, $2, $3);', [key, label, userId]);
|
|
||||||
res.send({ label, key });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.delete('/api/keys', apiMiddlewares, async (req: any, res: any, next: any) => {
|
|
||||||
if (!req.body.key) {
|
|
||||||
res.status(400).send('key has not been provided.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const key = await db.one('SELECT EXISTS(SELECT 1 FROM "ApiKey" WHERE id = $1)', req.body.key);
|
|
||||||
if (!key.exists) {
|
|
||||||
res.status(400).send('key does not exist.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
res.send({ key: req.body.key });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api/auth/twitch/callback", async (req: any, res: any) => {
|
|
||||||
console.log(req.headers['user-agent']);
|
|
||||||
const query = `client_id=${process.env.AUTH_CLIENT_ID}&client_secret=${process.env.AUTH_CLIENT_SECRET}&code=${req.body.code}&grant_type=authorization_code&redirect_uri=${process.env.AUTH_REDIRECT_URI}`
|
|
||||||
const rest = new httpm.HttpClient(null);
|
|
||||||
const response = await rest.post('https://id.twitch.tv/oauth2/token', query, {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
|
||||||
});
|
|
||||||
const body = await response.readBody();
|
|
||||||
const data = JSON.parse(body);
|
|
||||||
if (!data || data.message) {
|
|
||||||
console.log('Failed to validate Twitch code authentication:', data);
|
|
||||||
res.send({ authenticated: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('Successfully validated Twitch code authentication. Attempting to read user data from Twitch.');
|
|
||||||
|
|
||||||
const resp = await rest.get('https://api.twitch.tv/helix/users', {
|
|
||||||
'Authorization': 'Bearer ' + data.access_token,
|
|
||||||
'Client-Id': process.env.AUTH_CLIENT_ID
|
|
||||||
});
|
|
||||||
const b = await resp.readBody();
|
|
||||||
const twitch = JSON.parse(b);
|
|
||||||
if (!twitch?.data) {
|
|
||||||
console.log('Failed to fetch twitch data:', twitch?.data);
|
|
||||||
res.send({ authenticated: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const account: any = await db.oneOrNone('SELECT "userId" FROM "Account" WHERE "providerAccountId" = $1', twitch.data[0].id);
|
|
||||||
if (account != null) {
|
|
||||||
const user: any = await db.one('SELECT id FROM "User" WHERE id = $1', account.userId);
|
|
||||||
console.log('User fetched successfully:', user.id);
|
|
||||||
|
|
||||||
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '30d' });
|
|
||||||
res.send({ authenticated: true, token: token });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
res.send({ authenticated: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const cors = require("cors");
|
||||||
|
app.use(cors({ credentials: true, origin: true }));
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded());
|
||||||
app.use(helmet());
|
app.use(helmet());
|
||||||
app.use(limiter);
|
app.use(limiter);
|
||||||
|
app.set('trust proxy', true);
|
||||||
|
|
||||||
|
app.get('/api/auth', passport.authenticate("openidconnect", { failureRedirect: '/login' }));
|
||||||
|
app.use(require('./routes/admin.route'));
|
||||||
|
app.use(require('./routes/api-keys.route'));
|
||||||
|
app.use(require('./routes/auth.route'));
|
||||||
|
app.use(require('./routes/twitch.route'));
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`[server]: Server is running at http://localhost:${port}`);
|
console.log(`[server]: Server is running at http://localhost:${port}`);
|
||||||
|
|||||||
25
src/middlewares/api-key.middleware.ts
Normal file
25
src/middlewares/api-key.middleware.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { database } from "../services/database";
|
||||||
|
import { passport } from "../services/passport";
|
||||||
|
|
||||||
|
export async function isApiKeyAuthenticated(req: any, res: any, next: any) {
|
||||||
|
if (!req.user) {
|
||||||
|
const key = req.get('x-api-key');
|
||||||
|
if (key) {
|
||||||
|
const data = await database.oneOrNone('SELECT "userId" from "ApiKey" WHERE id = $1', key);
|
||||||
|
if (data) {
|
||||||
|
req.user = await database.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', data.userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isJWTAuthenticated(req: any, res: any, next: () => void) {
|
||||||
|
if (req.user) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const check = passport.authenticate('jwt', { session: false });
|
||||||
|
check(req, res, next);
|
||||||
|
}
|
||||||
32
src/middlewares/authentication.middleware.ts
Normal file
32
src/middlewares/authentication.middleware.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export function isAuthenticated(req: any, res: any, next: () => void) {
|
||||||
|
if (req.user) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(401).send({ error: 'User is not authenticated.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAdminAuthenticated(req: any, res: any, next: () => void) {
|
||||||
|
if (req.user) {
|
||||||
|
if (req.user.role == 'ADMIN') {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(403).send('You do not have the permissions for this.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(401).send({ error: 'User is not authenticated.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function isNotAuthenticated(req: any, res: any, next: () => void) {
|
||||||
|
if (!req.user) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(403).send({ error: 'User is authenticated.' });
|
||||||
|
}
|
||||||
8
src/middlewares/common.middleware.ts
Normal file
8
src/middlewares/common.middleware.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { isApiKeyAuthenticated, isJWTAuthenticated } from "./api-key.middleware";
|
||||||
|
import { isAdminAuthenticated, isAuthenticated, isNotAuthenticated } from "./authentication.middleware";
|
||||||
|
import { checkImpersonation } from "./impersonation.middleware";
|
||||||
|
|
||||||
|
export const AUTH_MIDDLEWARES = [isJWTAuthenticated, checkImpersonation];
|
||||||
|
export const PUBLIC_API_MIDDLEWARES = [];
|
||||||
|
export const PROTECTED_API_MIDDLEWARES = [isApiKeyAuthenticated, isJWTAuthenticated, checkImpersonation, isAuthenticated];
|
||||||
|
export const ADMIN_API_MIDDLEWARES = [isApiKeyAuthenticated, isJWTAuthenticated, isAdminAuthenticated]
|
||||||
14
src/middlewares/impersonation.middleware.ts
Normal file
14
src/middlewares/impersonation.middleware.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { database } from "../services/database";
|
||||||
|
|
||||||
|
export async function checkImpersonation(req: any, res: any, next: () => void) {
|
||||||
|
if (req.user && req.user.role == 'ADMIN') {
|
||||||
|
const impersonationId = await database.oneOrNone('SELECT "targetId" FROM "Impersonation" WHERE "sourceId" = $1', req.user.id);
|
||||||
|
if (impersonationId) {
|
||||||
|
const impersonation = await database.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', impersonationId.targetId);
|
||||||
|
if (impersonation) {
|
||||||
|
req.user.impersonation = impersonation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
12
src/routes/admin.route.ts
Normal file
12
src/routes/admin.route.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { ADMIN_API_MIDDLEWARES } from "../middlewares/common.middleware";
|
||||||
|
|
||||||
|
export { };
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const adminController = require('../controllers/admin.controller');
|
||||||
|
|
||||||
|
router.get('/api/admin/users', ADMIN_API_MIDDLEWARES, adminController.getUsers);
|
||||||
|
router.put('/api/admin/impersonate', ADMIN_API_MIDDLEWARES, adminController.updateImpersonation);
|
||||||
|
router.delete('/api/admin/impersonate', ADMIN_API_MIDDLEWARES, adminController.deleteImpersonation);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
12
src/routes/api-keys.route.ts
Normal file
12
src/routes/api-keys.route.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { PROTECTED_API_MIDDLEWARES } from "../middlewares/common.middleware";
|
||||||
|
|
||||||
|
export { };
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const keyController = require('../controllers/api-keys.controller');
|
||||||
|
|
||||||
|
router.get('/api/keys', PROTECTED_API_MIDDLEWARES, keyController.getApiKeys);
|
||||||
|
router.post('/api/keys', PROTECTED_API_MIDDLEWARES, keyController.createApiKey);
|
||||||
|
router.delete('/api/keys', PROTECTED_API_MIDDLEWARES, keyController.deleteApiKey);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
13
src/routes/auth.route.ts
Normal file
13
src/routes/auth.route.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { AUTH_MIDDLEWARES, PROTECTED_API_MIDDLEWARES, PUBLIC_API_MIDDLEWARES } from "../middlewares/common.middleware";
|
||||||
|
|
||||||
|
export { };
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const authController = require('../controllers/auth.controller');
|
||||||
|
|
||||||
|
router.get('/api/auth/connections', authController.connectionCallback);
|
||||||
|
router.post('/api/auth/connections', PROTECTED_API_MIDDLEWARES, authController.connectionPreparation);
|
||||||
|
router.post('/api/auth/twitch/callback', authController.twitchCallback);
|
||||||
|
router.get('/api/auth/validate', AUTH_MIDDLEWARES, authController.validate);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
11
src/routes/twitch.route.ts
Normal file
11
src/routes/twitch.route.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { PROTECTED_API_MIDDLEWARES } from "../middlewares/common.middleware";
|
||||||
|
|
||||||
|
export { };
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const twitchController = require('../controllers/twitch.controller');
|
||||||
|
|
||||||
|
router.get('/api/twitch/redemptions', PROTECTED_API_MIDDLEWARES, twitchController.getTwitchRedemptions);
|
||||||
|
router.get('/api/twitch/users', PROTECTED_API_MIDDLEWARES, twitchController.getTwitchUsers);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
4
src/services/database.ts
Normal file
4
src/services/database.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import pgPromise from "pg-promise";
|
||||||
|
|
||||||
|
const pgp = pgPromise();
|
||||||
|
export const database = pgp(process.env.CONNECTION_STRING!);
|
||||||
56
src/services/passport.ts
Normal file
56
src/services/passport.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { OAuthData } from "../../data/oauth";
|
||||||
|
import { database } from "./database";
|
||||||
|
|
||||||
|
export const jwt = require('jsonwebtoken');
|
||||||
|
export const passport = require('passport');
|
||||||
|
|
||||||
|
const JwtStrat = require('passport-jwt').Strategy;
|
||||||
|
const ExtractJwt = require('passport-jwt').ExtractJwt;
|
||||||
|
passport.use(new JwtStrat({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
secretOrKey: process.env.JWT_SECRET,
|
||||||
|
}, async (jwt_payload: any, done: any) => {
|
||||||
|
const user = await database.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', jwt_payload.id);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
done(null, user);
|
||||||
|
} else {
|
||||||
|
done(null, false);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const OpenIDConnectStrategy = require('passport-openidconnect');
|
||||||
|
|
||||||
|
passport.use(new OpenIDConnectStrategy({
|
||||||
|
issuer: 'https://id.twitch.tv/oauth2',
|
||||||
|
authorizationURL: 'https://id.twitch.tv/oauth2/authorize',
|
||||||
|
tokenURL: 'https://id.twitch.tv/oauth2/token',
|
||||||
|
clientID: process.env.AUTH_CLIENT_ID,
|
||||||
|
clientSecret: process.env.AUTH_CLIENT_SECRET,
|
||||||
|
callbackURL: process.env.AUTH_REDIRECT_URI,
|
||||||
|
scope: 'user_read ' + OAuthData['twitch'].scopes.join(' '),
|
||||||
|
},
|
||||||
|
async (url: any, profile: any, something: any, done: any) => {
|
||||||
|
console.log('login', 'profile:', profile, 'url', url, 'something', something);
|
||||||
|
const account: any = await database.oneOrNone('SELECT "userId" FROM "Account" WHERE "providerAccountId" = $1', profile.id);
|
||||||
|
if (account != null) {
|
||||||
|
const user: any = await database.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', account.userId);
|
||||||
|
if (user.name != profile.username) {
|
||||||
|
await database.none('UPDATE "User" SET name = $1 WHERE id = $2', [profile.username, profile.id]);
|
||||||
|
user.name = profile.username;
|
||||||
|
}
|
||||||
|
return done(null, user);
|
||||||
|
}
|
||||||
|
return done(new Error('Account does not exist.'), null);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
passport.serializeUser((user: any, done: any) => {
|
||||||
|
if (!user)
|
||||||
|
return done(new Error('user is null'), null);
|
||||||
|
return done(null, user);
|
||||||
|
});
|
||||||
|
|
||||||
|
passport.deserializeUser((user: any, done: any) => {
|
||||||
|
done(null, user);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user