Compare commits

..

8 Commits

20 changed files with 979 additions and 434 deletions

6
.gitignore vendored
View File

@@ -1,3 +1,3 @@
dist/* dist/
node_modules/* node_modules/
.env *.env

34
data/oauth.ts Normal file
View 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
View File

@@ -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"

View File

@@ -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",

View 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 });
};

View 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 });
};

View 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 });
}

View 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
View 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
);

View File

@@ -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}`);

View 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);
}

View 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.' });
}

View 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]

View 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
View 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;

View 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
View 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;

View 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
View 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
View 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);
});