mirror of
https://github.com/diced/zipline.git
synced 2025-12-12 15:50:11 -08:00
refactor: express -> fastify
This commit is contained in:
17
package.json
17
package.json
@@ -8,12 +8,11 @@
|
||||
"build:prisma": "prisma generate",
|
||||
"build:next": "next build",
|
||||
"build:server": "tsup",
|
||||
"dev": "pnpm run build:server && pnpm run dev:server",
|
||||
"dev:server": "NODE_ENV=development DEBUG=zipline node --require dotenv/config --enable-source-maps ./build/server.js",
|
||||
"dev:inspector": "NODE_ENV=development DEBUG=zipline node --require dotenv/config --inspect=0.0.0.0:9229 --enable-source-maps ./build/server.js",
|
||||
"dev": "NODE_ENV=development DEBUG=zipline tsx --require dotenv/config --enable-source-maps ./src/server",
|
||||
"dev:inspector": "NODE_ENV=development DEBUG=zipline tsx --require dotenv/config --inspect=0.0.0.0:9229 --enable-source-maps ./src/server",
|
||||
"dev:ctl": "tsup --config tsup.ctl.config.ts --watch",
|
||||
"start": "NODE_ENV=production node --require dotenv/config --enable-source-maps ./build/server.js",
|
||||
"start:inspector": "NODE_ENV=production node --require dotenv/config --inspect=0.0.0.0:9229 --enable-source-maps ./build/server.js",
|
||||
"start": "NODE_ENV=production node --require dotenv/config --enable-source-maps ./build/server",
|
||||
"start:inspector": "NODE_ENV=production node --require dotenv/config --inspect=0.0.0.0:9229 --enable-source-maps ./build/server",
|
||||
"ctl": "NODE_ENV=production node --require dotenv/config --enable-source-maps ./build/ctl.js",
|
||||
"validate": "pnpm run \"/^validate:.*/\"",
|
||||
"validate:lint": "eslint --cache --ignore-path .gitignore --fix .",
|
||||
@@ -22,6 +21,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/plots": "^1.2.6",
|
||||
"@fastify/cookie": "^9.3.1",
|
||||
"@fastify/cors": "^9.0.1",
|
||||
"@fastify/sensible": "^5.5.0",
|
||||
"@github/webauthn-json": "^2.1.1",
|
||||
"@mantine/code-highlight": "^7.2.2",
|
||||
"@mantine/core": "^7.2.2",
|
||||
@@ -44,6 +46,9 @@
|
||||
"dayjs": "^1.11.10",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fastify": "^4.26.2",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
"isomorphic-dompurify": "^1.11.0",
|
||||
@@ -92,5 +97,5 @@
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"packageManager": "pnpm@8.7.0"
|
||||
"packageManager": "pnpm@8.15.6"
|
||||
}
|
||||
|
||||
342
pnpm-lock.yaml
generated
342
pnpm-lock.yaml
generated
@@ -8,6 +8,15 @@ dependencies:
|
||||
'@ant-design/plots':
|
||||
specifier: ^1.2.6
|
||||
version: 1.2.6(react-dom@18.2.0)(react@18.2.0)
|
||||
'@fastify/cookie':
|
||||
specifier: ^9.3.1
|
||||
version: 9.3.1
|
||||
'@fastify/cors':
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1
|
||||
'@fastify/sensible':
|
||||
specifier: ^5.5.0
|
||||
version: 5.5.0
|
||||
'@github/webauthn-json':
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
@@ -74,6 +83,15 @@ dependencies:
|
||||
express:
|
||||
specifier: ^4.18.2
|
||||
version: 4.18.2
|
||||
fast-glob:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
fastify:
|
||||
specifier: ^4.26.2
|
||||
version: 4.26.2
|
||||
fastify-plugin:
|
||||
specifier: ^4.5.1
|
||||
version: 4.5.1
|
||||
ffmpeg-static:
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0
|
||||
@@ -1099,6 +1117,56 @@ packages:
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/@fastify/ajv-compiler@3.5.0:
|
||||
resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==}
|
||||
dependencies:
|
||||
ajv: 8.12.0
|
||||
ajv-formats: 2.1.1(ajv@8.12.0)
|
||||
fast-uri: 2.3.0
|
||||
dev: false
|
||||
|
||||
/@fastify/cookie@9.3.1:
|
||||
resolution: {integrity: sha512-h1NAEhB266+ZbZ0e9qUE6NnNR07i7DnNXWG9VbbZ8uC6O/hxHpl+Zoe5sw1yfdZ2U6XhToUGDnzQtWJdCaPwfg==}
|
||||
dependencies:
|
||||
cookie-signature: 1.2.1
|
||||
fastify-plugin: 4.5.1
|
||||
dev: false
|
||||
|
||||
/@fastify/cors@9.0.1:
|
||||
resolution: {integrity: sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==}
|
||||
dependencies:
|
||||
fastify-plugin: 4.5.1
|
||||
mnemonist: 0.39.6
|
||||
dev: false
|
||||
|
||||
/@fastify/error@3.4.1:
|
||||
resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==}
|
||||
dev: false
|
||||
|
||||
/@fastify/fast-json-stringify-compiler@4.3.0:
|
||||
resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==}
|
||||
dependencies:
|
||||
fast-json-stringify: 5.14.1
|
||||
dev: false
|
||||
|
||||
/@fastify/merge-json-schemas@0.1.1:
|
||||
resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==}
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
dev: false
|
||||
|
||||
/@fastify/sensible@5.5.0:
|
||||
resolution: {integrity: sha512-D0zpl+nocsRXLceSbc4gasQaO3ZNQR4dy9Uu8Ym0mh8VUdrjpZ4g8Ca9O3pGXbBVOnPIGHUJNTV7Yf9dg/OYdg==}
|
||||
dependencies:
|
||||
'@lukeed/ms': 2.0.2
|
||||
fast-deep-equal: 3.1.3
|
||||
fastify-plugin: 4.5.1
|
||||
forwarded: 0.2.0
|
||||
http-errors: 2.0.0
|
||||
type-is: 1.6.18
|
||||
vary: 1.1.2
|
||||
dev: false
|
||||
|
||||
/@floating-ui/core@1.5.1:
|
||||
resolution: {integrity: sha512-QgcKYwzcc8vvZ4n/5uklchy8KVdjJwcOeI+HnnTNclJjs2nYsy23DOCf+sSV1kBwD9yDAoVKCkv/gEPzgQU3Pw==}
|
||||
dependencies:
|
||||
@@ -1208,6 +1276,11 @@ packages:
|
||||
call-bind: 1.0.5
|
||||
dev: false
|
||||
|
||||
/@lukeed/ms@2.0.2:
|
||||
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/@mantine/code-highlight@7.2.2(@mantine/core@7.2.2)(@mantine/hooks@7.2.2)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-zR3J8TvHVZjXESTLNd49pZobhJIZcXUr0CGLs8F4TdnslIoN4SNWipQCPa0kDw7qO5CJhfMqgMeEYNh/slPpPg==}
|
||||
peerDependencies:
|
||||
@@ -2109,6 +2182,10 @@ packages:
|
||||
event-target-shim: 5.0.1
|
||||
dev: false
|
||||
|
||||
/abstract-logging@2.0.1:
|
||||
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
|
||||
dev: false
|
||||
|
||||
/accepts@1.3.8:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -2157,6 +2234,28 @@ packages:
|
||||
indent-string: 4.0.0
|
||||
dev: false
|
||||
|
||||
/ajv-formats@2.1.1(ajv@8.12.0):
|
||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||
peerDependencies:
|
||||
ajv: ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
ajv:
|
||||
optional: true
|
||||
dependencies:
|
||||
ajv: 8.12.0
|
||||
dev: false
|
||||
|
||||
/ajv-formats@3.0.1(ajv@8.12.0):
|
||||
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
|
||||
peerDependencies:
|
||||
ajv: ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
ajv:
|
||||
optional: true
|
||||
dependencies:
|
||||
ajv: 8.12.0
|
||||
dev: false
|
||||
|
||||
/ajv@6.12.6:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
dependencies:
|
||||
@@ -2166,6 +2265,15 @@ packages:
|
||||
uri-js: 4.4.1
|
||||
dev: true
|
||||
|
||||
/ajv@8.12.0:
|
||||
resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
uri-js: 4.4.1
|
||||
dev: false
|
||||
|
||||
/align-text@0.1.4:
|
||||
resolution: {integrity: sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2259,6 +2367,10 @@ packages:
|
||||
zip-stream: 5.0.1
|
||||
dev: false
|
||||
|
||||
/archy@1.0.0:
|
||||
resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==}
|
||||
dev: false
|
||||
|
||||
/are-we-there-yet@2.0.0:
|
||||
resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2402,10 +2514,26 @@ packages:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
dev: false
|
||||
|
||||
/atomic-sleep@1.0.0:
|
||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
dev: false
|
||||
|
||||
/available-typed-arrays@1.0.5:
|
||||
resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
/avvio@8.3.0:
|
||||
resolution: {integrity: sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==}
|
||||
dependencies:
|
||||
'@fastify/error': 3.4.1
|
||||
archy: 1.0.0
|
||||
debug: 4.3.4
|
||||
fastq: 1.17.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/axe-core@4.7.0:
|
||||
resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2898,11 +3026,21 @@ packages:
|
||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||
dev: false
|
||||
|
||||
/cookie-signature@1.2.1:
|
||||
resolution: {integrity: sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==}
|
||||
engines: {node: '>=6.6.0'}
|
||||
dev: false
|
||||
|
||||
/cookie@0.5.0:
|
||||
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/cookie@0.6.0:
|
||||
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
dev: false
|
||||
@@ -3871,6 +4009,14 @@ packages:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
dev: false
|
||||
|
||||
/fast-content-type-parse@1.1.0:
|
||||
resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==}
|
||||
dev: false
|
||||
|
||||
/fast-decode-uri-component@1.0.1:
|
||||
resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
|
||||
dev: false
|
||||
|
||||
/fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
@@ -3896,15 +4042,75 @@ packages:
|
||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||
dev: true
|
||||
|
||||
/fast-json-stringify@5.14.1:
|
||||
resolution: {integrity: sha512-J1Grbf0oSXV3lKsBf3itz1AvRk43qVrx3Ac10sNvi3LZaz1by4oDdYKFrJycPhS8+Gb7y8rgV/Jqw1UZVjyNvw==}
|
||||
dependencies:
|
||||
'@fastify/merge-json-schemas': 0.1.1
|
||||
ajv: 8.12.0
|
||||
ajv-formats: 3.0.1(ajv@8.12.0)
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 2.3.0
|
||||
json-schema-ref-resolver: 1.0.1
|
||||
rfdc: 1.3.0
|
||||
dev: false
|
||||
|
||||
/fast-levenshtein@2.0.6:
|
||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||
dev: true
|
||||
|
||||
/fast-querystring@1.1.2:
|
||||
resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==}
|
||||
dependencies:
|
||||
fast-decode-uri-component: 1.0.1
|
||||
dev: false
|
||||
|
||||
/fast-redact@3.5.0:
|
||||
resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/fast-uri@2.3.0:
|
||||
resolution: {integrity: sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==}
|
||||
dev: false
|
||||
|
||||
/fastify-plugin@4.5.1:
|
||||
resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==}
|
||||
dev: false
|
||||
|
||||
/fastify@4.26.2:
|
||||
resolution: {integrity: sha512-90pjTuPGrfVKtdpLeLzND5nyC4woXZN5VadiNQCicj/iJU4viNHKhsAnb7jmv1vu2IzkLXyBiCzdWuzeXgQ5Ug==}
|
||||
dependencies:
|
||||
'@fastify/ajv-compiler': 3.5.0
|
||||
'@fastify/error': 3.4.1
|
||||
'@fastify/fast-json-stringify-compiler': 4.3.0
|
||||
abstract-logging: 2.0.1
|
||||
avvio: 8.3.0
|
||||
fast-content-type-parse: 1.1.0
|
||||
fast-json-stringify: 5.14.1
|
||||
find-my-way: 8.1.0
|
||||
light-my-request: 5.13.0
|
||||
pino: 8.20.0
|
||||
process-warning: 3.0.0
|
||||
proxy-addr: 2.0.7
|
||||
rfdc: 1.3.0
|
||||
secure-json-parse: 2.7.0
|
||||
semver: 7.5.4
|
||||
toad-cache: 3.7.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/fastq@1.15.0:
|
||||
resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
|
||||
dependencies:
|
||||
reusify: 1.0.4
|
||||
|
||||
/fastq@1.17.1:
|
||||
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
|
||||
dependencies:
|
||||
reusify: 1.0.4
|
||||
dev: false
|
||||
|
||||
/fecha@4.2.3:
|
||||
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
|
||||
dev: false
|
||||
@@ -3959,6 +4165,15 @@ packages:
|
||||
pkg-dir: 4.2.0
|
||||
dev: false
|
||||
|
||||
/find-my-way@8.1.0:
|
||||
resolution: {integrity: sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-querystring: 1.1.2
|
||||
safe-regex2: 2.0.0
|
||||
dev: false
|
||||
|
||||
/find-up@3.0.0:
|
||||
resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -4826,7 +5041,7 @@ packages:
|
||||
whatwg-encoding: 3.1.1
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.0.0
|
||||
ws: 8.14.2
|
||||
ws: 8.16.0
|
||||
xml-name-validator: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
@@ -4848,10 +5063,20 @@ packages:
|
||||
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||
dev: false
|
||||
|
||||
/json-schema-ref-resolver@1.0.1:
|
||||
resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==}
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
dev: false
|
||||
|
||||
/json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
dev: true
|
||||
|
||||
/json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
dev: false
|
||||
|
||||
/json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
dev: true
|
||||
@@ -5011,6 +5236,14 @@ packages:
|
||||
type-check: 0.4.0
|
||||
dev: true
|
||||
|
||||
/light-my-request@5.13.0:
|
||||
resolution: {integrity: sha512-9IjUN9ZyCS9pTG+KqTDEQo68Sui2lHsYBrfMyVUTTZ3XhH8PMZq7xO94Kr+eP9dhi/kcKsx4N41p2IXEBil1pQ==}
|
||||
dependencies:
|
||||
cookie: 0.6.0
|
||||
process-warning: 3.0.0
|
||||
set-cookie-parser: 2.6.0
|
||||
dev: false
|
||||
|
||||
/lilconfig@3.0.0:
|
||||
resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -5686,6 +5919,12 @@ packages:
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/mnemonist@0.39.6:
|
||||
resolution: {integrity: sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==}
|
||||
dependencies:
|
||||
obliterator: 2.0.4
|
||||
dev: false
|
||||
|
||||
/mock-property@1.0.3:
|
||||
resolution: {integrity: sha512-2emPTb1reeLLYwHxyVx993iYyCHEiRRO+y8NFXFPL5kl5q14sgTK76cXyEKkeKCHeRw35SfdkUJ10Q1KfHuiIQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6082,6 +6321,15 @@ packages:
|
||||
es-abstract: 1.22.3
|
||||
dev: true
|
||||
|
||||
/obliterator@2.0.4:
|
||||
resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==}
|
||||
dev: false
|
||||
|
||||
/on-exit-leak-free@2.1.2:
|
||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
dev: false
|
||||
|
||||
/on-finished@2.4.1:
|
||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -6378,6 +6626,34 @@ packages:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
/pino-abstract-transport@1.1.0:
|
||||
resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==}
|
||||
dependencies:
|
||||
readable-stream: 4.4.2
|
||||
split2: 4.2.0
|
||||
dev: false
|
||||
|
||||
/pino-std-serializers@6.2.2:
|
||||
resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==}
|
||||
dev: false
|
||||
|
||||
/pino@8.20.0:
|
||||
resolution: {integrity: sha512-uhIfMj5TVp+WynVASaVEJFTncTUe4dHBq6CWplu/vBgvGHhvBvQfxz+vcOrnnBQdORH3izaGEurLfNlq3YxdFQ==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
atomic-sleep: 1.0.0
|
||||
fast-redact: 3.5.0
|
||||
on-exit-leak-free: 2.1.2
|
||||
pino-abstract-transport: 1.1.0
|
||||
pino-std-serializers: 6.2.2
|
||||
process-warning: 3.0.0
|
||||
quick-format-unescaped: 4.0.4
|
||||
real-require: 0.2.0
|
||||
safe-stable-stringify: 2.4.3
|
||||
sonic-boom: 3.8.1
|
||||
thread-stream: 2.4.1
|
||||
dev: false
|
||||
|
||||
/pirates@4.0.6:
|
||||
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -6558,6 +6834,10 @@ packages:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
dev: false
|
||||
|
||||
/process-warning@3.0.0:
|
||||
resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==}
|
||||
dev: false
|
||||
|
||||
/process@0.11.10:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
@@ -6639,6 +6919,10 @@ packages:
|
||||
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
|
||||
dev: false
|
||||
|
||||
/quick-format-unescaped@4.0.4:
|
||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||
dev: false
|
||||
|
||||
/range-parser@1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -6889,6 +7173,11 @@ packages:
|
||||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/real-require@0.2.0:
|
||||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
||||
engines: {node: '>= 12.13.0'}
|
||||
dev: false
|
||||
|
||||
/reflect.getprototypeof@1.0.4:
|
||||
resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6957,6 +7246,11 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/require-from-string@2.0.2:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/require-main-filename@2.0.0:
|
||||
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||
dev: false
|
||||
@@ -7004,6 +7298,11 @@ packages:
|
||||
signal-exit: 3.0.7
|
||||
dev: false
|
||||
|
||||
/ret@0.2.2:
|
||||
resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/retry@0.13.1:
|
||||
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -7098,6 +7397,17 @@ packages:
|
||||
get-intrinsic: 1.2.2
|
||||
is-regex: 1.1.4
|
||||
|
||||
/safe-regex2@2.0.0:
|
||||
resolution: {integrity: sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==}
|
||||
dependencies:
|
||||
ret: 0.2.2
|
||||
dev: false
|
||||
|
||||
/safe-stable-stringify@2.4.3:
|
||||
resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==}
|
||||
engines: {node: '>=10'}
|
||||
dev: false
|
||||
|
||||
/safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
dev: false
|
||||
@@ -7115,6 +7425,10 @@ packages:
|
||||
loose-envify: 1.4.0
|
||||
dev: false
|
||||
|
||||
/secure-json-parse@2.7.0:
|
||||
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
|
||||
dev: false
|
||||
|
||||
/semver@5.7.2:
|
||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||
hasBin: true
|
||||
@@ -7168,6 +7482,10 @@ packages:
|
||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||
dev: false
|
||||
|
||||
/set-cookie-parser@2.6.0:
|
||||
resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==}
|
||||
dev: false
|
||||
|
||||
/set-function-length@1.1.1:
|
||||
resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -7276,6 +7594,12 @@ packages:
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
dev: false
|
||||
|
||||
/sonic-boom@3.8.1:
|
||||
resolution: {integrity: sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==}
|
||||
dependencies:
|
||||
atomic-sleep: 1.0.0
|
||||
dev: false
|
||||
|
||||
/source-map-js@1.0.2:
|
||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -7718,6 +8042,12 @@ packages:
|
||||
engines: {node: '>=0.2.6'}
|
||||
dev: false
|
||||
|
||||
/thread-stream@2.4.1:
|
||||
resolution: {integrity: sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==}
|
||||
dependencies:
|
||||
real-require: 0.2.0
|
||||
dev: false
|
||||
|
||||
/titleize@3.0.0:
|
||||
resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -7741,6 +8071,11 @@ packages:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
|
||||
/toad-cache@3.7.0:
|
||||
resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/toidentifier@1.0.1:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
@@ -8078,7 +8413,6 @@ packages:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
dev: true
|
||||
|
||||
/url-parse@1.5.10:
|
||||
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||
@@ -8380,8 +8714,8 @@ packages:
|
||||
/wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
/ws@8.14.2:
|
||||
resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==}
|
||||
/ws@8.16.0:
|
||||
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function SettingsUser() {
|
||||
const { data, error } = await fetchApi<Response['/api/user']>('/api/user', 'PATCH', send);
|
||||
|
||||
if (!data && error) {
|
||||
if (error.field === 'username') {
|
||||
if (error.message === 'Username already exists') {
|
||||
form.setFieldError('username', error.message);
|
||||
} else {
|
||||
notifications.show({
|
||||
|
||||
@@ -5,14 +5,7 @@ import { ApiLogoutResponse } from '@/pages/api/auth/logout';
|
||||
import { ApiAuthOauthResponse } from '@/pages/api/auth/oauth';
|
||||
import { ApiAuthRegisterResponse } from '@/pages/api/auth/register';
|
||||
import { ApiAuthWebauthnResponse } from '@/pages/api/auth/webauthn';
|
||||
import { ApiHealthcheckResponse } from '@/pages/api/healthcheck';
|
||||
import { ApiServerClearTempResponse } from '@/pages/api/server/clear_temp';
|
||||
import { ApiServerClearZerosResponse } from '@/pages/api/server/clear_zeros';
|
||||
import { ApiServerRequerySizeResponse } from '@/pages/api/server/requery_size';
|
||||
import { ApiSetupResponse } from '@/pages/api/setup';
|
||||
import { ApiStatsResponse } from '@/pages/api/stats';
|
||||
import { ApiUploadResponse } from '@/pages/api/upload';
|
||||
import { ApiUserResponse } from '@/pages/api/user';
|
||||
import { ApiUserFilesResponse } from '@/pages/api/user/files';
|
||||
import { ApiUserFilesIdResponse } from '@/pages/api/user/files/[id]';
|
||||
import { ApiUserFilesIdPasswordResponse } from '@/pages/api/user/files/[id]/password';
|
||||
@@ -22,16 +15,25 @@ import { ApiUserFoldersResponse } from '@/pages/api/user/folders';
|
||||
import { ApiUserFoldersIdResponse } from '@/pages/api/user/folders/[id]';
|
||||
import { ApiUserMfaPasskeyResponse } from '@/pages/api/user/mfa/passkey';
|
||||
import { ApiUserMfaTotpResponse } from '@/pages/api/user/mfa/totp';
|
||||
import { ApiUserRecentResponse } from '@/pages/api/user/recent';
|
||||
import { ApiUserStatsResponse } from '@/pages/api/user/stats';
|
||||
import { ApiUserTagsResponse } from '@/pages/api/user/tags';
|
||||
import { ApiUserTagsIdResponse } from '@/pages/api/user/tags/[id]';
|
||||
import { ApiUserTokenResponse } from '@/pages/api/user/token';
|
||||
import { ApiUserUrlsResponse } from '@/pages/api/user/urls';
|
||||
import { ApiUserUrlsIdResponse } from '@/pages/api/user/urls/[id]';
|
||||
import { ApiUsersResponse } from '@/pages/api/users';
|
||||
import { ApiUsersIdResponse } from '@/pages/api/users/[id]';
|
||||
import { ApiVersionResponse } from '@/pages/api/version';
|
||||
|
||||
// migrated routes
|
||||
import { ApiHealthcheckResponse } from '@/server/routes/api/healthcheck';
|
||||
import { ApiServerClearTempResponse } from '@/server/routes/api/server/clear_temp';
|
||||
import { ApiServerClearZerosResponse } from '@/server/routes/api/server/clear_zeros';
|
||||
import { ApiServerRequerySizeResponse } from '@/server/routes/api/server/requery_size';
|
||||
import { ApiSetupResponse } from '@/server/routes/api/setup';
|
||||
import { ApiStatsResponse } from '@/server/routes/api/stats';
|
||||
import { ApiUserResponse } from '@/server/routes/api/user';
|
||||
import { ApiUserRecentResponse } from '@/server/routes/api/user/recent';
|
||||
import { ApiUserStatsResponse } from '@/server/routes/api/user/stats';
|
||||
import { ApiUserTokenResponse } from '@/server/routes/api/user/token';
|
||||
import { ApiUsersResponse } from '@/server/routes/api/users';
|
||||
import { ApiUsersIdResponse } from '@/server/routes/api/users/[id]';
|
||||
import { ApiVersionResponse } from '@/server/routes/api/version';
|
||||
|
||||
export type Response = {
|
||||
'/api/auth/invites/[id]': ApiAuthInvitesIdResponse;
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { log } from '@/lib/logger';
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
|
||||
export type ApiHealthcheckResponse = {
|
||||
pass: boolean;
|
||||
};
|
||||
|
||||
export async function handler(_: NextApiReq, res: NextApiRes<ApiHealthcheckResponse>) {
|
||||
if (!config.features.healthcheck) return res.notFound();
|
||||
|
||||
const logger = log('api').c('healthcheck');
|
||||
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1;`;
|
||||
|
||||
return res.ok({ pass: true });
|
||||
} catch (e) {
|
||||
logger.error('there was an error during a healthcheck').error(e as Error);
|
||||
|
||||
return res.serverError('there was an error during a healthcheck', {
|
||||
pass: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default combine([method(['GET'])], handler);
|
||||
@@ -1,17 +0,0 @@
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { ziplineAuth } from '@/lib/middleware/ziplineAuth';
|
||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
import { clearTemp } from '@/lib/server-util/clearTemp';
|
||||
|
||||
export type ApiServerClearTempResponse = {
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export async function handler(_: NextApiReq, res: NextApiRes<ApiServerClearTempResponse>) {
|
||||
const response = await clearTemp();
|
||||
|
||||
return res.ok({ status: response });
|
||||
}
|
||||
|
||||
export default combine([method(['DELETE']), ziplineAuth({ administratorOnly: true })], handler);
|
||||
@@ -1,24 +0,0 @@
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { ziplineAuth } from '@/lib/middleware/ziplineAuth';
|
||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
import { clearZeros, clearZerosFiles } from '@/lib/server-util/clearZeros';
|
||||
|
||||
export type ApiServerClearZerosResponse = {
|
||||
status?: string;
|
||||
files?: Awaited<ReturnType<typeof clearZerosFiles>>;
|
||||
};
|
||||
|
||||
export async function handler(req: NextApiReq, res: NextApiRes<ApiServerClearZerosResponse>) {
|
||||
const filesToDelete = await clearZerosFiles();
|
||||
|
||||
if (req.method === 'GET') {
|
||||
return res.ok({ files: filesToDelete });
|
||||
}
|
||||
|
||||
const response = await clearZeros(filesToDelete);
|
||||
|
||||
return res.ok({ status: response });
|
||||
}
|
||||
|
||||
export default combine([method(['GET', 'DELETE']), ziplineAuth({ administratorOnly: true })], handler);
|
||||
@@ -1,25 +0,0 @@
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { ziplineAuth } from '@/lib/middleware/ziplineAuth';
|
||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
import { requerySize } from '@/lib/server-util/requerySize';
|
||||
|
||||
export type ApiServerRequerySizeResponse = {
|
||||
status?: string;
|
||||
};
|
||||
|
||||
type Body = {
|
||||
forceDelete?: boolean;
|
||||
forceUpdate?: boolean;
|
||||
};
|
||||
|
||||
export async function handler(req: NextApiReq<Body>, res: NextApiRes<ApiServerRequerySizeResponse>) {
|
||||
const response = await requerySize({
|
||||
forceDelete: req.body.forceDelete || false,
|
||||
forceUpdate: req.body.forceUpdate || false,
|
||||
});
|
||||
|
||||
return res.ok({ status: response });
|
||||
}
|
||||
|
||||
export default combine([method(['POST']), ziplineAuth({ administratorOnly: true })], handler);
|
||||
@@ -1,63 +0,0 @@
|
||||
import { createToken, hashPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { getZipline } from '@/lib/db/models/zipline';
|
||||
import { log } from '@/lib/logger';
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
|
||||
export type ApiSetupResponse = {
|
||||
firstSetup?: boolean;
|
||||
user?: User;
|
||||
};
|
||||
|
||||
type Body = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export async function handler(req: NextApiReq<Body>, res: NextApiRes<ApiSetupResponse>) {
|
||||
const logger = log('api').c('setup');
|
||||
const { firstSetup, id } = await getZipline();
|
||||
|
||||
if (!firstSetup) return res.forbidden();
|
||||
|
||||
logger.info('first setup running');
|
||||
|
||||
if (req.method === 'GET') {
|
||||
return res.ok({ firstSetup });
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
if (!username) return res.badRequest('Username is required');
|
||||
if (!password) return res.badRequest('Password is required');
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
password: await hashPassword(password),
|
||||
role: 'SUPERADMIN',
|
||||
token: createToken(),
|
||||
},
|
||||
select: userSelect,
|
||||
});
|
||||
|
||||
logger.info('first setup complete');
|
||||
|
||||
await prisma.zipline.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
firstSetup: false,
|
||||
},
|
||||
});
|
||||
|
||||
return res.ok({
|
||||
firstSetup,
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
export default combine([method(['GET', 'POST'])], handler);
|
||||
@@ -1,49 +0,0 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { Metric } from '@/lib/db/models/metric';
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
|
||||
export type ApiStatsResponse = Metric[];
|
||||
|
||||
type Query = {
|
||||
from?: string;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
export async function handler(req: NextApiReq<any, Query>, res: NextApiRes<ApiStatsResponse>) {
|
||||
if (!config.features.metrics) return res.forbidden();
|
||||
|
||||
const { from, to } = req.query;
|
||||
|
||||
const fromDate = from ? new Date(from) : new Date(Date.now() - 86400000);
|
||||
const toDate = to ? new Date(to) : new Date();
|
||||
|
||||
if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) return res.badRequest('invalid date');
|
||||
|
||||
const stats = await prisma.metric.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: fromDate,
|
||||
lte: toDate,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
if (!config.features.metrics.showUserSpecific) {
|
||||
for (let i = 0; i !== stats.length; ++i) {
|
||||
const stat = stats[i].data;
|
||||
|
||||
stat.filesUsers = [];
|
||||
stat.urlsUsers = [];
|
||||
}
|
||||
}
|
||||
|
||||
return res.ok(stats);
|
||||
}
|
||||
|
||||
export default combine([method(['GET'])], handler);
|
||||
@@ -1,28 +0,0 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User } from '@/lib/db/models/user';
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { ziplineAuth } from '@/lib/middleware/ziplineAuth';
|
||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
|
||||
export type ApiUserTokenResponse = {
|
||||
user?: User;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export async function handler(req: NextApiReq, res: NextApiRes<ApiUserTokenResponse>) {
|
||||
const u = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: req.user.id,
|
||||
},
|
||||
select: {
|
||||
avatar: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!u.avatar) return res.notFound();
|
||||
|
||||
return res.status(200).send(u.avatar);
|
||||
}
|
||||
|
||||
export default combine([method(['GET']), ziplineAuth()], handler);
|
||||
@@ -1,92 +0,0 @@
|
||||
import { hashPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { ziplineAuth } from '@/lib/middleware/ziplineAuth';
|
||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
|
||||
export type ApiUserResponse = {
|
||||
user?: User;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
type EditBody = {
|
||||
username?: string;
|
||||
password?: string;
|
||||
avatar?: string;
|
||||
view?: {
|
||||
content?: string;
|
||||
embed?: boolean;
|
||||
embedTitle?: string;
|
||||
embedDescription?: string;
|
||||
embedColor?: string;
|
||||
embedSiteName?: string;
|
||||
enabled?: boolean;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
showMimetype?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export async function handler(req: NextApiReq<EditBody>, res: NextApiRes<ApiUserResponse>) {
|
||||
if (req.method === 'GET') {
|
||||
return res.ok({ user: req.user, token: req.cookies.zipline_token });
|
||||
} else if (req.method === 'PATCH') {
|
||||
if (req.body.username) {
|
||||
const existing = await prisma.user.findUnique({
|
||||
where: {
|
||||
username: req.body.username,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) return res.badRequest('Username already taken', { field: 'username' });
|
||||
}
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: {
|
||||
id: req.user.id,
|
||||
},
|
||||
data: {
|
||||
...(req.body.username && { username: req.body.username }),
|
||||
...(req.body.password && { password: await hashPassword(req.body.password) }),
|
||||
...(req.body.avatar !== undefined && { avatar: req.body.avatar || null }),
|
||||
...(req.body.view && {
|
||||
view: {
|
||||
...req.user.view,
|
||||
...(req.body.view.enabled !== undefined && { enabled: req.body.view.enabled || false }),
|
||||
...(req.body.view.content !== undefined && { content: req.body.view.content || null }),
|
||||
...(req.body.view.embed !== undefined && { embed: req.body.view.embed || false }),
|
||||
...(req.body.view.embedTitle !== undefined && { embedTitle: req.body.view.embedTitle || null }),
|
||||
...(req.body.view.embedDescription !== undefined && {
|
||||
embedDescription: req.body.view.embedDescription || null,
|
||||
}),
|
||||
...(req.body.view.embedColor !== undefined && { embedColor: req.body.view.embedColor || null }),
|
||||
...(req.body.view.embedSiteName !== undefined && {
|
||||
embedSiteName: req.body.view.embedSiteName || null,
|
||||
}),
|
||||
...(req.body.view.align !== undefined && { align: req.body.view.align || 'center' }),
|
||||
...(req.body.view.showMimetype !== undefined && {
|
||||
showMimetype: req.body.view.showMimetype || false,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
select: {
|
||||
...userSelect,
|
||||
},
|
||||
});
|
||||
|
||||
return res.ok({ user, token: req.cookies.zipline_token });
|
||||
}
|
||||
}
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
responseLimit: false,
|
||||
bodyParser: {
|
||||
sizeLimit: '100gb',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default combine([method(['GET', 'PATCH']), ziplineAuth()], handler);
|
||||
@@ -1,34 +0,0 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { File, cleanFiles, fileSelect } from '@/lib/db/models/file';
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { ziplineAuth } from '@/lib/middleware/ziplineAuth';
|
||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
|
||||
export type ApiUserRecentResponse = File[];
|
||||
|
||||
type Query = {
|
||||
page?: string;
|
||||
};
|
||||
|
||||
export async function handler(req: NextApiReq<any, Query>, res: NextApiRes<ApiUserRecentResponse>) {
|
||||
const files = cleanFiles(
|
||||
await prisma.file.findMany({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
},
|
||||
select: {
|
||||
...fileSelect,
|
||||
password: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: 3,
|
||||
}),
|
||||
);
|
||||
|
||||
return res.ok(files);
|
||||
}
|
||||
|
||||
export default combine([method(['GET', 'PATCH']), ziplineAuth()], handler);
|
||||
@@ -1,92 +0,0 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { ziplineAuth } from '@/lib/middleware/ziplineAuth';
|
||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
|
||||
export type ApiUserStatsResponse = {
|
||||
filesUploaded: number;
|
||||
favoriteFiles: number;
|
||||
views: number;
|
||||
avgViews: number;
|
||||
storageUsed: number;
|
||||
avgStorageUsed: number;
|
||||
urlsCreated: number;
|
||||
urlViews: number;
|
||||
|
||||
sortTypeCount: { [type: string]: number };
|
||||
};
|
||||
|
||||
export async function handler(req: NextApiReq, res: NextApiRes<ApiUserStatsResponse>) {
|
||||
const aggFile = await prisma.file.aggregate({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
},
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
_sum: {
|
||||
views: true,
|
||||
size: true,
|
||||
},
|
||||
_avg: {
|
||||
views: true,
|
||||
size: true,
|
||||
},
|
||||
});
|
||||
|
||||
const favCount = await prisma.file.count({
|
||||
where: {
|
||||
favorite: true,
|
||||
},
|
||||
});
|
||||
|
||||
const aggUrl = await prisma.url.aggregate({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
},
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
_avg: {
|
||||
views: true,
|
||||
},
|
||||
_sum: {
|
||||
views: true,
|
||||
},
|
||||
});
|
||||
|
||||
const sortType = await prisma.file.findMany({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
},
|
||||
select: {
|
||||
type: true,
|
||||
},
|
||||
});
|
||||
|
||||
const sortTypeCount = sortType.reduce(
|
||||
(acc, cur) => {
|
||||
if (acc[cur.type]) acc[cur.type] += 1;
|
||||
else acc[cur.type] = 1;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as { [type: string]: number },
|
||||
);
|
||||
|
||||
return res.ok({
|
||||
filesUploaded: aggFile._count._all ?? 0,
|
||||
favoriteFiles: favCount ?? 0,
|
||||
views: aggFile._sum.views ?? 0,
|
||||
avgViews: aggFile._avg.views ?? 0,
|
||||
storageUsed: Number(aggFile._sum.size ?? 0),
|
||||
avgStorageUsed: Number(aggFile._avg.size ?? 0),
|
||||
urlsCreated: aggUrl._count._all ?? 0,
|
||||
urlViews: aggUrl._sum.views ?? 0,
|
||||
|
||||
sortTypeCount,
|
||||
});
|
||||
}
|
||||
|
||||
export default combine([method(['GET']), ziplineAuth()], handler);
|
||||
@@ -1,57 +0,0 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { createToken, encryptToken } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { loginToken } from '@/lib/login';
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { ziplineAuth } from '@/lib/middleware/ziplineAuth';
|
||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
|
||||
export type ApiUserTokenResponse = {
|
||||
user?: User;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export async function handler(req: NextApiReq, res: NextApiRes<ApiUserTokenResponse>) {
|
||||
if (req.method === 'GET') {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: req.user.id,
|
||||
},
|
||||
select: {
|
||||
token: true,
|
||||
},
|
||||
});
|
||||
|
||||
const token = encryptToken(user!.token, config.core.secret);
|
||||
|
||||
return res.ok({
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: {
|
||||
id: req.user.id,
|
||||
},
|
||||
data: {
|
||||
token: createToken(),
|
||||
},
|
||||
select: {
|
||||
...userSelect,
|
||||
token: true,
|
||||
},
|
||||
});
|
||||
|
||||
const token = loginToken(res, user);
|
||||
|
||||
delete (user as any).token;
|
||||
|
||||
return res.ok({
|
||||
user,
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
export default combine([method(['GET', 'PATCH']), ziplineAuth()], handler);
|
||||
@@ -1,210 +0,0 @@
|
||||
import { bytes } from '@/lib/bytes';
|
||||
import { hashPassword } from '@/lib/crypto';
|
||||
import { datasource } from '@/lib/datasource';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { log } from '@/lib/logger';
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { ziplineAuth } from '@/lib/middleware/ziplineAuth';
|
||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
import { canInteract } from '@/lib/role';
|
||||
import { UserFilesQuota } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
export type ApiUsersIdResponse = User;
|
||||
|
||||
type Body = {
|
||||
username?: string;
|
||||
password?: string;
|
||||
avatar?: string;
|
||||
role?: 'USER' | 'ADMIN' | 'SUPERADMIN';
|
||||
quota?: {
|
||||
filesType?: UserFilesQuota & 'NONE';
|
||||
maxFiles?: number;
|
||||
maxBytes?: string;
|
||||
|
||||
maxUrls?: number;
|
||||
};
|
||||
|
||||
delete?: boolean;
|
||||
};
|
||||
|
||||
type Query = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const logger = log('api').c('users').c('[id]');
|
||||
const zNumber = z.number();
|
||||
|
||||
export async function handler(req: NextApiReq<Body, Query>, res: NextApiRes<ApiUsersIdResponse>) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: req.query.id,
|
||||
},
|
||||
select: userSelect,
|
||||
});
|
||||
if (!user) return res.notFound('User not found');
|
||||
|
||||
if (req.method === 'PATCH') {
|
||||
const { username, password, avatar, role, quota } = req.body;
|
||||
|
||||
if (role && !z.enum(['USER', 'ADMIN']).safeParse(role).success)
|
||||
return res.badRequest('Invalid role (USER, ADMIN)');
|
||||
|
||||
if (role && !canInteract(req.user.role, role)) return res.forbidden('You cannot create this role');
|
||||
|
||||
let finalQuota:
|
||||
| {
|
||||
filesQuota?: UserFilesQuota;
|
||||
maxFiles?: number | null;
|
||||
maxBytes?: string | null;
|
||||
maxUrls?: number | null;
|
||||
}
|
||||
| undefined = undefined;
|
||||
if (quota) {
|
||||
if (quota.filesType && !z.enum(['BY_BYTES', 'BY_FILES', 'NONE']).safeParse(quota.filesType).success)
|
||||
return res.badRequest('Invalid filesType (BY_BYTES, BY_FILES, NONE)');
|
||||
|
||||
if (quota.maxFiles && !zNumber.safeParse(quota.maxFiles).success)
|
||||
return res.badRequest('Invalid maxFiles');
|
||||
if (quota.maxUrls && !zNumber.safeParse(quota.maxUrls).success)
|
||||
return res.badRequest('Invalid maxUrls');
|
||||
|
||||
if (quota.filesType === 'BY_BYTES' && quota.maxBytes === undefined)
|
||||
return res.badRequest('maxBytes is required');
|
||||
if (quota.filesType === 'BY_FILES' && quota.maxFiles === undefined)
|
||||
return res.badRequest('maxFiles is required');
|
||||
|
||||
finalQuota = {
|
||||
...(quota.filesType === 'BY_BYTES' && {
|
||||
filesQuota: 'BY_BYTES',
|
||||
maxBytes: bytes(quota.maxBytes || '0') > 0 ? quota.maxBytes : null,
|
||||
maxFiles: null,
|
||||
}),
|
||||
...(quota.filesType === 'BY_FILES' && {
|
||||
filesQuota: 'BY_FILES',
|
||||
maxFiles: quota.maxFiles,
|
||||
maxBytes: null,
|
||||
}),
|
||||
...(quota.filesType === 'NONE' && {
|
||||
filesQuota: 'BY_BYTES',
|
||||
maxFiles: null,
|
||||
maxBytes: null,
|
||||
}),
|
||||
maxUrls: (quota.maxUrls || 0) > 0 ? quota.maxUrls : null,
|
||||
};
|
||||
}
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
...(username && { username }),
|
||||
...(password && { password: await hashPassword(password) }),
|
||||
...(role !== undefined && { role: 'USER' }),
|
||||
...(avatar && { avatar }),
|
||||
...(finalQuota && {
|
||||
quota: {
|
||||
upsert: {
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
create: {
|
||||
filesQuota: finalQuota.filesQuota || 'BY_BYTES',
|
||||
maxFiles: finalQuota.maxFiles ?? null,
|
||||
maxBytes: finalQuota.maxBytes ?? null,
|
||||
maxUrls: finalQuota.maxUrls ?? null,
|
||||
},
|
||||
update: finalQuota,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
select: {
|
||||
...userSelect,
|
||||
totpSecret: false,
|
||||
passkeys: false,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`${req.user.username} updated another user`, {
|
||||
username: updatedUser.username,
|
||||
role: updatedUser.role,
|
||||
});
|
||||
|
||||
return res.ok(updatedUser);
|
||||
} else if (req.method === 'DELETE') {
|
||||
if (user.id === req.user.id) return res.forbidden('You cannot delete yourself');
|
||||
if (!canInteract(req.user.role, user.role)) return res.forbidden('You cannot delete this user');
|
||||
|
||||
if (req.body.delete) {
|
||||
const files = await prisma.file.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [{ count: filesDeleted }, { count: urlsDeleted }] = await prisma.$transaction([
|
||||
prisma.file.deleteMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
}),
|
||||
prisma.url.deleteMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
logger.debug(`preparing to delete ${files.length} files from datasource`, {
|
||||
username: user.username,
|
||||
});
|
||||
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
await datasource.delete(files[i].name);
|
||||
}
|
||||
|
||||
logger.info(`${req.user.username} deleted another user's files & urls`, {
|
||||
username: user.username,
|
||||
deletedFiles: filesDeleted,
|
||||
deletedUrls: urlsDeleted,
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.oAuthProvider.deleteMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
const deletedUser = await prisma.user.delete({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
select: {
|
||||
...userSelect,
|
||||
totpSecret: false,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`${req.user.username} deleted another user`, {
|
||||
username: deletedUser.username,
|
||||
role: deletedUser.role,
|
||||
});
|
||||
|
||||
return res.ok(deletedUser);
|
||||
}
|
||||
|
||||
return res.ok(user);
|
||||
}
|
||||
|
||||
export default combine(
|
||||
[method(['GET', 'PATCH', 'DELETE']), ziplineAuth({ administratorOnly: true })],
|
||||
handler,
|
||||
);
|
||||
@@ -1,90 +0,0 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { createToken, hashPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { log } from '@/lib/logger';
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { ziplineAuth } from '@/lib/middleware/ziplineAuth';
|
||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
import { canInteract } from '@/lib/role';
|
||||
import { Role } from '@prisma/client';
|
||||
import { readFile } from 'fs/promises';
|
||||
import z from 'zod';
|
||||
|
||||
export type ApiUsersResponse = User[] | User;
|
||||
|
||||
type Query = {
|
||||
noincl?: 'true' | 'false';
|
||||
};
|
||||
|
||||
type Body = {
|
||||
username?: string;
|
||||
password?: string;
|
||||
avatar?: string;
|
||||
role?: Role;
|
||||
};
|
||||
|
||||
const logger = log('api').c('users');
|
||||
|
||||
export async function handler(req: NextApiReq<Body, Query>, res: NextApiRes<ApiUsersResponse>) {
|
||||
if (req.method === 'POST') {
|
||||
const { username, password, avatar, role } = req.body;
|
||||
|
||||
if (!username) return res.badRequest('Username is required');
|
||||
if (!password) return res.badRequest('Password is required');
|
||||
|
||||
let avatar64 = null;
|
||||
|
||||
try {
|
||||
if (config.website.defaultAvatar) {
|
||||
avatar64 = (await readFile(config.website.defaultAvatar)).toString('base64');
|
||||
} else if (avatar) {
|
||||
avatar64 = avatar;
|
||||
}
|
||||
} catch {
|
||||
logger.debug('failed to read default avatar', { path: config.website.defaultAvatar });
|
||||
}
|
||||
|
||||
if (role && !z.enum(['USER', 'ADMIN']).safeParse(role).success)
|
||||
return res.badRequest('Invalid role (USER, ADMIN)');
|
||||
|
||||
if (role && !canInteract(req.user.role, role)) return res.forbidden('You cannot create this role');
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
password: await hashPassword(password),
|
||||
role: role ?? 'USER',
|
||||
avatar: avatar64 ?? null,
|
||||
token: createToken(),
|
||||
},
|
||||
select: {
|
||||
...userSelect,
|
||||
totpSecret: false,
|
||||
passkeys: false,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`${req.user.username} created a new user`, {
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
});
|
||||
|
||||
return res.ok(user);
|
||||
}
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
...userSelect,
|
||||
avatar: true,
|
||||
},
|
||||
where: {
|
||||
...(req.query.noincl === 'true' && { id: { not: req.user.id } }),
|
||||
},
|
||||
});
|
||||
|
||||
return res.ok(users);
|
||||
}
|
||||
|
||||
export default combine([method(['GET', 'POST']), ziplineAuth({ administratorOnly: true })], handler);
|
||||
@@ -1,15 +0,0 @@
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { ziplineAuth } from '@/lib/middleware/ziplineAuth';
|
||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
import packageJson from '../../../package.json';
|
||||
|
||||
export type ApiVersionResponse = {
|
||||
version: string;
|
||||
};
|
||||
|
||||
export async function handler(_: NextApiReq, res: NextApiRes<ApiVersionResponse>) {
|
||||
return res.ok({ version: packageJson.version });
|
||||
}
|
||||
|
||||
export default combine([method(['GET']), ziplineAuth({ administratorOnly: true })], handler);
|
||||
@@ -1,30 +1,28 @@
|
||||
import { readEnv } from '@/lib/config/read';
|
||||
import { validateEnv } from '@/lib/config/validate';
|
||||
import { verifyPassword } from '@/lib/crypto';
|
||||
import { datasource } from '@/lib/datasource';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { runMigrations } from '@/lib/db/migration';
|
||||
import { log } from '@/lib/logger';
|
||||
import express from 'express';
|
||||
import { mkdir } from 'fs/promises';
|
||||
import next from 'next';
|
||||
import { parse } from 'url';
|
||||
|
||||
import { version } from '../../package.json';
|
||||
import { filesRoute } from './routes/files';
|
||||
import { urlsRoute } from './routes/urls';
|
||||
import { Scheduler } from '@/lib/scheduler';
|
||||
import deleteFiles from '@/lib/scheduler/jobs/deleteFiles';
|
||||
import clearInvites from '@/lib/scheduler/jobs/clearInvites';
|
||||
import deleteFiles from '@/lib/scheduler/jobs/deleteFiles';
|
||||
import maxViews from '@/lib/scheduler/jobs/maxViews';
|
||||
import thumbnails from '@/lib/scheduler/jobs/thumbnails';
|
||||
import metrics from '@/lib/scheduler/jobs/metrics';
|
||||
import { parseRange } from '@/lib/api/range';
|
||||
import thumbnails from '@/lib/scheduler/jobs/thumbnails';
|
||||
import { fastifyCookie } from '@fastify/cookie';
|
||||
import { fastifyCors } from '@fastify/cors';
|
||||
import { fastifySensible } from '@fastify/sensible';
|
||||
import fastify from 'fastify';
|
||||
import { mkdir } from 'fs/promises';
|
||||
import { parse } from 'url';
|
||||
import { version } from '../../package.json';
|
||||
import next, { ALL_METHODS } from './plugins/next';
|
||||
import loadRoutes from './routes';
|
||||
import { filesRoute } from './routes/files.dy';
|
||||
import { urlsRoute } from './routes/urls.dy';
|
||||
|
||||
const MODE = process.env.NODE_ENV || 'production';
|
||||
|
||||
const logger = log('server');
|
||||
const scheduler = new Scheduler();
|
||||
|
||||
declare global {
|
||||
interface BigInt {
|
||||
@@ -38,9 +36,6 @@ BigInt.prototype.toJSON = function () {
|
||||
|
||||
async function main() {
|
||||
logger.info('starting zipline', { mode: MODE, version: version });
|
||||
|
||||
const server = express();
|
||||
|
||||
logger.info('reading environment for configuration');
|
||||
const config = validateEnv(readEnv());
|
||||
|
||||
@@ -49,34 +44,30 @@ async function main() {
|
||||
}
|
||||
|
||||
await mkdir(config.core.tempDirectory, { recursive: true });
|
||||
|
||||
process.env.DATABASE_URL = config.core.databaseUrl;
|
||||
|
||||
await runMigrations();
|
||||
|
||||
server.disable('x-powered-by');
|
||||
server.use(express.static('public', { maxAge: '1h' }));
|
||||
const server = fastify({ ignoreTrailingSlash: true });
|
||||
|
||||
const app = next({
|
||||
dev: MODE === 'development',
|
||||
quiet: MODE === 'production',
|
||||
hostname: config.core.hostname,
|
||||
port: config.core.port,
|
||||
dir: '.',
|
||||
await server.register(fastifyCookie, {
|
||||
secret: config.core.secret,
|
||||
hook: 'onRequest',
|
||||
});
|
||||
const handle = app.getRequestHandler();
|
||||
|
||||
await app.prepare();
|
||||
await server.register(fastifyCors);
|
||||
|
||||
await server.register(fastifySensible);
|
||||
|
||||
if (config.files.route === '/' && config.urls.route === '/') {
|
||||
logger.debug('files & urls route are both /, using catch-all route');
|
||||
logger.debug('files & urls route = /, using catch-all route');
|
||||
|
||||
server.get('/:id', async (req, res) => {
|
||||
server.get<{ Params: { id: string } }>('/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const parsedUrl = parse(req.url!, true);
|
||||
|
||||
if (id === '') return app.render404(req, res, parsedUrl);
|
||||
else if (id === 'dashboard') return app.render(req, res, '/dashboard');
|
||||
if (id === '') return server.nextServer.render404(req.raw, res.raw, parsedUrl);
|
||||
else if (id === 'dashboard') return server.nextServer.render(req.raw, res.raw, '/dashboard');
|
||||
|
||||
const url = await prisma.url.findFirst({
|
||||
where: {
|
||||
@@ -84,122 +75,78 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
if (url) return urlsRoute.bind(server)(app, req, res);
|
||||
else return filesRoute.bind(server)(app, req, res);
|
||||
if (url) return urlsRoute(req as any, res);
|
||||
else return filesRoute(req as any, res);
|
||||
});
|
||||
} else {
|
||||
server.get(config.files.route === '/' ? '/:id' : `${config.files.route}/:id`, async (req, res) => {
|
||||
filesRoute.bind(server)(app, req, res);
|
||||
});
|
||||
|
||||
server.get(config.urls.route === '/' ? '/:id' : `${config.urls.route}/:id`, async (req, res) => {
|
||||
urlsRoute.bind(server)(app, req, res);
|
||||
});
|
||||
server.get(config.files.route === '/' ? '/:id' : `${config.files.route}/:id`, filesRoute);
|
||||
server.get(config.urls.route === '/' ? '/:id' : `${config.urls.route}/:id`, urlsRoute);
|
||||
}
|
||||
|
||||
server.get('/raw/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { pw } = req.query;
|
||||
await server.register(next, {
|
||||
dev: MODE === 'development',
|
||||
quiet: MODE === 'production',
|
||||
hostname: config.core.hostname,
|
||||
port: config.core.port,
|
||||
dir: '.',
|
||||
});
|
||||
|
||||
const parsedUrl = parse(req.url!, true);
|
||||
const routes = await loadRoutes();
|
||||
const routesOptions = Object.values(routes);
|
||||
Promise.all(routesOptions.map((route) => server.register(route)));
|
||||
|
||||
const file = await prisma.file.findFirst({
|
||||
where: {
|
||||
name: id,
|
||||
},
|
||||
});
|
||||
server.next('/*', ALL_METHODS);
|
||||
server.get('/', (_, res) => res.redirect('/dashboard'));
|
||||
|
||||
if (file?.password) {
|
||||
if (!pw) return res.status(403).json({ code: 403, message: 'Password protected.' });
|
||||
const verified = await verifyPassword(pw as string, file.password!);
|
||||
|
||||
if (!verified) return res.status(403).json({ code: 403, message: 'Incorrect password.' });
|
||||
}
|
||||
|
||||
const size = file?.size || (await datasource.size(file?.name ?? id));
|
||||
|
||||
if (req.headers.range) {
|
||||
const [start, end] = parseRange(req.headers.range, size);
|
||||
if (start >= size || end >= size) {
|
||||
res.writeHead(416, {
|
||||
'Content-Length': size,
|
||||
'Content-Type': file?.type || 'application/octet-stream',
|
||||
...(file?.originalName && {
|
||||
'Content-Disposition': `${req.query.download ? 'attachment; ' : ''}filename="${
|
||||
file.originalName
|
||||
}"`,
|
||||
}),
|
||||
});
|
||||
|
||||
const buf = await datasource.get(file?.name ?? id);
|
||||
if (!buf) return app.render404(req, res, parsedUrl);
|
||||
|
||||
return buf.pipe(res);
|
||||
}
|
||||
|
||||
res.writeHead(206, {
|
||||
'Content-Range': `bytes ${start}-${end}/${size}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': end - start + 1,
|
||||
'Content-Type': file?.type || 'application/octet-stream',
|
||||
...(file?.originalName && {
|
||||
'Content-Disposition': `${req.query.download ? 'attachment; ' : ''}filename="${file.originalName}"`,
|
||||
}),
|
||||
// TODO: no longer need this when all the api routes are handled by fastify :)
|
||||
const routeKeys = Object.keys(routes); // holds "currently migrated routes" so we can parse json through fastify
|
||||
server.addContentTypeParser('application/json', (req, body, done) => {
|
||||
if (routeKeys.includes(req.routeOptions.config.url)) {
|
||||
let bodyString = '';
|
||||
body.on('data', (chunk) => {
|
||||
bodyString += chunk;
|
||||
});
|
||||
|
||||
const buf = await datasource.range(file?.name ?? id, start || 0, end);
|
||||
if (!buf) return app.render404(req, res, parsedUrl);
|
||||
body.on('end', () => {
|
||||
server.getDefaultJsonParser('error', 'ignore')(req, bodyString, done);
|
||||
});
|
||||
} else done(null, body);
|
||||
});
|
||||
|
||||
return buf.pipe(res);
|
||||
// TODO: no longer need this when /api/upload is handled by fastify
|
||||
server.addContentTypeParser('multipart/form-data', (_, body, done) => {
|
||||
done(null, body);
|
||||
});
|
||||
|
||||
await server.listen({
|
||||
port: config.core.port,
|
||||
host: config.core.hostname,
|
||||
});
|
||||
|
||||
logger.info('server started', { hostname: config.core.hostname, port: config.core.port });
|
||||
|
||||
const scheduler = new Scheduler();
|
||||
scheduler.interval('deletefiles', config.scheduler.deleteInterval, deleteFiles(prisma));
|
||||
scheduler.interval('maxviews', config.scheduler.maxViewsInterval, maxViews(prisma));
|
||||
|
||||
if (config.features.metrics)
|
||||
scheduler.interval('metrics', config.scheduler.metricsInterval, metrics(prisma));
|
||||
|
||||
if (config.features.thumbnails.enabled) {
|
||||
scheduler.interval('thumbnails', config.scheduler.thumbnailsInterval, thumbnails(prisma));
|
||||
|
||||
for (let i = 0; i !== config.features.thumbnails.num_threads; ++i) {
|
||||
scheduler.worker(`thumbnail-${i}`, './build/offload/thumbnails.js', {
|
||||
id: `thumbnail-${i}`,
|
||||
enabled: config.features.thumbnails.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Length': size,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Type': file?.type || 'application/octet-stream',
|
||||
...(file?.originalName && {
|
||||
'Content-Disposition': `${req.query.download ? 'attachment; ' : ''}filename="${file.originalName}"`,
|
||||
}),
|
||||
});
|
||||
scheduler.interval('clearinvites', config.scheduler.clearInvitesInterval, clearInvites(prisma));
|
||||
}
|
||||
|
||||
const buf = await datasource.get(file?.name ?? id);
|
||||
if (!buf) return app.render404(req, res, parsedUrl);
|
||||
|
||||
return buf.pipe(res);
|
||||
});
|
||||
|
||||
server.all('*', (req, res) => {
|
||||
const parsedUrl = parse(req.url!, true);
|
||||
return handle(req, res, parsedUrl);
|
||||
});
|
||||
|
||||
server.listen(config.core.port, config.core.hostname, () => {
|
||||
logger.info('server listening', {
|
||||
hostname: config.core.hostname,
|
||||
port: config.core.port,
|
||||
});
|
||||
|
||||
scheduler.interval('deletefiles', config.scheduler.deleteInterval, deleteFiles(prisma));
|
||||
scheduler.interval('maxviews', config.scheduler.maxViewsInterval, maxViews(prisma));
|
||||
|
||||
if (config.features.metrics)
|
||||
scheduler.interval('metrics', config.scheduler.metricsInterval, metrics(prisma));
|
||||
|
||||
if (config.features.thumbnails.enabled) {
|
||||
scheduler.interval('thumbnails', config.scheduler.thumbnailsInterval, thumbnails(prisma));
|
||||
|
||||
for (let i = 0; i !== config.features.thumbnails.num_threads; ++i) {
|
||||
scheduler.worker(`thumbnail-${i}`, './build/offload/thumbnails.js', {
|
||||
id: `thumbnail-${i}`,
|
||||
enabled: config.features.thumbnails.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
scheduler.interval('clearinvites', config.scheduler.clearInvitesInterval, clearInvites(prisma));
|
||||
}
|
||||
|
||||
scheduler.start();
|
||||
});
|
||||
logger.info('starting scheduler');
|
||||
scheduler.start();
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
21
src/server/loginToken.ts
Normal file
21
src/server/loginToken.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { serializeCookie } from '@/lib/cookie';
|
||||
import { encryptToken } from '@/lib/crypto';
|
||||
import { User } from '@/lib/db/models/user';
|
||||
import { FastifyReply } from 'fastify';
|
||||
|
||||
export function loginToken(res: FastifyReply, user: User) {
|
||||
const token = encryptToken(user.token!, config.core.secret);
|
||||
|
||||
const cookie = serializeCookie('zipline_token', token, {
|
||||
// week
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
expires: new Date(Date.now() + 60 * 60 * 24 * 7 * 1000),
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
});
|
||||
|
||||
res.header('Set-Cookie', cookie);
|
||||
|
||||
return token;
|
||||
}
|
||||
8
src/server/middleware/administrator.ts
Normal file
8
src/server/middleware/administrator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { isAdministrator } from '@/lib/role';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
export async function administratorMiddleware(req: FastifyRequest, res: FastifyReply) {
|
||||
if (!req.user) return res.forbidden('not logged in');
|
||||
|
||||
if (!isAdministrator(req.user.role)) return res.forbidden();
|
||||
}
|
||||
62
src/server/middleware/user.ts
Normal file
62
src/server/middleware/user.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { decryptToken } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { FastifyRequest } from 'fastify/types/request';
|
||||
|
||||
declare module 'fastify' {
|
||||
export interface FastifyRequest {
|
||||
user: User;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseUserToken(encryptedToken: string | undefined | null): string;
|
||||
export function parseUserToken(encryptedToken: string | undefined | null, noThrow: true): string | null;
|
||||
export function parseUserToken(
|
||||
encryptedToken: string | undefined | null,
|
||||
noThrow: boolean = false,
|
||||
): string | null {
|
||||
if (!encryptedToken) {
|
||||
if (noThrow) return null;
|
||||
throw { error: 'no token' };
|
||||
}
|
||||
|
||||
const decryptedToken = decryptToken(encryptedToken, config.core.secret);
|
||||
if (!decryptedToken) {
|
||||
if (noThrow) return null;
|
||||
throw { error: 'could not decrypt token' };
|
||||
}
|
||||
|
||||
const [date, token] = decryptedToken;
|
||||
if (isNaN(new Date(date).getTime())) {
|
||||
if (noThrow) return null;
|
||||
throw { error: 'invalid token' };
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function userMiddleware(req: FastifyRequest, res: FastifyReply) {
|
||||
let rawToken: string | undefined;
|
||||
|
||||
if (req.cookies.zipline_token) rawToken = req.cookies.zipline_token;
|
||||
else if (req.headers.authorization) rawToken = req.headers.authorization;
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line no-var
|
||||
var token = parseUserToken(rawToken);
|
||||
} catch (e) {
|
||||
return res.unauthorized((e as { error: string }).error);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
select: userSelect,
|
||||
});
|
||||
if (!user) return res.unauthorized('invalid token');
|
||||
|
||||
req.user = user;
|
||||
}
|
||||
60
src/server/plugins/next.ts
Normal file
60
src/server/plugins/next.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { FastifyInstance, FastifyReply, FastifyRequest, HTTPMethods } from 'fastify';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import next from 'next';
|
||||
import { NextServerOptions, RequestHandler } from 'next/dist/server/next';
|
||||
|
||||
export const ALL_METHODS: HTTPMethods[] = [
|
||||
'DELETE',
|
||||
'GET',
|
||||
'HEAD',
|
||||
'PATCH',
|
||||
'POST',
|
||||
'PUT',
|
||||
// 'OPTIONS',
|
||||
'COPY',
|
||||
'MOVE',
|
||||
'TRACE',
|
||||
];
|
||||
|
||||
async function nextPlugin(fastify: FastifyInstance, options: NextServerOptions) {
|
||||
const nextServer = next(options);
|
||||
const handle = nextServer.getRequestHandler();
|
||||
|
||||
fastify
|
||||
.decorate('nextServer', nextServer)
|
||||
.decorate('nextHandle', handle)
|
||||
.decorate('next', route.bind(fastify));
|
||||
|
||||
return nextServer.prepare();
|
||||
|
||||
function route(this: FastifyInstance, path: string, method: HTTPMethods | HTTPMethods[] = 'GET') {
|
||||
this.route({
|
||||
method,
|
||||
url: path,
|
||||
handler,
|
||||
});
|
||||
|
||||
async function handler(req: FastifyRequest, reply: FastifyReply) {
|
||||
for (const [key, value] of Object.entries(reply.getHeaders())) {
|
||||
if (value !== undefined) reply.raw.setHeader(key, value);
|
||||
}
|
||||
|
||||
await handle(req.raw, reply.raw);
|
||||
|
||||
reply.hijack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default fastifyPlugin(nextPlugin, {
|
||||
name: 'next',
|
||||
fastify: '4.x',
|
||||
});
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
nextServer: ReturnType<typeof next>;
|
||||
next: (path: string, method?: HTTPMethods | HTTPMethods[]) => void;
|
||||
nextHandle: RequestHandler;
|
||||
}
|
||||
}
|
||||
30
src/server/routes/api/healthcheck.ts
Normal file
30
src/server/routes/api/healthcheck.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { log } from '@/lib/logger';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiHealthcheckResponse = {
|
||||
pass: boolean;
|
||||
};
|
||||
|
||||
const logger = log('api').c('healthcheck');
|
||||
|
||||
export const PATH = '/api/healthcheck';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get(PATH, async (_, res) => {
|
||||
if (!config.features.healthcheck) return res.notFound();
|
||||
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1;`;
|
||||
return res.send({ pass: true });
|
||||
} catch (e) {
|
||||
logger.error('there was an error during a healthcheck').error(e as Error);
|
||||
return res.internalServerError('there was an error during a healthcheck');
|
||||
}
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
28
src/server/routes/api/server/clear_temp.ts
Normal file
28
src/server/routes/api/server/clear_temp.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { clearTemp } from '@/lib/server-util/clearTemp';
|
||||
import { administratorMiddleware } from '@/server/middleware/administrator';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiServerClearTempResponse = {
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export const PATH = '/api/server/clear_temp';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.delete(
|
||||
PATH,
|
||||
{
|
||||
preHandler: [userMiddleware, administratorMiddleware],
|
||||
},
|
||||
async (_, res) => {
|
||||
const status = await clearTemp();
|
||||
|
||||
return res.send({ status });
|
||||
},
|
||||
);
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
42
src/server/routes/api/server/clear_zeros.ts
Normal file
42
src/server/routes/api/server/clear_zeros.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { clearZeros, clearZerosFiles } from '@/lib/server-util/clearZeros';
|
||||
import { administratorMiddleware } from '@/server/middleware/administrator';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiServerClearZerosResponse = {
|
||||
status?: string;
|
||||
files?: Awaited<ReturnType<typeof clearZerosFiles>>;
|
||||
};
|
||||
|
||||
export const PATH = '/api/server/clear_zeros';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get(
|
||||
PATH,
|
||||
{
|
||||
preHandler: [userMiddleware, administratorMiddleware],
|
||||
},
|
||||
async (_, res) => {
|
||||
const files = await clearZerosFiles();
|
||||
|
||||
return res.send({ files });
|
||||
},
|
||||
);
|
||||
|
||||
server.delete(
|
||||
PATH,
|
||||
{
|
||||
preHandler: [userMiddleware, administratorMiddleware],
|
||||
},
|
||||
async (_, res) => {
|
||||
const files = await clearZerosFiles();
|
||||
const status = await clearZeros(files);
|
||||
|
||||
return res.send({ status });
|
||||
},
|
||||
);
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
36
src/server/routes/api/server/requery_size.ts
Normal file
36
src/server/routes/api/server/requery_size.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { requerySize } from '@/lib/server-util/requerySize';
|
||||
import { administratorMiddleware } from '@/server/middleware/administrator';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiServerRequerySizeResponse = {
|
||||
status?: string;
|
||||
};
|
||||
|
||||
type Body = {
|
||||
forceDelete?: boolean;
|
||||
forceUpdate?: boolean;
|
||||
};
|
||||
|
||||
export const PATH = '/api/server/requery_size';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.post<{ Body: Body }>(
|
||||
PATH,
|
||||
{
|
||||
preHandler: [userMiddleware, administratorMiddleware],
|
||||
},
|
||||
async (req, res) => {
|
||||
const status = await requerySize({
|
||||
forceDelete: req.body.forceDelete || false,
|
||||
forceUpdate: req.body.forceUpdate || false,
|
||||
});
|
||||
|
||||
return res.send({ status });
|
||||
},
|
||||
);
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
71
src/server/routes/api/setup.ts
Normal file
71
src/server/routes/api/setup.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { createToken, hashPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { getZipline } from '@/lib/db/models/zipline';
|
||||
import { log } from '@/lib/logger';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiSetupResponse = {
|
||||
firstSetup?: boolean;
|
||||
user?: User;
|
||||
};
|
||||
|
||||
type Body = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
const logger = log('api').c('setup');
|
||||
|
||||
export const PATH = '/api/setup';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get(PATH, async (_, res) => {
|
||||
const { firstSetup } = await getZipline();
|
||||
if (!firstSetup) return res.forbidden();
|
||||
|
||||
return res.send({ firstSetup });
|
||||
});
|
||||
|
||||
server.post<{ Body: Body }>(PATH, async (req, res) => {
|
||||
const { firstSetup, id } = await getZipline();
|
||||
|
||||
if (!firstSetup) return res.forbidden();
|
||||
|
||||
logger.info('first setup running');
|
||||
|
||||
const { username, password } = req.body;
|
||||
if (!username) return res.badRequest('Username is required');
|
||||
if (!password) return res.badRequest('Password is required');
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
password: await hashPassword(password),
|
||||
role: 'SUPERADMIN',
|
||||
token: createToken(),
|
||||
},
|
||||
select: userSelect,
|
||||
});
|
||||
|
||||
logger.info('first setup complete');
|
||||
|
||||
await prisma.zipline.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
firstSetup: false,
|
||||
},
|
||||
});
|
||||
|
||||
return res.send({
|
||||
firstSetup,
|
||||
user,
|
||||
});
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
54
src/server/routes/api/stats.ts
Normal file
54
src/server/routes/api/stats.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { Metric } from '@/lib/db/models/metric';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiStatsResponse = Metric[];
|
||||
|
||||
type Query = {
|
||||
from?: string;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
export const PATH = '/api/stats';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get<{ Querystring: Query }>(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
|
||||
if (!config.features.metrics) return res.forbidden('metrics are disabled');
|
||||
|
||||
const { from, to } = req.query;
|
||||
|
||||
const fromDate = from ? new Date(from) : new Date(Date.now() - 86400000);
|
||||
const toDate = to ? new Date(to) : new Date();
|
||||
|
||||
if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) return res.badRequest('invalid date(s)');
|
||||
|
||||
const stats = await prisma.metric.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: fromDate,
|
||||
lte: toDate,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
if (!config.features.metrics.showUserSpecific) {
|
||||
for (let i = 0; i !== stats.length; ++i) {
|
||||
const stat = stats[i].data;
|
||||
|
||||
stat.filesUsers = [];
|
||||
stat.urlsUsers = [];
|
||||
}
|
||||
}
|
||||
|
||||
return res.send(stats);
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
32
src/server/routes/api/user/avatar.ts
Normal file
32
src/server/routes/api/user/avatar.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User } from '@/lib/db/models/user';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiUserTokenResponse = {
|
||||
user?: User;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export const PATH = '/api/user/avatar';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
|
||||
const u = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: req.user.id,
|
||||
},
|
||||
select: {
|
||||
avatar: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!u.avatar) return res.notFound();
|
||||
|
||||
return res.send(u.avatar);
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
87
src/server/routes/api/user/index.ts
Normal file
87
src/server/routes/api/user/index.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { hashPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiUserResponse = {
|
||||
user?: User;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
type Body = {
|
||||
username?: string;
|
||||
password?: string;
|
||||
avatar?: string;
|
||||
view?: {
|
||||
content?: string;
|
||||
embed?: boolean;
|
||||
embedTitle?: string;
|
||||
embedDescription?: string;
|
||||
embedColor?: string;
|
||||
embedSiteName?: string;
|
||||
enabled?: boolean;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
showMimetype?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export const PATH = '/api/user';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
|
||||
return res.send({ user: req.user, token: req.cookies.zipline_token });
|
||||
});
|
||||
|
||||
server.patch<{ Body: Body }>(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
|
||||
if (req.body.username) {
|
||||
const existing = await prisma.user.findUnique({
|
||||
where: {
|
||||
username: req.body.username,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) return res.badRequest('Username already exists');
|
||||
}
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: {
|
||||
id: req.user.id,
|
||||
},
|
||||
data: {
|
||||
...(req.body.username && { username: req.body.username }),
|
||||
...(req.body.password && { password: await hashPassword(req.body.password) }),
|
||||
...(req.body.avatar !== undefined && { avatar: req.body.avatar || null }),
|
||||
...(req.body.view && {
|
||||
view: {
|
||||
...req.user.view,
|
||||
...(req.body.view.enabled !== undefined && { enabled: req.body.view.enabled || false }),
|
||||
...(req.body.view.content !== undefined && { content: req.body.view.content || null }),
|
||||
...(req.body.view.embed !== undefined && { embed: req.body.view.embed || false }),
|
||||
...(req.body.view.embedTitle !== undefined && { embedTitle: req.body.view.embedTitle || null }),
|
||||
...(req.body.view.embedDescription !== undefined && {
|
||||
embedDescription: req.body.view.embedDescription || null,
|
||||
}),
|
||||
...(req.body.view.embedColor !== undefined && { embedColor: req.body.view.embedColor || null }),
|
||||
...(req.body.view.embedSiteName !== undefined && {
|
||||
embedSiteName: req.body.view.embedSiteName || null,
|
||||
}),
|
||||
...(req.body.view.align !== undefined && { align: req.body.view.align || 'center' }),
|
||||
...(req.body.view.showMimetype !== undefined && {
|
||||
showMimetype: req.body.view.showMimetype || false,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
select: {
|
||||
...userSelect,
|
||||
},
|
||||
});
|
||||
|
||||
return res.send({ user, token: req.cookies.zipline_token });
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
38
src/server/routes/api/user/recent.ts
Normal file
38
src/server/routes/api/user/recent.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { File, cleanFiles, fileSelect } from '@/lib/db/models/file';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiUserRecentResponse = File[];
|
||||
|
||||
type Query = {
|
||||
page?: string;
|
||||
};
|
||||
|
||||
export const PATH = '/api/user/recent';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get<{ Querystring: Query }>(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
|
||||
const files = cleanFiles(
|
||||
await prisma.file.findMany({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
},
|
||||
select: {
|
||||
...fileSelect,
|
||||
password: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: 3,
|
||||
}),
|
||||
);
|
||||
|
||||
return res.send(files);
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
96
src/server/routes/api/user/stats.ts
Normal file
96
src/server/routes/api/user/stats.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiUserStatsResponse = {
|
||||
filesUploaded: number;
|
||||
favoriteFiles: number;
|
||||
views: number;
|
||||
avgViews: number;
|
||||
storageUsed: number;
|
||||
avgStorageUsed: number;
|
||||
urlsCreated: number;
|
||||
urlViews: number;
|
||||
|
||||
sortTypeCount: { [type: string]: number };
|
||||
};
|
||||
|
||||
export const PATH = '/api/user/stats';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
|
||||
const aggFile = await prisma.file.aggregate({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
},
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
_sum: {
|
||||
views: true,
|
||||
size: true,
|
||||
},
|
||||
_avg: {
|
||||
views: true,
|
||||
size: true,
|
||||
},
|
||||
});
|
||||
|
||||
const favCount = await prisma.file.count({
|
||||
where: {
|
||||
favorite: true,
|
||||
},
|
||||
});
|
||||
|
||||
const aggUrl = await prisma.url.aggregate({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
},
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
_avg: {
|
||||
views: true,
|
||||
},
|
||||
_sum: {
|
||||
views: true,
|
||||
},
|
||||
});
|
||||
|
||||
const sortType = await prisma.file.findMany({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
},
|
||||
select: {
|
||||
type: true,
|
||||
},
|
||||
});
|
||||
|
||||
const sortTypeCount = sortType.reduce(
|
||||
(acc, cur) => {
|
||||
if (acc[cur.type]) acc[cur.type] += 1;
|
||||
else acc[cur.type] = 1;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as { [type: string]: number },
|
||||
);
|
||||
|
||||
return res.send({
|
||||
filesUploaded: aggFile._count._all ?? 0,
|
||||
favoriteFiles: favCount ?? 0,
|
||||
views: aggFile._sum.views ?? 0,
|
||||
avgViews: aggFile._avg.views ?? 0,
|
||||
storageUsed: Number(aggFile._sum.size ?? 0),
|
||||
avgStorageUsed: Number(aggFile._avg.size ?? 0),
|
||||
urlsCreated: aggUrl._count._all ?? 0,
|
||||
urlViews: aggUrl._sum.views ?? 0,
|
||||
|
||||
sortTypeCount,
|
||||
});
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
61
src/server/routes/api/user/token.ts
Normal file
61
src/server/routes/api/user/token.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { createToken, encryptToken } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { loginToken } from '@/server/loginToken';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiUserTokenResponse = {
|
||||
user?: User;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export const PATH = '/api/user/token';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: req.user.id,
|
||||
},
|
||||
select: {
|
||||
token: true,
|
||||
},
|
||||
});
|
||||
|
||||
const token = encryptToken(user!.token, config.core.secret);
|
||||
|
||||
return res.send({
|
||||
token,
|
||||
});
|
||||
});
|
||||
|
||||
server.patch(PATH, async (req, res) => {
|
||||
const user = await prisma.user.update({
|
||||
where: {
|
||||
id: req.user.id,
|
||||
},
|
||||
data: {
|
||||
token: createToken(),
|
||||
},
|
||||
select: {
|
||||
...userSelect,
|
||||
token: true,
|
||||
},
|
||||
});
|
||||
|
||||
const token = loginToken(res, user);
|
||||
|
||||
delete (user as any).token;
|
||||
|
||||
return res.send({
|
||||
user,
|
||||
token,
|
||||
});
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
244
src/server/routes/api/users/[id].ts
Normal file
244
src/server/routes/api/users/[id].ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { bytes } from '@/lib/bytes';
|
||||
import { hashPassword } from '@/lib/crypto';
|
||||
import { datasource } from '@/lib/datasource';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { log } from '@/lib/logger';
|
||||
import { canInteract } from '@/lib/role';
|
||||
import { administratorMiddleware } from '@/server/middleware/administrator';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import { UserFilesQuota } from '@prisma/client';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import { z } from 'zod';
|
||||
|
||||
export type ApiUsersIdResponse = User;
|
||||
|
||||
type Body = {
|
||||
username?: string;
|
||||
password?: string;
|
||||
avatar?: string;
|
||||
role?: 'USER' | 'ADMIN' | 'SUPERADMIN';
|
||||
quota?: {
|
||||
filesType?: UserFilesQuota & 'NONE';
|
||||
maxFiles?: number;
|
||||
maxBytes?: string;
|
||||
|
||||
maxUrls?: number;
|
||||
};
|
||||
|
||||
delete?: boolean;
|
||||
};
|
||||
|
||||
type Params = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const logger = log('api').c('users').c('[id]');
|
||||
const zNumber = z.number();
|
||||
|
||||
export const PATH = '/api/users/:id';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get<{ Params: Params }>(
|
||||
PATH,
|
||||
{ preHandler: [userMiddleware, administratorMiddleware] },
|
||||
async (req, res) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: req.params.id,
|
||||
},
|
||||
select: userSelect,
|
||||
});
|
||||
|
||||
if (!user) return res.notFound('User not found');
|
||||
|
||||
return res.send(req.user);
|
||||
},
|
||||
);
|
||||
|
||||
server.patch<{ Params: Params; Body: Body }>(
|
||||
PATH,
|
||||
{ preHandler: [userMiddleware, administratorMiddleware] },
|
||||
async (req, res) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: req.params.id,
|
||||
},
|
||||
select: userSelect,
|
||||
});
|
||||
|
||||
if (!user) return res.notFound('User not found');
|
||||
|
||||
const { username, password, avatar, role, quota } = req.body;
|
||||
|
||||
if (role && !z.enum(['USER', 'ADMIN']).safeParse(role).success)
|
||||
return res.badRequest('Invalid role (USER, ADMIN)');
|
||||
|
||||
if (role && !canInteract(req.user.role, role)) return res.forbidden('You cannot create this role');
|
||||
|
||||
let finalQuota:
|
||||
| {
|
||||
filesQuota?: UserFilesQuota;
|
||||
maxFiles?: number | null;
|
||||
maxBytes?: string | null;
|
||||
maxUrls?: number | null;
|
||||
}
|
||||
| undefined = undefined;
|
||||
if (quota) {
|
||||
if (quota.filesType && !z.enum(['BY_BYTES', 'BY_FILES', 'NONE']).safeParse(quota.filesType).success)
|
||||
return res.badRequest('Invalid filesType (BY_BYTES, BY_FILES, NONE)');
|
||||
|
||||
if (quota.maxFiles && !zNumber.safeParse(quota.maxFiles).success)
|
||||
return res.badRequest('Invalid maxFiles');
|
||||
if (quota.maxUrls && !zNumber.safeParse(quota.maxUrls).success)
|
||||
return res.badRequest('Invalid maxUrls');
|
||||
|
||||
if (quota.filesType === 'BY_BYTES' && quota.maxBytes === undefined)
|
||||
return res.badRequest('maxBytes is required');
|
||||
if (quota.filesType === 'BY_FILES' && quota.maxFiles === undefined)
|
||||
return res.badRequest('maxFiles is required');
|
||||
|
||||
finalQuota = {
|
||||
...(quota.filesType === 'BY_BYTES' && {
|
||||
filesQuota: 'BY_BYTES',
|
||||
maxBytes: bytes(quota.maxBytes || '0') > 0 ? quota.maxBytes : null,
|
||||
maxFiles: null,
|
||||
}),
|
||||
...(quota.filesType === 'BY_FILES' && {
|
||||
filesQuota: 'BY_FILES',
|
||||
maxFiles: quota.maxFiles,
|
||||
maxBytes: null,
|
||||
}),
|
||||
...(quota.filesType === 'NONE' && {
|
||||
filesQuota: 'BY_BYTES',
|
||||
maxFiles: null,
|
||||
maxBytes: null,
|
||||
}),
|
||||
maxUrls: (quota.maxUrls || 0) > 0 ? quota.maxUrls : null,
|
||||
};
|
||||
}
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
...(username && { username }),
|
||||
...(password && { password: await hashPassword(password) }),
|
||||
...(role !== undefined && { role: 'USER' }),
|
||||
...(avatar && { avatar }),
|
||||
...(finalQuota && {
|
||||
quota: {
|
||||
upsert: {
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
create: {
|
||||
filesQuota: finalQuota.filesQuota || 'BY_BYTES',
|
||||
maxFiles: finalQuota.maxFiles ?? null,
|
||||
maxBytes: finalQuota.maxBytes ?? null,
|
||||
maxUrls: finalQuota.maxUrls ?? null,
|
||||
},
|
||||
update: finalQuota,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
select: {
|
||||
...userSelect,
|
||||
totpSecret: false,
|
||||
passkeys: false,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`${req.user.username} updated another user`, {
|
||||
username: updatedUser.username,
|
||||
role: updatedUser.role,
|
||||
});
|
||||
|
||||
return res.send(updatedUser);
|
||||
},
|
||||
);
|
||||
|
||||
server.delete<{ Params: Params; Body: Body }>(
|
||||
PATH,
|
||||
{ preHandler: [userMiddleware, administratorMiddleware] },
|
||||
async (req, res) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: req.params.id,
|
||||
},
|
||||
select: userSelect,
|
||||
});
|
||||
|
||||
if (!user) return res.notFound('User not found');
|
||||
if (user.id === req.user.id) return res.forbidden('You cannot delete yourself');
|
||||
if (!canInteract(req.user.role, user.role)) return res.forbidden('You cannot delete this user');
|
||||
|
||||
if (req.body.delete) {
|
||||
const files = await prisma.file.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [{ count: filesDeleted }, { count: urlsDeleted }] = await prisma.$transaction([
|
||||
prisma.file.deleteMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
}),
|
||||
prisma.url.deleteMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
logger.debug(`preparing to delete ${files.length} files from datasource`, {
|
||||
username: user.username,
|
||||
});
|
||||
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
await datasource.delete(files[i].name);
|
||||
}
|
||||
|
||||
logger.info(`${req.user.username} deleted another user's files & urls`, {
|
||||
username: user.username,
|
||||
deletedFiles: filesDeleted,
|
||||
deletedUrls: urlsDeleted,
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.oAuthProvider.deleteMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
const deletedUser = await prisma.user.delete({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
select: {
|
||||
...userSelect,
|
||||
totpSecret: false,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`${req.user.username} deleted another user`, {
|
||||
username: deletedUser.username,
|
||||
role: deletedUser.role,
|
||||
});
|
||||
|
||||
return res.send(deletedUser);
|
||||
},
|
||||
);
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
103
src/server/routes/api/users/index.ts
Normal file
103
src/server/routes/api/users/index.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { createToken, hashPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { log } from '@/lib/logger';
|
||||
import { canInteract } from '@/lib/role';
|
||||
import { administratorMiddleware } from '@/server/middleware/administrator';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import { Role } from '@prisma/client';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { z } from 'zod';
|
||||
|
||||
export type ApiUsersResponse = User[] | User;
|
||||
|
||||
type Query = {
|
||||
noincl?: 'true' | 'false';
|
||||
};
|
||||
|
||||
type Body = {
|
||||
username?: string;
|
||||
password?: string;
|
||||
avatar?: string;
|
||||
role?: Role;
|
||||
};
|
||||
|
||||
const logger = log('api').c('users');
|
||||
|
||||
export const PATH = '/api/users';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get<{ Querystring: Query }>(
|
||||
PATH,
|
||||
{ preHandler: [userMiddleware, administratorMiddleware] },
|
||||
async (req, res) => {
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
...userSelect,
|
||||
avatar: true,
|
||||
},
|
||||
where: {
|
||||
...(req.query.noincl === 'true' && { id: { not: req.user.id } }),
|
||||
},
|
||||
});
|
||||
|
||||
return res.send(users);
|
||||
},
|
||||
);
|
||||
|
||||
server.post<{ Querystring: Query; Body: Body }>(
|
||||
PATH,
|
||||
{ preHandler: [userMiddleware, administratorMiddleware] },
|
||||
async (req, res) => {
|
||||
const { username, password, avatar, role } = req.body;
|
||||
|
||||
if (!username) return res.badRequest('Username is required');
|
||||
if (!password) return res.badRequest('Password is required');
|
||||
|
||||
let avatar64 = null;
|
||||
|
||||
try {
|
||||
if (config.website.defaultAvatar) {
|
||||
avatar64 = (await readFile(config.website.defaultAvatar)).toString('base64');
|
||||
} else if (avatar) {
|
||||
avatar64 = avatar;
|
||||
}
|
||||
} catch {
|
||||
logger.debug('failed to read default avatar', { path: config.website.defaultAvatar });
|
||||
}
|
||||
|
||||
if (role && !z.enum(['USER', 'ADMIN']).safeParse(role).success)
|
||||
return res.badRequest('Invalid role (USER, ADMIN)');
|
||||
|
||||
if (role && !canInteract(req.user.role, role)) return res.forbidden('You cannot create this role');
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
password: await hashPassword(password),
|
||||
role: role ?? 'USER',
|
||||
avatar: avatar64 ?? null,
|
||||
token: createToken(),
|
||||
},
|
||||
select: {
|
||||
...userSelect,
|
||||
totpSecret: false,
|
||||
passkeys: false,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`${req.user.username} created a new user`, {
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
});
|
||||
|
||||
return res.send(user);
|
||||
},
|
||||
);
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
22
src/server/routes/api/version.ts
Normal file
22
src/server/routes/api/version.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { administratorMiddleware } from '@/server/middleware/administrator';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import { version } from '../../../../package.json';
|
||||
|
||||
export type ApiVersionResponse = {
|
||||
version: string;
|
||||
};
|
||||
|
||||
export const PATH = '/api/version';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get(PATH, { preHandler: [userMiddleware, administratorMiddleware] }, async (_, res) => {
|
||||
return res.send({
|
||||
version,
|
||||
});
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
67
src/server/routes/files.dy.ts
Normal file
67
src/server/routes/files.dy.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { verifyPassword } from '@/lib/crypto';
|
||||
import { datasource } from '@/lib/datasource';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { parse } from 'url';
|
||||
|
||||
type Params = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
type Query = {
|
||||
pw?: string;
|
||||
download?: string;
|
||||
};
|
||||
|
||||
export async function filesRoute(
|
||||
req: FastifyRequest<{ Params: Params; Querystring: Query }>,
|
||||
res: FastifyReply,
|
||||
) {
|
||||
const { id } = req.params;
|
||||
const { pw, download } = req.query;
|
||||
|
||||
const parsedUrl = parse(req.url!, true);
|
||||
|
||||
const file = await prisma.file.findFirst({
|
||||
where: {
|
||||
name: id,
|
||||
},
|
||||
include: {
|
||||
User: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!file) return req.server.nextServer.render404(req.raw, res.raw, parsedUrl);
|
||||
|
||||
if (file.User?.view.enabled) return res.redirect(`/view/${file.name}`);
|
||||
|
||||
const stream = await datasource.get(file.name);
|
||||
if (!stream) return req.server.nextServer.render404(req.raw, res.raw, parsedUrl);
|
||||
if (file.password) {
|
||||
if (!pw) return res.forbidden('Password protected.');
|
||||
const verified = await verifyPassword(pw as string, file.password!);
|
||||
|
||||
if (!verified) return res.forbidden('Incorrect password.');
|
||||
}
|
||||
|
||||
await prisma.file.update({
|
||||
where: {
|
||||
id: file.id,
|
||||
},
|
||||
data: {
|
||||
views: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return res
|
||||
.headers({
|
||||
'Content-Type': file.type || 'application/octet-stream',
|
||||
'Content-Length': file.size,
|
||||
...(file.originalName && {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${file.originalName}"`,
|
||||
}),
|
||||
})
|
||||
.send(stream);
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { verifyPassword } from '@/lib/crypto';
|
||||
import { datasource } from '@/lib/datasource';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { log } from '@/lib/logger';
|
||||
import express, { Request, Response } from 'express';
|
||||
import { NextServer } from 'next/dist/server/next';
|
||||
import { parse } from 'url';
|
||||
|
||||
const logger = log('server').c('files');
|
||||
|
||||
export async function filesRoute(
|
||||
this: ReturnType<typeof express>,
|
||||
app: NextServer,
|
||||
req: Request,
|
||||
res: Response,
|
||||
) {
|
||||
const { id } = req.params;
|
||||
const { pw } = req.query;
|
||||
|
||||
const parsedUrl = parse(req.url!, true);
|
||||
|
||||
const file = await prisma.file.findFirst({
|
||||
where: {
|
||||
name: id,
|
||||
},
|
||||
include: {
|
||||
User: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!file) return app.render404(req, res, parsedUrl);
|
||||
|
||||
if (file.maxViews && file.views >= file.maxViews) {
|
||||
if (config.features.deleteOnMaxViews) {
|
||||
await prisma.file.delete({
|
||||
where: {
|
||||
id: file.id,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`${file.name} deleted due to reaching max views`, {
|
||||
id: file.id,
|
||||
views: file.views,
|
||||
});
|
||||
}
|
||||
|
||||
return app.render404(req, res, parsedUrl);
|
||||
}
|
||||
|
||||
if (file.User?.view.enabled) return res.redirect(`/view/${file.name}`);
|
||||
|
||||
const stream = await datasource.get(file.name);
|
||||
if (!stream) return app.render404(req, res, parsedUrl);
|
||||
if (file.password) {
|
||||
if (!pw) return res.status(403).json({ code: 403, message: 'Password protected.' });
|
||||
const verified = await verifyPassword(pw as string, file.password!);
|
||||
|
||||
if (!verified) return res.status(403).json({ code: 403, message: 'Incorrect password.' });
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.setHeader('Content-Length', file.size);
|
||||
file.originalName &&
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
`${req.query.download ? 'attachment; ' : ''}filename="${file.originalName}"`,
|
||||
);
|
||||
|
||||
await prisma.file.update({
|
||||
where: {
|
||||
id: file.id,
|
||||
},
|
||||
data: {
|
||||
views: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
stream.pipe(res);
|
||||
}
|
||||
25
src/server/routes/index.ts
Normal file
25
src/server/routes/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import glob from 'fast-glob';
|
||||
import { pathToFileURL } from 'url';
|
||||
|
||||
const DEV = process.env.NODE_ENV === 'development';
|
||||
|
||||
const loadRoutes = async (): Promise<Record<string, ReturnType<typeof fastifyPlugin>>> => {
|
||||
const files = await glob(`./${DEV ? 'src' : 'build'}/server/routes/**/!(*.dy).@(ts|js)`, {
|
||||
ignore: [`./${DEV ? 'src' : 'build'}/server/routes/index.(ts|js)`],
|
||||
});
|
||||
|
||||
const routes: Record<string, ReturnType<typeof fastifyPlugin>> = {};
|
||||
|
||||
for (const file of files) {
|
||||
const a = await import(pathToFileURL(file).href);
|
||||
if (!a.default) throw new Error(`Route ${file} does not have a default export.`);
|
||||
if (!a.PATH) throw new Error(`Route ${file} does not have a PATH export.`);
|
||||
|
||||
routes[a.PATH] = a.default;
|
||||
}
|
||||
|
||||
return routes;
|
||||
};
|
||||
|
||||
export default loadRoutes;
|
||||
98
src/server/routes/raw.ts
Normal file
98
src/server/routes/raw.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { parseRange } from '@/lib/api/range';
|
||||
import { verifyPassword } from '@/lib/crypto';
|
||||
import { datasource } from '@/lib/datasource';
|
||||
import { prisma } from '@/lib/db';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import { parse } from 'url';
|
||||
|
||||
type Params = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
type Querystring = {
|
||||
pw?: string;
|
||||
download?: string;
|
||||
};
|
||||
|
||||
export const PATH = '/raw/:id';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get<{
|
||||
Querystring: Querystring;
|
||||
Params: Params;
|
||||
}>(PATH, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { pw, download } = req.query;
|
||||
|
||||
const parsedUrl = parse(req.url!, true);
|
||||
|
||||
const file = await prisma.file.findFirst({
|
||||
where: {
|
||||
name: id,
|
||||
},
|
||||
});
|
||||
|
||||
if (file?.password) {
|
||||
if (!pw) return res.forbidden('Password protected.');
|
||||
const verified = await verifyPassword(pw, file.password!);
|
||||
|
||||
if (!verified) return res.forbidden('Incorrect password.');
|
||||
}
|
||||
|
||||
const size = file?.size || (await datasource.size(file?.name ?? id));
|
||||
|
||||
if (req.headers.range) {
|
||||
const [start, end] = parseRange(req.headers.range, size);
|
||||
if (start >= size || end >= size) {
|
||||
const buf = await datasource.get(file?.name ?? id);
|
||||
if (!buf) return req.server.nextServer.render404(req.raw, res.raw, parsedUrl);
|
||||
|
||||
return res
|
||||
.type(file?.type || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Length': size,
|
||||
...(file?.originalName && {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${file.originalName}"`,
|
||||
}),
|
||||
})
|
||||
.status(416)
|
||||
.send(buf);
|
||||
}
|
||||
|
||||
const buf = await datasource.range(file?.name ?? id, start || 0, end);
|
||||
if (!buf) return req.server.nextServer.render404(req.raw, res.raw, parsedUrl);
|
||||
|
||||
return res
|
||||
.type(file?.type || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Range': `bytes ${start}-${end}/${size}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': end - start + 1,
|
||||
...(file?.originalName && {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${file.originalName}"`,
|
||||
}),
|
||||
})
|
||||
.status(206)
|
||||
.send(buf);
|
||||
}
|
||||
|
||||
const buf = await datasource.get(file?.name ?? id);
|
||||
if (!buf) return req.server.nextServer.render404(req.raw, res.raw, parsedUrl);
|
||||
|
||||
return res
|
||||
.type(file?.type || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Length': size,
|
||||
'Accept-Ranges': 'bytes',
|
||||
...(file?.originalName && {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${file.originalName}"`,
|
||||
}),
|
||||
})
|
||||
.status(200)
|
||||
.send(buf);
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
@@ -1,18 +1,23 @@
|
||||
import { NextServer } from 'next/dist/server/next';
|
||||
import express, { Request, Response } from 'express';
|
||||
import { parse } from 'url';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { config } from '@/lib/config';
|
||||
import { log } from '@/lib/logger';
|
||||
import { verifyPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { log } from '@/lib/logger';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { parse } from 'url';
|
||||
|
||||
type Params = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
type Query = {
|
||||
pw?: string;
|
||||
};
|
||||
|
||||
const logger = log('server').c('urls');
|
||||
|
||||
export async function urlsRoute(
|
||||
this: ReturnType<typeof express>,
|
||||
app: NextServer,
|
||||
req: Request,
|
||||
res: Response,
|
||||
req: FastifyRequest<{ Params: Params; Querystring: Query }>,
|
||||
res: FastifyReply,
|
||||
) {
|
||||
const { id } = req.params;
|
||||
const { pw } = req.query;
|
||||
@@ -24,7 +29,7 @@ export async function urlsRoute(
|
||||
OR: [{ code: id }, { vanity: id }, { id }],
|
||||
},
|
||||
});
|
||||
if (!url) return app.render404(req, res, parsedUrl);
|
||||
if (!url) return req.server.nextServer.render404(req.raw, res.raw, parsedUrl);
|
||||
|
||||
if (url.maxViews && url.views >= url.maxViews) {
|
||||
if (config.features.deleteOnMaxViews) {
|
||||
@@ -41,7 +46,7 @@ export async function urlsRoute(
|
||||
});
|
||||
}
|
||||
|
||||
return app.render404(req, res, parsedUrl);
|
||||
return req.server.nextServer.render404(req.raw, res.raw, parsedUrl);
|
||||
}
|
||||
|
||||
if (url.password) {
|
||||
@@ -18,6 +18,6 @@
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "src/client/mount.js"],
|
||||
"exclude": ["node_modules", "uploads"]
|
||||
}
|
||||
|
||||
@@ -1,50 +1,19 @@
|
||||
import glob from 'fast-glob';
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
treeshake: true,
|
||||
clean: false,
|
||||
sourcemap: true,
|
||||
entryPoints: {
|
||||
server: 'src/server/index.ts',
|
||||
export default defineConfig(async (_) => {
|
||||
return [
|
||||
{
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
treeshake: true,
|
||||
clean: false,
|
||||
sourcemap: true,
|
||||
entryPoints: await glob('./src/**/*.ts', {
|
||||
ignore: ['./src/components/**/*.ts', './src/pages/**/*.ts'],
|
||||
}),
|
||||
outDir: 'build',
|
||||
external: ['argon2'],
|
||||
},
|
||||
outDir: 'build',
|
||||
},
|
||||
{
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
treeshake: true,
|
||||
clean: false,
|
||||
sourcemap: true,
|
||||
entryPoints: {
|
||||
ctl: 'src/ctl/index.ts',
|
||||
},
|
||||
outDir: 'build',
|
||||
bundle: true,
|
||||
minify: true,
|
||||
},
|
||||
{
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
treeshake: true,
|
||||
clean: false,
|
||||
sourcemap: true,
|
||||
entryPoints: {
|
||||
thumbnails: 'src/offload/thumbnails.ts',
|
||||
},
|
||||
outDir: 'build/offload',
|
||||
},
|
||||
{
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
treeshake: true,
|
||||
clean: false,
|
||||
sourcemap: true,
|
||||
entryPoints: {
|
||||
partial: 'src/offload/partial.ts',
|
||||
},
|
||||
outDir: 'build/offload',
|
||||
},
|
||||
]);
|
||||
];
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user