Merge pull request 'Overhaul settings and implement user preferences. Also implements dark theme toggle as part of the user settings.' (#19) from feat/user-preferences into main
This commit is contained in:
commit
b400c3c2bb
27 changed files with 1343 additions and 606 deletions
|
@ -31,6 +31,10 @@
|
|||
"pdfkit": "^0.17.0",
|
||||
"pdfmake": "^0.2.18",
|
||||
"pinia": "^3.0.2",
|
||||
"pino": "^9.6.0",
|
||||
"pino-abstract-transport": "^2.0.0",
|
||||
"pino-http": "^10.4.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"quasar": "^2.16.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vue": "^3.4.18",
|
||||
|
|
168
pnpm-lock.yaml
generated
168
pnpm-lock.yaml
generated
|
@ -62,6 +62,18 @@ importers:
|
|||
pinia:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))
|
||||
pino:
|
||||
specifier: ^9.6.0
|
||||
version: 9.6.0
|
||||
pino-abstract-transport:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
pino-http:
|
||||
specifier: ^10.4.0
|
||||
version: 10.4.0
|
||||
pino-pretty:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
quasar:
|
||||
specifier: ^2.16.0
|
||||
version: 2.18.1
|
||||
|
@ -876,6 +888,10 @@ packages:
|
|||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
atomic-sleep@1.0.0:
|
||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
autoprefixer@10.4.21:
|
||||
resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
@ -1059,6 +1075,9 @@ packages:
|
|||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
colorette@2.0.20:
|
||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||
|
||||
colorjs.io@0.5.2:
|
||||
resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==}
|
||||
|
||||
|
@ -1152,6 +1171,9 @@ packages:
|
|||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||
|
||||
dateformat@4.6.3:
|
||||
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
|
||||
|
||||
debug@2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
|
@ -1447,6 +1469,9 @@ packages:
|
|||
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
fast-copy@3.0.2:
|
||||
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
|
@ -1466,6 +1491,13 @@ packages:
|
|||
fast-levenshtein@2.0.6:
|
||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||
|
||||
fast-redact@3.5.0:
|
||||
resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
fast-safe-stringify@2.1.1:
|
||||
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
|
||||
|
||||
fastq@1.19.1:
|
||||
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
||||
|
||||
|
@ -1645,6 +1677,9 @@ packages:
|
|||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
help-me@5.0.0:
|
||||
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
||||
|
||||
hookable@5.5.3:
|
||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||
|
||||
|
@ -1803,6 +1838,10 @@ packages:
|
|||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
joycon@3.1.1:
|
||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
jpeg-exif@1.1.4:
|
||||
resolution: {integrity: sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==}
|
||||
|
||||
|
@ -2075,6 +2114,10 @@ packages:
|
|||
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
on-exit-leak-free@2.1.2:
|
||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
on-finished@2.4.1:
|
||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
@ -2196,6 +2239,23 @@ packages:
|
|||
typescript:
|
||||
optional: true
|
||||
|
||||
pino-abstract-transport@2.0.0:
|
||||
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
|
||||
|
||||
pino-http@10.4.0:
|
||||
resolution: {integrity: sha512-vjQsKBE+VN1LVchjbfLE7B6nBeGASZNRNKsR68VS0DolTm5R3zo+47JX1wjm0O96dcbvA7vnqt8YqOWlG5nN0w==}
|
||||
|
||||
pino-pretty@13.0.0:
|
||||
resolution: {integrity: sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==}
|
||||
hasBin: true
|
||||
|
||||
pino-std-serializers@7.0.0:
|
||||
resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
|
||||
|
||||
pino@9.6.0:
|
||||
resolution: {integrity: sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==}
|
||||
hasBin: true
|
||||
|
||||
pkg-types@1.3.1:
|
||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||
|
||||
|
@ -2244,6 +2304,9 @@ packages:
|
|||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
process-warning@4.0.1:
|
||||
resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==}
|
||||
|
||||
process@0.11.10:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
@ -2287,6 +2350,9 @@ packages:
|
|||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
quick-format-unescaped@4.0.4:
|
||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||
|
||||
random-bytes@1.0.0:
|
||||
resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
@ -2328,6 +2394,10 @@ packages:
|
|||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
|
||||
real-require@0.2.0:
|
||||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
||||
engines: {node: '>= 12.13.0'}
|
||||
|
||||
regexp.prototype.flags@1.5.4:
|
||||
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
@ -2396,6 +2466,10 @@ packages:
|
|||
safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
safe-stable-stringify@2.5.0:
|
||||
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
|
@ -2530,6 +2604,9 @@ packages:
|
|||
sax@1.4.1:
|
||||
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
|
||||
|
||||
secure-json-parse@2.7.0:
|
||||
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
|
||||
|
||||
selderee@0.11.0:
|
||||
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
|
||||
|
||||
|
@ -2617,6 +2694,9 @@ packages:
|
|||
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
sonic-boom@4.2.0:
|
||||
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -2636,6 +2716,10 @@ packages:
|
|||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
split2@4.2.0:
|
||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||
engines: {node: '>= 10.x'}
|
||||
|
||||
stack-trace@1.0.0-pre2:
|
||||
resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==}
|
||||
engines: {node: '>=16'}
|
||||
|
@ -2723,6 +2807,9 @@ packages:
|
|||
text-decoder@1.2.3:
|
||||
resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
|
||||
|
||||
thread-stream@3.1.0:
|
||||
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
||||
|
||||
tiny-inflate@1.0.3:
|
||||
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
|
||||
|
||||
|
@ -3854,6 +3941,8 @@ snapshots:
|
|||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
atomic-sleep@1.0.0: {}
|
||||
|
||||
autoprefixer@10.4.21(postcss@8.5.3):
|
||||
dependencies:
|
||||
browserslist: 4.24.4
|
||||
|
@ -4060,6 +4149,8 @@ snapshots:
|
|||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
colorette@2.0.20: {}
|
||||
|
||||
colorjs.io@0.5.2: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
|
@ -4149,6 +4240,8 @@ snapshots:
|
|||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
dateformat@4.6.3: {}
|
||||
|
||||
debug@2.6.9:
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
|
@ -4499,6 +4592,8 @@ snapshots:
|
|||
iconv-lite: 0.4.24
|
||||
tmp: 0.0.33
|
||||
|
||||
fast-copy@3.0.2: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-diff@1.3.0: {}
|
||||
|
@ -4517,6 +4612,10 @@ snapshots:
|
|||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fast-redact@3.5.0: {}
|
||||
|
||||
fast-safe-stringify@2.1.1: {}
|
||||
|
||||
fastq@1.19.1:
|
||||
dependencies:
|
||||
reusify: 1.1.0
|
||||
|
@ -4717,6 +4816,8 @@ snapshots:
|
|||
|
||||
he@1.2.0: {}
|
||||
|
||||
help-me@5.0.0: {}
|
||||
|
||||
hookable@5.5.3: {}
|
||||
|
||||
html-minifier-terser@7.2.0:
|
||||
|
@ -4876,6 +4977,8 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
joycon@3.1.1: {}
|
||||
|
||||
jpeg-exif@1.1.4: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
@ -5129,6 +5232,8 @@ snapshots:
|
|||
|
||||
object-keys@1.1.1: {}
|
||||
|
||||
on-exit-leak-free@2.1.2: {}
|
||||
|
||||
on-finished@2.4.1:
|
||||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
|
@ -5259,6 +5364,49 @@ snapshots:
|
|||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
|
||||
pino-abstract-transport@2.0.0:
|
||||
dependencies:
|
||||
split2: 4.2.0
|
||||
|
||||
pino-http@10.4.0:
|
||||
dependencies:
|
||||
get-caller-file: 2.0.5
|
||||
pino: 9.6.0
|
||||
pino-std-serializers: 7.0.0
|
||||
process-warning: 4.0.1
|
||||
|
||||
pino-pretty@13.0.0:
|
||||
dependencies:
|
||||
colorette: 2.0.20
|
||||
dateformat: 4.6.3
|
||||
fast-copy: 3.0.2
|
||||
fast-safe-stringify: 2.1.1
|
||||
help-me: 5.0.0
|
||||
joycon: 3.1.1
|
||||
minimist: 1.2.8
|
||||
on-exit-leak-free: 2.1.2
|
||||
pino-abstract-transport: 2.0.0
|
||||
pump: 3.0.2
|
||||
secure-json-parse: 2.7.0
|
||||
sonic-boom: 4.2.0
|
||||
strip-json-comments: 3.1.1
|
||||
|
||||
pino-std-serializers@7.0.0: {}
|
||||
|
||||
pino@9.6.0:
|
||||
dependencies:
|
||||
atomic-sleep: 1.0.0
|
||||
fast-redact: 3.5.0
|
||||
on-exit-leak-free: 2.1.2
|
||||
pino-abstract-transport: 2.0.0
|
||||
pino-std-serializers: 7.0.0
|
||||
process-warning: 4.0.1
|
||||
quick-format-unescaped: 4.0.4
|
||||
real-require: 0.2.0
|
||||
safe-stable-stringify: 2.5.0
|
||||
sonic-boom: 4.2.0
|
||||
thread-stream: 3.1.0
|
||||
|
||||
pkg-types@1.3.1:
|
||||
dependencies:
|
||||
confbox: 0.1.8
|
||||
|
@ -5315,6 +5463,8 @@ snapshots:
|
|||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
process-warning@4.0.1: {}
|
||||
|
||||
process@0.11.10: {}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
|
@ -5349,6 +5499,8 @@ snapshots:
|
|||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
quick-format-unescaped@4.0.4: {}
|
||||
|
||||
random-bytes@1.0.0: {}
|
||||
|
||||
randombytes@2.1.0:
|
||||
|
@ -5405,6 +5557,8 @@ snapshots:
|
|||
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
real-require@0.2.0: {}
|
||||
|
||||
regexp.prototype.flags@1.5.4:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
|
@ -5482,6 +5636,8 @@ snapshots:
|
|||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
safe-stable-stringify@2.5.0: {}
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
sass-embedded-android-arm64@1.87.0:
|
||||
|
@ -5580,6 +5736,8 @@ snapshots:
|
|||
|
||||
sax@1.4.1: {}
|
||||
|
||||
secure-json-parse@2.7.0: {}
|
||||
|
||||
selderee@0.11.0:
|
||||
dependencies:
|
||||
parseley: 0.12.1
|
||||
|
@ -5698,6 +5856,10 @@ snapshots:
|
|||
dependencies:
|
||||
semver: 7.7.1
|
||||
|
||||
sonic-boom@4.2.0:
|
||||
dependencies:
|
||||
atomic-sleep: 1.0.0
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map-support@0.5.21:
|
||||
|
@ -5711,6 +5873,8 @@ snapshots:
|
|||
|
||||
speakingurl@14.0.1: {}
|
||||
|
||||
split2@4.2.0: {}
|
||||
|
||||
stack-trace@1.0.0-pre2: {}
|
||||
|
||||
statuses@2.0.1: {}
|
||||
|
@ -5813,6 +5977,10 @@ snapshots:
|
|||
dependencies:
|
||||
b4a: 1.6.7
|
||||
|
||||
thread-stream@3.1.0:
|
||||
dependencies:
|
||||
real-require: 0.2.0
|
||||
|
||||
tiny-inflate@1.0.3: {}
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
|
10
prisma/migrations/20250425181700_add_log_table/migration.sql
Normal file
10
prisma/migrations/20250425181700_add_log_table/migration.sql
Normal file
|
@ -0,0 +1,10 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "Log" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"level" TEXT NOT NULL,
|
||||
"message" TEXT NOT NULL,
|
||||
"meta" JSONB,
|
||||
|
||||
CONSTRAINT "Log_pkey" PRIMARY KEY ("id")
|
||||
);
|
|
@ -129,3 +129,11 @@ model Session {
|
|||
data String
|
||||
expiresAt DateTime @map("expires_at")
|
||||
}
|
||||
|
||||
model Log {
|
||||
id Int @id @default(autoincrement())
|
||||
timestamp DateTime @default(now())
|
||||
level String
|
||||
message String
|
||||
meta Json? // Optional field for additional structured data
|
||||
}
|
||||
|
|
|
@ -90,7 +90,7 @@ export default defineConfig((/* ctx */) =>
|
|||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework
|
||||
framework: {
|
||||
config: {
|
||||
dark: true
|
||||
dark: 'auto'
|
||||
},
|
||||
|
||||
// iconSet: 'material-icons', // Quasar icon set
|
||||
|
|
|
@ -4,4 +4,16 @@ import { PrismaClient } from '@prisma/client';
|
|||
const prisma = new PrismaClient();
|
||||
|
||||
// Export the Prisma Client instance for use in other modules
|
||||
export default prisma;
|
||||
export default prisma;
|
||||
|
||||
// Helper function for consistent error handling
|
||||
export const handlePrismaError = (res, err, context) =>
|
||||
{
|
||||
console.error(`Error ${context}:`, err.message);
|
||||
// Basic error handling, can be expanded (e.g., check for Prisma-specific error codes)
|
||||
if (err.code === 'P2025')
|
||||
{ // Prisma code for record not found
|
||||
return res.status(404).json({ error: `${context}: Record not found` });
|
||||
}
|
||||
res.status(500).json({ error: `Failed to ${context}: ${err.message}` });
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
import { Router } from 'express';
|
||||
import prisma from '../database.js';
|
||||
import { handlePrismaError } from '../database.js';
|
||||
import PDFDocument from 'pdfkit';
|
||||
import { join } from 'path';
|
||||
import { generateTodaysSummary } from '../services/mantisSummarizer.js';
|
||||
|
@ -9,18 +10,6 @@ const router = Router();
|
|||
|
||||
const __dirname = new URL('.', import.meta.url).pathname.replace(/\/$/, '');
|
||||
|
||||
// Helper function for consistent error handling
|
||||
const handlePrismaError = (res, err, context) =>
|
||||
{
|
||||
console.error(`Error ${context}:`, err.message);
|
||||
// Basic error handling, can be expanded (e.g., check for Prisma-specific error codes)
|
||||
if (err.code === 'P2025')
|
||||
{ // Prisma code for record not found
|
||||
return res.status(404).json({ error: `${context}: Record not found` });
|
||||
}
|
||||
res.status(500).json({ error: `Failed to ${context}: ${err.message}` });
|
||||
};
|
||||
|
||||
// --- Forms API --- //
|
||||
|
||||
// GET /api/forms - List all forms
|
||||
|
@ -632,7 +621,6 @@ router.post('/mantis-summaries/generate', async(req, res) =>
|
|||
generateTodaysSummary()
|
||||
.then(() =>
|
||||
{
|
||||
console.log('Summary generation process finished successfully (async).');
|
||||
})
|
||||
.catch(error =>
|
||||
{
|
||||
|
@ -647,59 +635,4 @@ router.post('/mantis-summaries/generate', async(req, res) =>
|
|||
}
|
||||
});
|
||||
|
||||
// --- Settings API --- //
|
||||
|
||||
// GET /api/settings/:key - Get a specific setting value
|
||||
router.get('/settings/:key', async(req, res) =>
|
||||
{
|
||||
const { key } = req.params;
|
||||
try
|
||||
{
|
||||
const setting = await prisma.setting.findUnique({
|
||||
where: { key: key },
|
||||
select: { value: true }
|
||||
});
|
||||
|
||||
if (setting !== null)
|
||||
{
|
||||
res.json({ key, value: setting.value });
|
||||
}
|
||||
else
|
||||
{
|
||||
res.json({ key, value: '' }); // Return empty value if not found
|
||||
}
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
handlePrismaError(res, err, `fetch setting '${key}'`);
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/settings/:key - Update or create a specific setting
|
||||
router.put('/settings/:key', async(req, res) =>
|
||||
{
|
||||
const { key } = req.params;
|
||||
const { value } = req.body;
|
||||
|
||||
if (typeof value === 'undefined')
|
||||
{
|
||||
return res.status(400).json({ error: 'Setting value is required in the request body' });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const upsertedSetting = await prisma.setting.upsert({
|
||||
where: { key: key },
|
||||
update: { value: String(value) },
|
||||
create: { key: key, value: String(value) },
|
||||
select: { key: true, value: true } // Select to return the updated/created value
|
||||
});
|
||||
res.status(200).json(upsertedSetting);
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
handlePrismaError(res, err, `update setting '${key}'`);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -48,7 +48,8 @@ async function getAuthenticatorByCredentialID(credentialID)
|
|||
// Generate Registration Options
|
||||
router.post('/generate-registration-options', async(req, res) =>
|
||||
{
|
||||
const { username } = req.body;
|
||||
// Destructure username, email, and fullName from the request body
|
||||
const { username, email, fullName } = req.body;
|
||||
|
||||
if (!username)
|
||||
{
|
||||
|
@ -59,13 +60,18 @@ router.post('/generate-registration-options', async(req, res) =>
|
|||
{
|
||||
let user = await getUserByUsername(username);
|
||||
|
||||
// If user doesn't exist, create one
|
||||
// If user doesn't exist, create one with the provided details
|
||||
if (!user)
|
||||
{
|
||||
const userData = { username };
|
||||
if (email) userData.email = email; // Add email if provided
|
||||
if (fullName) userData.fullName = fullName; // Add fullName if provided
|
||||
|
||||
user = await prisma.user.create({
|
||||
data: { username },
|
||||
data: userData,
|
||||
});
|
||||
}
|
||||
// ... rest of the existing logic ...
|
||||
|
||||
const userAuthenticators = await getUserAuthenticators(user.id);
|
||||
|
||||
|
@ -107,6 +113,11 @@ router.post('/generate-registration-options', async(req, res) =>
|
|||
catch (error)
|
||||
{
|
||||
console.error('Registration options error:', error);
|
||||
// Handle potential Prisma unique constraint errors (e.g., email already exists)
|
||||
if (error.code === 'P2002' && error.meta?.target?.includes('email'))
|
||||
{
|
||||
return res.status(409).json({ error: 'Email address is already in use.' });
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to generate registration options' });
|
||||
}
|
||||
});
|
||||
|
@ -147,8 +158,6 @@ router.post('/verify-registration', async(req, res) =>
|
|||
|
||||
const { verified, registrationInfo } = verification;
|
||||
|
||||
console.log(verification);
|
||||
|
||||
if (verified && registrationInfo)
|
||||
{
|
||||
const { credential, credentialDeviceType, credentialBackedUp } = registrationInfo;
|
||||
|
@ -190,12 +199,18 @@ router.post('/verify-registration', async(req, res) =>
|
|||
}
|
||||
else
|
||||
{
|
||||
// This else block was previously misplaced before the if block
|
||||
res.status(400).json({ error: 'Registration verification failed' });
|
||||
}
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Registration verification error:', error);
|
||||
// Handle potential Prisma unique constraint errors (e.g., email already exists)
|
||||
if (error.code === 'P2002' && error.meta?.target?.includes('email'))
|
||||
{
|
||||
return res.status(409).json({ error: 'Email address is already in use.' });
|
||||
}
|
||||
challengeStore.delete(userId); // Clean up challenge on error
|
||||
delete req.session.userId;
|
||||
res.status(500).json({ error: 'Failed to verify registration', details: error.message });
|
||||
|
@ -225,12 +240,8 @@ router.post('/generate-authentication-options', async(req, res) =>
|
|||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
console.log('User found:', user);
|
||||
|
||||
const userAuthenticators = await getUserAuthenticators(user.id);
|
||||
|
||||
console.log('User authenticators:', userAuthenticators);
|
||||
|
||||
const options = await generateAuthenticationOptions({
|
||||
rpID,
|
||||
// Require users to use a previously-registered authenticator
|
||||
|
@ -437,7 +448,8 @@ router.get('/status', async(req, res) =>
|
|||
{});
|
||||
return res.status(401).json({ status: 'unauthenticated' });
|
||||
}
|
||||
return res.json({ status: 'authenticated', user: { id: user.id, username: user.username, email: user.email } });
|
||||
// Include email and fullName in the response
|
||||
return res.json({ status: 'authenticated', user: { id: user.id, username: user.username, email: user.email, fullName: user.fullName } });
|
||||
}
|
||||
res.json({ status: 'unauthenticated' });
|
||||
});
|
||||
|
|
51
src-server/routes/settings.js
Normal file
51
src-server/routes/settings.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import express from 'express';
|
||||
import { getSetting, setSetting, getUserPreference, setUserPreference } from '../utils/settings.js';
|
||||
import { handlePrismaError } from '../database.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
||||
// --- Settings API --- //
|
||||
|
||||
// GET /api/settings/:key - Get a specific setting value
|
||||
router.get('/:key', async(req, res) =>
|
||||
{
|
||||
const { key } = req.params;
|
||||
try
|
||||
{
|
||||
const setting = await getSetting(key);
|
||||
if (!setting)
|
||||
{
|
||||
return res.status(404).json({ error: `Setting '${key}' not found` });
|
||||
}
|
||||
res.status(200).json(setting);
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
handlePrismaError(res, err, `fetch setting '${key}'`);
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/settings/:key - Update or create a specific setting
|
||||
router.put('/:key', async(req, res) =>
|
||||
{
|
||||
const { key } = req.params;
|
||||
const { value } = req.body;
|
||||
|
||||
if (typeof value === 'undefined')
|
||||
{
|
||||
return res.status(400).json({ error: 'Setting value is required in the request body' });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await setSetting(key, value);
|
||||
res.status(200).json({ message: `Setting '${key}' updated successfully` });
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
handlePrismaError(res, err, `update setting '${key}'`);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
57
src-server/routes/userPreferences.js
Normal file
57
src-server/routes/userPreferences.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
import express from 'express';
|
||||
|
||||
import { getUserPreference, setUserPreference } from '../utils/settings.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// GET /api/user-preferences/:key - Get a user preference for the logged-in user
|
||||
router.get('/:key', async(req, res) =>
|
||||
{
|
||||
const { key } = req.params;
|
||||
const userId = req.session?.loggedInUserId;
|
||||
if (!userId)
|
||||
{
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
try
|
||||
{
|
||||
const value = await getUserPreference(userId, key);
|
||||
if (typeof value === 'undefined' || value === null)
|
||||
{
|
||||
return res.status(404).json({ error: `Preference '${key}' not found for user` });
|
||||
}
|
||||
res.status(200).json({ key, value });
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
handlePrismaError(res, err, `fetch user preference '${key}'`);
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/user-preferences/:key - Set a user preference for the logged-in user
|
||||
router.put('/:key', async(req, res) =>
|
||||
{
|
||||
const { key } = req.params;
|
||||
const { value } = req.body;
|
||||
const userId = req.session?.loggedInUserId;
|
||||
if (!userId)
|
||||
{
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
if (typeof value === 'undefined')
|
||||
{
|
||||
return res.status(400).json({ error: 'Preference value is required in the request body' });
|
||||
}
|
||||
try
|
||||
{
|
||||
await setUserPreference(userId, key, value);
|
||||
res.status(200).json({ message: `Preference '${key}' updated for user` });
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
handlePrismaError(res, err, `update user preference '${key}'`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default router;
|
|
@ -12,33 +12,82 @@
|
|||
import dotenv from 'dotenv';
|
||||
import express from 'express';
|
||||
import compression from 'compression';
|
||||
import session from 'express-session'; // Added for session management
|
||||
import { PrismaSessionStore } from '@quixo3/prisma-session-store'; // Import Prisma session store
|
||||
import { PrismaClient } from '@prisma/client'; // Import Prisma Client
|
||||
import { v4 as uuidv4 } from 'uuid'; // Added for generating session IDs
|
||||
import session from 'express-session';
|
||||
import { PrismaSessionStore } from '@quixo3/prisma-session-store';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import pino from 'pino';
|
||||
import pinoHttp from 'pino-http';
|
||||
import apiRoutes from './routes/api.js';
|
||||
import authRoutes from './routes/auth.js'; // Added for WebAuthn routes
|
||||
import chatRoutes from './routes/chat.js'; // Added for Chat routes
|
||||
import authRoutes from './routes/auth.js';
|
||||
import chatRoutes from './routes/chat.js';
|
||||
import settingsRoutes from './routes/settings.js';
|
||||
import userPreferencesRoutes from './routes/userPreferences.js';
|
||||
import cron from 'node-cron';
|
||||
import { generateAndStoreMantisSummary } from './services/mantisSummarizer.js';
|
||||
import { requireAuth } from './middlewares/authMiddleware.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Initialize Pino logger
|
||||
const targets = [];
|
||||
|
||||
// Console logging (pretty-printed in development)
|
||||
if (process.env.NODE_ENV !== 'production')
|
||||
{
|
||||
targets.push({
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
colorize: true
|
||||
},
|
||||
level: process.env.LOG_LEVEL || 'info'
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Basic console logging in production
|
||||
targets.push({
|
||||
target: 'pino/file', // Log to stdout in production
|
||||
options: { destination: 1 }, // 1 is stdout
|
||||
level: process.env.LOG_LEVEL || 'info'
|
||||
});
|
||||
}
|
||||
|
||||
// Database logging via custom transport
|
||||
targets.push({
|
||||
target: './utils/prisma-pino-transport.js', // Path to the custom transport
|
||||
options: {}, // No specific options needed for this transport
|
||||
level: process.env.DB_LOG_LEVEL || 'info' // Separate level for DB logging if needed
|
||||
});
|
||||
|
||||
const logger = pino({
|
||||
level: process.env.LOG_LEVEL || 'info', // Overall minimum level
|
||||
transport: {
|
||||
targets: targets
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize pino-http middleware
|
||||
const httpLogger = pinoHttp({ logger });
|
||||
|
||||
// Define Relying Party details (Update with your actual details)
|
||||
export const rpID = process.env.NODE_ENV === 'production' ? 'your-production-domain.com' : 'localhost';
|
||||
export const rpID = process.env.NODE_ENV === 'production' ? 'stylepoint.uk' : 'localhost';
|
||||
export const rpName = 'StylePoint';
|
||||
export const origin = process.env.NODE_ENV === 'production' ? `https://${rpID}` : `http://${rpID}:9000`;
|
||||
|
||||
// In-memory store for challenges (Replace with a persistent store in production)
|
||||
export const challengeStore = new Map();
|
||||
|
||||
const prisma = new PrismaClient(); // Instantiate Prisma Client
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const app = express();
|
||||
|
||||
// Add pino-http middleware
|
||||
app.use(httpLogger);
|
||||
|
||||
if(!process.env.SESSION_SECRET)
|
||||
{
|
||||
console.error('SESSION_SECRET environment variable is not set. Please set it to a strong secret key.');
|
||||
logger.error('SESSION_SECRET environment variable is not set. Please set it to a strong secret key.');
|
||||
process.exit(1); // Exit the process if the secret is not set
|
||||
}
|
||||
|
||||
|
@ -67,15 +116,14 @@ app.use(session({
|
|||
// Run daily at 1:00 AM server time (adjust as needed)
|
||||
cron.schedule('0 1 * * *', async() =>
|
||||
{
|
||||
console.log('Running scheduled Mantis summary task...');
|
||||
try
|
||||
{
|
||||
await generateAndStoreMantisSummary();
|
||||
console.log('Scheduled Mantis summary task completed.');
|
||||
logger.info('Scheduled Mantis summary task completed successfully.');
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error running scheduled Mantis summary task:', error);
|
||||
logger.error({ error }, 'Error running scheduled Mantis summary task');
|
||||
}
|
||||
}, {
|
||||
scheduled: true,
|
||||
|
@ -90,12 +138,12 @@ app.disable('x-powered-by');
|
|||
app.use(express.json());
|
||||
|
||||
// Add API routes
|
||||
app.use('/api', apiRoutes);
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/chat', chatRoutes);
|
||||
app.use('/api/chat', requireAuth, chatRoutes);
|
||||
app.use('/api/user-preferences', requireAuth, userPreferencesRoutes);
|
||||
app.use('/api/settings', requireAuth, settingsRoutes);
|
||||
app.use('/api', requireAuth, apiRoutes);
|
||||
|
||||
// place here any middlewares that
|
||||
// absolutely need to run before anything else
|
||||
if (process.env.PROD)
|
||||
{
|
||||
app.use(compression());
|
||||
|
@ -105,5 +153,5 @@ app.use(express.static('public', { index: false }));
|
|||
|
||||
app.listen(8000, () =>
|
||||
{
|
||||
console.log('Server is running on http://localhost:8000');
|
||||
logger.info('Server is running on http://localhost:8000');
|
||||
});
|
|
@ -99,16 +99,11 @@ export async function generateAndStoreMantisSummary()
|
|||
{
|
||||
try
|
||||
{
|
||||
// Get the prompt from the database settings using Prisma
|
||||
const setting = await prisma.setting.findUnique({
|
||||
where: { key: 'mantisPrompt' },
|
||||
select: { value: true }
|
||||
});
|
||||
const promptTemplate = setting?.value;
|
||||
const promptTemplate = await getSetting('MANTIS_PROMPT');
|
||||
|
||||
if (!promptTemplate)
|
||||
{
|
||||
console.error('Mantis prompt not found in database settings (key: mantisPrompt). Skipping summary generation.');
|
||||
console.error('Mantis prompt not found in database settings (key: MANTIS_PROMPT). Skipping summary generation.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -118,16 +113,13 @@ export async function generateAndStoreMantisSummary()
|
|||
if (tickets.length === 0)
|
||||
{
|
||||
summaryText = 'No Mantis tickets updated recently.';
|
||||
console.log('No recent Mantis tickets found.');
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log(`Found ${tickets.length} recent Mantis tickets. Generating summary...`);
|
||||
let prompt = promptTemplate.replaceAll('$DATE', new Date().toISOString().split('T')[0]);
|
||||
prompt = prompt.replaceAll('$MANTIS_TICKETS', JSON.stringify(tickets, null, 2));
|
||||
|
||||
summaryText = await askGemini(prompt);
|
||||
console.log('Mantis summary generated successfully by AI.');
|
||||
}
|
||||
|
||||
// Store the summary in the database using Prisma upsert
|
||||
|
@ -144,7 +136,6 @@ export async function generateAndStoreMantisSummary()
|
|||
summaryText: summaryText,
|
||||
},
|
||||
});
|
||||
console.log(`Mantis summary for ${today.toISOString().split('T')[0]} stored/updated in the database.`);
|
||||
|
||||
}
|
||||
catch (error)
|
||||
|
|
|
@ -10,8 +10,6 @@ export async function askGemini(content)
|
|||
|
||||
const GOOGLE_API_KEY = await getSetting('GEMINI_API_KEY');
|
||||
|
||||
console.log('Google API Key:', GOOGLE_API_KEY); // Debugging line to check the key
|
||||
|
||||
if (!GOOGLE_API_KEY)
|
||||
{
|
||||
throw new Error('Google API key is not set in the database.');
|
||||
|
|
47
src-server/utils/prisma-pino-transport.js
Normal file
47
src-server/utils/prisma-pino-transport.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import build from 'pino-abstract-transport';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default async function(opts)
|
||||
{
|
||||
return build(async(source) =>
|
||||
{
|
||||
for await (const obj of source)
|
||||
{
|
||||
try
|
||||
{
|
||||
const { time, level, msg, ...meta } = obj;
|
||||
// Pino levels are numeric, convert to string names if needed
|
||||
const levelMap = {
|
||||
'10': 'trace',
|
||||
'20': 'debug',
|
||||
'30': 'info',
|
||||
'40': 'warn',
|
||||
'50': 'error',
|
||||
'60': 'fatal'
|
||||
};
|
||||
const levelString = levelMap[level] || 'info'; // Default to info
|
||||
|
||||
await prisma.log.create({
|
||||
data: {
|
||||
timestamp: new Date(time),
|
||||
level: levelString,
|
||||
message: msg,
|
||||
// Store remaining properties in the meta field if it exists
|
||||
meta: Object.keys(meta).length > 0 ? meta : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Failed to write log to database:', error);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
async close(err)
|
||||
{
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -7,14 +7,44 @@ export async function getSetting(key)
|
|||
select: { value: true }
|
||||
});
|
||||
|
||||
console.log(`getSetting(${key})`, setting);
|
||||
|
||||
return setting?.value ? JSON.parse(setting.value) : null;
|
||||
}
|
||||
|
||||
export async function setSetting(key, value)
|
||||
{
|
||||
//Replace all CRLFs with LF
|
||||
if (typeof value === 'string')
|
||||
{
|
||||
value = value.replace(/\r\n/g, '\n').trim();
|
||||
}
|
||||
|
||||
await prisma.setting.upsert({
|
||||
where: { key },
|
||||
update: { value: JSON.stringify(value) },
|
||||
create: { key, value }
|
||||
create: { key, value: JSON.stringify(value) }
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserPreference(userId, key)
|
||||
{
|
||||
const pref = await prisma.userPreference.findUnique({
|
||||
where: { userId_key: { userId, key } },
|
||||
select: { value: true }
|
||||
});
|
||||
return pref?.value ? JSON.parse(pref.value) : null;
|
||||
}
|
||||
|
||||
export async function setUserPreference(userId, key, value)
|
||||
{
|
||||
if (typeof value === 'string')
|
||||
{
|
||||
value = value.replace(/\r\n/g, '\n').trim();
|
||||
}
|
||||
await prisma.userPreference.upsert({
|
||||
where: { userId_key: { userId, key } },
|
||||
update: { value: JSON.stringify(value) },
|
||||
create: { userId, key, value: JSON.stringify(value) }
|
||||
});
|
||||
}
|
18
src/App.vue
18
src/App.vue
|
@ -4,12 +4,22 @@
|
|||
|
||||
<script setup>
|
||||
import { useAuthStore } from './stores/auth';
|
||||
import { usePreferencesStore } from './stores/preferences';
|
||||
|
||||
defineOptions({
|
||||
preFetch()
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const preferencesStore = usePreferencesStore();
|
||||
|
||||
onMounted(async() =>
|
||||
{
|
||||
// Check if user is authenticated
|
||||
if (!authStore.isAuthenticated)
|
||||
{
|
||||
const authStore = useAuthStore();
|
||||
return authStore.checkAuthStatus();
|
||||
authStore.checkAuthStatus();
|
||||
}
|
||||
|
||||
// Load user preferences
|
||||
await preferencesStore.loadPreferences();
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -10,12 +10,13 @@
|
|||
clickable
|
||||
v-ripple
|
||||
@click="toggleLeftDrawer"
|
||||
class="relative"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="menu" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-h6">
|
||||
<q-item-section v-if="leftDrawerOpen">
|
||||
<q-item-label class="text-h4 absolute-center">
|
||||
StylePoint
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
@ -25,16 +26,41 @@
|
|||
v-if="leftDrawerOpen"
|
||||
bordered
|
||||
flat
|
||||
class="q-ma-sm text-center"
|
||||
class="q-ma-sm text-center relative"
|
||||
>
|
||||
<q-card-section>
|
||||
<q-btn
|
||||
class="absolute"
|
||||
style="top: 10px; left: 10px;"
|
||||
flat
|
||||
round
|
||||
:to="{ name: 'passkeys' }"
|
||||
>
|
||||
<q-icon
|
||||
name="key"
|
||||
/>
|
||||
<q-tooltip>
|
||||
Passkey Management
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
class="absolute"
|
||||
style="top: 10px; right: 10px;"
|
||||
flat
|
||||
round
|
||||
:to="{ name: 'userPreferences' }"
|
||||
>
|
||||
<q-icon
|
||||
name="mdi-account-cog"
|
||||
/>
|
||||
<q-tooltip>
|
||||
User Preferences
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-avatar
|
||||
class="bg-primary cursor-pointer text-white"
|
||||
>
|
||||
<q-icon name="mdi-account" />
|
||||
<q-tooltip>
|
||||
{{ authStore.user.username }}
|
||||
</q-tooltip>
|
||||
</q-avatar>
|
||||
<div class="text-h6">
|
||||
{{ authStore.user.username }}
|
||||
|
@ -54,6 +80,50 @@
|
|||
class="menu-list"
|
||||
v-else
|
||||
>
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
dense
|
||||
:to="{ name: 'passkeys' }"
|
||||
class="q-mb-sm"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="center right"
|
||||
self="center left"
|
||||
>
|
||||
<span>Passkey Management</span>
|
||||
</q-tooltip>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="key" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-h6">
|
||||
Passkey Management
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
dense
|
||||
:to="{ name: 'userPreferences' }"
|
||||
class="q-mb-sm"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="center right"
|
||||
self="center left"
|
||||
>
|
||||
<span>User Preferences</span>
|
||||
</q-tooltip>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="mdi-account-cog" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-h6">
|
||||
User Preferences
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
|
@ -123,7 +193,7 @@
|
|||
:offset="[18, 18]"
|
||||
>
|
||||
<q-fab
|
||||
v-model="chatStore.isChatVisible"
|
||||
v-model="fabOpen"
|
||||
icon="chat"
|
||||
color="accent"
|
||||
direction="up"
|
||||
|
@ -200,6 +270,8 @@ const router = useRouter();
|
|||
const authStore = useAuthStore(); // Use the auth store
|
||||
const chatStore = useChatStore();
|
||||
|
||||
const fabOpen = ref(false); // Local state for FAB animation, not chat visibility
|
||||
|
||||
// Computed properties to get state from the store
|
||||
const isChatVisible = computed(() => chatStore.isChatVisible);
|
||||
const chatMessages = computed(() => chatStore.chatMessages);
|
||||
|
|
|
@ -1,96 +1,100 @@
|
|||
<template>
|
||||
<q-page padding>
|
||||
<div class="q-mb-md row justify-between items-center">
|
||||
<div class="text-h4">
|
||||
Forms
|
||||
</div>
|
||||
<q-btn
|
||||
outline
|
||||
label="Create New Form"
|
||||
color="primary"
|
||||
:to="{ name: 'formCreate' }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-list
|
||||
<q-card
|
||||
flat
|
||||
bordered
|
||||
separator
|
||||
v-if="forms.length > 0"
|
||||
>
|
||||
<q-item
|
||||
v-for="form in forms"
|
||||
:key="form.id"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ form.title }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ form.description || 'No description' }}
|
||||
</q-item-label>
|
||||
<q-item-label caption>
|
||||
Created: {{ formatDate(form.createdAt) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<div class="q-gutter-sm">
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="edit_note"
|
||||
color="info"
|
||||
:to="{ name: 'formFill', params: { id: form.id } }"
|
||||
title="Fill Form"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="visibility"
|
||||
color="secondary"
|
||||
:to="{ name: 'formResponses', params: { id: form.id } }"
|
||||
title="View Responses"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="edit"
|
||||
color="warning"
|
||||
:to="{ name: 'formEdit', params: { id: form.id } }"
|
||||
title="Edit Form"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="delete"
|
||||
color="negative"
|
||||
@click.stop="confirmDeleteForm(form.id)"
|
||||
title="Delete Form"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-banner
|
||||
v-else
|
||||
class="bg-info text-white"
|
||||
>
|
||||
<template #avatar>
|
||||
<q-icon
|
||||
name="info"
|
||||
color="white"
|
||||
<q-card-section class="row items-center justify-between">
|
||||
<div class="text-h4">
|
||||
Forms
|
||||
</div>
|
||||
<q-btn
|
||||
label="Create New Form"
|
||||
color="primary"
|
||||
:to="{ name: 'formCreate' }"
|
||||
/>
|
||||
</template>
|
||||
No forms created yet. Click the button above to create your first form.
|
||||
</q-banner>
|
||||
</q-card-section>
|
||||
|
||||
<q-inner-loading :showing="loading">
|
||||
<q-spinner-gears
|
||||
size="50px"
|
||||
color="primary"
|
||||
/>
|
||||
</q-inner-loading>
|
||||
<q-list
|
||||
bordered
|
||||
separator
|
||||
v-if="forms.length > 0"
|
||||
>
|
||||
<q-item
|
||||
v-for="form in forms"
|
||||
:key="form.id"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ form.title }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ form.description || 'No description' }}
|
||||
</q-item-label>
|
||||
<q-item-label caption>
|
||||
Created: {{ formatDate(form.createdAt) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<div class="q-gutter-sm">
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="edit_note"
|
||||
color="info"
|
||||
:to="{ name: 'formFill', params: { id: form.id } }"
|
||||
title="Fill Form"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="visibility"
|
||||
color="secondary"
|
||||
:to="{ name: 'formResponses', params: { id: form.id } }"
|
||||
title="View Responses"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="edit"
|
||||
color="warning"
|
||||
:to="{ name: 'formEdit', params: { id: form.id } }"
|
||||
title="Edit Form"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="delete"
|
||||
color="negative"
|
||||
@click.stop="confirmDeleteForm(form.id)"
|
||||
title="Delete Form"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-banner
|
||||
v-else
|
||||
class="bg-info text-white"
|
||||
>
|
||||
<template #avatar>
|
||||
<q-icon
|
||||
name="info"
|
||||
color="white"
|
||||
/>
|
||||
</template>
|
||||
No forms created yet. Click the button above to create your first form.
|
||||
</q-banner>
|
||||
|
||||
<q-inner-loading :showing="loading">
|
||||
<q-spinner-gears
|
||||
size="50px"
|
||||
color="primary"
|
||||
/>
|
||||
</q-inner-loading>
|
||||
</q-card>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ const $q = useQuasar();
|
|||
const currentYear = ref(new Date().getFullYear());
|
||||
|
||||
const features = ref([
|
||||
'Auatomated Daily Reports',
|
||||
'Automated Daily Reports',
|
||||
'Deep Mantis Integration',
|
||||
'Easy Authentication',
|
||||
'And more..?'
|
||||
|
|
|
@ -80,12 +80,10 @@ async function handleLogin()
|
|||
|
||||
if (verificationRes.data.verified)
|
||||
{
|
||||
// Update the auth store on successful login
|
||||
authStore.isAuthenticated = true;
|
||||
authStore.user = verificationRes.data.user;
|
||||
authStore.error = null; // Clear any previous errors
|
||||
console.log('Login successful:', verificationRes.data.user);
|
||||
router.push('/'); // Redirect to home page
|
||||
authStore.error = null;
|
||||
router.push('/');
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
bordered
|
||||
>
|
||||
<q-card-section class="row items-center justify-between">
|
||||
<div class="text-h6">
|
||||
<div class="text-h4">
|
||||
Mantis Summaries
|
||||
</div>
|
||||
<q-btn
|
||||
|
|
|
@ -1,133 +1,152 @@
|
|||
<template>
|
||||
<q-page padding>
|
||||
<div class="q-mb-md row justify-between items-center">
|
||||
<div class="text-h4">
|
||||
Passkey Management
|
||||
</div>
|
||||
<div>
|
||||
<q-btn
|
||||
label="Identify Passkey"
|
||||
color="secondary"
|
||||
class="q-mx-md q-mt-md"
|
||||
@click="handleIdentify"
|
||||
:loading="identifyLoading"
|
||||
:disable="identifyLoading || !isLoggedIn"
|
||||
outline
|
||||
/>
|
||||
<q-btn
|
||||
label="Register New Passkey"
|
||||
color="primary"
|
||||
class="q-mx-md q-mt-md"
|
||||
@click="handleRegister"
|
||||
:loading="registerLoading"
|
||||
:disable="registerLoading || !isLoggedIn"
|
||||
outline
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<q-card
|
||||
bordered
|
||||
flat
|
||||
>
|
||||
<q-card-section class="row items-center justify-between">
|
||||
<div class="text-h4">
|
||||
Passkey Management
|
||||
</div>
|
||||
<div>
|
||||
<q-btn
|
||||
label="Identify Passkey"
|
||||
color="secondary"
|
||||
class="q-mx-md"
|
||||
@click="handleIdentify"
|
||||
:loading="identifyLoading"
|
||||
:disable="identifyLoading || !isLoggedIn"
|
||||
/>
|
||||
<q-btn
|
||||
label="Register New Passkey"
|
||||
color="primary"
|
||||
@click="handleRegister"
|
||||
:loading="registerLoading"
|
||||
:disable="registerLoading || !isLoggedIn"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<!-- Passkey List Section -->
|
||||
<q-card-section>
|
||||
<h5>Your Registered Passkeys</h5>
|
||||
<q-list
|
||||
bordered
|
||||
separator
|
||||
v-if="passkeys.length > 0 && !fetchLoading"
|
||||
>
|
||||
<q-item v-if="registerSuccessMessage || registerErrorMessage">
|
||||
<div
|
||||
v-if="registerSuccessMessage"
|
||||
class="text-positive q-mt-md"
|
||||
>
|
||||
{{ registerSuccessMessage }}
|
||||
</div>
|
||||
<div
|
||||
v-if="registerErrorMessage"
|
||||
class="text-negative q-mt-md"
|
||||
>
|
||||
{{ registerErrorMessage }}
|
||||
</div>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-for="passkey in passkeys"
|
||||
:key="passkey.credentialID"
|
||||
:class="{ 'bg-info text-h6': identifiedPasskeyId === passkey.credentialID }"
|
||||
<q-separator />
|
||||
|
||||
<q-card-section>
|
||||
<div class="text-h6 q-mb-sm">
|
||||
Registered Passkeys
|
||||
</div>
|
||||
|
||||
<!-- Display registration messages above the grid -->
|
||||
<div
|
||||
v-if="registerSuccessMessage"
|
||||
class="text-positive q-mb-md"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label>Passkey ID: {{ passkey.credentialID }} </q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-if="identifiedPasskeyId === passkey.credentialID"
|
||||
>
|
||||
Verified just now!
|
||||
</q-item-label>
|
||||
<!-- <q-item-label caption>Registered: {{ new Date(passkey.createdAt).toLocaleDateString() }}</q-item-label> -->
|
||||
</q-item-section>
|
||||
{{ registerSuccessMessage }}
|
||||
</div>
|
||||
<div
|
||||
v-if="registerErrorMessage"
|
||||
class="text-negative q-mb-md"
|
||||
>
|
||||
{{ registerErrorMessage }}
|
||||
</div>
|
||||
|
||||
<q-item-section
|
||||
side
|
||||
class="row no-wrap items-center"
|
||||
<!-- Responsive Grid for Passkeys -->
|
||||
<div
|
||||
v-if="passkeys.length > 0 && !fetchLoading"
|
||||
class="row q-col-gutter-md justify-center align-center"
|
||||
>
|
||||
<div
|
||||
v-for="passkey in passkeys"
|
||||
:key="passkey.credentialID"
|
||||
class="col-12 col-sm-6 col-md-4 col-lg-3"
|
||||
>
|
||||
<!-- Delete Button -->
|
||||
<q-btn
|
||||
<q-card
|
||||
bordered
|
||||
flat
|
||||
dense
|
||||
round
|
||||
color="negative"
|
||||
icon="delete"
|
||||
@click="handleDelete(passkey.credentialID)"
|
||||
:loading="deleteLoading === passkey.credentialID"
|
||||
:disable="!!deleteLoading || !!identifyLoading"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<div
|
||||
v-else-if="fetchLoading"
|
||||
class="q-mt-md"
|
||||
>
|
||||
Loading passkeys...
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="q-mt-md"
|
||||
>
|
||||
You have no passkeys registered yet.
|
||||
</div>
|
||||
:class="{ 'bg-info': identifiedPasskeyId === passkey.credentialID }"
|
||||
>
|
||||
<q-card-section>
|
||||
<div class="text-subtitle2 ellipsis">
|
||||
Passkey ID:
|
||||
<q-tooltip>{{ passkey.credentialID }}</q-tooltip>
|
||||
</div>
|
||||
<div class="text-caption ellipsis">
|
||||
{{ passkey.credentialID }}
|
||||
</div>
|
||||
<q-item-label
|
||||
caption
|
||||
class="text-positive"
|
||||
v-if="identifiedPasskeyId === passkey.credentialID"
|
||||
>
|
||||
Verified just now!
|
||||
</q-item-label>
|
||||
</q-card-section>
|
||||
|
||||
<div
|
||||
v-if="fetchErrorMessage"
|
||||
class="text-negative q-mt-md"
|
||||
>
|
||||
{{ fetchErrorMessage }}
|
||||
</div>
|
||||
<div
|
||||
v-if="deleteSuccessMessage"
|
||||
class="text-positive q-mt-md"
|
||||
>
|
||||
{{ deleteSuccessMessage }}
|
||||
</div>
|
||||
<div
|
||||
v-if="deleteErrorMessage"
|
||||
class="text-negative q-mt-md"
|
||||
>
|
||||
{{ deleteErrorMessage }}
|
||||
</div>
|
||||
<div
|
||||
v-if="identifyErrorMessage"
|
||||
class="text-negative q-mt-md"
|
||||
>
|
||||
{{ identifyErrorMessage }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
round
|
||||
color="negative"
|
||||
icon="delete"
|
||||
@click="handleDelete(passkey.credentialID)"
|
||||
:loading="deleteLoading === passkey.credentialID"
|
||||
:disable="!!deleteLoading || !!identifyLoading"
|
||||
>
|
||||
<q-tooltip>Delete Passkey</q-tooltip>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="fetchLoading"
|
||||
class="q-mt-md"
|
||||
>
|
||||
Loading passkeys...
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="q-mt-md"
|
||||
>
|
||||
You have no passkeys registered yet.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="fetchErrorMessage"
|
||||
class="text-negative q-mt-md"
|
||||
>
|
||||
{{ fetchErrorMessage }}
|
||||
</div>
|
||||
<div
|
||||
v-if="deleteSuccessMessage"
|
||||
class="text-positive q-mt-md"
|
||||
>
|
||||
{{ deleteSuccessMessage }}
|
||||
</div>
|
||||
<div
|
||||
v-if="deleteErrorMessage"
|
||||
class="text-negative q-mt-md"
|
||||
>
|
||||
{{ deleteErrorMessage }}
|
||||
</div>
|
||||
<div
|
||||
v-if="identifyErrorMessage"
|
||||
class="text-negative q-mt-md"
|
||||
>
|
||||
{{ identifyErrorMessage }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { startRegistration, startAuthentication } from '@simplewebauthn/browser'; // Import startAuthentication
|
||||
import { startRegistration, startAuthentication } from '@simplewebauthn/browser';
|
||||
import axios from 'boot/axios';
|
||||
import { useAuthStore } from 'stores/auth';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
const registerLoading = ref(false);
|
||||
const registerErrorMessage = ref('');
|
||||
|
@ -137,27 +156,27 @@ const fetchErrorMessage = ref('');
|
|||
const deleteLoading = ref(null);
|
||||
const deleteErrorMessage = ref('');
|
||||
const deleteSuccessMessage = ref('');
|
||||
const identifyLoading = ref(null); // Store the ID of the passkey being identified
|
||||
const identifyLoading = ref(null);
|
||||
const identifyErrorMessage = ref('');
|
||||
const identifiedPasskeyId = ref(null); // Store the ID of the successfully identified passkey
|
||||
const identifiedPasskeyId = ref(null);
|
||||
|
||||
const $q = useQuasar();
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const passkeys = ref([]); // To store the list of passkeys
|
||||
const passkeys = ref([]);
|
||||
|
||||
// Computed properties to get state from the store
|
||||
const isLoggedIn = computed(() => authStore.isAuthenticated);
|
||||
const username = computed(() => authStore.user?.username);
|
||||
|
||||
// Fetch existing passkeys
|
||||
async function fetchPasskeys()
|
||||
{
|
||||
if (!isLoggedIn.value) return;
|
||||
fetchLoading.value = true;
|
||||
fetchErrorMessage.value = '';
|
||||
deleteSuccessMessage.value = ''; // Clear delete messages on refresh
|
||||
deleteSuccessMessage.value = '';
|
||||
deleteErrorMessage.value = '';
|
||||
identifyErrorMessage.value = ''; // Clear identify message
|
||||
identifiedPasskeyId.value = null; // Clear identified key
|
||||
identifyErrorMessage.value = '';
|
||||
identifiedPasskeyId.value = null;
|
||||
try
|
||||
{
|
||||
const response = await axios.get('/api/auth/passkeys');
|
||||
|
@ -167,7 +186,7 @@ async function fetchPasskeys()
|
|||
{
|
||||
console.error('Error fetching passkeys:', error);
|
||||
fetchErrorMessage.value = error.response?.data?.error || 'Failed to load passkeys.';
|
||||
passkeys.value = []; // Clear passkeys on error
|
||||
passkeys.value = [];
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -175,7 +194,6 @@ async function fetchPasskeys()
|
|||
}
|
||||
}
|
||||
|
||||
// Check auth status and fetch passkeys on component mount
|
||||
onMounted(async() =>
|
||||
{
|
||||
let initialAuthError = '';
|
||||
|
@ -189,12 +207,11 @@ onMounted(async() =>
|
|||
}
|
||||
if (!isLoggedIn.value)
|
||||
{
|
||||
// Use register error message ref for consistency if login is required first
|
||||
registerErrorMessage.value = initialAuthError || 'You must be logged in to manage passkeys.';
|
||||
}
|
||||
else
|
||||
{
|
||||
fetchPasskeys(); // Fetch passkeys if logged in
|
||||
fetchPasskeys();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -208,23 +225,20 @@ async function handleRegister()
|
|||
registerLoading.value = true;
|
||||
registerErrorMessage.value = '';
|
||||
registerSuccessMessage.value = '';
|
||||
deleteSuccessMessage.value = ''; // Clear other messages
|
||||
deleteSuccessMessage.value = '';
|
||||
deleteErrorMessage.value = '';
|
||||
identifyErrorMessage.value = '';
|
||||
identifiedPasskeyId.value = null;
|
||||
|
||||
try
|
||||
{
|
||||
// 1. Get options from server
|
||||
const optionsRes = await axios.post('/api/auth/generate-registration-options', {
|
||||
username: username.value, // Use username from store
|
||||
username: username.value,
|
||||
});
|
||||
const options = optionsRes.data;
|
||||
|
||||
// 2. Start registration ceremony in browser
|
||||
const regResp = await startRegistration(options);
|
||||
|
||||
// 3. Send response to server for verification
|
||||
const verificationRes = await axios.post('/api/auth/verify-registration', {
|
||||
registrationResponse: regResp,
|
||||
});
|
||||
|
@ -232,7 +246,7 @@ async function handleRegister()
|
|||
if (verificationRes.data.verified)
|
||||
{
|
||||
registerSuccessMessage.value = 'New passkey registered successfully!';
|
||||
fetchPasskeys(); // Refresh the list of passkeys
|
||||
fetchPasskeys();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -243,7 +257,6 @@ async function handleRegister()
|
|||
{
|
||||
console.error('Registration error:', error);
|
||||
const message = error.response?.data?.error || error.message || 'An unknown error occurred during registration.';
|
||||
// Handle specific simplewebauthn errors
|
||||
if (error.name === 'InvalidStateError')
|
||||
{
|
||||
registerErrorMessage.value = 'Authenticator may already be registered.';
|
||||
|
@ -268,42 +281,63 @@ async function handleRegister()
|
|||
}
|
||||
|
||||
|
||||
// Handle deleting a passkey
|
||||
async function handleDelete(credentialID)
|
||||
{
|
||||
if (!credentialID) return;
|
||||
|
||||
// Optional: Add a confirmation dialog here
|
||||
// if (!confirm('Are you sure you want to delete this passkey?')) {
|
||||
// return;
|
||||
// }
|
||||
if (passkeys.value.length <= 1)
|
||||
{
|
||||
deleteErrorMessage.value = 'You cannot delete your last passkey. Register another one first.';
|
||||
deleteSuccessMessage.value = '';
|
||||
registerSuccessMessage.value = '';
|
||||
registerErrorMessage.value = '';
|
||||
identifyErrorMessage.value = '';
|
||||
identifiedPasskeyId.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
deleteLoading.value = credentialID; // Set loading state for the specific button
|
||||
deleteErrorMessage.value = '';
|
||||
deleteSuccessMessage.value = '';
|
||||
registerSuccessMessage.value = ''; // Clear other messages
|
||||
registerErrorMessage.value = '';
|
||||
identifyErrorMessage.value = '';
|
||||
identifiedPasskeyId.value = null;
|
||||
$q.dialog({
|
||||
title: 'Confirm Deletion',
|
||||
message: 'Are you sure you want to delete this passkey? This action cannot be undone.',
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
ok: {
|
||||
label: 'Delete',
|
||||
color: 'negative',
|
||||
flat: true,
|
||||
},
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
flat: true,
|
||||
},
|
||||
}).onOk(async() =>
|
||||
{
|
||||
deleteLoading.value = credentialID;
|
||||
deleteErrorMessage.value = '';
|
||||
deleteSuccessMessage.value = '';
|
||||
registerSuccessMessage.value = '';
|
||||
registerErrorMessage.value = '';
|
||||
identifyErrorMessage.value = '';
|
||||
identifiedPasskeyId.value = null;
|
||||
|
||||
try
|
||||
{
|
||||
await axios.delete(`/api/auth/passkeys/${credentialID}`);
|
||||
deleteSuccessMessage.value = 'Passkey deleted successfully.';
|
||||
fetchPasskeys(); // Refresh the list
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error deleting passkey:', error);
|
||||
deleteErrorMessage.value = error.response?.data?.error || 'Failed to delete passkey.';
|
||||
}
|
||||
finally
|
||||
{
|
||||
deleteLoading.value = null; // Clear loading state
|
||||
}
|
||||
try
|
||||
{
|
||||
await axios.delete(`/api/auth/passkeys/${credentialID}`);
|
||||
deleteSuccessMessage.value = 'Passkey deleted successfully.';
|
||||
fetchPasskeys();
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error deleting passkey:', error);
|
||||
deleteErrorMessage.value = error.response?.data?.error || 'Failed to delete passkey.';
|
||||
}
|
||||
finally
|
||||
{
|
||||
deleteLoading.value = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle identifying a passkey
|
||||
async function handleIdentify()
|
||||
{
|
||||
if (!isLoggedIn.value)
|
||||
|
@ -314,8 +348,7 @@ async function handleIdentify()
|
|||
|
||||
identifyLoading.value = true;
|
||||
identifyErrorMessage.value = '';
|
||||
identifiedPasskeyId.value = null; // Reset identified key
|
||||
// Clear other messages
|
||||
identifiedPasskeyId.value = null;
|
||||
registerSuccessMessage.value = '';
|
||||
registerErrorMessage.value = '';
|
||||
deleteSuccessMessage.value = '';
|
||||
|
@ -323,30 +356,20 @@ async function handleIdentify()
|
|||
|
||||
try
|
||||
{
|
||||
// 1. Get authentication options from the server
|
||||
// We don't need to send username as the server should use the session
|
||||
const optionsRes = await axios.post('/api/auth/generate-authentication-options', {}); // Send empty body
|
||||
const optionsRes = await axios.post('/api/auth/generate-authentication-options', {});
|
||||
const options = optionsRes.data;
|
||||
|
||||
// Optionally filter options to only allow the specific key if needed, but usually not necessary for identification
|
||||
// options.allowCredentials = options.allowCredentials?.filter(cred => cred.id === credentialIDToIdentify);
|
||||
|
||||
// 2. Start authentication ceremony in the browser
|
||||
const authResp = await startAuthentication(options);
|
||||
|
||||
// 3. If successful, the response contains the ID of the key used
|
||||
identifiedPasskeyId.value = authResp.id;
|
||||
console.log('Identified Passkey ID:', identifiedPasskeyId.value);
|
||||
|
||||
// Optional: Add a small delay before clearing the highlight
|
||||
setTimeout(() =>
|
||||
{
|
||||
// Only clear if it's still the same identified key
|
||||
if (identifiedPasskeyId.value === authResp.id)
|
||||
{
|
||||
identifiedPasskeyId.value = null;
|
||||
}
|
||||
}, 5000); // Clear highlight after 5 seconds
|
||||
}, 5000);
|
||||
|
||||
}
|
||||
catch (error)
|
||||
|
@ -364,7 +387,7 @@ async function handleIdentify()
|
|||
}
|
||||
finally
|
||||
{
|
||||
identifyLoading.value = null; // Clear loading state
|
||||
identifyLoading.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
<q-page class="flex flex-center">
|
||||
<q-card style="width: 400px; max-width: 90vw;">
|
||||
<q-card-section>
|
||||
<!-- Update title based on login status from store -->
|
||||
<!-- Update title -->
|
||||
<div class="text-h6">
|
||||
{{ isLoggedIn ? 'Register New Passkey' : 'Register Passkey' }}
|
||||
Register Passkey
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
|
@ -13,21 +13,35 @@
|
|||
v-model="username"
|
||||
label="Username"
|
||||
outlined
|
||||
dense
|
||||
class="q-mb-md"
|
||||
:rules="[val => !!val || 'Username is required']"
|
||||
@keyup.enter="handleRegister"
|
||||
:disable="isLoggedIn"
|
||||
:hint="isLoggedIn ? 'Registering a new passkey for your current account.' : ''"
|
||||
:readonly="isLoggedIn"
|
||||
/>
|
||||
<!-- Make Email and Full Name required -->
|
||||
<q-input
|
||||
v-model="email"
|
||||
label="Email"
|
||||
type="email"
|
||||
outlined
|
||||
class="q-mb-md"
|
||||
:rules="[val => !!val || 'Email is required', val => /.+@.+\..+/.test(val) || 'Email must be valid']"
|
||||
@keyup.enter="handleRegister"
|
||||
/>
|
||||
<q-input
|
||||
v-model="fullName"
|
||||
label="Full Name"
|
||||
outlined
|
||||
class="q-mb-md"
|
||||
:rules="[val => !!val || 'Full Name is required']"
|
||||
@keyup.enter="handleRegister"
|
||||
/>
|
||||
<q-btn
|
||||
:label="isLoggedIn ? 'Register New Passkey' : 'Register Passkey'"
|
||||
label="Register Passkey"
|
||||
color="primary"
|
||||
class="full-width"
|
||||
@click="handleRegister"
|
||||
:loading="loading"
|
||||
:disable="loading || (!username && !isLoggedIn)"
|
||||
:disable="loading || !username || !email || !fullName"
|
||||
/>
|
||||
<div
|
||||
v-if="successMessage"
|
||||
|
@ -44,9 +58,8 @@
|
|||
</q-card-section>
|
||||
|
||||
<q-card-actions align="center">
|
||||
<!-- Hide login link if already logged in based on store state -->
|
||||
<!-- Always show login link -->
|
||||
<q-btn
|
||||
v-if="!isLoggedIn"
|
||||
flat
|
||||
label="Already have an account? Login"
|
||||
to="/login"
|
||||
|
@ -57,63 +70,56 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'; // Import computed
|
||||
import { ref } from 'vue'; // Remove computed and onMounted
|
||||
import { useRouter } from 'vue-router';
|
||||
import { startRegistration } from '@simplewebauthn/browser';
|
||||
import axios from 'boot/axios';
|
||||
import { useAuthStore } from 'stores/auth'; // Import the auth store
|
||||
// Remove auth store import
|
||||
|
||||
const loading = ref(false);
|
||||
const errorMessage = ref('');
|
||||
const successMessage = ref('');
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore(); // Use the auth store
|
||||
// Remove auth store usage
|
||||
|
||||
// Computed properties to get state from the store
|
||||
const isLoggedIn = computed(() => authStore.isAuthenticated);
|
||||
// Remove isLoggedIn computed property
|
||||
|
||||
const username = ref(''); // Local ref for username input
|
||||
const username = ref('');
|
||||
const email = ref('');
|
||||
const fullName = ref('');
|
||||
|
||||
// Check auth status on component mount using the store action
|
||||
onMounted(async() =>
|
||||
{
|
||||
if (!authStore.isAuthenticated)
|
||||
{
|
||||
await authStore.checkAuthStatus();
|
||||
if (authStore.error)
|
||||
{
|
||||
errorMessage.value = authStore.error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLoggedIn.value)
|
||||
{
|
||||
username.value = ''; // Clear username if not logged in
|
||||
}
|
||||
else
|
||||
{
|
||||
username.value = authStore.user?.username || ''; // Use username from store if logged in
|
||||
}
|
||||
});
|
||||
// Remove onMounted hook
|
||||
|
||||
async function handleRegister()
|
||||
{
|
||||
const currentUsername = isLoggedIn.value ? authStore.user?.username : username.value;
|
||||
if (!currentUsername)
|
||||
// Validate all fields
|
||||
if (!username.value || !email.value || !fullName.value)
|
||||
{
|
||||
errorMessage.value = 'Username is missing.';
|
||||
errorMessage.value = 'Please fill in all required fields.';
|
||||
return;
|
||||
}
|
||||
// Basic email validation
|
||||
if (!/.+@.+\..+/.test(email.value))
|
||||
{
|
||||
errorMessage.value = 'Please enter a valid email address.';
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
errorMessage.value = '';
|
||||
successMessage.value = '';
|
||||
|
||||
try
|
||||
{
|
||||
// Prepare payload - always include all fields
|
||||
const payload = {
|
||||
username: username.value,
|
||||
email: email.value,
|
||||
fullName: fullName.value,
|
||||
};
|
||||
|
||||
// 1. Get options from server
|
||||
const optionsRes = await axios.post('/api/auth/generate-registration-options', {
|
||||
username: currentUsername, // Use username from store
|
||||
});
|
||||
const optionsRes = await axios.post('/api/auth/generate-registration-options', payload);
|
||||
const options = optionsRes.data;
|
||||
|
||||
// 2. Start registration ceremony in browser
|
||||
|
@ -126,23 +132,12 @@ async function handleRegister()
|
|||
|
||||
if (verificationRes.data.verified)
|
||||
{
|
||||
// Adjust success message based on login state
|
||||
successMessage.value = isLoggedIn.value
|
||||
? 'New passkey registered successfully!'
|
||||
: 'Registration successful! Redirecting to login...';
|
||||
if (!isLoggedIn.value)
|
||||
// Simplify success message and always redirect
|
||||
successMessage.value = 'Registration successful! Redirecting to login...';
|
||||
setTimeout(() =>
|
||||
{
|
||||
// Redirect to login page only if they weren't logged in
|
||||
setTimeout(() =>
|
||||
{
|
||||
router.push('/login');
|
||||
}, 2000);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Maybe redirect to a profile page or dashboard if already logged in
|
||||
// setTimeout(() => { router.push('/dashboard'); }, 2000);
|
||||
}
|
||||
router.push('/login');
|
||||
}, 2000);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -164,7 +159,8 @@ async function handleRegister()
|
|||
}
|
||||
else if (error.response?.status === 409)
|
||||
{
|
||||
errorMessage.value = 'This passkey seems to be registered already.';
|
||||
// More specific message for username conflict
|
||||
errorMessage.value = error.response?.data?.error || 'Username or passkey might already be registered.';
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -1,202 +1,223 @@
|
|||
<template>
|
||||
<q-page padding>
|
||||
<div
|
||||
class="q-gutter-md"
|
||||
style="max-width: 800px; margin: auto;"
|
||||
<q-card
|
||||
bordered
|
||||
flat
|
||||
>
|
||||
<h5 class="q-mt-none q-mb-md">
|
||||
Settings
|
||||
</h5>
|
||||
<q-card-section class="row items-center justify-between">
|
||||
<div class="text-h4">
|
||||
Application Settings
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card
|
||||
flat
|
||||
bordered
|
||||
>
|
||||
<q-card-section>
|
||||
<div class="text-h6">
|
||||
Mantis Summary Prompt
|
||||
</div>
|
||||
<div class="text-caption text-grey q-mb-sm">
|
||||
Edit the prompt used to generate Mantis summaries. Use $DATE and $MANTIS_TICKETS as placeholders.
|
||||
</div>
|
||||
<q-input
|
||||
v-model="mantisPrompt"
|
||||
type="textarea"
|
||||
filled
|
||||
autogrow
|
||||
label="Mantis Prompt"
|
||||
:loading="loadingPrompt"
|
||||
:disable="savingPrompt"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
label="Save Prompt"
|
||||
color="primary"
|
||||
@click="saveMantisPrompt"
|
||||
:loading="savingPrompt"
|
||||
:disable="!mantisPrompt || loadingPrompt"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
<q-separator />
|
||||
|
||||
<q-card
|
||||
flat
|
||||
bordered
|
||||
>
|
||||
<q-card-section>
|
||||
<div class="text-h6">
|
||||
Email Summary Prompt
|
||||
<q-card-section v-if="loading">
|
||||
<q-spinner-dots size="2em" /> Loading settings...
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-else-if="loadError">
|
||||
<q-banner
|
||||
inline-actions
|
||||
class="text-white bg-red"
|
||||
>
|
||||
<template #avatar>
|
||||
<q-icon name="error" />
|
||||
</template>
|
||||
{{ loadError }}
|
||||
<template #action>
|
||||
<q-btn
|
||||
flat
|
||||
color="white"
|
||||
label="Retry"
|
||||
@click="loadSettings"
|
||||
/>
|
||||
</template>
|
||||
</q-banner>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-else>
|
||||
<q-form @submit.prevent="saveSettings">
|
||||
<div
|
||||
v-for="(group, groupName) in settings"
|
||||
:key="groupName"
|
||||
class="q-mb-lg"
|
||||
>
|
||||
<div class="text-h6 q-mb-sm">
|
||||
{{ groupName }}
|
||||
</div>
|
||||
<div
|
||||
v-for="setting in group"
|
||||
:key="setting.key"
|
||||
class="q-mb-md"
|
||||
>
|
||||
<q-input
|
||||
v-model="settingValues[setting.key]"
|
||||
:label="setting.name"
|
||||
:type="setting.type || 'text'"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-separator class="q-my-md" />
|
||||
</div>
|
||||
<div class="text-caption text-grey q-mb-sm">
|
||||
Edit the prompt used to generate Email summaries. Use $EMAIL_DATA as a placeholder for the JSON email array.
|
||||
|
||||
<div class="row justify-end">
|
||||
<q-btn
|
||||
label="Save Settings"
|
||||
type="submit"
|
||||
color="primary"
|
||||
:loading="saving"
|
||||
:disable="loading || saving"
|
||||
/>
|
||||
</div>
|
||||
<q-input
|
||||
v-model="emailPrompt"
|
||||
type="textarea"
|
||||
filled
|
||||
autogrow
|
||||
label="Email Prompt"
|
||||
:loading="loadingEmailPrompt"
|
||||
:disable="savingEmailPrompt"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
label="Save Prompt"
|
||||
color="primary"
|
||||
@click="saveEmailPrompt"
|
||||
:loading="savingEmailPrompt"
|
||||
:disable="!emailPrompt || loadingEmailPrompt"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import axios from 'boot/axios';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import axios from 'boot/axios'; // Import axios
|
||||
|
||||
const $q = useQuasar();
|
||||
|
||||
const mantisPrompt = ref('');
|
||||
const loadingPrompt = ref(false);
|
||||
const savingPrompt = ref(false);
|
||||
// Define the structure of settings
|
||||
const settings = ref({
|
||||
Mantis: [
|
||||
{
|
||||
name: 'Mantis API Key',
|
||||
key: 'MANTIS_API_KEY',
|
||||
},
|
||||
{
|
||||
name: 'Mantis API Endpoint',
|
||||
key: 'MANTIS_API_ENDPOINT'
|
||||
},
|
||||
{
|
||||
name: 'Mantis Prompt',
|
||||
key: 'MANTIS_PROMPT',
|
||||
type: 'textarea'
|
||||
}
|
||||
],
|
||||
Gemini: [
|
||||
{
|
||||
name: 'Gemini API Key',
|
||||
key: 'GEMINI_API_KEY'
|
||||
}
|
||||
],
|
||||
Database: [
|
||||
{
|
||||
name: 'MySQL Host',
|
||||
key: 'MYSQL_HOST'
|
||||
},
|
||||
{
|
||||
name: 'MySQL Port',
|
||||
key: 'MYSQL_PORT'
|
||||
},
|
||||
{
|
||||
name: 'MySQL User',
|
||||
key: 'MYSQL_USER'
|
||||
},
|
||||
{
|
||||
name: 'MySQL Password',
|
||||
key: 'MYSQL_PASSWORD'
|
||||
},
|
||||
{
|
||||
name: 'MySQL Database',
|
||||
key: 'MYSQL_DATABASE'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const fetchMantisPrompt = async() =>
|
||||
// Reactive state for setting values, loading, saving, and errors
|
||||
const settingValues = ref({});
|
||||
const loading = ref(true);
|
||||
const saving = ref(false);
|
||||
const loadError = ref(null);
|
||||
|
||||
// Function to load settings from the server
|
||||
async function loadSettings()
|
||||
{
|
||||
loadingPrompt.value = true;
|
||||
loading.value = true;
|
||||
loadError.value = null;
|
||||
settingValues.value = {}; // Reset values
|
||||
const allSettingKeys = Object.values(settings.value).flat().map(s => s.key);
|
||||
|
||||
try
|
||||
{
|
||||
const response = await axios.get('/api/settings/mantisPrompt');
|
||||
mantisPrompt.value = response.data.value || ''; // Handle case where setting might not exist yet
|
||||
const responses = await Promise.all(allSettingKeys.map(key => axios.get(`/api/settings/${key}`, {
|
||||
validateStatus: status => status === 200 || status === 404 // Accept 404 as a valid response
|
||||
})));
|
||||
responses.forEach((response, index) =>
|
||||
{
|
||||
const key = allSettingKeys[index];
|
||||
//If the response status is 404, set the value to an empty string
|
||||
if (response.status === 404)
|
||||
{
|
||||
settingValues.value[key] = '';
|
||||
return;
|
||||
}
|
||||
settingValues.value[key] = response.data;
|
||||
});
|
||||
}
|
||||
catch (error)
|
||||
catch (err)
|
||||
{
|
||||
console.error('Error fetching Mantis prompt:', error);
|
||||
console.error('Error loading settings:', err);
|
||||
loadError.value = err.response?.data?.error || 'Failed to load settings. Please check the console.';
|
||||
$q.notify({
|
||||
color: 'negative',
|
||||
message: 'Failed to load Mantis prompt setting.',
|
||||
icon: 'report_problem'
|
||||
icon: 'error',
|
||||
message: loadError.value,
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
loadingPrompt.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const saveMantisPrompt = async() =>
|
||||
// Function to save settings to the server
|
||||
async function saveSettings()
|
||||
{
|
||||
savingPrompt.value = true;
|
||||
saving.value = true;
|
||||
loadError.value = null; // Clear previous load errors
|
||||
|
||||
const allSettingKeys = Object.keys(settingValues.value);
|
||||
const requests = allSettingKeys.map(key =>
|
||||
axios.put(`/api/settings/${key}`, { value: settingValues.value[key] })
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
await axios.put('/api/settings/mantisPrompt', { value: mantisPrompt.value });
|
||||
await Promise.all(requests);
|
||||
$q.notify({
|
||||
color: 'positive',
|
||||
message: 'Mantis prompt updated successfully.',
|
||||
icon: 'check_circle'
|
||||
icon: 'check_circle',
|
||||
message: 'Settings saved successfully!',
|
||||
});
|
||||
}
|
||||
catch (error)
|
||||
catch (err)
|
||||
{
|
||||
console.error('Error saving Mantis prompt:', error);
|
||||
console.error('Error saving settings:', err);
|
||||
const errorMessage = err.response?.data?.error || 'Failed to save settings. Please try again.';
|
||||
$q.notify({
|
||||
color: 'negative',
|
||||
message: 'Failed to save Mantis prompt setting.',
|
||||
icon: 'report_problem'
|
||||
icon: 'error',
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
savingPrompt.value = false;
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const emailPrompt = ref('');
|
||||
const loadingEmailPrompt = ref(false);
|
||||
const savingEmailPrompt = ref(false);
|
||||
|
||||
const fetchEmailPrompt = async() =>
|
||||
{
|
||||
loadingEmailPrompt.value = true;
|
||||
try
|
||||
{
|
||||
const response = await axios.get('/api/settings/emailPrompt');
|
||||
emailPrompt.value = response.data.value || ''; // Handle case where setting might not exist yet
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error fetching Email prompt:', error);
|
||||
$q.notify({
|
||||
color: 'negative',
|
||||
message: 'Failed to load Email prompt setting.',
|
||||
icon: 'report_problem'
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
loadingEmailPrompt.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveEmailPrompt = async() =>
|
||||
{
|
||||
savingEmailPrompt.value = true;
|
||||
try
|
||||
{
|
||||
await axios.put('/api/settings/emailPrompt', { value: emailPrompt.value });
|
||||
$q.notify({
|
||||
color: 'positive',
|
||||
message: 'Email prompt updated successfully.',
|
||||
icon: 'check_circle'
|
||||
});
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error saving Email prompt:', error);
|
||||
$q.notify({
|
||||
color: 'negative',
|
||||
message: 'Failed to save Email prompt setting.',
|
||||
icon: 'report_problem'
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
savingEmailPrompt.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Load settings when the component is mounted
|
||||
onMounted(() =>
|
||||
{
|
||||
fetchMantisPrompt();
|
||||
fetchEmailPrompt();
|
||||
loadSettings();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add any specific styles if needed */
|
||||
</style>
|
||||
|
|
122
src/pages/UserPreferencesPage.vue
Normal file
122
src/pages/UserPreferencesPage.vue
Normal file
|
@ -0,0 +1,122 @@
|
|||
<template>
|
||||
<q-page padding>
|
||||
<q-card
|
||||
flat
|
||||
bordered
|
||||
>
|
||||
<q-card-section>
|
||||
<div class="text-h4">
|
||||
User Preferences
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section v-if="loading">
|
||||
<q-spinner-dots size="2em" /> Loading preferences...
|
||||
</q-card-section>
|
||||
<q-card-section v-else-if="error">
|
||||
<q-banner
|
||||
inline-actions
|
||||
class="text-white bg-red"
|
||||
>
|
||||
<template #avatar>
|
||||
<q-icon name="error" />
|
||||
</template>
|
||||
{{ error }}
|
||||
<template #action>
|
||||
<q-btn
|
||||
flat
|
||||
color="white"
|
||||
label="Retry"
|
||||
@click="loadPreferences"
|
||||
/>
|
||||
</template>
|
||||
</q-banner>
|
||||
</q-card-section>
|
||||
<q-card-section v-else>
|
||||
<q-form @submit.prevent="savePreferences">
|
||||
<div
|
||||
v-for="(group, groupName) in preferences"
|
||||
:key="groupName"
|
||||
class="q-mb-lg"
|
||||
>
|
||||
<div class="text-h6 q-mb-sm">
|
||||
{{ groupName.replaceAll('_', ' ') }}
|
||||
</div>
|
||||
<div
|
||||
v-for="pref in group"
|
||||
:key="pref.key"
|
||||
class="q-mb-md"
|
||||
>
|
||||
<q-select
|
||||
v-if="pref.options"
|
||||
v-model="preferenceValues[pref.key]"
|
||||
:label="pref.name"
|
||||
:options="pref.options"
|
||||
outlined
|
||||
dense
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
<q-input
|
||||
v-else
|
||||
v-model="preferenceValues[pref.key]"
|
||||
:label="pref.name"
|
||||
:type="pref.type || 'text'"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
</div>
|
||||
<q-separator class="q-my-md" />
|
||||
</div>
|
||||
<div class="row justify-end">
|
||||
<q-btn
|
||||
label="Save Preferences"
|
||||
type="submit"
|
||||
color="primary"
|
||||
:loading="saving"
|
||||
:disable="loading || saving"
|
||||
/>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useQuasar } from 'quasar';
|
||||
import { onMounted, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { usePreferencesStore } from 'stores/preferences';
|
||||
|
||||
const $q = useQuasar();
|
||||
const preferencesStore = usePreferencesStore();
|
||||
const { preferences, values: preferenceValues, loading, saving, error } = storeToRefs(preferencesStore);
|
||||
|
||||
function notifyError(msg)
|
||||
{
|
||||
$q.notify({ color: 'negative', icon: 'error', message: msg });
|
||||
}
|
||||
function notifySuccess(msg)
|
||||
{
|
||||
$q.notify({ color: 'positive', icon: 'check_circle', message: msg });
|
||||
}
|
||||
|
||||
async function loadPreferences()
|
||||
{
|
||||
await preferencesStore.loadPreferences();
|
||||
if (error.value) notifyError(error.value);
|
||||
}
|
||||
|
||||
async function savePreferences()
|
||||
{
|
||||
await preferencesStore.savePreferences();
|
||||
if (error.value) notifyError(error.value);
|
||||
else notifySuccess('Preferences saved!');
|
||||
}
|
||||
|
||||
onMounted(() =>
|
||||
{
|
||||
loadPreferences();
|
||||
});
|
||||
</script>
|
|
@ -40,7 +40,6 @@ const routes = [
|
|||
component: () => import('pages/PasskeyManagementPage.vue'), // Assuming this page exists or will be created
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
navGroup: 'auth', // Show only when logged in
|
||||
icon: 'key',
|
||||
title: 'Passkeys',
|
||||
caption: 'Manage your passkeys'
|
||||
|
@ -85,6 +84,14 @@ const routes = [
|
|||
title: 'Settings',
|
||||
caption: 'Manage application settings'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'user-preferences',
|
||||
name: 'userPreferences',
|
||||
component: () => import('pages/UserPreferencesPage.vue'),
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
115
src/stores/preferences.js
Normal file
115
src/stores/preferences.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import axios from 'boot/axios';
|
||||
import { Dark } from 'quasar';
|
||||
|
||||
export const usePreferencesStore = defineStore('preferences', () =>
|
||||
{
|
||||
// Grouped user preferences structure (can be imported/shared if needed)
|
||||
const preferences = ref({
|
||||
UI: [
|
||||
{
|
||||
name: 'Theme',
|
||||
key: 'theme',
|
||||
type: 'text',
|
||||
options: [
|
||||
{ label: 'Light', value: 'light' },
|
||||
{ label: 'Dark', value: 'dark' },
|
||||
],
|
||||
},
|
||||
],
|
||||
API_Tokens: [
|
||||
{
|
||||
name: 'Mantis API Key',
|
||||
key: 'MANTIS_API_KEY',
|
||||
type: 'text',
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const values = ref({});
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
async function loadPreferences()
|
||||
{
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
values.value = {};
|
||||
const allKeys = Object.values(preferences.value).flat().map(p => p.key);
|
||||
try
|
||||
{
|
||||
const responses = await Promise.all(
|
||||
allKeys.map(key =>
|
||||
axios.get(`/api/user-preferences/${key}`, {
|
||||
validateStatus: status => status === 200 || status === 404,
|
||||
})
|
||||
)
|
||||
);
|
||||
responses.forEach((response, idx) =>
|
||||
{
|
||||
const key = allKeys[idx];
|
||||
if (response.status === 404)
|
||||
{
|
||||
values.value[key] = '';
|
||||
}
|
||||
else
|
||||
{
|
||||
values.value[key] = response.data.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
error.value = err.response?.data?.error || 'Failed to load preferences.';
|
||||
}
|
||||
finally
|
||||
{
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
//If we have a "theme" preference, change it in Quasar
|
||||
if (values.value.theme)
|
||||
{
|
||||
Dark.set(values.value.theme === 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
async function savePreferences()
|
||||
{
|
||||
saving.value = true;
|
||||
error.value = null;
|
||||
const allKeys = Object.keys(values.value);
|
||||
const requests = allKeys.map(key =>
|
||||
axios.put(`/api/user-preferences/${key}`, { value: values.value[key] })
|
||||
);
|
||||
try
|
||||
{
|
||||
await Promise.all(requests);
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
error.value = err.response?.data?.error || 'Failed to save preferences.';
|
||||
}
|
||||
finally
|
||||
{
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
if (values.value.theme)
|
||||
{
|
||||
Dark.set(values.value.theme === 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
preferences,
|
||||
values,
|
||||
loading,
|
||||
saving,
|
||||
error,
|
||||
loadPreferences,
|
||||
savePreferences,
|
||||
};
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue