This commit is contained in:
2026-04-26 00:08:27 +02:00
parent 92074e7f60
commit c3e5448597
40 changed files with 1783 additions and 54 deletions

View File

@@ -1,3 +1,3 @@
PORT=5000
DB_URI=mongodb://10.1.1.7:27017/
DB_URI=mongodb://10.1.1.7:27017/dragonroll
NODE_ENV=production

View File

@@ -9,11 +9,17 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"bcryptjs": "^3.0.3",
"cookie-parser": "^1.4.7",
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"express-rate-limit": "^8.4.1",
"jsonwebtoken": "^9.0.3",
"mongoose": "^9.3.0",
"nodemon": "^3.1.14"
"multer": "^2.1.1",
"nodemon": "^3.1.14",
"passport": "^0.7.0"
}
},
"node_modules/@mongodb-js/saslprep": {
@@ -66,6 +72,12 @@
"node": ">= 8"
}
},
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -75,6 +87,15 @@
"node": "18 || 20 || >=22"
}
},
"node_modules/bcryptjs": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -144,6 +165,29 @@
"node": ">=20.19.0"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -206,6 +250,21 @@
"fsevents": "~2.3.2"
}
},
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/content-disposition": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
@@ -237,6 +296,25 @@
"node": ">= 0.6"
}
},
"node_modules/cookie-parser": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cookie-signature": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
@@ -315,6 +393,15 @@
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -418,6 +505,24 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/express-rate-limit": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz",
"integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==",
"license": "MIT",
"dependencies": {
"ip-address": "10.1.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": ">= 4.11"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -634,6 +739,15 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -691,6 +805,49 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"license": "MIT",
"dependencies": {
"jws": "^4.0.1",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/kareem": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-3.2.0.tgz",
@@ -700,6 +857,48 @@
"node": ">=18.0.0"
}
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -880,6 +1079,68 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/multer": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
"integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"type-is": "^1.6.18"
},
"engines": {
"node": ">= 10.16.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/multer/node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
@@ -977,6 +1238,32 @@
"node": ">= 0.8"
}
},
"node_modules/passport": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
"license": "MIT",
"dependencies": {
"passport-strategy": "1.x.x",
"pause": "0.0.1",
"utils-merge": "^1.0.1"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/path-to-regexp": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
@@ -987,6 +1274,11 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
@@ -1066,6 +1358,20 @@
"node": ">= 0.10"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -1094,6 +1400,26 @@
"node": ">= 18"
}
},
"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"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -1271,6 +1597,23 @@
"node": ">= 0.8"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"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==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -1339,6 +1682,12 @@
"node": ">= 0.6"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
@@ -1354,6 +1703,21 @@
"node": ">= 0.8"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@@ -10,10 +10,16 @@
"dev": "nodemon src/index.js"
},
"dependencies": {
"bcryptjs": "^3.0.3",
"cookie-parser": "^1.4.7",
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"express-rate-limit": "^8.4.1",
"jsonwebtoken": "^9.0.3",
"mongoose": "^9.3.0",
"nodemon": "^3.1.14"
"multer": "^2.1.1",
"nodemon": "^3.1.14",
"passport": "^0.7.0"
}
}

View File

@@ -1,5 +1,7 @@
const express = require("express");
const cors = require('cors');
const cookieParser = require('cookie-parser');
const passport = require('passport');
const dotenv = require('dotenv');
@@ -14,14 +16,39 @@ if(process.env.NODE_ENV) {
const app = express();
const connectDB = require("./db");
// JSON LIMIT EXPRESS
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({
extended: true,
limit: '50mb'
}));
// connect database
connectDB();
// CORS
app.use(cookieParser());
app.use(cors({
origin: 'http://localhost:3000',
credentials: true, // if using cookies/auth
}));
// ROUTES (NO AUTH)
app.use('/user', require('./routes/user'));
// AUTH
checkAuth = passport.authenticate('jwt', { session: false });
app.use(checkAuth);
// ROUTES WITH AUTH
/*
app.use('/campaign', require('./routes/campaign'));
app.use('/maps', require('./routes/map'));
app.use('/datagen', require('./routes/datagen'));
app.use('/admin', require('./routes/admin'));
*/
app.get("/api/test", (req, res) => {
console.log("Hey");
res.json({"message": "Hello from backend!"});

View File

@@ -0,0 +1,16 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const UserSchema = new Schema({
name: {type: String, required: true},
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String },
date: { type: Date, default: Date.now},
admin: {type: Boolean, default: false},
image: { type: String },
setupCode: { type: String },
settings: { type: Object }
});
module.exports = mongoose.model('User', UserSchema);

175
backend/src/routes/user.js Normal file
View File

@@ -0,0 +1,175 @@
const express = require('express')
const router = express.Router();
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const passport = require('passport');
const secret = require('../services/keys').secret;
const rateLimitMiddleware = require("../services/rate-limiter");
const crypto = require("crypto");
const { isAdmin } = require('../services/middleware');
const User = require("../models/User");
const upload = require("../services/storage");
// Admin registers new user
router.post('/register', isAdmin, async (req, res) => {
try {
let setupCode = crypto.randomBytes(64).toString('base64url');
let user = new User({
admin: false,
name: crypto.randomBytes(16).toString('base64url'),
username: crypto.randomBytes(16).toString('base64url'),
email: crypto.randomBytes(16).toString('base64url'),
setupCode
});
await user.save();
res.json({ status: "ok", code: setupCode });
} catch (err) {
res.json({ status: "error", msg: "internal" });
}
});
// User gets if setup account exists given the query code
router.get('/verify-setup', async (req, res) => {
try {
const user = await User.findOne({ setupCode: req.query.code });
if (user) {
res.json({ status: "ok", code: req.query.code });
} else {
res.json({ status: "error", msg: "not-exists" });
}
} catch (err) {
res.json({ status: "error", msg: "internal" });
}
});
// User posts the parameters of his new account given by admin
router.post('/setup', rateLimitMiddleware, async (req, res) => {
const { name, username, email, password } = req.body;
const setupCode = req.query.code;
if (!(name && username && email && password && setupCode)) {
return res.json({ status: "error", msg: "params" });
}
try {
const user = await User.findOne({ setupCode });
if (!user) {
return res.json({ status: "error", msg: "not-found" });
}
const sameUser = await User.findOne({ email });
if (sameUser) {
return res.json({ status: "error", msg: "already-email" });
}
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
user.username = username;
user.email = email;
user.setupCode = undefined;
await user.save();
res.json({ status: "ok" });
} catch (err) {
res.json({ status: "error", msg: "internal" });
}
});
// Login post
router.post('/login', rateLimitMiddleware, async (req, res) => {
const { username, password } = req.body;
if (!(username && password)) {
return res.json({ status: "error", msg: "params" });
}
try {
const user = await User.findOne({ username });
if (!user) {
return res.json({ status: "error", msg: "wrong" });
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.json({ status: "error", msg: "wrong" });
}
const payload = {
_id: user._id,
username: user.username,
name: user.name,
email: user.email,
admin: user.admin,
settings: user.settings
};
const token = await new Promise((resolve, reject) => {
jwt.sign(payload, secret, { expiresIn: 172800 }, (err, token) => {
if (err) reject(err);
else resolve(token);
});
});
res.json({ status: "ok", token, msg: "success" });
} catch (err) {
res.json({ status: "error", msg: "internal" });
}
});
// Upload avatar post
router.post("/upload-avatar", upload.single("image"), passport.authenticate('jwt', {session: false}), async (req, res) => {
try {
const imageName = req.file.filename;
await User.updateOne(req.user, { image: imageName });
res.json({ status: "ok", msg: "uploaded" });
} catch (err) {
res.json({ status: "error", msg: "internal" });
}
});
router.get("/retrieve-avatar", async (req, res) => {
try {
const data = await User.findOne({ username: req.query.username });
res.json({ status: "ok", image: data.image });
} catch (err) {
res.json({ status: "error" });
}
});
router.get("/has-admin", async (req, res) => {
try {
const data = await User.findOne({ admin: true });
if (data) res.json({ status: "ok" });
else res.json({ status: "init" });
} catch (err) {
res.json({ status: "error" });
}
});
router.post("/update-settings", passport.authenticate('jwt', {session: false}), async (req, res) => {
try {
await User.updateOne(req.user, { settings: req.body.settings });
res.json({ status: "ok", settings: req.body.settings });
} catch (err) {
res.json({ status: "error", msg: "internal" });
}
});
router.get('/get-settings', passport.authenticate('jwt', {session: false}), async (req, res) => {
try {
const data = await User.findOne(req.user);
res.json({ status: "ok", settings: data.settings });
} catch (err) {
res.json({ status: "error", msg: "internal" });
}
});
module.exports = router;

View File

@@ -0,0 +1,5 @@
const fs = require('fs');
module.exports = {
secret: "putyoursecrethere"
}

View File

@@ -0,0 +1,17 @@
const User = require("../models/User");
async function isAdmin(req, res, next) {
try {
const user = await User.findOne(req.user).lean();
if (user && user.admin) {
return next();
}
res.json({ status: "error", msg: "unauthorized" });
} catch (err) {
res.json({ status: "error", msg: err.message });
}
}
module.exports = {
isAdmin
}

View File

@@ -0,0 +1,11 @@
const setRateLimit = require("express-rate-limit");
// Rate limit middleware
const rateLimitMiddleware = setRateLimit({
windowMs: 60 * 60 * 1000,
max: 150,
message: "Has fet masses peticions de login en una hora (ets un robot???)",
headers: true,
});
module.exports = rateLimitMiddleware;

View File

@@ -0,0 +1,14 @@
const multer = require('multer');
var storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads')
},
filename: function (req, file, cb) {
cb(null, file.fieldname + '-' + Date.now())
}
});
var upload = multer({storage: storage});
module.exports = upload;

View File

@@ -1,9 +1,7 @@
<script setup lang="ts">
import ContentManager from './components/managers/ContentManager.vue';
import ToastManager from './components/managers/ToastManager.vue';
import WindowManager from './components/managers/WindowManager.vue';
import Content from './components/viewer/content/Content.vue';
import StatusBar from './components/viewer/statusbar/StatusBar.vue';
import TopBar from './components/viewer/TopBar.vue';
import { CreateWindow } from '@/services/Windows'
@@ -14,19 +12,17 @@ async function start(){
}
onMounted(() => {
setupTheme();
setTheme('dark');
start();
})
</script>
<template>
<div class="viewer">
<ToastManager></ToastManager>
<WindowManager></WindowManager>
<TopBar></TopBar>
<Content></Content>
<StatusBar></StatusBar>
<ContentManager></ContentManager>
<!-- Managers -->
</div>
</template>

View File

@@ -3,27 +3,53 @@
$themes: (
dark: (
background: #141414,
background-light: #202020,
background-line: #202324,
background-fore: #10141f,
window-handle-background: #191919,
window-background: #141414,
window-border: #202324,
window-shadow: #00000077,
button-background: #20202077,
button-hover: #202020aa,
button-active: #202020cc,
hover: #21262d,
selected: #4a4a4b,
border-color: #819796,
border: #202324,
text: #ebede9,
container-shadow: #151d28,
sticky-header-bg: #20202077
sticky-header-bg: #20202077,
icon-invert: 100%
),
light: (
background: #ffffff,
background-light: #f9f9f9,
background-line: #f0f0f0,
background-fore: #ffffff,
window-handle-background: #f0f0f0,
window-background: #ffffff,
window-border: #e0e0e0,
window-shadow: #d4d4d4,
button-background: #f0f0f0,
button-hover: #e9e9e9,
button-active: #d4d4d4,
border-color: #e0e0e0,
border: #f0f0f0,
hover: #e9e9e9,
selected: #d4d4d4,
text: #1e1e1e,
container-shadow: #5f6774,
sticky-header-bg: #fff
sticky-header-bg: #fff,
icon-invert: 0%
)
);

View File

@@ -1,25 +1,31 @@
body {
color: var(--text-color);
color: var(--color-text);
font-family: "BookInsanityRemake", Arial, Helvetica, sans-serif;
}
body {
background-color: var(--background-color);
background-color: var(--color-background);
margin: 0;
}
* {
color: var(--text-color);
color: var(--color-text);
}
a {
color: var(--link-color);
color: var(--color-link);
}
.icon {
height: 12px;
filter: invert(var(--color-icon-invert));
}
* {
font-family: BookInsanityRemake;
}
*::-webkit-scrollbar
{
width: 6px;
@@ -39,6 +45,141 @@ a {
color: var(--error-link);
}
.buttons-row {
width: 100%;
padding-right: 10px;
padding-left: 10px;
display: flex;
flex-direction: row;
justify-content: center;
}
.button-row {
margin-left: 5px;
margin-right: 5px;
flex-grow: 1;
}
.form-field {
padding-bottom: 10px;
display: flex;
align-items: left;
flex-direction: column;
justify-content: left;
}
hr {
border: 0;
height: 1px;
width: 30%;
overflow: visible;
position: relative;
margin: 16px auto 16px auto;
background-color: var(--separator);
}
hr:before {
content: "";
display: inline-block;
width: 8px;
height: 8px;
background-color: var(--separator);
position: absolute;
transform: rotate(45deg);
top: -2.5px;
left: 50%;
margin: -1px 0 0 -1px;
}
input[type=text], input[type=password], input[type=email] {
background-color: var(--color-background-softer);
border: none;
padding: 8px;
border-radius: 6px;
color: var(--color-text);
transition: 300ms background-color;
border: solid 1px var(--color-border);
}
textarea {
background-color: var(--color-background-softer);
padding: 12px;
color: var(--color-text);
border: none;
}
input[type=text]:focus, input[type=password]:focus, input[type=email]:focus {
outline: none;
background-color: var(--color-background-softest);
}
textarea:focus {
outline: none;
}
button {
margin-top: 5px;
margin-bottom: 5px;
padding: 14px;
font-size: 15px;
border-radius: 6px;
outline: none;
border: solid 1px var(--color-border);
-webkit-box-shadow: 0px 0px 10px -2px rgba(0,0,0,0.25);
-moz-box-shadow: 0px 0px 10px -2px rgba(0,0,0,0.25);
box-shadow: 0px 0px 10px -2px rgba(0,0,0,0.25);
transition: 300ms background-color;
background-color: var(--color-button-background);
color: var(--color-text);
}
button:hover {
background-color: var(--color-button-hover);
}
button:active {
background-color: var(--color-button-active);
}
.render-image {
max-width: 600px;
margin-left: auto;
margin-right: auto;
display: block;
}
.confirm-form-button {
margin-top: 15px;
}
.parameters {
display: flex;
flex-direction: column;
width: 100%;
padding: 10px;
}
.param-element {
width: 100%;
display: flex;
flex-direction: row;
}
.param-text {
margin-right: auto;
}
.param-value {
margin-left: auto;
}
.centered {
text-align: center;
}
.window-wrapper {
display: flex;
@@ -52,3 +193,165 @@ a {
-moz-box-shadow: 0px 0px 10px -2px var(--shadow-color);
box-shadow: 0px 0px 10px -2px var(--shadow-color);
}
.document {
text-align: left;
width: 100%;
}
.document.centered {
text-align: center;
justify-content: center;
}
.document.item {
text-align: center;
width: 220px;
}
.document.item img {
width: 64px;
height: 64px;
}
.document h1 {
font-weight: normal;
font-size: 32px;
}
.document b {
font-weight: bold;
}
.text-icon {
height: 18px;
width: 18px;
margin-bottom: -4px;
}
.invert {
filter: invert(0.9);
}
.main-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.row {
width: 100%;
display: flex;
overflow-x: auto;
scrollbar-width: thin;
}
span.important {
font-family: NodestoCapsCondensed;
font-size: 24px;
line-height: 32px;
}
span.common {
color: var(--color-common);
}
span.uncommon {
color: var(--color-uncommon);
}
span.rare {
color: var(--color-rare);
}
span.very-rare {
color: var(--color-very-rare);
}
span.legendary {
color: var(--color-legendary);
}
span.artifact {
color: var(--color-artifact);
}
.form-container {
width: 100%;
}
.form-element {
padding: 10px 0 10px 0;
margin: 0 10px 0 10px;
display: flex;
align-items: center;
border-bottom: 1px dashed var(--color-border);
}
.form-element label {
flex-grow: 0;
margin-right: 6px;
margin-left: 6px;
}
.form-element.centered {
justify-content: center;
}
.grow {
flex-grow: 1;
}
.subsection.border:first-child {
border-left: none;
}
.subsection.border {
border-left: 1px solid var(--color-border);
}
.subsection {
margin-left: 5px;
margin-right: 5px;
height: 32px;
display: flex;
align-items: left;
justify-content: left;
}
.subsection.left {
align-items: left;
justify-content: left;
}
.subsection.right {
align-items: right;
justify-content: right;
}
.subsection.center {
align-items: center;
justify-content: center;
}
.window-enter-active,
.window-leave-active {
transition: all 0.15s ease;
}
.window-enter-from,
.window-leave-to {
opacity: 0;
transform: translateY(15px);
}
.window-wrapper {
background-color: var(--window-background);
/* backdrop-filter: blur(10px); */
position: fixed;
display: flex;
flex-direction: column;
}

View File

@@ -0,0 +1,18 @@
<script setup>
import Content from '../viewer/content/Content.vue';
import StatusBar from '../viewer/statusbar/StatusBar.vue';
import TopBar from '../viewer/TopBar.vue';
import { ShowContent } from '../../services/Content.js';
</script>
<template>
<div v-show="ShowContent">
<TopBar></TopBar>
<Content></Content>
<StatusBar></StatusBar>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,120 @@
<script setup>
import { ref } from 'vue';
import { emitter } from '@/services/Emitter';
const text = ref("");
const toast = ref(null);
let toastQueue = [];
let displayingToast = false;
function DisplayToast(){
if(displayingToast) return;
if(toastQueue.length == 0) return;
displayingToast = true;
let data = toastQueue.pop();
text.value = data.text;
toast.value.classList.add(data.color);
toast.value.classList.add("show");
setTimeout(() => {
toast.value.classList.add("sliding");
setTimeout(() => {
toast.value.style = {};
toast.value.classList.remove("show");
toast.value.classList.remove("sliding");
toast.value.classList.remove(data.color);
displayingToast = false;
DisplayToast();
}, 400);
}, data.duration);
}
emitter.on('toast', data => {
toastQueue.push(data);
DisplayToast();
});
</script>
<template>
<div class="toast" ref="toast">
<div class="toast-container">{{ text }}</div>
</div>
</template>
<style scoped lang="scss">
.toast-container {
height: 100%;
background-color: var(--color-background-soft);
padding: 10px;
margin-left: 5px;
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
transform: translate(2px,0px)
}
.toast {
position: absolute;
display: none;
top: 10px;
left: 50%;
transform: translate(-50%, 0);
min-width: 400px;
min-height: 40px;
border-radius: 6px;
text-align: center;
z-index: 9999999;
animation: slide-in 0.4s ease-in-out;
@keyframes slide-in {
0% {
transform: translate(-50%,-50px);
opacity: 0;
}
100% {
opacity: 1;
}
}
&.sliding {
@keyframes slide-out {
0% {
opacity: 1;
}
100% {
transform: translate(-50%,-50px);
opacity: 0;
}
}
animation: slide-out .4s ease-in-out forwards;
}
&.show {
display: block;
}
/* Colors!!!! */
&.red {
background-color: rgb(243, 68, 68);
}
&.green {
background-color: rgb(92, 199, 92);
}
&.aqua {
background-color: rgb(113, 250, 250);
}
}
</style>

View File

@@ -29,7 +29,7 @@ const windows = Windows();
}
.window-wrapper {
background-color: var(--window-background);
background-color: var(--color-window-background);
/* backdrop-filter: blur(10px); */
position: fixed;
@@ -37,6 +37,15 @@ const windows = Windows();
display: flex;
flex-direction: column;
align-items: center;
border: solid 1px var(--color-window-border);
/* opacity: 0; */
user-select: none;
-webkit-box-shadow: 0px 0px 10px -2px var(--color-window-shadow);
-moz-box-shadow: 0px 0px 10px -2px var(--color-window-shadow);
box-shadow: 0px 0px 10px -2px var(--color-window-shadow);
}
</style>

View File

@@ -21,7 +21,7 @@ import TopSearchBar from './topbar/TopSearchBar.vue';
flex-shrink: 0;
min-height: 40px;
width: 100%;
background-color: var(--top-bar-background-color);
background-color: var(--color-background-light);
display: flex;
}

View File

@@ -1,8 +1,7 @@
<script setup>
import { ref, onMounted } from 'vue';
import Note from './Note.vue';
const emitter = useEmitter();
import { emitter } from '~/services/Emitter';
let noteData = ref([]);
@@ -61,6 +60,7 @@ emitter.on("delete-note", (key) => {
height: 100%;
margin: 0;
height: 100%;
background-color: var(--color-background);
}
</style>

View File

@@ -1,6 +1,7 @@
<script setup>
import { ref,onMounted } from 'vue';
const emitter = useEmitter();
import { emitter } from '~/services/Emitter';
const statusIcon = ref(null);
const statusMessage = ref(null);

View File

@@ -22,7 +22,7 @@ import FetchStatus from './FetchStatus.vue';
min-height: 24px;
max-height: 24px;
width: 100%;
background-color: var(--top-bar-background-color);
background-color: var(--color-background-light);
display: flex;
font-size: 14px;
}

View File

@@ -0,0 +1,42 @@
<script setup>
import { onMounted, ref } from 'vue';
import { SetupHandle, SetSize, ResetPosition } from '@/services/Windows';
import WindowHandle from './partials/WindowHandle.vue';
const handle = ref(null);
const props = defineProps(['data']);
const data = props.data;
let id = data.id;
const test = ref(null)
onMounted(() => {
SetupHandle(id, handle);
SetSize(id, {width: 500, height: 380});
ResetPosition(id, "center");
});
</script>
<template>
<div class="window-wrapper" :id="'window-wrapper-' + id">
<WindowHandle :window="id" ref="handle"></WindowHandle>
<!-- Body -->
<div ref="test"></div>
</div>
</template>
<style scoped>
.window-wrapper {
display: flex;
align-items: center;
}
</style>

View File

@@ -1,8 +1,19 @@
<script setup>
import { onMounted, ref } from 'vue';
import { SetupHandle, SetSize, ResetPosition } from '@/services/Windows';
import {
SetupHandle,
SetSize,
ResetPosition,
SetResizable,
SetMovable,
ClearWindow,
CreateWindow,
} from '@/services/Windows';
import WindowHandle from './partials/WindowHandle.vue';
import { DisplayToast } from '~/services/Toaster';
import Server from '~/services/Server';
import { SetUser } from '~/services/User';
const handle = ref(null);
@@ -11,13 +22,44 @@ const data = props.data;
let id = data.id;
const test = ref(null)
const username = ref("");
const password = ref("");
onMounted(() => {
SetupHandle(id, handle);
SetSize(id, {width: 500, height: 380});
SetSize(id, {width: 450, height: 480});
SetMovable(id, false);
SetResizable(id, false);
ResetPosition(id, "center");
});
function login() {
Server().post('/user/login', { username: username.value, password: password.value }).then((response) => {
const data = response.data;
console.log(data);
if(data.status == "error"){
DisplayToast('red', "Wrong username or password", 3000)
} else {
SetUser(data.token);
ShowMainMenu();
}
}).catch((error) => {
console.log(error);
if(error.response.status == 429){
// errorMessage.value = error.response.data;
} else {
// errorMessage.value = "Hi ha hagut un error intern, torna'ho a provar més tard";
console.log(error);
}
});
}
function toRegister(){
CreateWindow('register');
ClearWindow('login');
}
</script>
@@ -26,8 +68,30 @@ onMounted(() => {
<WindowHandle :window="id" ref="handle"></WindowHandle>
<!-- Body -->
<div ref="test">
<p>Hola</p>
<div class="vert-expand">
<picture align="center">
<source media="(prefers-color-scheme: dark)" srcset="/img/logo-splash.png">
<source media="(prefers-color-scheme: light)" srcset="/img/logo-splash-light.png">
<img alt="Dragonroll logo" src="/img/logo-splash.png" class="splash-image" draggable="false">
</picture>
<form v-on:submit.prevent="login">
<div class="form-field">
<label for="username">Username</label>
<input id="username-field" type="text" placeholder="Enter your username here..." name="username" v-model="username" autocomplete="off" >
</div>
<div class="form-field">
<label for="password">Password</label>
<input id="password-field" type="password" placeholder="Enter your password..." name="password" v-model="password" autocomplete="off" >
</div>
<div class="form-field">
<button class="btn-primary sound-click">Log in</button>
</div>
<div class="form-field center">
<p>You don't have an account? <a href="#" @click.prevent="toRegister">Register</a></p>
</div>
</form>
</div>
</div>
@@ -35,10 +99,41 @@ onMounted(() => {
<style scoped>
p {
user-select: none;
}
.vert-expand {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
}
.window-wrapper {
user-select: none;
display: flex;
align-items: center;
}
.splash-image {
width: 450px;
}
form {
margin-left: 30px;
margin-right: 30px;
}
label {
text-align: left;
}
.center {
text-align: center;
}
</style>

View File

@@ -0,0 +1,42 @@
<script setup>
import { onMounted, ref } from 'vue';
import { SetupHandle, SetSize, ResetPosition } from '@/services/Windows';
import WindowHandle from './partials/WindowHandle.vue';
const handle = ref(null);
const props = defineProps(['data']);
const data = props.data;
let id = data.id;
const test = ref(null)
onMounted(() => {
SetupHandle(id, handle);
SetSize(id, {width: 500, height: 380});
ResetPosition(id, "center");
});
</script>
<template>
<div class="window-wrapper" :id="'window-wrapper-' + id">
<WindowHandle :window="id" ref="handle"></WindowHandle>
<!-- Body -->
<div ref="test"></div>
</div>
</template>
<style scoped>
.window-wrapper {
display: flex;
align-items: center;
}
</style>

View File

@@ -133,7 +133,7 @@ defineExpose({
display: flex;
background-color: var(--color-handler);
background-color: var(--color-window-handle-background);
}
</style>

View File

@@ -0,0 +1,47 @@
import { ref, onMounted, watch } from 'vue'
type Theme = 'light' | 'dark'
type Accent = 'katlum'
const theme = ref<Theme>('light')
const accent = ref<Accent>('katlum')
const applyTheme = () => {
document.documentElement.setAttribute('data-theme', theme.value)
document.documentElement.setAttribute('data-accent', accent.value)
}
const setTheme = (value: Theme) => {
theme.value = value
localStorage.setItem('theme', value)
applyTheme();
}
const setAccent = (value: Accent) => {
accent.value = value
localStorage.setItem('accent', value)
applyTheme();
}
const setupTheme = () => {
const savedTheme = localStorage.getItem('theme') as Theme | null
const savedAccent = localStorage.getItem('accent') as Accent | null
const media = window.matchMedia('(prefers-color-scheme: dark)')
theme.value = savedTheme || (media.matches ? 'dark' : 'light')
accent.value = savedAccent || 'katlum'
applyTheme()
media.addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
theme.value = e.matches ? 'dark' : 'light'
applyTheme()
}
})
};
watch([theme, accent], applyTheme)
export { theme, accent, setTheme, setAccent, setupTheme}

View File

@@ -1,11 +0,0 @@
import mitt from 'mitt'
export default defineNuxtPlugin(() => {
const emitter = mitt()
return {
provide: {
emitter
}
}
})

View File

@@ -0,0 +1,11 @@
var backendUrl = ''
if (import.meta.env.PROD) {
backendUrl = 'https://api.aranroig.com/';
} else {
backendUrl = 'http://localhost:5000/'
}
export {
backendUrl
};

View File

@@ -0,0 +1,12 @@
import { ref } from 'vue';
const ShowContent = ref(false);
function SetShowContent(value) {
ShowContent.value = value;
}
export {
ShowContent,
SetShowContent
}

View File

@@ -0,0 +1,3 @@
import mitt from 'mitt'
export const emitter = mitt();

View File

@@ -0,0 +1,21 @@
import axios from 'axios';
import { backendUrl } from './BackendURL';
const server = axios.create({
baseURL: backendUrl,
headers: {
"Access-Control-Allow-Origin": "*",
}
});
// Attach token dynamically on each request via interceptor
server.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export default () => server;

View File

@@ -0,0 +1,9 @@
import { emitter } from './Emitter';
function DisplayToast(color, text, duration = 1000){
emitter.emit("toast", {color, text, duration});
}
export {
DisplayToast,
}

View File

@@ -0,0 +1,80 @@
import { ref } from 'vue';
import Server from './Server';
const UserStatus = ref(0);
function parseJwt(token) {
return JSON.parse(atob(token.split('.')[1]));
}
function SetUser(token){
localStorage.setItem('token', token);
UserStatus.value = 1;
}
async function HasAdmin(){
let response = await Server().get('/user/has-admin');
return response.data.status != "init";
}
async function SetUserSetting(key, value){
let user = GetUser();
if(!user.settings) user.settings = {};
user.settings[key] = value;
const response = await Server().post('/user/update-settings', { settings: user.settings });
return response.data.settings;
}
async function GetUserSetting(key){
const response = await Server().get('/user/get-settings');
if (response.data.settings)
return response.data.settings[key];
return undefined;
}
function GetUser(){
const token = localStorage.getItem('token');
if(token){
const data = parseJwt(token);
// Check if token is expired
const now = Date.now() / 1000;
if(now > data.exp){
LogoutUser();
return undefined;
}
return data;
}
return undefined;
}
function IsAdmin(){
const user = GetUser();
if(user){
return user.admin;
}
}
function LoadUser(){
const token = localStorage.getItem('token');
if(token) UserStatus.value = 1;
}
function LogoutUser(){
localStorage.removeItem("token");
UserStatus.value = 0;
}
export {
UserStatus,
GetUser,
SetUser,
LoadUser,
IsAdmin,
LogoutUser,
HasAdmin,
GetUserSetting,
SetUserSetting
}

View File

@@ -3,22 +3,29 @@ import { ref } from 'vue'
const windows = ref([]);
import LoginWindow from '~/components/windows/LoginWindow.vue';
import RegisterWindow from '~/components/windows/RegisterWindow.vue';
import ExampleWindow from '~/components/windows/ExampleWindow.vue';
let windowMap = {
login: LoginWindow
login: LoginWindow,
register: RegisterWindow,
example: ExampleWindow
};
async function InjectWindow(window_type, plugin, window_component) {
let systemWidows = {};
systemWidows[window_type] = (await import(`../../plugins/${plugin}/views/${window_component}.vue`)).default;
windowMap = { ...windowMap, ...systemWidows };
}
// Presets
const defValues = {
'example': {
id: "example",
title: "Example",
close: () => ClearWindow('example')
},
'login': {
id: 'login',
title: 'Login',
},
'register': {
id: 'register',
title: 'Register'
}
}
@@ -59,7 +66,9 @@ function SetupHandle(id, handle) {
SetOnTop(id);
});
// Move window listeners
handler.addEventListener("mousedown", (event) => {
if(win.noMove) return;
draggingWindow = true;
let windowRect = currentWindow.getBoundingClientRect();
@@ -67,8 +76,8 @@ function SetupHandle(id, handle) {
offsetY = windowRect.top - event.clientY;
})
// Move window listeners
document.addEventListener("mousemove", (event) => {
if(win.noMove) return;
if (!draggingWindow) return;
if (event.clientX + offsetX < -currentWindow.getBoundingClientRect().width + 20) currentWindow.style.left = (-currentWindow.getBoundingClientRect().width + 20) + "px";
@@ -81,6 +90,7 @@ function SetupHandle(id, handle) {
})
document.addEventListener("mouseup", (event) => {
if(win.noMove) return;
draggingWindow = false;
// ummm suposo que no pots tancar mentres mous?
SaveWindowPos({ id, x: parseInt(currentWindow.style.left, 10), y: parseInt(currentWindow.style.top, 10) });
@@ -126,6 +136,11 @@ function SetResizable(id, resizable) {
win.resizable = resizable;
}
function SetMovable(id, movable) {
let win = GetWindowWithId(id);
win.noMove = !movable;
}
function SetSize(id, size) {
let currentWindowId = "window-wrapper-" + id;
let currentWindow = document.getElementById(currentWindowId);
@@ -284,10 +299,10 @@ export {
SetMaxSize,
SetMinSize,
SetPosition,
SetMovable,
ResetPosition,
Windows,
WindowMap,
InjectWindow,
ReloadRef,
ClearWindows,
CreateWindow,

View File

@@ -4,7 +4,8 @@ export default defineNuxtConfig({
optimizeDeps: {
include: [
'@vue/devtools-core',
'@vue/devtools-kit'
'@vue/devtools-kit',
'axios'
]
}
},

View File

@@ -7,6 +7,7 @@
"name": "dragonroll",
"hasInstallScript": true,
"dependencies": {
"axios": "^1.15.2",
"mitt": "^3.0.1",
"nuxt": "^4.4.2",
"pixelarticons": "^2.1.0",
@@ -3991,6 +3992,12 @@
"integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/autoprefixer": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
@@ -4027,6 +4034,17 @@
"postcss": "^8.1.0"
}
},
"node_modules/axios": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
"integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0"
}
},
"node_modules/b4a": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
@@ -4345,6 +4363,19 @@
"node": ">=8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -4448,6 +4479,18 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -4895,6 +4938,15 @@
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -5037,6 +5089,20 @@
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@@ -5103,6 +5169,15 @@
"integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==",
"license": "MIT"
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
@@ -5118,6 +5193,33 @@
"integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
"license": "MIT"
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
@@ -5359,6 +5461,26 @@
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -5375,6 +5497,43 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
@@ -5469,12 +5628,49 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-port-please": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz",
"integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==",
"license": "MIT"
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stream": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
@@ -5560,6 +5756,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -5604,6 +5812,33 @@
"integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==",
"license": "MIT"
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
@@ -6268,6 +6503,15 @@
"source-map-js": "^1.2.1"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mdn-data": {
"version": "2.27.1",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
@@ -7622,6 +7866,15 @@
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/quansync": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",

View File

@@ -10,6 +10,7 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
"axios": "^1.15.2",
"mitt": "^3.0.1",
"nuxt": "^4.4.2",
"pixelarticons": "^2.1.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB