Compare commits

..

2 Commits

Author SHA1 Message Date
b69e571202 Enough for today
All checks were successful
Build and Deploy Nuxt / build (push) Successful in 49s
2026-04-26 00:52:59 +02:00
c3e5448597 backup 2026-04-26 00:08:27 +02:00
120 changed files with 4482 additions and 84 deletions

View File

@@ -0,0 +1,39 @@
name: Build and Deploy Nuxt
on:
push:
branches: [master]
jobs:
build:
runs-on: docker
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build frontend
run: |
docker build -t git.aranroig.com/${{ secrets.REGISTRY_USER }}/dragonroll-frontend:latest ./frontend
docker push git.aranroig.com/${{ secrets.REGISTRY_USER }}/dragonroll-frontend:latest
- name: Build backend
run: |
docker build -t git.aranroig.com/${{ secrets.REGISTRY_USER }}/dragonroll-backend:latest ./backend
docker push git.aranroig.com/${{ secrets.REGISTRY_USER }}/dragonroll-backend:latest
# - name: Copy files
# run: |
# scp docker-compose.yml deploy@${{ secrets.DEPLOY_HOST}}:/var/www/app/
# scp nginx.conf deploy@${{ secrets.DEPLOY_HOST }}:/var/www/app/nginx.conf
#- name: Deploy
# run: |
# ssh deploy@${{ secrets.DEPLOY_HOST }} << 'EOF'
# echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.aranroig.com -u "${{ secrets.REGISTRY_USER }}" --password-stdin
# cd /var/www/app/
# docker-compose pull
# docker-compose up -d
# EOF

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;

6
frontend/18n.config.ts Normal file
View File

@@ -0,0 +1,6 @@
// i18n.config.ts
export default defineI18nConfig(() => ({
fallbackLocale: 'en',
missingWarn: true,
fallbackWarn: true
}))

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;
@@ -51,4 +192,166 @@ a {
-webkit-box-shadow: 0px 0px 10px -2px var(--shadow-color);
-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

@@ -1,6 +1,6 @@
<script setup>
import { TransitionGroup } from 'vue'
import { Windows, ReloadRef, WindowMap } from '@/services/Windows';
import { Windows, ReloadRef, WindowMap, getComponent } from '@/services/Windows';
// Gestionem ventanas
const reload = ReloadRef();
@@ -11,7 +11,7 @@ const windows = Windows();
<template>
<div class="window-container" :key="reload">
<TransitionGroup name="window">
<component v-for="win in windows" :is="WindowMap()[win.type]" :key="win.id" :data="win"></component>
<component v-for="win in windows" :is="getComponent(win.type)" :key="win.id" :data="win"></component>
</TransitionGroup>
</div>
</template>
@@ -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.type;
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,23 +1,64 @@
<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);
const props = defineProps(['data']);
const data = props.data;
let id = data.id;
let id = data.type;
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});
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 +67,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">{{$t('login.username')}}</label>
<input id="username-field" type="text" :placeholder="$t('login.username-placeholder')" name="username" v-model="username" autocomplete="off" >
</div>
<div class="form-field">
<label for="password">{{$t('login.password')}}</label>
<input id="password-field" type="password" :placeholder="$t('login.password-placeholder')" name="password" v-model="password" autocomplete="off" >
</div>
<div class="form-field">
<button class="btn-primary sound-click">{{$t('login.log-in')}}</button>
</div>
<div class="form-field center">
<p>{{$t('login.no-account')}} <a href="#" @click.prevent="toRegister">{{$t('login.register')}}</a></p>
</div>
</form>
</div>
</div>
@@ -35,10 +98,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.type;
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

@@ -5,6 +5,7 @@ import { GetWindowWithId, ClearWindow, Windows } from '@/services/Windows';
import ArrowLeftIcon from '/icons/iconoir/regular/arrow-left.svg';
import XMarkIcon from '/icons/iconoir/regular/xmark.svg';
import ResizeHandleIcon from '/icons/ui/resize-handle.svg';
import { AddSound } from '~/services/Sound';
const props = defineProps(['window', 'handleHeight', 'custom', 'color']);
const id = props.window;
@@ -25,7 +26,7 @@ let backFunction;
function setupHandle() {
let win = GetWindowWithId(id);
if(win.title) title.value = win.title;
if(win.title) title.value = $t(win.title);
if(win.close){
close.value = true;
closeAction = win.close;
@@ -43,6 +44,8 @@ function setupHandle() {
// Setup sounds
let currentWindowId = "window-wrapper-" + id;
let currentWindow = document.getElementById(currentWindowId);
AddSound(currentWindow);
}
function CloseButton(){
@@ -133,7 +136,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,15 @@
function AddSound(element){
let soundClicks = element.getElementsByClassName("sound-click");
for (let i = 0; i < soundClicks.length; i++) {
soundClicks[i].addEventListener("click", async (event) => {
const audio = new Audio('/sounds/snap.wav');
audio.type = "audio/wav"
audio.play().catch((e)=>{});
})
}
}
export {
AddSound
};

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

@@ -0,0 +1,24 @@
/*
Put here all dragonroll windows
*/
const defWindows = {
login: {
title: 'Login',
movable: false,
component: () => import('~/components/windows/LoginWindow.vue'),
},
register: {
title: 'Register',
movable: false,
component: () => import('~/components/windows/RegisterWindow.vue'),
},
example: {
title: 'Example',
component: () => import('~/components/windows/ExampleWindow.vue'),
}
}
export {
defWindows
}

View File

@@ -1,26 +1,9 @@
import { ref } from 'vue'
import { defWindows } from './WindowDefinitions';
const windows = ref([]);
import LoginWindow from '~/components/windows/LoginWindow.vue';
let windowMap = {
login: LoginWindow
};
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 = {
'login': {
id: 'login',
title: 'Login',
}
}
const getComponent = (type) => defineAsyncComponent(defWindows[type]?.component)
const reload = ref(0);
@@ -29,11 +12,12 @@ let Windows = () => { return windows };
let WindowMap = () => { return windowMap };
let currentIndex = 10;
let currentId = 0;
function SetupHandle(id, handle) {
// Update window info with handle info
console.log(id);
let win = GetWindowWithId(id);
let currentWindowId = "window-wrapper-" + id;
@@ -59,7 +43,9 @@ function SetupHandle(id, handle) {
SetOnTop(id);
});
// Move window listeners
handler.addEventListener("mousedown", (event) => {
if(!win.movable) return;
draggingWindow = true;
let windowRect = currentWindow.getBoundingClientRect();
@@ -67,8 +53,8 @@ function SetupHandle(id, handle) {
offsetY = windowRect.top - event.clientY;
})
// Move window listeners
document.addEventListener("mousemove", (event) => {
if(!win.movable) return;
if (!draggingWindow) return;
if (event.clientX + offsetX < -currentWindow.getBoundingClientRect().width + 20) currentWindow.style.left = (-currentWindow.getBoundingClientRect().width + 20) + "px";
@@ -81,6 +67,7 @@ function SetupHandle(id, handle) {
})
document.addEventListener("mouseup", (event) => {
if(!win.movable) 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) });
@@ -118,6 +105,13 @@ function SetupHandle(id, handle) {
win.height = parseInt(currentWindow.style.height, 10);
});
// Should move eventually?
window.addEventListener('resize', (event) => {
if(!win.movable){
ResetPosition(id, "center");
}
})
handle.value.setupHandle();
}
@@ -126,6 +120,11 @@ function SetResizable(id, resizable) {
win.resizable = resizable;
}
function SetMovable(id, movable) {
let win = GetWindowWithId(id);
win.movable = movable;
}
function SetSize(id, size) {
let currentWindowId = "window-wrapper-" + id;
let currentWindow = document.getElementById(currentWindowId);
@@ -196,35 +195,38 @@ function ResetPosition(id, pos) {
function CreateWindow(type, data = {}) {
let finalData = { ...{ type }, ...defValues[type], ...data }
let finalData = { ...{ type, id: currentId }, ...defWindows[type], ...data }
console.log(finalData);
let contains = false;
for (let i = 0; i < windows.value.length; i++) {
if (windows.value[i].id == finalData.id) {
if (windows.value[i].type == finalData.type) {
contains = true;
console.log("It contains")
break;
}
}
if (!contains) {
windows.value.push(finalData);
console.log("Pushed ", finalData.id);
currentId++;
console.log(finalData);
console.log("Pushed ", finalData.type);
// reload.value += 1;
console.log(windows.value);
setTimeout(() => {
SetOnTop(finalData.id);
SetOnTop(finalData.type);
if (finalData.create) finalData.create();
}, 0);
}
}
function CreateChildWindow(parentId, type, data = {}) {
let finalData = { ...{ type }, ...defValues[type], ...data }
let finalData = { ...{ type }, ...defWindows[type], ...data }
let parent = GetWindowWithId(parentId);
if (parent.children) parent.children.push(finalData.id);
else parent.children = [finalData.id];
if (parent.children) parent.children.push(finalData.type);
else parent.children = [finalData.type];
CreateWindow(type, data);
}
@@ -236,22 +238,23 @@ function ClearAll() {
function ClearWindows(data) {
for (let i = 0; i < windows.value.length; i++) {
ClearWindow(windows.value[i].id);
ClearWindow(windows.value[i].type);
}
// reload.value += 1;
}
function ClearWindow(id) {
let win = GetWindowWithId(id);
console.log(win);
if (!win) return;
if (win.children) for (let i = 0; i < win.children.length; i++) ClearWindow(win.children[i]);
windows.value = windows.value.filter((e) => { return e.id !== id });
windows.value = windows.value.filter((e) => { return e.type !== id });
// reload.value += 1;
}
function GetWindowWithId(id) {
for (let i = 0; i < windows.value.length; i++) {
if (windows.value[i].id == id) {
if (windows.value[i].type == id) {
return windows.value[i];
}
}
@@ -273,8 +276,10 @@ function SetOnTop(id) {
let currentWindowId = "window-wrapper-" + id;
let currentWindow = document.getElementById(currentWindowId);
currentIndex += 1;
currentWindow.style.zIndex = currentIndex;
try {
currentIndex += 1;
currentWindow.style.zIndex = currentIndex;
} catch(e) {}
}
export {
@@ -284,10 +289,10 @@ export {
SetMaxSize,
SetMinSize,
SetPosition,
SetMovable,
ResetPosition,
Windows,
WindowMap,
InjectWindow,
ReloadRef,
ClearWindows,
CreateWindow,
@@ -297,5 +302,6 @@ export {
SaveWindowPos,
GetPosition,
ClearWindow,
ClearAll
ClearAll,
getComponent
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,16 @@
{
"windows": {
"login": "Login",
"register": "Register",
"example": "Example Window"
},
"login": {
"username": "Username",
"username-placeholder": "Enter your username here...",
"password": "Password",
"password-placeholder": "Enter your password...",
"log-in": "Log in",
"no-account": "You don't have an account?",
"register": "Register"
}
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -4,7 +4,9 @@ export default defineNuxtConfig({
optimizeDeps: {
include: [
'@vue/devtools-core',
'@vue/devtools-kit'
'@vue/devtools-kit',
'axios',
'mitt'
]
}
},
@@ -23,4 +25,16 @@ export default defineNuxtConfig({
apiBaseUrl: process.env.API_BASE_URL || 'http://localhost:5000/api'
}
},
i18n: {
locales: [
{ code: 'en', iso: 'en-US', name: '🇺🇸 English', file: 'en.json' },
{ code: 'es', iso: 'es-ES', name: '🇪🇸 Spanish', file: 'es.json' },
{ code: 'ca', iso: 'ca-ES', name: '🇦🇩 Catalan', file: 'ca.json' }
],
defaultLocale: 'en',
vueI18n: './i18n.config.ts',
langDir: 'locales/'
},
modules: ['@nuxtjs/i18n']
})

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,8 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxtjs/i18n": "^10.3.0",
"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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More