Compare commits

..

16 commits
v1.7.3 ... main

Author SHA1 Message Date
a7149b2712 1.7.5 2025-07-24 08:57:54 +02:00
e54a6b91e0 Merge pull request 'patches v1.7.5' (#120) from develop into main
Reviewed-on: #120
2025-07-24 06:56:46 +00:00
4621c3e4e0 Merge branch 'main' into develop 2025-07-24 06:56:06 +00:00
9d3348b6d5 enhance: set username and routing on invite and setup 2025-07-24 08:51:36 +02:00
e10bfdd315 fix: navigation with restricted permissions 2025-07-24 08:49:18 +02:00
89bc0f9373 change: UI 2025-07-24 08:11:02 +02:00
d057512514 fix: allow admin user creation on setup with pw or totp 2025-07-24 07:53:31 +02:00
c4cc68fb78 package update and CVE close 2025-07-24 07:41:54 +02:00
167c26afe7 adapt changed version tag format 2025-07-22 08:59:53 +02:00
ff9beda069 1.7.4 2025-07-22 08:24:55 +02:00
720337c9c4 Merge pull request 'patches v1.7.4' (#118) from develop into main
Reviewed-on: #118
2025-07-22 06:24:04 +00:00
a2d1fdf97b Merge branch 'main' into develop 2025-07-22 06:23:50 +00:00
e223e1fd9f package update and CVE close 2025-07-22 08:23:10 +02:00
4d1ec2616d change: view style of new versions 2025-07-22 08:22:41 +02:00
63c9045d98 change: UI text and interaction 2025-07-22 07:50:38 +02:00
e755a4ec37 enhance: allow longpress and contextmenu copy paste 2025-07-22 07:50:26 +02:00
22 changed files with 408 additions and 161 deletions

216
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "ff-admin", "name": "ff-admin",
"version": "1.7.3", "version": "1.7.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ff-admin", "name": "ff-admin",
"version": "1.7.3", "version": "1.7.5",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@fullcalendar/core": "^6.1.18", "@fullcalendar/core": "^6.1.18",
@ -18,7 +18,8 @@
"@headlessui/vue": "^1.7.23", "@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.2.0", "@heroicons/vue": "^2.2.0",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"axios": "^1.10.0", "@vueuse/core": "^13.5.0",
"axios": "^1.11.0",
"event-source-polyfill": "^1.0.31", "event-source-polyfill": "^1.0.31",
"grapesjs": "^0.22.11", "grapesjs": "^0.22.11",
"grapesjs-preset-newsletter": "^1.0.2", "grapesjs-preset-newsletter": "^1.0.2",
@ -41,7 +42,7 @@
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"unplugin-vue-markdown": "^29.1.0", "unplugin-vue-markdown": "^29.1.0",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vue": "^3.5.17", "vue": "^3.5.18",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
@ -55,7 +56,7 @@
"@types/lodash.differencewith": "^4.5.9", "@types/lodash.differencewith": "^4.5.9",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/node": "^24.0.15", "@types/node": "^24.1.0",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/qs": "^6.14.0", "@types/qs": "^6.14.0",
@ -71,7 +72,7 @@
"prettier": "^3.6.2", "prettier": "^3.6.2",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^7.0.5", "vite": "^7.0.6",
"vite-plugin-pwa": "^1.0.1", "vite-plugin-pwa": "^1.0.1",
"vite-plugin-vue-devtools": "^7.7.7", "vite-plugin-vue-devtools": "^7.7.7",
"vue-tsc": "^3.0.3" "vue-tsc": "^3.0.3"
@ -490,12 +491,12 @@
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.27.5", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.27.3" "@babel/types": "^7.28.0"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@ -1759,9 +1760,9 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.27.6", "version": "7.28.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz",
"integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.27.1", "@babel/helper-string-parser": "^7.27.1",
@ -3901,9 +3902,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.0.15", "version": "24.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
"integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==", "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -3967,6 +3968,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.36.0", "version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz",
@ -4335,39 +4342,39 @@
} }
}, },
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
"version": "3.5.17", "version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.17.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz",
"integrity": "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==", "integrity": "sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.27.5", "@babel/parser": "^7.28.0",
"@vue/shared": "3.5.17", "@vue/shared": "3.5.18",
"entities": "^4.5.0", "entities": "^4.5.0",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
} }
}, },
"node_modules/@vue/compiler-dom": { "node_modules/@vue/compiler-dom": {
"version": "3.5.17", "version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.18.tgz",
"integrity": "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==", "integrity": "sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-core": "3.5.17", "@vue/compiler-core": "3.5.18",
"@vue/shared": "3.5.17" "@vue/shared": "3.5.18"
} }
}, },
"node_modules/@vue/compiler-sfc": { "node_modules/@vue/compiler-sfc": {
"version": "3.5.17", "version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.18.tgz",
"integrity": "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==", "integrity": "sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.27.5", "@babel/parser": "^7.28.0",
"@vue/compiler-core": "3.5.17", "@vue/compiler-core": "3.5.18",
"@vue/compiler-dom": "3.5.17", "@vue/compiler-dom": "3.5.18",
"@vue/compiler-ssr": "3.5.17", "@vue/compiler-ssr": "3.5.18",
"@vue/shared": "3.5.17", "@vue/shared": "3.5.18",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"postcss": "^8.5.6", "postcss": "^8.5.6",
@ -4375,13 +4382,13 @@
} }
}, },
"node_modules/@vue/compiler-ssr": { "node_modules/@vue/compiler-ssr": {
"version": "3.5.17", "version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.17.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.18.tgz",
"integrity": "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==", "integrity": "sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.17", "@vue/compiler-dom": "3.5.18",
"@vue/shared": "3.5.17" "@vue/shared": "3.5.18"
} }
}, },
"node_modules/@vue/compiler-vue2": { "node_modules/@vue/compiler-vue2": {
@ -4545,53 +4552,53 @@
} }
}, },
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.5.17", "version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.17.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.18.tgz",
"integrity": "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==", "integrity": "sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/shared": "3.5.17" "@vue/shared": "3.5.18"
} }
}, },
"node_modules/@vue/runtime-core": { "node_modules/@vue/runtime-core": {
"version": "3.5.17", "version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.17.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.18.tgz",
"integrity": "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==", "integrity": "sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/reactivity": "3.5.17", "@vue/reactivity": "3.5.18",
"@vue/shared": "3.5.17" "@vue/shared": "3.5.18"
} }
}, },
"node_modules/@vue/runtime-dom": { "node_modules/@vue/runtime-dom": {
"version": "3.5.17", "version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.17.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.18.tgz",
"integrity": "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==", "integrity": "sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/reactivity": "3.5.17", "@vue/reactivity": "3.5.18",
"@vue/runtime-core": "3.5.17", "@vue/runtime-core": "3.5.18",
"@vue/shared": "3.5.17", "@vue/shared": "3.5.18",
"csstype": "^3.1.3" "csstype": "^3.1.3"
} }
}, },
"node_modules/@vue/server-renderer": { "node_modules/@vue/server-renderer": {
"version": "3.5.17", "version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.17.tgz", "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.18.tgz",
"integrity": "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==", "integrity": "sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-ssr": "3.5.17", "@vue/compiler-ssr": "3.5.18",
"@vue/shared": "3.5.17" "@vue/shared": "3.5.18"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "3.5.17" "vue": "3.5.18"
} }
}, },
"node_modules/@vue/shared": { "node_modules/@vue/shared": {
"version": "3.5.17", "version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.17.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.18.tgz",
"integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==", "integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/tsconfig": { "node_modules/@vue/tsconfig": {
@ -4613,6 +4620,44 @@
} }
} }
}, },
"node_modules/@vueuse/core": {
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.5.0.tgz",
"integrity": "sha512-wV7z0eUpifKmvmN78UBZX8T7lMW53Nrk6JP5+6hbzrB9+cJ3jr//hUlhl9TZO/03bUkMK6gGkQpqOPWoabr72g==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "13.5.0",
"@vueuse/shared": "13.5.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/metadata": {
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.5.0.tgz",
"integrity": "sha512-euhItU3b0SqXxSy8u1XHxUCdQ8M++bsRs+TYhOLDU/OykS7KvJnyIFfep0XM5WjIFry9uAPlVSjmVHiqeshmkw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.5.0.tgz",
"integrity": "sha512-K7GrQIxJ/ANtucxIXbQlUHdB0TPA8c+q5i+zbrjxuhJCnJ9GtBg75sBSnvmLSxHKPg2Yo8w62PWksl9kwH0Q8g==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@ -4778,13 +4823,13 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.10.0", "version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
@ -6409,14 +6454,15 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.2", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0", "es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
}, },
"engines": { "engines": {
@ -10680,14 +10726,14 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.0.5", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
"integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==", "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.6", "fdir": "^6.4.6",
"picomatch": "^4.0.2", "picomatch": "^4.0.3",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rollup": "^4.40.0", "rollup": "^4.40.0",
"tinyglobby": "^0.2.14" "tinyglobby": "^0.2.14"
@ -10887,9 +10933,9 @@
} }
}, },
"node_modules/vite/node_modules/picomatch": { "node_modules/vite/node_modules/picomatch": {
"version": "4.0.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@ -10906,16 +10952,16 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vue": { "node_modules/vue": {
"version": "3.5.17", "version": "3.5.18",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.17.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.18.tgz",
"integrity": "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==", "integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.17", "@vue/compiler-dom": "3.5.18",
"@vue/compiler-sfc": "3.5.17", "@vue/compiler-sfc": "3.5.18",
"@vue/runtime-dom": "3.5.17", "@vue/runtime-dom": "3.5.18",
"@vue/server-renderer": "3.5.17", "@vue/server-renderer": "3.5.18",
"@vue/shared": "3.5.17" "@vue/shared": "3.5.18"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "*" "typescript": "*"

View file

@ -1,10 +1,10 @@
{ {
"name": "ff-admin", "name": "ff-admin",
"version": "1.7.3", "version": "1.7.5",
"description": "Feuerwehr/Verein Mitgliederverwaltung UI", "description": "Feuerwehr/Verein Mitgliederverwaltung UI",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host",
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview", "preview": "vite preview",
"build-only": "vite build", "build-only": "vite build",
@ -33,7 +33,8 @@
"@headlessui/vue": "^1.7.23", "@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.2.0", "@heroicons/vue": "^2.2.0",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"axios": "^1.10.0", "@vueuse/core": "^13.5.0",
"axios": "^1.11.0",
"event-source-polyfill": "^1.0.31", "event-source-polyfill": "^1.0.31",
"grapesjs": "^0.22.11", "grapesjs": "^0.22.11",
"grapesjs-preset-newsletter": "^1.0.2", "grapesjs-preset-newsletter": "^1.0.2",
@ -56,7 +57,7 @@
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"unplugin-vue-markdown": "^29.1.0", "unplugin-vue-markdown": "^29.1.0",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vue": "^3.5.17", "vue": "^3.5.18",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
@ -70,7 +71,7 @@
"@types/lodash.differencewith": "^4.5.9", "@types/lodash.differencewith": "^4.5.9",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/node": "^24.0.15", "@types/node": "^24.1.0",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/qs": "^6.14.0", "@types/qs": "^6.14.0",
@ -86,7 +87,7 @@
"prettier": "^3.6.2", "prettier": "^3.6.2",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^7.0.5", "vite": "^7.0.6",
"vite-plugin-pwa": "^1.0.1", "vite-plugin-pwa": "^1.0.1",
"vite-plugin-vue-devtools": "^7.7.7", "vite-plugin-vue-devtools": "^7.7.7",
"vue-tsc": "^3.0.3" "vue-tsc": "^3.0.3"

View file

@ -2,11 +2,11 @@
<Modal /> <Modal />
<ContextMenu /> <ContextMenu />
<Header @contextmenu.prevent /> <AppHeader />
<div class="grow overflow-x-hidden overflow-y-auto p-2 md:p-4" @contextmenu.prevent> <div class="grow overflow-x-hidden overflow-y-auto p-2 md:p-4">
<RouterView /> <RouterView />
</div> </div>
<Footer @contextmenu.prevent /> <AppFooter />
<Notification /> <Notification />
<Teleport to="head"> <Teleport to="head">
@ -18,10 +18,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { defineAsyncComponent, defineComponent, markRaw } from "vue";
import { onLongPress } from "@vueuse/core";
import { RouterView } from "vue-router"; import { RouterView } from "vue-router";
import Header from "./components/Header.vue"; import AppHeader from "./components/Header.vue";
import Footer from "./components/Footer.vue"; import AppFooter from "./components/Footer.vue";
import { mapActions, mapState } from "pinia"; import { mapActions, mapState } from "pinia";
import { useAuthStore } from "./stores/auth"; import { useAuthStore } from "./stores/auth";
import { isAuthenticatedPromise } from "./router/authGuard"; import { isAuthenticatedPromise } from "./router/authGuard";
@ -31,6 +32,7 @@ import Notification from "./components/Notification.vue";
import { config } from "./config"; import { config } from "./config";
import { useConfigurationStore } from "@/stores/configuration"; import { useConfigurationStore } from "@/stores/configuration";
import { resetAllPiniaStores } from "@/helpers/piniaReset"; import { resetAllPiniaStores } from "@/helpers/piniaReset";
import { useContextMenuStore } from "./stores/context-menu";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -40,6 +42,11 @@ export default defineComponent({
...mapState(useConfigurationStore, ["clubName"]), ...mapState(useConfigurationStore, ["clubName"]),
}, },
mounted() { mounted() {
document.body.addEventListener("contextmenu", (event) => {
this.handleContextMenu(event);
});
onLongPress(document.body, this.handleContextMenu);
resetAllPiniaStores(); resetAllPiniaStores();
this.configure(); this.configure();
@ -52,6 +59,21 @@ export default defineComponent({
}, },
methods: { methods: {
...mapActions(useConfigurationStore, ["configure"]), ...mapActions(useConfigurationStore, ["configure"]),
...mapActions(useContextMenuStore, ["openContextMenu"]),
handleContextMenu(e: MouseEvent) {
e.preventDefault();
// TODO allow contextmenu on elements with special attribute with reduced selection
const target = e.target as HTMLElement | null;
if (!target) return;
if (["INPUT", "TEXTAREA", "P", "H1", "H2", "H3", "H4"].includes((target as HTMLElement).nodeName)) {
this.openContextMenu(e, {
component_ref: markRaw(defineAsyncComponent(() => import("@/components/CopyPasteContextMenu.vue"))),
data: ["INPUT", "TEXTAREA"].includes((e.target as HTMLElement).nodeName) ? "" : "nopaste",
});
}
},
}, },
}); });
</script> </script>

View file

@ -1,10 +1,9 @@
<template> <template>
<div <div
ref="contextMenu" ref="contextMenu"
class="absolute flex flex-col gap-1 border border-gray-400 bg-white rounded-md select-none text-left shadow-md z-50 p-1" class="absolute flex flex-col gap-1 border border-gray-400 bg-white rounded-md select-none text-left shadow-md z-[100] p-1"
v-show="show" v-show="show"
:style="contextMenuStyle" :style="contextMenuStyle"
@contextmenu.prevent
@click="closeContextMenu" @click="closeContextMenu"
> >
<component :is="component_ref" :data="data" /> <component :is="component_ref" :data="data" />

View file

@ -0,0 +1,61 @@
<template>
<div class="flex flex-row gap-2 cursor-pointer hover:bg-gray-300 p-1 rounded-md" @click="copy">
<DocumentDuplicateIcon class="w-5 h-5" />
<p>kopieren</p>
</div>
<div
v-if="data != 'nopaste'"
class="flex flex-row gap-2 cursor-pointer hover:bg-gray-300 p-1 rounded-md"
@click="paste"
>
<ClipboardDocumentIcon class="w-5 h-5" />
<p>einfügen</p>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { ClipboardDocumentIcon, DocumentDuplicateIcon } from "@heroicons/vue/24/outline";
import { mapState } from "pinia";
import { useContextMenuStore } from "@/stores/context-menu";
</script>
<script lang="ts">
export default defineComponent({
props: ["data"],
data() {
return {
selectedText: "",
};
},
computed: {
...mapState(useContextMenuStore, ["clickedOnEl"]),
},
mounted() {
this.selectedText =
document.getSelection()?.toString() || this.clickedOnEl.value || this.clickedOnEl.innerText || "";
let selection = document.getSelection()?.toString();
console.log(selection);
if (selection == "") {
console.log("jo");
const range = document.createRange();
range.selectNode(this.clickedOnEl);
console.log(range);
window.getSelection()?.removeAllRanges();
window.getSelection()?.addRange(range);
}
},
methods: {
copy() {
navigator.clipboard.writeText(this.selectedText);
},
paste() {
const el = this.clickedOnEl;
navigator.clipboard.readText().then((e) => {
el.value = e;
});
},
},
});
</script>

View file

@ -6,7 +6,7 @@
<div class="w-full flex flex-row gap-2 h-full align-middle"> <div class="w-full flex flex-row gap-2 h-full align-middle">
<TopLevelLink <TopLevelLink
v-if="routeName == 'admin' || routeName.includes('admin-')" v-if="routeName == 'admin' || routeName.includes('admin-')"
v-for="item in topLevel" v-for="item in topLevelObject"
:key="item.key" :key="item.key"
:link="item" :link="item"
:disableSubLink="true" :disableSubLink="true"
@ -34,7 +34,7 @@ import TopLevelLink from "./admin/TopLevelLink.vue";
export default defineComponent({ export default defineComponent({
computed: { computed: {
...mapState(useAuthStore, ["authCheck"]), ...mapState(useAuthStore, ["authCheck"]),
...mapState(useNavigationStore, ["topLevel"]), ...mapState(useNavigationStore, ["topLevelObject"]),
routeName() { routeName() {
return typeof this.$route.name == "string" ? this.$route.name : ""; return typeof this.$route.name == "string" ? this.$route.name : "";
}, },

View file

@ -10,7 +10,7 @@
<div v-if="authCheck" class="hidden md:flex flex-row gap-2 h-full align-middle"> <div v-if="authCheck" class="hidden md:flex flex-row gap-2 h-full align-middle">
<TopLevelLink <TopLevelLink
v-if="routeName == 'admin' || routeName.includes('admin-')" v-if="routeName == 'admin' || routeName.includes('admin-')"
v-for="item in topLevel" v-for="item in topLevelObject"
:key="item.key" :key="item.key"
:link="item" :link="item"
/> />
@ -46,7 +46,7 @@ import { useConfigurationStore } from "@/stores/configuration";
export default defineComponent({ export default defineComponent({
computed: { computed: {
...mapState(useAuthStore, ["authCheck"]), ...mapState(useAuthStore, ["authCheck"]),
...mapState(useNavigationStore, ["topLevel"]), ...mapState(useNavigationStore, ["topLevelObject"]),
...mapState(useConfigurationStore, ["clubName"]), ...mapState(useConfigurationStore, ["clubName"]),
routeName() { routeName() {
return typeof this.$route.name == "string" ? this.$route.name : ""; return typeof this.$route.name == "string" ? this.$route.name : "";

View file

@ -3,21 +3,15 @@
ref="contextMenu" ref="contextMenu"
class="absolute inset-0 w-full h-full flex justify-center items-center bg-black/50 select-none z-50 p-2" class="absolute inset-0 w-full h-full flex justify-center items-center bg-black/50 select-none z-50 p-2"
v-show="show" v-show="show"
@contextmenu.prevent
> >
<!-- @click="closeModal" --> <component :is="component_ref" :data="data" class="p-4 bg-white rounded-lg max-h-[95%] overflow-y-auto" />
<component
:is="component_ref"
:data="data"
@click.stop
class="p-4 bg-white rounded-lg max-h-[95%] overflow-y-auto"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { mapState, mapActions } from "pinia"; import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal"; import { useModalStore } from "@/stores/modal";
import { useContextMenuStore } from "@/stores/context-menu";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -27,6 +21,7 @@ export default {
}, },
methods: { methods: {
...mapActions(useModalStore, ["closeModal"]), ...mapActions(useModalStore, ["closeModal"]),
...mapActions(useContextMenuStore, ["closeContextMenu"]),
}, },
}; };
</script> </script>

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="flex relative"> <div class="flex relative">
<input type="text" :value="copyText" /> <input type="text" readonly :value="copyText" />
<ClipboardIcon <ClipboardIcon
class="w-5 h-5 p-2 box-content absolute right-1 top-1/2 -translate-y-1/2 bg-white cursor-pointer" class="w-5 h-5 p-2 box-content absolute right-1 top-1/2 -translate-y-1/2 bg-white cursor-pointer"
@click="copyToClipboard" @click="copyToClipboard"

View file

@ -0,0 +1,52 @@
<template>
<div class="flex flex-col gap-1">
<p>
<span class="font-semibold text-lg">{{ version.title }}</span> vom
{{
new Date(version.isoDate).toLocaleDateString("de", {
month: "2-digit",
day: "2-digit",
year: "numeric",
})
}}
</p>
<div class="versionDisplay flex flex-col" v-html="version['content:encoded']"></div>
</div>
</template>
<script setup lang="ts">
import type { Release } from "@/viewmodels/version.models";
import { defineComponent, type PropType } from "vue";
</script>
<script lang="ts">
export default defineComponent({
props: {
version: {
type: Object as PropType<Release>,
required: true,
},
},
});
</script>
<style lang="css" scoped>
@reference "@/main.css";
.versionDisplay :deep() ul {
list-style: none;
padding-left: 10px;
padding-bottom: 5px;
}
.versionDisplay :deep() ul li::before {
content: "-";
margin-right: 10px;
color: black;
font-weight: 600;
}
.versionDisplay :deep() a {
@apply text-primary;
}
</style>

View file

@ -19,11 +19,16 @@ export async function abilityAndNavUpdate(to: any, from: any, next: any) {
navigation.updateNavigation(); navigation.updateNavigation();
NProgress.done(); NProgress.done();
next(); next();
} else if ((admin && ability.isAdmin()) || ability.can(type, section, module)) { } else if (module && ((admin && ability.isAdmin()) || ability.can(type, section, module))) {
NProgress.done(); NProgress.done();
navigation.activeNavigation = to.name.split("-")[1]; navigation.activeNavigation = to.name.split("-")[1];
navigation.activeLink = to.name.split("-")[2]; navigation.activeLink = to.name.split("-")[2];
next(); next();
} else if (!module && ((admin && ability.isAdmin()) || ability.canSection(type, section))) {
NProgress.done();
navigation.activeNavigation = to.name.split("-")[1];
navigation.activeLink = null;
next();
} else { } else {
NProgress.done(); NProgress.done();
next({ name: "admin-default" }); next({ name: "admin-default" });

View file

@ -33,7 +33,8 @@ export const useAbilityStore = defineStore("ability", {
if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false; if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false;
if (permissions?.admin || permissions?.adminByOwner) return true; if (permissions?.admin || permissions?.adminByOwner) return true;
if ( if (
(permissions[section]?.all == "*" || permissions[section]?.all?.includes(type)) && permissions[section]?.all == "*" ||
permissions[section]?.all?.includes(type) ||
permissions[section] != undefined permissions[section] != undefined
) )
return true; return true;

View file

@ -49,7 +49,8 @@ export const useMembershipStore = defineStore("membership", {
http http
.get(`/admin/member/${memberId}/memberships/totalstatistics`) .get(`/admin/member/${memberId}/memberships/totalstatistics`)
.then((result) => { .then((result) => {
this.totalMembershipStatistics = result.data; if (result.status == 200) this.totalMembershipStatistics = result.data;
else this.totalMembershipStatistics = undefined;
}) })
.catch((err) => {}); .catch((err) => {});
}, },

View file

@ -37,6 +37,11 @@ export const useNavigationStore = defineStore("navigation", {
}; };
}, },
getters: { getters: {
topLevelObject: (state) =>
state.topLevel.map((tl) => ({
...tl,
levelDefault: state.navigation[tl.key].main.filter((m) => !m.key.includes("divider"))[0]?.key ?? "",
})),
activeNavigationObject: (state) => (state.navigation[state.activeNavigation] ?? {}) as navigationSplitModel, activeNavigationObject: (state) => (state.navigation[state.activeNavigation] ?? {}) as navigationSplitModel,
activeTopLevelObject: (state) => activeTopLevelObject: (state) =>
(state.topLevel.find((elem) => elem.key == state.activeNavigation) ?? {}) as topLevelNavigationModel, (state.topLevel.find((elem) => elem.key == state.activeNavigation) ?? {}) as topLevelNavigationModel,

View file

@ -8,6 +8,7 @@ export const useContextMenuStore = defineStore("context-menu", {
show: false, show: false,
component_ref: null as any, component_ref: null as any,
data: null as any, data: null as any,
clickedOnEl: null as any,
}; };
}, },
getters: { getters: {
@ -16,16 +17,18 @@ export const useContextMenuStore = defineStore("context-menu", {
}, },
}, },
actions: { actions: {
openContextMenu(e: MouseEvent, content: { component_ref: any; data: any }) { openContextMenu(e: MouseEvent, content: { component_ref: any; data?: any }) {
this.component_ref = content.component_ref; this.component_ref = content.component_ref;
this.data = content.data; this.data = content.data;
this.contextX = e.pageX; this.contextX = e.pageX;
this.contextY = e.pageY; this.contextY = e.pageY;
this.clickedOnEl = e.target;
this.show = true; this.show = true;
}, },
closeContextMenu() { closeContextMenu() {
this.component_ref = null; this.component_ref = null;
this.data = null; this.data = null;
this.clickedOnEl = null;
this.show = false; this.show = false;
}, },
}, },

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="flex flex-col items-center"> <div class="flex flex-col gap-2 items-center">
<br /> <br />
<h1 class="w-full p-4 text-center font-bold text-3xl">Kein Zugriff</h1> <h1 class="w-full p-4 text-center font-bold text-3xl">Kein Zugriff</h1>
<br /> <br />

View file

@ -24,7 +24,7 @@
type="text" type="text"
name="title" name="title"
id="title" id="title"
placeholder="Entscheidung" placeholder="Beschluss"
autocomplete="off" autocomplete="off"
v-model="item.topic" v-model="item.topic"
@keyup.prevent @keyup.prevent
@ -57,7 +57,7 @@
<QuillEditor <QuillEditor
id="top" id="top"
theme="snow" theme="snow"
placeholder="Entscheidung Inhalt..." placeholder="Beschluss Inhalt..."
style="height: 250px; max-height: 250px; min-height: 250px" style="height: 250px; max-height: 250px; min-height: 250px"
contentType="html" contentType="html"
:toolbar="toolbarOptions" :toolbar="toolbarOptions"

View file

@ -57,8 +57,8 @@
<QuillEditor <QuillEditor
id="top" id="top"
theme="snow" theme="snow"
placeholder="Entscheidung Inhalt..." placeholder="Abstimmung Inhalt..."
style="height: 100px; max-height: 100px; min-height: 100px" style="height: 150px; max-height: 150px; min-height: 150px"
contentType="html" contentType="html"
:toolbar="toolbarOptions" :toolbar="toolbarOptions"
v-model:content="item.context" v-model:content="item.context"

View file

@ -11,7 +11,7 @@
</small> </small>
</h1> </h1>
<p> <p>
V{{ clientVersion }} ({{ v{{ clientVersion }} ({{
new Date(clientVersionRelease).toLocaleDateString("de", { new Date(clientVersionRelease).toLocaleDateString("de", {
month: "2-digit", month: "2-digit",
day: "2-digit", day: "2-digit",
@ -23,19 +23,7 @@
</p> </p>
</div> </div>
<div class="grow flex flex-col gap-4 overflow-y-scroll"> <div class="grow flex flex-col gap-4 overflow-y-scroll">
<div v-for="version in newerClientVersions"> <VersionItem v-for="version in newerClientVersions" :key="version.title" :version="version" />
<p>
<span class="font-semibold text-lg">V{{ version.title }}</span> vom
{{
new Date(version.isoDate).toLocaleDateString("de", {
month: "2-digit",
day: "2-digit",
year: "numeric",
})
}}
</p>
<div class="flex flex-col" v-html="version['content:encoded']"></div>
</div>
<div v-if="newerClientVersions.length == 0" class="flex items-center justify-center"> <div v-if="newerClientVersions.length == 0" class="flex items-center justify-center">
<p>Der Client ist auf der neuesten Version.</p> <p>Der Client ist auf der neuesten Version.</p>
</div> </div>
@ -50,7 +38,7 @@
</small> </small>
</h1> </h1>
<p> <p>
V{{ serverVersion }} ({{ v{{ serverVersion }} ({{
new Date(serverVersionRelease).toLocaleDateString("de", { new Date(serverVersionRelease).toLocaleDateString("de", {
month: "2-digit", month: "2-digit",
day: "2-digit", day: "2-digit",
@ -61,20 +49,8 @@
}}) }})
</p> </p>
</div> </div>
<div class="grow flex flex-col gap-2 overflow-y-scroll"> <div class="grow flex flex-col gap-4 overflow-y-scroll">
<div v-for="version in newerServerVersions"> <VersionItem v-for="version in newerServerVersions" :key="version.title" :version="version" />
<p>
<span class="font-semibold text-lg">V{{ version.title }}</span> vom
{{
new Date(version.isoDate).toLocaleDateString("de", {
month: "2-digit",
day: "2-digit",
year: "numeric",
})
}}
</p>
<div class="flex flex-col" v-html="version['content:encoded']"></div>
</div>
<div v-if="newerServerVersions.length == 0" class="flex items-center justify-center"> <div v-if="newerServerVersions.length == 0" class="flex items-center justify-center">
<p>Der Server ist auf der neuesten Version.</p> <p>Der Server ist auf der neuesten Version.</p>
</div> </div>
@ -90,6 +66,7 @@ import { defineComponent } from "vue";
import MainTemplate from "@/templates/Main.vue"; import MainTemplate from "@/templates/Main.vue";
import clientPackage from "../../../../../package.json"; import clientPackage from "../../../../../package.json";
import type { Releases } from "@/viewmodels/version.models"; import type { Releases } from "@/viewmodels/version.models";
import VersionItem from "@/components/admin/management/version/VersionItem.vue";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -113,11 +90,11 @@ export default defineComponent({
}, },
serverVersionRelease() { serverVersionRelease() {
if (!this.serverRss) return ""; if (!this.serverRss) return "";
return this.serverRss.items.find((i) => i.title == this.serverVersion)?.isoDate ?? ""; return this.serverRss.items.find((i) => i.title == `v${this.serverVersion}`)?.isoDate ?? "";
}, },
clientVersionRelease() { clientVersionRelease() {
if (!this.clientRss) return ""; if (!this.clientRss) return "";
return this.clientRss.items.find((i) => i.title == this.clientVersion)?.isoDate ?? ""; return this.clientRss.items.find((i) => i.title == `v${this.clientVersion}`)?.isoDate ?? "";
}, },
}, },
mounted() { mounted() {

View file

@ -157,6 +157,8 @@ export default defineComponent({
this.inviteStatus = "success"; this.inviteStatus = "success";
localStorage.setItem("accessToken", result.data.accessToken); localStorage.setItem("accessToken", result.data.accessToken);
localStorage.setItem("refreshToken", result.data.refreshToken); localStorage.setItem("refreshToken", result.data.refreshToken);
localStorage.setItem("routine", this.tab);
localStorage.setItem("username", this.username);
setTimeout(() => { setTimeout(() => {
this.$router.push(`/admin`); this.$router.push(`/admin`);
}, 1000); }, 1000);

View file

@ -16,7 +16,7 @@
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<button type="submit" primary :disabled="resetStatus == 'loading' || resetStatus == 'success'"> <button type="submit" primary :disabled="resetStatus == 'loading' || resetStatus == 'success'">
TOTP zurücksetzen Zugangsdaten zurücksetzen
</button> </button>
<Spinner v-if="resetStatus == 'loading'" class="my-auto" /> <Spinner v-if="resetStatus == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="resetStatus == 'success'" /> <SuccessCheckmark v-else-if="resetStatus == 'success'" />

View file

@ -15,6 +15,28 @@
<RouterLink to="/setup" class="text-primary">Zum Einrichtungsstart</RouterLink> <RouterLink to="/setup" class="text-primary">Zum Einrichtungsstart</RouterLink>
</div> </div>
<form v-else class="flex flex-col gap-2" @submit.prevent="setup"> <form v-else class="flex flex-col gap-2" @submit.prevent="setup">
<div class="w-full flex flex-row gap-2 justify-center">
<p
class="w-1/2 p-0.5 pl-0 rounded-lg py-2.5 text-sm text-center font-medium leading-5 outline-hidden cursor-pointer"
:class="
tab == 'totp' ? 'bg-red-200 shadow-sm border-b-2 border-primary rounded-b-none' : ' hover:bg-red-200'
"
@click="tab = 'totp'"
>
TOTP
</p>
<p
class="w-1/2 p-0.5 rounded-lg py-2.5 text-sm text-center font-medium leading-5 outline-hidden cursor-pointer"
:class="
tab == 'password' ? 'bg-red-200 shadow-sm border-b-2 border-primary rounded-b-none' : 'hover:bg-red-200'
"
@click="tab = 'password'"
>
Passwort
</p>
</div>
<p class="text-center">Dein Nutzername: {{ username }}</p>
<div v-if="tab == 'totp'" class="flex flex-col gap-2">
<img :src="image" alt="totp" class="w-56 h-56 self-center" /> <img :src="image" alt="totp" class="w-56 h-56 self-center" />
<TextCopy :copyText="otp" /> <TextCopy :copyText="otp" />
@ -24,6 +46,30 @@
<input id="totp" name="totp" type="text" required placeholder="TOTP" /> <input id="totp" name="totp" type="text" required placeholder="TOTP" />
</div> </div>
</div> </div>
</div>
<div v-else>
<input
id="password"
name="password"
type="password"
required
placeholder="Passwort"
class="rounded-b-none!"
autocomplete="new-password"
:class="notMatching ? 'border-red-600!' : ''"
/>
<input
id="password_rep"
name="password_rep"
type="password"
required
placeholder="Passwort wiederholen"
class="rounded-t-none!"
autocomplete="new-password"
:class="notMatching ? 'border-red-600!' : ''"
/>
<p v-if="notMatching">Passwörter stimmen nicht überein</p>
</div>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<button type="submit" primary :disabled="setupStatus == 'loading' || setupStatus == 'success'"> <button type="submit" primary :disabled="setupStatus == 'loading' || setupStatus == 'success'">
@ -51,6 +97,7 @@ import { RouterLink } from "vue-router";
import FormBottomBar from "@/components/FormBottomBar.vue"; import FormBottomBar from "@/components/FormBottomBar.vue";
import TextCopy from "@/components/TextCopy.vue"; import TextCopy from "@/components/TextCopy.vue";
import AppLogo from "@/components/AppLogo.vue"; import AppLogo from "@/components/AppLogo.vue";
import { hashString } from "@/helpers/crypto";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -61,11 +108,14 @@ export default defineComponent({
}, },
data() { data() {
return { return {
tab: "totp",
verification: "loading" as "success" | "loading" | "failed", verification: "loading" as "success" | "loading" | "failed",
image: undefined as undefined | string, image: undefined as undefined | string,
otp: undefined as undefined | string, otp: undefined as undefined | string,
username: "" as string,
setupStatus: undefined as undefined | "loading" | "success" | "failed", setupStatus: undefined as undefined | "loading" | "success" | "failed",
setupError: "" as string, setupError: "" as string,
notMatching: false as boolean,
}; };
}, },
mounted() { mounted() {
@ -79,6 +129,7 @@ export default defineComponent({
this.verification = "success"; this.verification = "success";
this.image = result.data.dataUrl; this.image = result.data.dataUrl;
this.otp = result.data.otp; this.otp = result.data.otp;
this.username = result.data.username;
}, 1000); }, 1000);
}) })
.catch((err) => { .catch((err) => {
@ -88,20 +139,28 @@ export default defineComponent({
}); });
}, },
methods: { methods: {
setup(e: any) { async setup(e: any) {
let formData = e.target.elements; let secret = "";
if (this.tab == "totp") secret = this.totp(e);
else secret = await this.password(e);
if (secret == "") return;
this.setupStatus = "loading"; this.setupStatus = "loading";
this.setupError = ""; this.setupError = "";
this.$http this.$http
.post(`/setup/finish`, { .post(`/setup/finish`, {
token: this.token, token: this.token,
mail: this.mail, mail: this.mail,
totp: formData.totp.value, secret: secret,
routine: this.tab,
}) })
.then((result) => { .then((result) => {
this.setupStatus = "success"; this.setupStatus = "success";
localStorage.setItem("accessToken", result.data.accessToken); localStorage.setItem("accessToken", result.data.accessToken);
localStorage.setItem("refreshToken", result.data.refreshToken); localStorage.setItem("refreshToken", result.data.refreshToken);
localStorage.setItem("routine", this.tab);
localStorage.setItem("username", this.username);
setTimeout(() => { setTimeout(() => {
this.$router.push(`/admin`); this.$router.push(`/admin`);
}, 1000); }, 1000);
@ -111,6 +170,24 @@ export default defineComponent({
this.setupError = err.response.data; this.setupError = err.response.data;
}); });
}, },
totp(e: any) {
let formData = e.target.elements;
return formData.totp.value;
},
async password(e: any) {
let formData = e.target.elements;
let new_pw = await hashString(formData.password.value);
let new_rep = await hashString(formData.password_rep.value);
if (new_pw != new_rep) {
this.notMatching = true;
return "";
}
this.notMatching = false;
return await hashString(formData.password.value);
},
}, },
}); });
</script> </script>