From 31cc6e51b5261f4dc7a79e7dd457be236fa8af6e Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 9 Apr 2024 22:43:47 +0300 Subject: [PATCH] Add user backups download --- package-lock.json | 807 +++++++++++++++++++++++++++- package.json | 1 + public/scripts/templates/admin.html | 27 +- public/scripts/user.js | 94 ++++ src/endpoints/users-admin.js | 231 +++++--- src/endpoints/users-private.js | 33 +- src/users.js | 40 +- 7 files changed, 1137 insertions(+), 96 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2b7c6c0d6..d1f8b11cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@agnai/web-tokenizers": "^0.1.3", "@dqbd/tiktoken": "^1.0.13", "@zeldafan0225/ai_horde": "^4.0.1", + "archiver": "^7.0.1", "bing-translate-api": "^2.9.1", "body-parser": "^1.20.2", "command-exists": "^1.2.9", @@ -228,6 +229,95 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jimp/bmp": { "version": "0.22.10", "license": "MIT", @@ -647,6 +737,15 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "license": "BSD-3-Clause" @@ -892,6 +991,221 @@ "version": "1.0.0", "license": "MIT" }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/archiver/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "dev": true, @@ -905,6 +1219,11 @@ "version": "2.1.3", "license": "ISC" }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/asynckit": { "version": "0.4.0", "license": "MIT" @@ -918,11 +1237,21 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==" + }, "node_modules/balanced-match": { "version": "1.0.2", - "dev": true, "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "optional": true + }, "node_modules/base-64": { "version": "0.1.0" }, @@ -1212,6 +1541,97 @@ "version": "1.2.9", "license": "MIT" }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/compress-commons/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/compress-commons/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/compressible": { "version": "2.0.18", "license": "MIT", @@ -1393,9 +1813,96 @@ "node": ">=0.8" } }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/crc32-stream/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/crc32-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -1581,6 +2088,11 @@ "node": ">=10" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "node_modules/ee-first": { "version": "1.1.1", "license": "MIT" @@ -1828,6 +2340,14 @@ "node": ">=6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/exif-parser": { "version": "0.1.12" }, @@ -1903,6 +2423,11 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "dev": true, @@ -2026,6 +2551,21 @@ } } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "license": "MIT", @@ -2206,6 +2746,11 @@ "node": ">=12" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, "node_modules/graphemer": { "version": "1.4.0", "dev": true, @@ -2458,6 +3003,17 @@ "node": ">=8" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "license": "MIT", @@ -2474,7 +3030,6 @@ }, "node_modules/isexe": { "version": "2.0.0", - "dev": true, "license": "ISC" }, "node_modules/isomorphic-fetch": { @@ -2485,6 +3040,23 @@ "whatwg-fetch": "^3.4.1" } }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jimp": { "version": "0.22.10", "license": "MIT", @@ -2605,6 +3177,17 @@ "json-buffer": "3.0.1" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, "node_modules/levn": { "version": "0.4.1", "dev": true, @@ -2669,6 +3252,14 @@ "node": ">=8" } }, + "node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/md5": { "version": "2.3.0", "license": "BSD-3-Clause", @@ -2757,6 +3348,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.0.0", "license": "MIT" @@ -2842,6 +3441,14 @@ "node": ">=10.12.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "6.1.0", "license": "MIT", @@ -3099,12 +3706,26 @@ }, "node_modules/path-key": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "0.1.7", "license": "MIT" @@ -3288,6 +3909,11 @@ ], "license": "MIT" }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, "node_modules/quick-lru": { "version": "5.1.1", "license": "MIT", @@ -3369,6 +3995,33 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/regenerator-runtime": { "version": "0.13.11", "license": "MIT" @@ -3529,7 +4182,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -3540,7 +4192,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3634,6 +4285,18 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "license": "MIT", @@ -3653,6 +4316,20 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "license": "MIT", @@ -3663,6 +4340,18 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strtok3": { "version": "6.3.0", "license": "MIT", @@ -3689,6 +4378,16 @@ "node": ">=8" } }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/text-table": { "version": "0.2.0", "dev": true, @@ -3892,7 +4591,6 @@ }, "node_modules/which": { "version": "2.0.2", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -3919,6 +4617,23 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "license": "ISC" @@ -4047,6 +4762,84 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/zip-stream/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/zip-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } } } } diff --git a/package.json b/package.json index ff6de4e59..0071f3337 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "@agnai/web-tokenizers": "^0.1.3", "@dqbd/tiktoken": "^1.0.13", "@zeldafan0225/ai_horde": "^4.0.1", + "archiver": "^7.0.1", "bing-translate-api": "^2.9.1", "body-parser": "^1.20.2", "command-exists": "^1.2.9", diff --git a/public/scripts/templates/admin.html b/public/scripts/templates/admin.html index 4db762b76..91bdc0b1e 100644 --- a/public/scripts/templates/admin.html +++ b/public/scripts/templates/admin.html @@ -37,9 +37,24 @@
- - - + + + + + +
@@ -56,15 +71,15 @@
Display Name: - +
Password: - +
Confirm Password: - +
This will create a new subfolder in the /data/ directory with the user's handle as the folder name. diff --git a/public/scripts/user.js b/public/scripts/user.js index b51a463c3..31f6285f5 100644 --- a/public/scripts/user.js +++ b/public/scripts/user.js @@ -116,6 +116,57 @@ async function disableUser(handle, callback) { } } +/** + * Promote a user to admin. + * @param {string} handle User handle + * @param {function} callback Success callback + * @returns {Promise} + */ +async function promoteUser(handle, callback) { + try { + const response = await fetch('/api/users/promote', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ handle }), + }); + + if (!response.ok) { + const data = await response.json(); + toastr.error(data.error || 'Unknown error', 'Failed to promote user'); + throw new Error('Failed to promote user'); + } + + callback(); + } catch (error) { + console.error('Error promoting user:', error); + } +} + +/** + * Demote a user from admin. + * @param {string} handle User handle + * @param {function} callback Success callback + */ +async function demoteUser(handle, callback) { + try { + const response = await fetch('/api/users/demote', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ handle }), + }); + + if (!response.ok) { + const data = await response.json(); + toastr.error(data.error || 'Unknown error', 'Failed to demote user'); + throw new Error('Failed to demote user'); + } + + callback(); + } catch (error) { + console.error('Error demoting user:', error); + } +} + /** * Create a new user. * @param {HTMLFormElement} form Form element @@ -168,6 +219,43 @@ async function createUser(form, callback) { } } +/** + * Backup a user's data. + * @param {string} handle Handle of the user to backup + * @param {function} callback Success callback + * @returns {Promise} + */ +async function backupUserData(handle, callback) { + try { + toastr.info('Please wait for the download to start.', 'Backup Requested'); + const response = await fetch('/api/users/backup', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ handle }), + }); + + if (!response.ok) { + const data = await response.json(); + toastr.error(data.error || 'Unknown error', 'Failed to backup user data'); + throw new Error('Failed to backup user data'); + } + + const blob = await response.blob(); + const header = response.headers.get('Content-Disposition'); + const parts = header.split(';'); + const filename = parts[1].split('=')[1].replaceAll('"', ''); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + callback(); + } catch (error) { + console.error('Error backing up user data:', error); + } +} + async function openAdminPanel() { async function renderUsers() { const users = await getUsers(); @@ -184,6 +272,12 @@ async function openAdminPanel() { userBlock.find('.userCreated').text(new Date(user.created).toLocaleString()); userBlock.find('.userEnableButton').toggle(!user.enabled).on('click', () => enableUser(user.handle, renderUsers)); userBlock.find('.userDisableButton').toggle(user.enabled).on('click', () => disableUser(user.handle, renderUsers)); + userBlock.find('.userPromoteButton').toggle(!user.admin).on('click', () => promoteUser(user.handle, renderUsers)); + userBlock.find('.userDemoteButton').toggle(user.admin).on('click', () => demoteUser(user.handle, renderUsers)); + userBlock.find('.userBackupButton').on('click', function () { + $(this).addClass('disabled').off('click'); + backupUserData(user.handle, renderUsers); + }); template.find('.usersList').append(userBlock); } } diff --git a/src/endpoints/users-admin.js b/src/endpoints/users-admin.js index d594b595e..0310d3b1d 100644 --- a/src/endpoints/users-admin.js +++ b/src/endpoints/users-admin.js @@ -19,103 +19,176 @@ const { const router = express.Router(); router.post('/get', requireAdminMiddleware, jsonParser, async (request, response) => { - /** @type {import('../users').User[]} */ - const users = await storage.values(x => x.key.startsWith(KEY_PREFIX)); + try { + /** @type {import('../users').User[]} */ + const users = await storage.values(x => x.key.startsWith(KEY_PREFIX)); - const viewModels = users - .sort((x, y) => x.created - y.created) - .map(user => ({ - handle: user.handle, - name: user.name, - avatar: getUserAvatar(user.handle), - admin: user.admin, - enabled: user.enabled, - created: user.created, - password: !!user.password, - })); + const viewModels = users + .sort((x, y) => x.created - y.created) + .map(user => ({ + handle: user.handle, + name: user.name, + avatar: getUserAvatar(user.handle), + admin: user.admin, + enabled: user.enabled, + created: user.created, + password: !!user.password, + })); - return response.json(viewModels); + return response.json(viewModels); + } catch (error) { + console.error('User list failed:', error); + return response.sendStatus(500); + } }); router.post('/disable', requireAdminMiddleware, jsonParser, async (request, response) => { - if (!request.body.handle) { - console.log('Disable user failed: Missing required fields'); - return response.status(400).json({ error: 'Missing required fields' }); + try { + if (!request.body.handle) { + console.log('Disable user failed: Missing required fields'); + return response.status(400).json({ error: 'Missing required fields' }); + } + + if (request.body.handle === request.user.profile.handle) { + console.log('Disable user failed: Cannot disable yourself'); + return response.status(400).json({ error: 'Cannot disable yourself' }); + } + + /** @type {import('../users').User} */ + const user = await storage.getItem(toKey(request.body.handle)); + + if (!user) { + console.log('Disable user failed: User not found'); + return response.status(404).json({ error: 'User not found' }); + } + + user.enabled = false; + await storage.setItem(toKey(request.body.handle), user); + return response.sendStatus(204); + } catch (error) { + console.error('User disable failed:', error); + return response.sendStatus(500); } - - if (request.body.handle === request.user.profile.handle) { - console.log('Disable user failed: Cannot disable yourself'); - return response.status(400).json({ error: 'Cannot disable yourself' }); - } - - /** @type {import('../users').User} */ - const user = await storage.getItem(toKey(request.body.handle)); - - if (!user) { - console.log('Disable user failed: User not found'); - return response.status(404).json({ error: 'User not found' }); - } - - user.enabled = false; - await storage.setItem(toKey(request.body.handle), user); - return response.sendStatus(204); }); router.post('/enable', requireAdminMiddleware, jsonParser, async (request, response) => { - if (!request.body.handle) { - console.log('Enable user failed: Missing required fields'); - return response.status(400).json({ error: 'Missing required fields' }); + try { + if (!request.body.handle) { + console.log('Enable user failed: Missing required fields'); + return response.status(400).json({ error: 'Missing required fields' }); + } + + /** @type {import('../users').User} */ + const user = await storage.getItem(toKey(request.body.handle)); + + if (!user) { + console.log('Enable user failed: User not found'); + return response.status(404).json({ error: 'User not found' }); + } + + user.enabled = true; + await storage.setItem(toKey(request.body.handle), user); + return response.sendStatus(204); + } catch (error) { + console.error('User enable failed:', error); + return response.sendStatus(500); } +}); - /** @type {import('../users').User} */ - const user = await storage.getItem(toKey(request.body.handle)); +router.post('/promote', requireAdminMiddleware, jsonParser, async (request, response) => { + try { + if (!request.body.handle) { + console.log('Promote user failed: Missing required fields'); + return response.status(400).json({ error: 'Missing required fields' }); + } - if (!user) { - console.log('Enable user failed: User not found'); - return response.status(404).json({ error: 'User not found' }); + /** @type {import('../users').User} */ + const user = await storage.getItem(toKey(request.body.handle)); + + if (!user) { + console.log('Promote user failed: User not found'); + return response.status(404).json({ error: 'User not found' }); + } + + user.admin = true; + await storage.setItem(toKey(request.body.handle), user); + return response.sendStatus(204); + } catch (error) { + console.error('User promote failed:', error); + return response.sendStatus(500); } +}); - user.enabled = true; - await storage.setItem(toKey(request.body.handle), user); - return response.sendStatus(204); +router.post('/demote', requireAdminMiddleware, jsonParser, async (request, response) => { + try { + if (!request.body.handle) { + console.log('Demote user failed: Missing required fields'); + return response.status(400).json({ error: 'Missing required fields' }); + } + + if (request.body.handle === request.user.profile.handle) { + console.log('Demote user failed: Cannot demote yourself'); + return response.status(400).json({ error: 'Cannot demote yourself' }); + } + + /** @type {import('../users').User} */ + const user = await storage.getItem(toKey(request.body.handle)); + + if (!user) { + console.log('Demote user failed: User not found'); + return response.status(404).json({ error: 'User not found' }); + } + + user.admin = false; + await storage.setItem(toKey(request.body.handle), user); + return response.sendStatus(204); + } catch (error) { + console.error('User demote failed:', error); + return response.sendStatus(500); + } }); router.post('/create', requireAdminMiddleware, jsonParser, async (request, response) => { - if (!request.body.handle || !request.body.name) { - console.log('Create user failed: Missing required fields'); - return response.status(400).json({ error: 'Missing required fields' }); + try { + if (!request.body.handle || !request.body.name) { + console.log('Create user failed: Missing required fields'); + return response.status(400).json({ error: 'Missing required fields' }); + } + + const handles = await getAllUserHandles(); + const handle = slugify(request.body.handle, { lower: true, trim: true }); + + if (handles.some(x => x === handle)) { + console.log('Create user failed: User with that handle already exists'); + return response.status(409).json({ error: 'User already exists' }); + } + + const salt = getPasswordSalt(); + const password = request.body.password ? getPasswordHash(request.body.password, salt) : ''; + + const newUser = { + uuid: uuid.v4(), + handle: handle, + name: request.body.name || 'Anonymous', + created: Date.now(), + password: password, + salt: salt, + admin: !!request.body.admin, + enabled: true, + }; + + await storage.setItem(toKey(handle), newUser); + + // Create user directories + console.log('Creating data directories for', newUser.handle); + await ensurePublicDirectoriesExist(); + const directories = getUserDirectories(newUser.handle); + await checkForNewContent([directories]); + return response.json({ handle: newUser.handle }); + } catch (error) { + console.error('User create failed:', error); + return response.sendStatus(500); } - - const handles = await getAllUserHandles(); - const handle = slugify(request.body.handle, { lower: true, trim: true }); - - if (handles.some(x => x === handle)) { - console.log('Create user failed: User with that handle already exists'); - return response.status(409).json({ error: 'User already exists' }); - } - - const salt = getPasswordSalt(); - const password = request.body.password ? getPasswordHash(request.body.password, salt) : ''; - - const newUser = { - uuid: uuid.v4(), - handle: handle, - name: request.body.name || 'Anonymous', - created: Date.now(), - password: password, - salt: salt, - admin: !!request.body.admin, - enabled: true, - }; - - await storage.setItem(toKey(handle), newUser); - - // Create user directories - console.log('Creating data directories for', newUser.handle); - await ensurePublicDirectoriesExist(); - const directories = getUserDirectories(newUser.handle); - await checkForNewContent([directories]); - return response.json({ handle: newUser.handle }); }); module.exports = { diff --git a/src/endpoints/users-private.js b/src/endpoints/users-private.js index 17fe99ad2..0b95eb94c 100644 --- a/src/endpoints/users-private.js +++ b/src/endpoints/users-private.js @@ -1,7 +1,7 @@ const storage = require('node-persist'); const express = require('express'); const { jsonParser } = require('../express-common'); -const { getUserAvatar, toKey, getPasswordHash, getPasswordSalt } = require('../users'); +const { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive } = require('../users'); const router = express.Router(); @@ -44,11 +44,16 @@ router.get('/me', async (request, response) => { router.post('/change-password', jsonParser, async (request, response) => { try { - if (!request.body.handle || !request.body.oldPassword || !request.body.newPassword) { + if (!request.body.handle) { console.log('Change password failed: Missing required fields'); return response.status(400).json({ error: 'Missing required fields' }); } + if (request.body.handle !== request.user.profile.handle && !request.user.profile.admin) { + console.log('Change password failed: Unauthorized'); + return response.status(403).json({ error: 'Unauthorized' }); + } + /** @type {import('../users').User} */ const user = await storage.getItem(toKey(request.body.handle)); @@ -62,7 +67,8 @@ router.post('/change-password', jsonParser, async (request, response) => { return response.status(403).json({ error: 'User is disabled' }); } - if (user.password && user.password !== getPasswordHash(request.body.oldPassword, user.salt)) { + const isAdminChange = request.user.profile.admin && request.body.handle !== request.user.profile.handle; + if (!isAdminChange && user.password && user.password !== getPasswordHash(request.body.oldPassword, user.salt)) { console.log('Change password failed: Incorrect password'); return response.status(401).json({ error: 'Incorrect password' }); } @@ -78,6 +84,27 @@ router.post('/change-password', jsonParser, async (request, response) => { } }); +router.post('/backup', jsonParser, async (request, response) => { + try { + const handle = request.body.handle; + + if (!handle) { + console.log('Backup failed: Missing required fields'); + return response.status(400).json({ error: 'Missing required fields' }); + } + + if (handle !== request.user.profile.handle && !request.user.profile.admin) { + console.log('Backup failed: Unauthorized'); + return response.status(403).json({ error: 'Unauthorized' }); + } + + await createBackupArchive(handle, response); + } catch (error) { + console.error('Backup failed', error); + return response.sendStatus(500); + } +}); + module.exports = { router, }; diff --git a/src/users.js b/src/users.js index d64655d0a..0a516e9cf 100644 --- a/src/users.js +++ b/src/users.js @@ -8,9 +8,10 @@ const os = require('os'); const storage = require('node-persist'); const express = require('express'); const mime = require('mime-types'); +const archiver = require('archiver'); const { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, DEFAULT_AVATAR } = require('./constants'); -const { getConfigValue, color, delay, setConfigValue } = require('./util'); +const { getConfigValue, color, delay, setConfigValue, generateTimestamp } = require('./util'); const { readSecret, writeSecret } = require('./endpoints/secrets'); const KEY_PREFIX = 'user:'; @@ -584,6 +585,42 @@ function requireAdminMiddleware(request, response, next) { return response.sendStatus(403); } +/** + * Creates an archive of the user's data root directory. + * @param {string} handle User handle + * @param {import('express').Response} response Express response object to write to + * @returns {Promise} Promise that resolves when the archive is created + */ +async function createBackupArchive(handle, response) { + const directories = getUserDirectories(handle); + + console.log('Backup requested for', handle); + const archive = archiver('zip'); + + archive.on('error', function (err) { + response.status(500).send({ error: err.message }); + }); + + // On stream closed we can end the request + archive.on('end', function () { + console.log('Archive wrote %d bytes', archive.pointer()); + response.end(); // End the Express response + }); + + const timestamp = generateTimestamp(); + + // Set the archive name + response.attachment(`${handle}-${timestamp}.zip`); + + // This is the streaming magic + // @ts-ignore + archive.pipe(response); + + // Append files from a sub-directory, putting its contents at the root of archive + archive.directory(directories.root, false); + archive.finalize(); +} + /** * Express router for serving files from the user's directories. */ @@ -614,6 +651,7 @@ module.exports = { getCookieSessionName, getUserAvatar, shouldRedirectToLogin, + createBackupArchive, tryAutoLogin, router, };