Admin user management UI (khanon/oai-reverse-proxy!32)

This commit is contained in:
khanon
2023-08-06 00:58:33 +00:00
parent 125bbe6441
commit bb230469b2
16 changed files with 825 additions and 165 deletions
+232 -18
View File
@@ -10,12 +10,15 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.3.5", "axios": "^1.3.5",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"ejs": "^3.1.9",
"express": "^4.18.2", "express": "^4.18.2",
"firebase-admin": "^11.10.1", "firebase-admin": "^11.10.1",
"googleapis": "^122.0.0", "googleapis": "^122.0.0",
"http-proxy-middleware": "^3.0.0-beta.1", "http-proxy-middleware": "^3.0.0-beta.1",
"multer": "^1.4.5-lts.1",
"pino": "^8.11.0", "pino": "^8.11.0",
"pino-http": "^8.3.3", "pino-http": "^8.3.3",
"showdown": "^2.1.0", "showdown": "^2.1.0",
@@ -25,8 +28,10 @@
"zod": "^3.21.4" "zod": "^3.21.4"
}, },
"devDependencies": { "devDependencies": {
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.13", "@types/cors": "^2.8.13",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/multer": "^1.4.7",
"@types/showdown": "^2.0.0", "@types/showdown": "^2.0.0",
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",
"concurrently": "^8.0.1", "concurrently": "^8.0.1",
@@ -776,6 +781,15 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/cookie-parser": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz",
"integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==",
"dev": true,
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/cors": { "node_modules/@types/cors": {
"version": "2.8.13", "version": "2.8.13",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz",
@@ -871,6 +885,15 @@
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"optional": true "optional": true
}, },
"node_modules/@types/multer": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz",
"integrity": "sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==",
"dev": true,
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "18.15.11", "version": "18.15.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
@@ -1021,7 +1044,6 @@
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"devOptional": true,
"dependencies": { "dependencies": {
"color-convert": "^2.0.1" "color-convert": "^2.0.1"
}, },
@@ -1045,6 +1067,11 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
},
"node_modules/arg": { "node_modules/arg": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@@ -1070,6 +1097,11 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/async": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
},
"node_modules/async-retry": { "node_modules/async-retry": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
@@ -1105,8 +1137,7 @@
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
"devOptional": true
}, },
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
@@ -1177,7 +1208,6 @@
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"devOptional": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -1225,8 +1255,18 @@
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
"dev": true },
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
}, },
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
@@ -1264,7 +1304,6 @@
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"devOptional": true,
"dependencies": { "dependencies": {
"ansi-styles": "^4.1.0", "ansi-styles": "^4.1.0",
"supports-color": "^7.1.0" "supports-color": "^7.1.0"
@@ -1280,7 +1319,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"devOptional": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -1289,7 +1327,6 @@
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"devOptional": true,
"dependencies": { "dependencies": {
"has-flag": "^4.0.0" "has-flag": "^4.0.0"
}, },
@@ -1342,7 +1379,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"devOptional": true,
"dependencies": { "dependencies": {
"color-name": "~1.1.4" "color-name": "~1.1.4"
}, },
@@ -1353,8 +1389,7 @@
"node_modules/color-name": { "node_modules/color-name": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
"devOptional": true
}, },
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
@@ -1390,8 +1425,48 @@
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
"devOptional": true },
"node_modules/concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"engines": [
"node >= 0.8"
],
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"node_modules/concat-stream/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/concat-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/concat-stream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
}, },
"node_modules/concurrently": { "node_modules/concurrently": {
"version": "8.0.1", "version": "8.0.1",
@@ -1471,11 +1546,36 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/cookie-parser": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
"integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
"dependencies": {
"cookie": "0.4.1",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": { "node_modules/cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
}, },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/cors": { "node_modules/cors": {
"version": "2.8.5", "version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
@@ -1602,6 +1702,20 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
}, },
"node_modules/ejs": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
"dependencies": {
"jake": "^10.8.5"
},
"bin": {
"ejs": "bin/cli.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -1988,6 +2102,33 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
"dependencies": {
"minimatch": "^5.0.1"
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/filelist/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -2632,6 +2773,28 @@
"integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==",
"optional": true "optional": true
}, },
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/jake": {
"version": "10.8.7",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz",
"integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==",
"dependencies": {
"async": "^3.2.3",
"chalk": "^4.0.2",
"filelist": "^1.0.4",
"minimatch": "^3.1.2"
},
"bin": {
"jake": "bin/cli.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/jose": { "node_modules/jose": {
"version": "4.14.4", "version": "4.14.4",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz",
@@ -2977,7 +3140,6 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"devOptional": true,
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@@ -2989,7 +3151,6 @@
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"optional": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@@ -3011,6 +3172,34 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}, },
"node_modules/multer": {
"version": "1.4.5-lts.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.0.0",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"object-assign": "^4.1.1",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/multer/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/negotiator": { "node_modules/negotiator": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -3265,6 +3454,11 @@
"node": ">= 0.6.0" "node": ">= 0.6.0"
} }
}, },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/process-warning": { "node_modules/process-warning": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.2.0.tgz", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.2.0.tgz",
@@ -3761,6 +3955,14 @@
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
"optional": true "optional": true
}, },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": { "node_modules/string_decoder": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -3977,6 +4179,11 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.0.4", "version": "5.0.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
@@ -4036,8 +4243,7 @@
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
"optional": true
}, },
"node_modules/utils-merge": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
@@ -4142,6 +4348,14 @@
"integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==",
"optional": true "optional": true
}, },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+6 -3
View File
@@ -3,10 +3,8 @@
"version": "1.0.0", "version": "1.0.0",
"description": "Reverse proxy for the OpenAI API", "description": "Reverse proxy for the OpenAI API",
"scripts": { "scripts": {
"build:watch": "esbuild src/server.ts --outfile=build/server.js --platform=node --target=es2020 --format=cjs --bundle --sourcemap --watch",
"build": "tsc", "build": "tsc",
"start:dev": "concurrently \"npm run build:watch\" \"npm run start:watch\"", "start:dev": "nodemon --watch src --exec ts-node --transpile-only src/server.ts",
"start:dev:tsc": "nodemon --watch src --exec ts-node --transpile-only src/server.ts",
"start:watch": "nodemon --require source-map-support/register build/server.js", "start:watch": "nodemon --require source-map-support/register build/server.js",
"start:replit": "tsc && node build/server.js", "start:replit": "tsc && node build/server.js",
"start": "node build/server.js", "start": "node build/server.js",
@@ -19,12 +17,15 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.3.5", "axios": "^1.3.5",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"ejs": "^3.1.9",
"express": "^4.18.2", "express": "^4.18.2",
"firebase-admin": "^11.10.1", "firebase-admin": "^11.10.1",
"googleapis": "^122.0.0", "googleapis": "^122.0.0",
"http-proxy-middleware": "^3.0.0-beta.1", "http-proxy-middleware": "^3.0.0-beta.1",
"multer": "^1.4.5-lts.1",
"pino": "^8.11.0", "pino": "^8.11.0",
"pino-http": "^8.3.3", "pino-http": "^8.3.3",
"showdown": "^2.1.0", "showdown": "^2.1.0",
@@ -34,8 +35,10 @@
"zod": "^3.21.4" "zod": "^3.21.4"
}, },
"devDependencies": { "devDependencies": {
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.13", "@types/cors": "^2.8.13",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/multer": "^1.4.7",
"@types/showdown": "^2.0.0", "@types/showdown": "^2.0.0",
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",
"concurrently": "^8.0.1", "concurrently": "^8.0.1",
+53
View File
@@ -0,0 +1,53 @@
import { Request, Response, RequestHandler } from "express";
import { config } from "../config";
const ADMIN_KEY = config.adminKey;
const failedAttempts = new Map<string, number>();
export const auth: RequestHandler = (req, res, next) => {
const bearerToken = req.headers.authorization?.slice("Bearer ".length);
const cookieToken = req.cookies["admin-token"];
const token = bearerToken ?? cookieToken;
const attempts = failedAttempts.get(req.ip) ?? 0;
if (!token) {
return res.redirect("/admin/login");
}
if (!ADMIN_KEY) {
req.log.warn(
{ ip: req.ip },
`Blocked admin request because no admin key is configured`
);
return res.status(401).json({ error: "Unauthorized" });
}
if (attempts > 5) {
req.log.warn(
{ ip: req.ip, token: bearerToken },
`Blocked admin request due to too many failed attempts`
);
return res.status(401).json({ error: "Too many attempts" });
}
if (token !== ADMIN_KEY) {
req.log.warn(
{ ip: req.ip, attempts, token },
`Attempted admin request with invalid token`
);
return handleFailedLogin(req, res);
}
req.log.info({ ip: req.ip }, `Admin request authorized`);
next();
};
function handleFailedLogin(req: Request, res: Response) {
const attempts = failedAttempts.get(req.ip) ?? 0;
const newAttempts = attempts + 1;
failedAttempts.set(req.ip, newAttempts);
if (req.accepts("json", "html") === "json") {
return res.status(401).json({ error: "Unauthorized" });
}
return res.redirect("/admin/login?failed=true");
}
+21
View File
@@ -0,0 +1,21 @@
import { Router } from "express";
const loginRouter = Router();
loginRouter.get("/login", (req, res) => {
res.render("admin/login", { failed: req.query.failed });
});
loginRouter.post("/login", (req, res) => {
res.cookie("admin-token", req.body.token, {
maxAge: 1000 * 60 * 60 * 24 * 14,
});
res.redirect("/admin");
});
loginRouter.get("/logout", (req, res) => {
res.clearCookie("admin-token");
res.redirect("/admin/login");
});
export { loginRouter };
+212
View File
@@ -0,0 +1,212 @@
import { Router } from "express";
import { Query } from "express-serve-static-core";
import multer from "multer";
import { z } from "zod";
import * as userStore from "../../proxy/auth/user-store";
import { config } from "../../config";
const upload = multer({
storage: multer.memoryStorage(),
fileFilter: (_req, file, cb) => {
if (file.mimetype !== "application/json") {
cb(new Error("Invalid file type"));
} else {
cb(null, true);
}
},
});
const usersRouter = Router();
export const UserSchema = z
.object({
ip: z.array(z.string()).optional(),
type: z.enum(["normal", "special"]).optional(),
promptCount: z.number().optional(),
tokenCount: z.number().optional(),
createdAt: z.number().optional(),
lastUsedAt: z.number().optional(),
disabledAt: z.number().optional(),
disabledReason: z.string().optional(),
})
.strict();
const UserSchemaWithToken = UserSchema.extend({
token: z.string(),
}).strict();
function paginate(set: unknown[], page: number, pageSize: number = 20) {
return {
page,
pageCount: Math.ceil(set.length / pageSize),
items: set.slice((page - 1) * pageSize, page * pageSize),
nextPage: page * pageSize < set.length ? page + 1 : null,
prevPage: page > 1 ? page - 1 : null,
};
}
function parseSort(sort: Query["sort"]) {
if (!sort) return null;
if (typeof sort === "string") return sort.split(",");
if (Array.isArray(sort)) return sort.splice(3) as string[];
return null;
}
function sortBy(fields: string[], asc = true) {
return (a: any, b: any) => {
for (const field of fields) {
if (a[field] !== b[field]) {
// always sort nulls to the end
if (a[field] == null) return 1;
if (b[field] == null) return -1;
const result = a[field] < b[field] ? -1 : 1;
return asc ? result : -result;
}
}
return 0;
};
}
function isFromUi(req: any) {
return req.accepts("json", "html") === "html";
}
// UI-specific routes
usersRouter.get("/create-user", (req, res) => {
const recentUsers = userStore
.getUsers()
.sort(sortBy(["createdAt"], false))
.slice(0, 5);
res.render("admin/create-user", {
recentUsers,
newToken: !!req.query.created,
});
});
usersRouter.get("/import-users", (req, res) => {
const imported = Number(req.query.imported) || 0;
res.render("admin/import-users", { imported });
});
usersRouter.post("/import-users", upload.single("users"), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: "No file uploaded" });
}
const data = JSON.parse(req.file.buffer.toString());
const result = z.array(UserSchemaWithToken).safeParse(data.users);
if (!result.success) {
return res.status(400).json({ error: result.error });
}
const upserts = result.data.map((user) => userStore.upsertUser(user));
res.redirect(`/admin/users/import-users?imported=${upserts.length}`);
});
usersRouter.get("/export-users", (req, res) => {
const users = userStore.getUsers();
res.render("admin/export-users", { users });
});
// API routes
/**
* Returns a list of all users, sorted by prompt count and then last used time.
* GET /admin/users
*/
usersRouter.get("/", (req, res) => {
const sort = parseSort(req.query.sort) || ["promptCount", "lastUsedAt"];
const users = userStore.getUsers().sort(sortBy(sort, false));
if (isFromUi(req)) {
const page = Number(req.query.page) || 1;
const { items, ...pagination } = paginate(users, page);
return res.render("admin/list-users", {
sort: sort.join(","),
users: items,
...pagination,
});
}
res.json({ users, count: users.length });
});
/**
* Returns the user with the given token.
* GET /admin/users/:token
*/
usersRouter.get("/:token", (req, res) => {
const user = userStore.getUser(req.params.token);
if (!user) {
return res.status(404).json({ error: "Not found" });
}
res.json(user);
});
/**
* Creates a new user.
* Returns the created user's token.
* POST /admin/users
*/
usersRouter.post("/", (req, res) => {
const token = userStore.createUser();
if (isFromUi(req)) {
return res.redirect(`/admin/users/create-user?created=true`);
}
res.json({ token });
});
/**
* Updates the user with the given token, creating them if they don't exist.
* Accepts a JSON body containing at least one field on the User type.
* Returns the upserted user.
* PUT /admin/users/:token
*/
usersRouter.put("/:token", (req, res) => {
const result = UserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ error: result.error });
}
userStore.upsertUser({ ...result.data, token: req.params.token });
res.json(userStore.getUser(req.params.token));
});
/**
* Bulk-upserts users given a list of User updates.
* Accepts a JSON body with the field `users` containing an array of updates.
* Returns an object containing the upserted users and the number of upserts.
* PUT /admin/users
*/
usersRouter.put("/", (req, res) => {
const result = z.array(UserSchemaWithToken).safeParse(req.body.users);
if (!result.success) {
return res.status(400).json({ error: result.error });
}
const upserts = result.data.map((user) => userStore.upsertUser(user));
res.json({
upserted_users: upserts,
count: upserts.length,
});
});
/**
* Disables the user with the given token. Optionally accepts a `disabledReason`
* query parameter.
* Returns the disabled user.
* DELETE /admin/users/:token
*/
usersRouter.delete("/:token", (req, res) => {
const user = userStore.getUser(req.params.token);
const disabledReason = z
.string()
.optional()
.safeParse(req.query.disabledReason);
if (!disabledReason.success) {
return res.status(400).json({ error: disabledReason.error });
}
if (!user) {
return res.status(404).json({ error: "Not found" });
}
userStore.disableUser(req.params.token, disabledReason.data);
res.json(userStore.getUser(req.params.token));
});
export { usersRouter };
+15 -30
View File
@@ -1,40 +1,25 @@
import express, { RequestHandler, Router } from "express"; import express, { Router } from "express";
import cookieParser from "cookie-parser";
import { config } from "../config"; import { config } from "../config";
import { usersRouter } from "./users"; import { auth } from "./auth";
import { loginRouter } from "./controllers/login";
const ADMIN_KEY = config.adminKey; import { usersRouter } from "./controllers/users";
const failedAttempts = new Map<string, number>();
const adminRouter = Router(); const adminRouter = Router();
const auth: RequestHandler = (req, res, next) => {
const token = req.headers.authorization?.slice("Bearer ".length);
const attempts = failedAttempts.get(req.ip) ?? 0;
if (attempts > 5) {
req.log.warn(
{ ip: req.ip, token },
`Blocked request to admin API due to too many failed attempts`
);
return res.status(401).json({ error: "Too many attempts" });
}
if (token !== ADMIN_KEY) {
const newAttempts = attempts + 1;
failedAttempts.set(req.ip, newAttempts);
req.log.warn(
{ ip: req.ip, attempts: newAttempts, token },
`Attempted admin API request with invalid token`
);
return res.status(401).json({ error: "Unauthorized" });
}
next();
};
adminRouter.use(auth);
adminRouter.use( adminRouter.use(
express.json({ limit: "20mb" }), express.json({ limit: "20mb" }),
express.urlencoded({ extended: true, limit: "20mb" }) express.urlencoded({ extended: true, limit: "20mb" })
); );
adminRouter.use(cookieParser());
adminRouter.use("/", loginRouter);
adminRouter.use(auth);
adminRouter.use("/users", usersRouter); adminRouter.use("/users", usersRouter);
adminRouter.get("/", (_req, res) => {
res.render("admin/index", {
isPersistenceEnabled: config.gatekeeperStore !== "memory",
});
});
export { adminRouter }; export { adminRouter };
-114
View File
@@ -1,114 +0,0 @@
import { Router } from "express";
import { z } from "zod";
import * as userStore from "../proxy/auth/user-store";
const usersRouter = Router();
const UserSchema = z
.object({
ip: z.array(z.string()).optional(),
type: z.enum(["normal", "special"]).optional(),
promptCount: z.number().optional(),
tokenCount: z.number().optional(),
createdAt: z.number().optional(),
lastUsedAt: z.number().optional(),
disabledAt: z.number().optional(),
disabledReason: z.string().optional(),
})
.strict();
const UserSchemaWithToken = UserSchema.extend({
token: z.string(),
}).strict();
/**
* Returns a list of all users, sorted by prompt count and then last used time.
* GET /admin/users
*/
usersRouter.get("/", (_req, res) => {
const users = userStore.getUsers().sort((a, b) => {
if (a.promptCount !== b.promptCount) {
return b.promptCount - a.promptCount;
}
return (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0);
});
res.json({ users, count: users.length });
});
/**
* Returns the user with the given token.
* GET /admin/users/:token
*/
usersRouter.get("/:token", (req, res) => {
const user = userStore.getUser(req.params.token);
if (!user) {
return res.status(404).json({ error: "Not found" });
}
res.json(user);
});
/**
* Creates a new user.
* Returns the created user's token.
* POST /admin/users
*/
usersRouter.post("/", (_req, res) => {
res.json({ token: userStore.createUser() });
});
/**
* Updates the user with the given token, creating them if they don't exist.
* Accepts a JSON body containing at least one field on the User type.
* Returns the upserted user.
* PUT /admin/users/:token
*/
usersRouter.put("/:token", (req, res) => {
const result = UserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ error: result.error });
}
userStore.upsertUser({ ...result.data, token: req.params.token });
res.json(userStore.getUser(req.params.token));
});
/**
* Bulk-upserts users given a list of User updates.
* Accepts a JSON body with the field `users` containing an array of updates.
* Returns an object containing the upserted users and the number of upserts.
* PUT /admin/users
*/
usersRouter.put("/", (req, res) => {
const result = z.array(UserSchemaWithToken).safeParse(req.body.users);
if (!result.success) {
return res.status(400).json({ error: result.error });
}
const upserts = result.data.map((user) => userStore.upsertUser(user));
res.json({
upserted_users: upserts,
count: upserts.length,
});
});
/**
* Disables the user with the given token. Optionally accepts a `disabledReason`
* query parameter.
* Returns the disabled user.
* DELETE /admin/users/:token
*/
usersRouter.delete("/:token", (req, res) => {
const user = userStore.getUser(req.params.token);
const disabledReason = z
.string()
.optional()
.safeParse(req.query.disabledReason);
if (!disabledReason.success) {
return res.status(400).json({ error: disabledReason.error });
}
if (!user) {
return res.status(404).json({ error: "Not found" });
}
userStore.disableUser(req.params.token, disabledReason.data);
res.json(userStore.getUser(req.params.token));
});
export { usersRouter };
+4
View File
@@ -2,6 +2,7 @@ import { assertConfigIsValid, config } from "./config";
import "source-map-support/register"; import "source-map-support/register";
import express from "express"; import express from "express";
import cors from "cors"; import cors from "cors";
import path from "path";
import pinoHttp from "pino-http"; import pinoHttp from "pino-http";
import childProcess from "child_process"; import childProcess from "child_process";
import { logger } from "./logger"; import { logger } from "./logger";
@@ -53,6 +54,9 @@ app.use(
// addresses correctly. // addresses correctly.
app.set("trust proxy", true); app.set("trust proxy", true);
app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));
app.get("/health", (_req, res) => res.sendStatus(200)); app.get("/health", (_req, res) => res.sendStatus(200));
app.use(cors()); app.use(cors());
app.use(checkOrigin); app.use(checkOrigin);
+6
View File
@@ -0,0 +1,6 @@
<hr />
<footer>
<a href="/admin">Index</a> | <a href="/admin/logout">Logout</a>
</footer>
</body>
</html>
+64
View File
@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><%= title %></title>
<style>
.pagination {
list-style-type: none;
/* margin: 0; */
padding: 0;
}
.pagination li {
display: inline-block;
}
.pagination li a {
display: block;
padding: 0.5em 1em;
text-decoration: none;
}
.pagination li.active a {
background-color: #58739c;
color: #fff;
}
table {
border-collapse: collapse;
border: 1px solid #ccc;
}
table td, table th {
border: 1px solid #ccc;
padding: 0.25em 0.5em;
}
th.active {
background-color: #ccc;
}
td.actions {
padding: 0;
text-align: center;
}
td.actions a {
text-decoration: none;
padding: 0.5em;
height: 100%;
width: 100%;
}
td.actions:hover {
background-color: #ccc;
}
@media (max-width: 600px) {
table {
width: 100%;
}
table td, table th {
display: block;
width: 100%;
}
}
</style>
</head>
<body style="font-family: sans-serif; background-color: #f0f0f0; padding: 1em;">
+17
View File
@@ -0,0 +1,17 @@
<%- include("../_partials/admin-header", { title: "Create User - OAI Reverse Proxy Admin" }) %>
<!--
-->
<h1>Create User Token</h1>
<form action="/admin/users" method="post">
<input type="submit" value="Create" />
</form>
<% if (newToken) { %>
<p>Just created <code><%= recentUsers[0].token %></code>.</p>
<% } %>
<h3>Recent Tokens</h2>
<ul>
<% recentUsers.forEach(function(user) { %>
<li><a href="/admin/users/<%= user.token %>"><%= user.token %></a></li>
<% }) %>
</ul>
<%- include("../_partials/admin-footer") %>
+28
View File
@@ -0,0 +1,28 @@
<%- include("../_partials/admin-header", { title: "Export Users - OAI Reverse Proxy Admin" }) %>
<h1>Export Users</h1>
<p>
Export users to JSON. The JSON will be an array of objects under the key
<code>users</code>. You can use this JSON to import users later.
</p>
<script>
function exportUsers() {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/admin/users", true);
xhr.responseType = "blob";
xhr.onload = function() {
if (this.status === 200) {
var blob = new Blob([this.response], { type: "application/json" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "users.json";
document.body.appendChild(a);
a.click();
a.remove();
}
};
xhr.send();
}
</script>
<button onclick="exportUsers()">Export</button>
<%- include("../_partials/admin-footer") %>
+44
View File
@@ -0,0 +1,44 @@
<%- include("../_partials/admin-header", { title: "Import Users - OAI Reverse Proxy Admin" }) %>
<h1>Import Users</h1>
<p>
Import users from JSON. The JSON should be an array of objects under the key
<code>users</code>. Each object should have the following fields:
</p>
<ul>
<li><code>token</code> (required): a unique identifier for the user</li>
<li><code>ip</code> (optional): IP addresses the user has connected from</li>
<li>
<code>type</code> (optional): either <code>normal</code> or
<code>special</code>
</li>
<li>
<code>promptCount</code> (optional): the number of times the user has sent a
prompt
</li>
<li>
<code>tokenCount</code> (optional): the number of tokens the user has
consumed (not yet implemented)
</li>
<li>
<code>createdAt</code> (optional): the timestamp when the user was created
</li>
<li>
<code>disabledAt</code> (optional): the timestamp when the user was disabled
</li>
<li>
<code>disabledReason</code> (optional): the reason the user was disabled
</li>
</ul>
<p>
If a user with the same token already exists, the existing user will be
updated with the new values.
</p>
<form action="/admin/users/import-users" method="post" enctype="multipart/form-data">
<input type="file" name="users" />
<input type="submit" value="Import" />
</form>
</form>
<% if (imported > 0) { %>
<p>Imported <code><%= imported %></code> users.</p>
<% } %>
<%- include("../_partials/admin-footer") %>
+20
View File
@@ -0,0 +1,20 @@
<%- include("../_partials/admin-header", { title: "OAI Reverse Proxy Admin" }) %>
<h1>OAI Reverse Proxy Admin</h1>
<% if (!isPersistenceEnabled) { %>
<p style="color: red; background-color: #eedddd; padding: 1em">
<strong>⚠️ Users will be lost when the server restarts because persistence is
not configured.</strong><br />
<br />Be sure to export your users and import them again after restarting the
server if you want to keep them.<br />
<br /> See the <a target="_blank"
href="https://gitgud.io/khanon/oai-reverse-proxy/-/blob/main/docs/user-management.md#firebase-realtime-database">
user management documentation</a> to learn how to set up persistence.
</p>
<% } %>
<ul>
<li><a href="/admin/users">List Users</a></li>
<li><a href="/admin/users/create-user">Create User</a></li>
<li><a href="/admin/users/import-users">Import Users</a></li>
<li><a href="/admin/users/export-users">Export Users</a></li>
</ul>
<%- include("../_partials/admin-footer") %>
+90
View File
@@ -0,0 +1,90 @@
<%- include("../_partials/admin-header", { title: "Users - OAI Reverse Proxy Admin" }) %>
<h1>User Token List</h1>
<% if (users.length === 0) { %>
<p>No users found.</p>
<% } %>
<table>
<thead>
<tr>
<th>Token</th>
<th <% if (sort==="ip") { %>class="active"<% } %> ><a href="/admin/users?sort=ip">IPs</a></th>
<th <% if (sort==="promptCount") { %>class="active"<% } %> ><a href="/admin/users?sort=promptCount">Prompts</a></th>
<th>Type</th>
<th <% if (sort==="createdAt") { %>class="active"<% } %> ><a href="/admin/users?sort=createdAt">Created (UTC)</a></th>
<th <% if (sort==="lastUsedAt") { %>class="active"<% } %> ><a href="/admin/users?sort=lastUsedAt">Last Used (UTC)</a></th>
<th colspan="2">Banned?</th>
</tr>
</thead>
<tbody>
<% users.forEach(function(user){ %>
<tr>
<td><%= user.token %></td>
<td><%= user.ip.length %></td>
<td><%= user.promptCount %></td>
<td><%= user.type %></td>
<td><%= user.createdAt %></td>
<td><%= user.lastUsedAt ?? "never" %></td>
<td class="actions">
<% if (user.disabledAt) { %>
<a title="Unban" href="#" class="unban" data-token="<%= user.token %>">🔄️</a>
<% } else { %>
<a title="Ban" href="#" class="ban" data-token="<%= user.token %>">🚫</a>
<% } %>
<td><%= user.disabledAt ? "Yes" : "No" %> <%= user.disabledReason ? `(${user.disabledReason})` : "" %></td>
</td>
</tr>
<% }); %>
</table>
<ul class="pagination">
<% if (page > 1) { %>
<li><a href="/admin/users?page=<%= page - 1 %>">&laquo;</a></li>
<% } %> <% for (var i = 1; i <= pageCount; i++) { %>
<li <% if (i === page) { %>class="active"<% } %>><a href="/admin/users?page=<%= i %>"><%= i %></a></li>
<% } %> <% if (page < pageCount) { %>
<li><a href="/admin/users?page=<%= page + 1 %>">&raquo;</a></li>
<% } %>
</ul>
<script>
document.querySelectorAll("td.actions a.ban").forEach(function (a) {
a.addEventListener("click", function (e) {
e.preventDefault();
var token = a.getAttribute("data-token");
if (confirm("Are you sure you want to ban this user?")) {
let reason = prompt("Reason for ban:");
fetch("/admin/users/" + token + "?disabledReason=" + encodeURIComponent(reason), {
method: "DELETE",
}).then(() => window.location.reload());
}
});
});
document.querySelectorAll("td.actions a.unban").forEach(function (a) {
a.addEventListener("click", function (e) {
e.preventDefault();
var token = a.getAttribute("data-token");
if (confirm("Are you sure you want to unban this user?")) {
fetch(
"/admin/users/" + token,
{ method: "PUT",
body: JSON.stringify({ disabledReason: "", disabledAt: 0 }),
headers: { "Content-Type": "application/json" },
},
).then(() => window.location.reload());
}
});
});
</script>
<script>
document.querySelectorAll("td").forEach(function(td) {
if (td.innerText.match(/^\d{13}$/)) {
if (td.innerText == 0) return 'never';
var date = new Date(parseInt(td.innerText));
td.innerText = date.toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
}
});
</script>
<%- include("../_partials/admin-footer") %>
+13
View File
@@ -0,0 +1,13 @@
<%- include("../_partials/admin-header", { title: "Login" }) %>
<body>
<h1>Login</h1>
<% if (failed) { %>
<p style="color: red;">Please try again.</p>
<% } %>
<form action="/admin/login" method="post">
<label for="token">Admin Key</label>
<input type="password" name="token" />
<input type="submit" value="Login" />
</form>
</body>
</html>