Add in Pino logging and make UI consistent across the app.

This commit is contained in:
Cameron Redmore 2025-04-25 19:34:17 +01:00
parent 727746030c
commit 300040bd58
19 changed files with 590 additions and 235 deletions

View file

@ -31,6 +31,10 @@
"pdfkit": "^0.17.0", "pdfkit": "^0.17.0",
"pdfmake": "^0.2.18", "pdfmake": "^0.2.18",
"pinia": "^3.0.2", "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", "quasar": "^2.16.0",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vue": "^3.4.18", "vue": "^3.4.18",

168
pnpm-lock.yaml generated
View file

@ -62,6 +62,18 @@ importers:
pinia: pinia:
specifier: ^3.0.2 specifier: ^3.0.2
version: 3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)) 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: quasar:
specifier: ^2.16.0 specifier: ^2.16.0
version: 2.18.1 version: 2.18.1
@ -876,6 +888,10 @@ packages:
asynckit@0.4.0: asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 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: autoprefixer@10.4.21:
resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
@ -1059,6 +1075,9 @@ packages:
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
colorjs.io@0.5.2: colorjs.io@0.5.2:
resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==}
@ -1152,6 +1171,9 @@ packages:
date-fns@4.1.0: date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
dateformat@4.6.3:
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
debug@2.6.9: debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies: peerDependencies:
@ -1447,6 +1469,9 @@ packages:
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
engines: {node: '>=4'} engines: {node: '>=4'}
fast-copy@3.0.2:
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
fast-deep-equal@3.1.3: fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@ -1466,6 +1491,13 @@ packages:
fast-levenshtein@2.0.6: fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 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: fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
@ -1645,6 +1677,9 @@ packages:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true hasBin: true
help-me@5.0.0:
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
hookable@5.5.3: hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
@ -1803,6 +1838,10 @@ packages:
jackspeak@3.4.3: jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} 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: jpeg-exif@1.1.4:
resolution: {integrity: sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==} resolution: {integrity: sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==}
@ -2075,6 +2114,10 @@ packages:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'} 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: on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -2196,6 +2239,23 @@ packages:
typescript: typescript:
optional: true 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: pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
@ -2244,6 +2304,9 @@ packages:
process-nextick-args@2.0.1: process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
process-warning@4.0.1:
resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==}
process@0.11.10: process@0.11.10:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'} engines: {node: '>= 0.6.0'}
@ -2287,6 +2350,9 @@ packages:
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
random-bytes@1.0.0: random-bytes@1.0.0:
resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -2328,6 +2394,10 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'} 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: regexp.prototype.flags@1.5.4:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2396,6 +2466,10 @@ packages:
safe-buffer@5.2.1: safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 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: safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@ -2530,6 +2604,9 @@ packages:
sax@1.4.1: sax@1.4.1:
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
secure-json-parse@2.7.0:
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
selderee@0.11.0: selderee@0.11.0:
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
@ -2617,6 +2694,9 @@ packages:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'} engines: {node: '>=10'}
sonic-boom@4.2.0:
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
source-map-js@1.2.1: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2636,6 +2716,10 @@ packages:
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
stack-trace@1.0.0-pre2: stack-trace@1.0.0-pre2:
resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==}
engines: {node: '>=16'} engines: {node: '>=16'}
@ -2723,6 +2807,9 @@ packages:
text-decoder@1.2.3: text-decoder@1.2.3:
resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} 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: tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
@ -3854,6 +3941,8 @@ snapshots:
asynckit@0.4.0: {} asynckit@0.4.0: {}
atomic-sleep@1.0.0: {}
autoprefixer@10.4.21(postcss@8.5.3): autoprefixer@10.4.21(postcss@8.5.3):
dependencies: dependencies:
browserslist: 4.24.4 browserslist: 4.24.4
@ -4060,6 +4149,8 @@ snapshots:
color-name@1.1.4: {} color-name@1.1.4: {}
colorette@2.0.20: {}
colorjs.io@0.5.2: {} colorjs.io@0.5.2: {}
combined-stream@1.0.8: combined-stream@1.0.8:
@ -4149,6 +4240,8 @@ snapshots:
date-fns@4.1.0: {} date-fns@4.1.0: {}
dateformat@4.6.3: {}
debug@2.6.9: debug@2.6.9:
dependencies: dependencies:
ms: 2.0.0 ms: 2.0.0
@ -4499,6 +4592,8 @@ snapshots:
iconv-lite: 0.4.24 iconv-lite: 0.4.24
tmp: 0.0.33 tmp: 0.0.33
fast-copy@3.0.2: {}
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
fast-diff@1.3.0: {} fast-diff@1.3.0: {}
@ -4517,6 +4612,10 @@ snapshots:
fast-levenshtein@2.0.6: {} fast-levenshtein@2.0.6: {}
fast-redact@3.5.0: {}
fast-safe-stringify@2.1.1: {}
fastq@1.19.1: fastq@1.19.1:
dependencies: dependencies:
reusify: 1.1.0 reusify: 1.1.0
@ -4717,6 +4816,8 @@ snapshots:
he@1.2.0: {} he@1.2.0: {}
help-me@5.0.0: {}
hookable@5.5.3: {} hookable@5.5.3: {}
html-minifier-terser@7.2.0: html-minifier-terser@7.2.0:
@ -4876,6 +4977,8 @@ snapshots:
optionalDependencies: optionalDependencies:
'@pkgjs/parseargs': 0.11.0 '@pkgjs/parseargs': 0.11.0
joycon@3.1.1: {}
jpeg-exif@1.1.4: {} jpeg-exif@1.1.4: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
@ -5129,6 +5232,8 @@ snapshots:
object-keys@1.1.1: {} object-keys@1.1.1: {}
on-exit-leak-free@2.1.2: {}
on-finished@2.4.1: on-finished@2.4.1:
dependencies: dependencies:
ee-first: 1.1.1 ee-first: 1.1.1
@ -5259,6 +5364,49 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.8.3 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: pkg-types@1.3.1:
dependencies: dependencies:
confbox: 0.1.8 confbox: 0.1.8
@ -5315,6 +5463,8 @@ snapshots:
process-nextick-args@2.0.1: {} process-nextick-args@2.0.1: {}
process-warning@4.0.1: {}
process@0.11.10: {} process@0.11.10: {}
proxy-addr@2.0.7: proxy-addr@2.0.7:
@ -5349,6 +5499,8 @@ snapshots:
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
quick-format-unescaped@4.0.4: {}
random-bytes@1.0.0: {} random-bytes@1.0.0: {}
randombytes@2.1.0: randombytes@2.1.0:
@ -5405,6 +5557,8 @@ snapshots:
readdirp@4.1.2: {} readdirp@4.1.2: {}
real-require@0.2.0: {}
regexp.prototype.flags@1.5.4: regexp.prototype.flags@1.5.4:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.8
@ -5482,6 +5636,8 @@ snapshots:
safe-buffer@5.2.1: {} safe-buffer@5.2.1: {}
safe-stable-stringify@2.5.0: {}
safer-buffer@2.1.2: {} safer-buffer@2.1.2: {}
sass-embedded-android-arm64@1.87.0: sass-embedded-android-arm64@1.87.0:
@ -5580,6 +5736,8 @@ snapshots:
sax@1.4.1: {} sax@1.4.1: {}
secure-json-parse@2.7.0: {}
selderee@0.11.0: selderee@0.11.0:
dependencies: dependencies:
parseley: 0.12.1 parseley: 0.12.1
@ -5698,6 +5856,10 @@ snapshots:
dependencies: dependencies:
semver: 7.7.1 semver: 7.7.1
sonic-boom@4.2.0:
dependencies:
atomic-sleep: 1.0.0
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
source-map-support@0.5.21: source-map-support@0.5.21:
@ -5711,6 +5873,8 @@ snapshots:
speakingurl@14.0.1: {} speakingurl@14.0.1: {}
split2@4.2.0: {}
stack-trace@1.0.0-pre2: {} stack-trace@1.0.0-pre2: {}
statuses@2.0.1: {} statuses@2.0.1: {}
@ -5813,6 +5977,10 @@ snapshots:
dependencies: dependencies:
b4a: 1.6.7 b4a: 1.6.7
thread-stream@3.1.0:
dependencies:
real-require: 0.2.0
tiny-inflate@1.0.3: {} tiny-inflate@1.0.3: {}
tiny-invariant@1.3.3: {} tiny-invariant@1.3.3: {}

View 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")
);

View file

@ -129,3 +129,11 @@ model Session {
data String data String
expiresAt DateTime @map("expires_at") 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
}

View file

@ -621,7 +621,6 @@ router.post('/mantis-summaries/generate', async(req, res) =>
generateTodaysSummary() generateTodaysSummary()
.then(() => .then(() =>
{ {
console.log('Summary generation process finished successfully (async).');
}) })
.catch(error => .catch(error =>
{ {

View file

@ -158,8 +158,6 @@ router.post('/verify-registration', async(req, res) =>
const { verified, registrationInfo } = verification; const { verified, registrationInfo } = verification;
console.log(verification);
if (verified && registrationInfo) if (verified && registrationInfo)
{ {
const { credential, credentialDeviceType, credentialBackedUp } = registrationInfo; const { credential, credentialDeviceType, credentialBackedUp } = registrationInfo;
@ -242,12 +240,8 @@ router.post('/generate-authentication-options', async(req, res) =>
return res.status(404).json({ error: 'User not found' }); return res.status(404).json({ error: 'User not found' });
} }
console.log('User found:', user);
const userAuthenticators = await getUserAuthenticators(user.id); const userAuthenticators = await getUserAuthenticators(user.id);
console.log('User authenticators:', userAuthenticators);
const options = await generateAuthenticationOptions({ const options = await generateAuthenticationOptions({
rpID, rpID,
// Require users to use a previously-registered authenticator // Require users to use a previously-registered authenticator

View file

@ -16,6 +16,8 @@ import session from 'express-session';
import { PrismaSessionStore } from '@quixo3/prisma-session-store'; import { PrismaSessionStore } from '@quixo3/prisma-session-store';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import pino from 'pino';
import pinoHttp from 'pino-http';
import apiRoutes from './routes/api.js'; import apiRoutes from './routes/api.js';
import authRoutes from './routes/auth.js'; import authRoutes from './routes/auth.js';
import chatRoutes from './routes/chat.js'; import chatRoutes from './routes/chat.js';
@ -27,6 +29,47 @@ import { requireAuth } from './middlewares/authMiddleware.js';
dotenv.config(); 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) // Define Relying Party details (Update with your actual details)
export const rpID = process.env.NODE_ENV === 'production' ? 'stylepoint.uk' : 'localhost'; export const rpID = process.env.NODE_ENV === 'production' ? 'stylepoint.uk' : 'localhost';
export const rpName = 'StylePoint'; export const rpName = 'StylePoint';
@ -39,9 +82,12 @@ const prisma = new PrismaClient();
const app = express(); const app = express();
// Add pino-http middleware
app.use(httpLogger);
if(!process.env.SESSION_SECRET) 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 process.exit(1); // Exit the process if the secret is not set
} }
@ -70,15 +116,14 @@ app.use(session({
// Run daily at 1:00 AM server time (adjust as needed) // Run daily at 1:00 AM server time (adjust as needed)
cron.schedule('0 1 * * *', async() => cron.schedule('0 1 * * *', async() =>
{ {
console.log('Running scheduled Mantis summary task...');
try try
{ {
await generateAndStoreMantisSummary(); await generateAndStoreMantisSummary();
console.log('Scheduled Mantis summary task completed.'); logger.info('Scheduled Mantis summary task completed successfully.');
} }
catch (error) catch (error)
{ {
console.error('Error running scheduled Mantis summary task:', error); logger.error({ error }, 'Error running scheduled Mantis summary task');
} }
}, { }, {
scheduled: true, scheduled: true,
@ -108,5 +153,5 @@ app.use(express.static('public', { index: false }));
app.listen(8000, () => app.listen(8000, () =>
{ {
console.log('Server is running on http://localhost:8000'); logger.info('Server is running on http://localhost:8000');
}); });

View file

@ -113,16 +113,13 @@ export async function generateAndStoreMantisSummary()
if (tickets.length === 0) if (tickets.length === 0)
{ {
summaryText = 'No Mantis tickets updated recently.'; summaryText = 'No Mantis tickets updated recently.';
console.log('No recent Mantis tickets found.');
} }
else else
{ {
console.log(`Found ${tickets.length} recent Mantis tickets. Generating summary...`);
let prompt = promptTemplate.replaceAll('$DATE', new Date().toISOString().split('T')[0]); let prompt = promptTemplate.replaceAll('$DATE', new Date().toISOString().split('T')[0]);
prompt = prompt.replaceAll('$MANTIS_TICKETS', JSON.stringify(tickets, null, 2)); prompt = prompt.replaceAll('$MANTIS_TICKETS', JSON.stringify(tickets, null, 2));
summaryText = await askGemini(prompt); summaryText = await askGemini(prompt);
console.log('Mantis summary generated successfully by AI.');
} }
// Store the summary in the database using Prisma upsert // Store the summary in the database using Prisma upsert
@ -139,7 +136,6 @@ export async function generateAndStoreMantisSummary()
summaryText: summaryText, summaryText: summaryText,
}, },
}); });
console.log(`Mantis summary for ${today.toISOString().split('T')[0]} stored/updated in the database.`);
} }
catch (error) catch (error)

View file

@ -10,8 +10,6 @@ export async function askGemini(content)
const GOOGLE_API_KEY = await getSetting('GEMINI_API_KEY'); 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) if (!GOOGLE_API_KEY)
{ {
throw new Error('Google API key is not set in the database.'); throw new Error('Google API key is not set in the database.');

View 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();
}
});
}

View file

@ -10,12 +10,13 @@
clickable clickable
v-ripple v-ripple
@click="toggleLeftDrawer" @click="toggleLeftDrawer"
class="relative"
> >
<q-item-section avatar> <q-item-section avatar>
<q-icon name="menu" /> <q-icon name="menu" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section v-if="leftDrawerOpen">
<q-item-label class="text-h6"> <q-item-label class="text-h4 absolute-center">
StylePoint StylePoint
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
@ -28,6 +29,20 @@
class="q-ma-sm text-center relative" class="q-ma-sm text-center relative"
> >
<q-card-section> <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 <q-btn
class="absolute" class="absolute"
style="top: 10px; right: 10px;" style="top: 10px; right: 10px;"
@ -36,16 +51,16 @@
:to="{ name: 'userPreferences' }" :to="{ name: 'userPreferences' }"
> >
<q-icon <q-icon
name="settings" name="mdi-account-cog"
/> />
<q-tooltip>
User Preferences
</q-tooltip>
</q-btn> </q-btn>
<q-avatar <q-avatar
class="bg-primary cursor-pointer text-white" class="bg-primary cursor-pointer text-white"
> >
<q-icon name="mdi-account" /> <q-icon name="mdi-account" />
<q-tooltip>
{{ authStore.user.username }}
</q-tooltip>
</q-avatar> </q-avatar>
<div class="text-h6"> <div class="text-h6">
{{ authStore.user.username }} {{ authStore.user.username }}
@ -65,6 +80,50 @@
class="menu-list" class="menu-list"
v-else 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 <q-item
clickable clickable
v-ripple v-ripple

View file

@ -1,96 +1,100 @@
<template> <template>
<q-page padding> <q-page padding>
<div class="q-mb-md row justify-between items-center"> <q-card
<div class="text-h4"> flat
Forms
</div>
<q-btn
outline
label="Create New Form"
color="primary"
:to="{ name: 'formCreate' }"
/>
</div>
<q-list
bordered bordered
separator
v-if="forms.length > 0"
> >
<q-item <q-card-section class="row items-center justify-between">
v-for="form in forms" <div class="text-h4">
:key="form.id" Forms
> </div>
<q-item-section> <q-btn
<q-item-label>{{ form.title }}</q-item-label> label="Create New Form"
<q-item-label caption> color="primary"
{{ form.description || 'No description' }} :to="{ name: 'formCreate' }"
</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> </q-card-section>
No forms created yet. Click the button above to create your first form.
</q-banner>
<q-inner-loading :showing="loading"> <q-list
<q-spinner-gears bordered
size="50px" separator
color="primary" v-if="forms.length > 0"
/> >
</q-inner-loading> <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> </q-page>
</template> </template>

View file

@ -45,7 +45,7 @@ const $q = useQuasar();
const currentYear = ref(new Date().getFullYear()); const currentYear = ref(new Date().getFullYear());
const features = ref([ const features = ref([
'Auatomated Daily Reports', 'Automated Daily Reports',
'Deep Mantis Integration', 'Deep Mantis Integration',
'Easy Authentication', 'Easy Authentication',
'And more..?' 'And more..?'

View file

@ -80,12 +80,10 @@ async function handleLogin()
if (verificationRes.data.verified) if (verificationRes.data.verified)
{ {
// Update the auth store on successful login
authStore.isAuthenticated = true; authStore.isAuthenticated = true;
authStore.user = verificationRes.data.user; authStore.user = verificationRes.data.user;
authStore.error = null; // Clear any previous errors authStore.error = null;
console.log('Login successful:', verificationRes.data.user); router.push('/');
router.push('/'); // Redirect to home page
} }
else else
{ {

View file

@ -5,7 +5,7 @@
bordered bordered
> >
<q-card-section class="row items-center justify-between"> <q-card-section class="row items-center justify-between">
<div class="text-h6"> <div class="text-h4">
Mantis Summaries Mantis Summaries
</div> </div>
<q-btn <q-btn

View file

@ -1,122 +1,143 @@
<template> <template>
<q-page padding> <q-page padding>
<div class="q-mb-md row justify-between items-center"> <q-card
<div class="text-h4"> bordered
Passkey Management flat
</div> >
<div> <q-card-section class="row items-center justify-between">
<q-btn <div class="text-h4">
label="Identify Passkey" Passkey Management
color="secondary" </div>
class="q-mx-md q-mt-md" <div>
@click="handleIdentify" <q-btn
:loading="identifyLoading" label="Identify Passkey"
:disable="identifyLoading || !isLoggedIn" color="secondary"
outline class="q-mx-md"
/> @click="handleIdentify"
<q-btn :loading="identifyLoading"
label="Register New Passkey" :disable="identifyLoading || !isLoggedIn"
color="primary" />
class="q-mx-md q-mt-md" <q-btn
@click="handleRegister" label="Register New Passkey"
:loading="registerLoading" color="primary"
:disable="registerLoading || !isLoggedIn" @click="handleRegister"
outline :loading="registerLoading"
/> :disable="registerLoading || !isLoggedIn"
</div> />
</div> </div>
</q-card-section>
<q-card-section> <q-separator />
<h5>Your Registered Passkeys</h5>
<q-list <q-card-section>
bordered <div class="text-h6 q-mb-sm">
separator Registered Passkeys
v-if="passkeys.length > 0 && !fetchLoading" </div>
>
<q-item v-if="registerSuccessMessage || registerErrorMessage"> <!-- Display registration messages above the grid -->
<div <div
v-if="registerSuccessMessage" v-if="registerSuccessMessage"
class="text-positive q-mt-md" class="text-positive q-mb-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-item-section> {{ registerSuccessMessage }}
<q-item-label>Passkey ID: {{ passkey.credentialID }} </q-item-label> </div>
<q-item-label <div
caption v-if="registerErrorMessage"
v-if="identifiedPasskeyId === passkey.credentialID" class="text-negative q-mb-md"
> >
Verified just now! {{ registerErrorMessage }}
</q-item-label> </div>
</q-item-section>
<q-item-section <!-- Responsive Grid for Passkeys -->
side <div
class="row no-wrap items-center" 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"
> >
<q-btn <q-card
bordered
flat flat
dense :class="{ 'bg-info': identifiedPasskeyId === passkey.credentialID }"
round >
color="negative" <q-card-section>
icon="delete" <div class="text-subtitle2 ellipsis">
@click="handleDelete(passkey.credentialID)" Passkey ID:
:loading="deleteLoading === passkey.credentialID" <q-tooltip>{{ passkey.credentialID }}</q-tooltip>
:disable="!!deleteLoading || !!identifyLoading" </div>
/> <div class="text-caption ellipsis">
</q-item-section> {{ passkey.credentialID }}
</q-item> </div>
</q-list> <q-item-label
<div caption
v-else-if="fetchLoading" class="text-positive"
class="q-mt-md" v-if="identifiedPasskeyId === passkey.credentialID"
> >
Loading passkeys... Verified just now!
</div> </q-item-label>
<div </q-card-section>
v-else
class="q-mt-md"
>
You have no passkeys registered yet.
</div>
<div <q-separator />
v-if="fetchErrorMessage"
class="text-negative q-mt-md" <q-card-actions align="right">
> <q-btn
{{ fetchErrorMessage }} flat
</div> dense
<div round
v-if="deleteSuccessMessage" color="negative"
class="text-positive q-mt-md" icon="delete"
> @click="handleDelete(passkey.credentialID)"
{{ deleteSuccessMessage }} :loading="deleteLoading === passkey.credentialID"
</div> :disable="!!deleteLoading || !!identifyLoading"
<div >
v-if="deleteErrorMessage" <q-tooltip>Delete Passkey</q-tooltip>
class="text-negative q-mt-md" </q-btn>
> </q-card-actions>
{{ deleteErrorMessage }} </q-card>
</div> </div>
<div </div>
v-if="identifyErrorMessage" <div
class="text-negative q-mt-md" v-else-if="fetchLoading"
> class="q-mt-md"
{{ identifyErrorMessage }} >
</div> Loading passkeys...
</q-card-section> </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> </q-page>
</template> </template>
@ -341,7 +362,6 @@ async function handleIdentify()
const authResp = await startAuthentication(options); const authResp = await startAuthentication(options);
identifiedPasskeyId.value = authResp.id; identifiedPasskeyId.value = authResp.id;
console.log('Identified Passkey ID:', identifiedPasskeyId.value);
setTimeout(() => setTimeout(() =>
{ {

View file

@ -1,8 +1,11 @@
<template> <template>
<q-page padding> <q-page padding>
<q-card class="q-mb-md"> <q-card
<q-card-section> bordered
<div class="text-h6"> flat
>
<q-card-section class="row items-center justify-between">
<div class="text-h4">
Application Settings Application Settings
</div> </div>
</q-card-section> </q-card-section>

View file

@ -1,8 +1,11 @@
<template> <template>
<q-page padding> <q-page padding>
<q-card class="q-mb-md"> <q-card
flat
bordered
>
<q-card-section> <q-card-section>
<div class="text-h6"> <div class="text-h4">
User Preferences User Preferences
</div> </div>
</q-card-section> </q-card-section>

View file

@ -40,7 +40,6 @@ const routes = [
component: () => import('pages/PasskeyManagementPage.vue'), // Assuming this page exists or will be created component: () => import('pages/PasskeyManagementPage.vue'), // Assuming this page exists or will be created
meta: { meta: {
requiresAuth: true, requiresAuth: true,
navGroup: 'auth', // Show only when logged in
icon: 'key', icon: 'key',
title: 'Passkeys', title: 'Passkeys',
caption: 'Manage your passkeys' caption: 'Manage your passkeys'