diff --git a/package.json b/package.json index 1208090..60ae1ed 100644 --- a/package.json +++ b/package.json @@ -16,22 +16,29 @@ "@google/genai": "^0.9.0", "@prisma/client": "^6.6.0", "@quasar/extras": "^1.16.4", + "@simplewebauthn/browser": "^13.1.0", + "@simplewebauthn/server": "^13.1.1", "axios": "^1.8.4", "better-sqlite3": "^11.9.1", "date-fns": "^4.1.0", "dotenv": "^16.5.0", + "express-session": "^1.18.1", "mailparser": "^3.7.2", "marked": "^15.0.9", "node-cron": "^3.0.3", "node-imap": "^0.9.6", "pdfkit": "^0.17.0", "pdfmake": "^0.2.18", + "pinia": "^3.0.2", "quasar": "^2.16.0", + "uuid": "^11.1.0", "vue": "^3.4.18", "vue-router": "^4.0.0" }, "devDependencies": { "@quasar/app-vite": "^2.1.0", + "@types/express-session": "^1.18.1", + "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.2", "postcss": "^8.4.14", "prisma": "^6.6.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bafe07c..806c09c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,12 @@ importers: '@quasar/extras': specifier: ^1.16.4 version: 1.16.17 + '@simplewebauthn/browser': + specifier: ^13.1.0 + version: 13.1.0 + '@simplewebauthn/server': + specifier: ^13.1.1 + version: 13.1.1 axios: specifier: ^1.8.4 version: 1.8.4 @@ -29,6 +35,9 @@ importers: dotenv: specifier: ^16.5.0 version: 16.5.0 + express-session: + specifier: ^1.18.1 + version: 1.18.1 mailparser: specifier: ^3.7.2 version: 3.7.2 @@ -47,9 +56,15 @@ importers: pdfmake: specifier: ^0.2.18 version: 0.2.18 + pinia: + specifier: ^3.0.2 + version: 3.0.2(vue@3.5.13) quasar: specifier: ^2.16.0 version: 2.18.1 + uuid: + specifier: ^11.1.0 + version: 11.1.0 vue: specifier: ^3.4.18 version: 3.5.13 @@ -59,7 +74,13 @@ importers: devDependencies: '@quasar/app-vite': specifier: ^2.1.0 - version: 2.2.0(@types/node@22.14.1)(quasar@2.18.1)(rollup@4.40.0)(terser@5.39.0)(vue-router@4.5.0(vue@3.5.13))(vue@3.5.13) + version: 2.2.0(@types/node@22.14.1)(pinia@3.0.2(vue@3.5.13))(quasar@2.18.1)(rollup@4.40.0)(terser@5.39.0)(vue-router@4.5.0(vue@3.5.13))(vue@3.5.13) + '@types/express-session': + specifier: ^1.18.1 + version: 1.18.1 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 autoprefixer: specifier: ^10.4.2 version: 10.4.21(postcss@8.5.3) @@ -258,6 +279,9 @@ packages: resolution: {integrity: sha512-FD2RizYGInsvfjeaN6O+wQGpRnGVglS1XWrGQr8K7D04AfMmvPodDSw94U9KyFtsVLzWH9kmlPyFM+G4jbmkqg==} engines: {node: '>=18.0.0'} + '@hexagon/base64@1.1.28': + resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} + '@inquirer/figures@1.0.11': resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==} engines: {node: '>=18'} @@ -287,6 +311,24 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@levischuck/tiny-cbor@0.2.11': + resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} + + '@peculiar/asn1-android@2.3.16': + resolution: {integrity: sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw==} + + '@peculiar/asn1-ecc@2.3.15': + resolution: {integrity: sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==} + + '@peculiar/asn1-rsa@2.3.15': + resolution: {integrity: sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==} + + '@peculiar/asn1-schema@2.3.15': + resolution: {integrity: sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==} + + '@peculiar/asn1-x509@2.3.15': + resolution: {integrity: sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -472,6 +514,13 @@ packages: '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@simplewebauthn/browser@13.1.0': + resolution: {integrity: sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg==} + + '@simplewebauthn/server@13.1.1': + resolution: {integrity: sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==} + engines: {node: '>=20.0.0'} + '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} @@ -496,6 +545,9 @@ packages: '@types/express-serve-static-core@4.19.6': resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + '@types/express-session@1.18.1': + resolution: {integrity: sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg==} + '@types/express@4.17.21': resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} @@ -532,6 +584,9 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@vitejs/plugin-vue@5.2.3': resolution: {integrity: sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -554,6 +609,15 @@ packages: '@vue/devtools-api@6.6.4': resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + '@vue/devtools-api@7.7.5': + resolution: {integrity: sha512-HYV3tJGARROq5nlVMJh5KKHk7GU8Au3IrrmNNqr978m0edxgpHgYPDoNUGrvEgIbObz09SQezFR3A1EVmB5WZg==} + + '@vue/devtools-kit@7.7.5': + resolution: {integrity: sha512-S9VAVJYVAe4RPx2JZb9ZTEi0lqTySz2CBeF0wHT5D3dkTLnT9yMMGegKNl4b2EIELwLSkcI9bl2qp0/jW+upqA==} + + '@vue/devtools-shared@7.7.5': + resolution: {integrity: sha512-QBjG72RfpM0DKtpns2RZOxBltO226kOAls9e4Lri6YxS2gWTgL0H+wj1R2K76lxxIeOrqo4+2Ty6RQnzv+WSTQ==} + '@vue/reactivity@3.5.13': resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} @@ -623,6 +687,10 @@ packages: array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + asn1js@3.0.6: + resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==} + engines: {node: '>=12.0.0'} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -671,6 +739,9 @@ packages: bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + birpc@2.3.0: + resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -835,10 +906,21 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -1068,6 +1150,10 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + express-session@1.18.1: + resolution: {integrity: sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==} + engines: {node: '>= 0.8.0'} + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} @@ -1230,6 +1316,9 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + html-minifier-terser@7.2.0: resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} engines: {node: ^14.13.1 || >=16.0.0} @@ -1341,6 +1430,10 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -1492,6 +1585,9 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -1653,6 +1749,9 @@ packages: peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1664,6 +1763,15 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + pinia@3.0.2: + resolution: {integrity: sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -1713,6 +1821,13 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.1.3: + resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} + engines: {node: '>=6.0.0'} + qs@6.13.0: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} @@ -1721,6 +1836,10 @@ packages: resolution: {integrity: sha512-db/P64Mzpt1uXJ0MapaG+IYJQ9hHDb5KtTCoszwC78DR7sA+Uoj7nBW2EytwYykIExEmqavOvKrdasTvqhkgEg==} engines: {node: '>= 10.18.1', npm: '>= 6.13.4', yarn: '>= 1.21.1'} + random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -1773,6 +1892,9 @@ packages: restructure@3.0.2: resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rollup-plugin-visualizer@5.14.0: resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==} engines: {node: '>=18'} @@ -2036,6 +2158,10 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + stack-trace@1.0.0-pre2: resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} engines: {node: '>=16'} @@ -2073,6 +2199,10 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2165,6 +2295,10 @@ packages: ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + uid-safe@2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -2198,6 +2332,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -2475,6 +2613,8 @@ snapshots: - supports-color - utf-8-validate + '@hexagon/base64@1.1.28': {} + '@inquirer/figures@1.0.11': {} '@isaacs/cliui@8.0.2': @@ -2508,6 +2648,41 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@levischuck/tiny-cbor@0.2.11': {} + + '@peculiar/asn1-android@2.3.16': + dependencies: + '@peculiar/asn1-schema': 2.3.15 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-ecc@2.3.15': + dependencies: + '@peculiar/asn1-schema': 2.3.15 + '@peculiar/asn1-x509': 2.3.15 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-rsa@2.3.15': + dependencies: + '@peculiar/asn1-schema': 2.3.15 + '@peculiar/asn1-x509': 2.3.15 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-schema@2.3.15': + dependencies: + asn1js: 3.0.6 + pvtsutils: 1.3.6 + tslib: 2.8.1 + + '@peculiar/asn1-x509@2.3.15': + dependencies: + '@peculiar/asn1-schema': 2.3.15 + asn1js: 3.0.6 + pvtsutils: 1.3.6 + tslib: 2.8.1 + '@pkgjs/parseargs@0.11.0': optional: true @@ -2543,7 +2718,7 @@ snapshots: dependencies: '@prisma/debug': 6.6.0 - '@quasar/app-vite@2.2.0(@types/node@22.14.1)(quasar@2.18.1)(rollup@4.40.0)(terser@5.39.0)(vue-router@4.5.0(vue@3.5.13))(vue@3.5.13)': + '@quasar/app-vite@2.2.0(@types/node@22.14.1)(pinia@3.0.2(vue@3.5.13))(quasar@2.18.1)(rollup@4.40.0)(terser@5.39.0)(vue-router@4.5.0(vue@3.5.13))(vue@3.5.13)': dependencies: '@quasar/render-ssr-error': 1.0.3 '@quasar/ssl-certificate': 1.0.0 @@ -2585,6 +2760,8 @@ snapshots: vue: 3.5.13 vue-router: 4.5.0(vue@3.5.13) webpack-merge: 6.0.1 + optionalDependencies: + pinia: 3.0.2(vue@3.5.13) transitivePeerDependencies: - '@types/node' - jiti @@ -2683,6 +2860,18 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 + '@simplewebauthn/browser@13.1.0': {} + + '@simplewebauthn/server@13.1.1': + dependencies: + '@hexagon/base64': 1.1.28 + '@levischuck/tiny-cbor': 0.2.11 + '@peculiar/asn1-android': 2.3.16 + '@peculiar/asn1-ecc': 2.3.15 + '@peculiar/asn1-rsa': 2.3.15 + '@peculiar/asn1-schema': 2.3.15 + '@peculiar/asn1-x509': 2.3.15 + '@swc/helpers@0.5.17': dependencies: tslib: 2.8.1 @@ -2716,6 +2905,10 @@ snapshots: '@types/range-parser': 1.2.7 '@types/send': 0.17.4 + '@types/express-session@1.18.1': + dependencies: + '@types/express': 4.17.21 + '@types/express@4.17.21': dependencies: '@types/body-parser': 1.19.5 @@ -2758,6 +2951,8 @@ snapshots: '@types/node': 22.14.1 '@types/send': 0.17.4 + '@types/uuid@10.0.0': {} + '@vitejs/plugin-vue@5.2.3(vite@6.3.2(@types/node@22.14.1)(sass-embedded@1.87.0)(terser@5.39.0))(vue@3.5.13)': dependencies: vite: 6.3.2(@types/node@22.14.1)(sass-embedded@1.87.0)(terser@5.39.0) @@ -2795,6 +2990,24 @@ snapshots: '@vue/devtools-api@6.6.4': {} + '@vue/devtools-api@7.7.5': + dependencies: + '@vue/devtools-kit': 7.7.5 + + '@vue/devtools-kit@7.7.5': + dependencies: + '@vue/devtools-shared': 7.7.5 + birpc: 2.3.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-shared@7.7.5': + dependencies: + rfdc: 1.4.1 + '@vue/reactivity@3.5.13': dependencies: '@vue/shared': 3.5.13 @@ -2873,6 +3086,12 @@ snapshots: array-flatten@1.1.1: {} + asn1js@3.0.6: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.1.3 + tslib: 2.8.1 + async@3.2.6: {} asynckit@0.4.0: {} @@ -2921,6 +3140,8 @@ snapshots: dependencies: file-uri-to-path: 1.0.0 + birpc@2.3.0: {} + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -3112,8 +3333,16 @@ snapshots: cookie-signature@1.0.6: {} + cookie-signature@1.0.7: {} + cookie@0.7.1: {} + cookie@0.7.2: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + core-util-is@1.0.3: {} crc-32@1.2.2: {} @@ -3330,6 +3559,19 @@ snapshots: expand-template@2.0.3: {} + express-session@1.18.1: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + on-headers: 1.0.2 + parseurl: 1.3.3 + safe-buffer: 5.2.1 + uid-safe: 2.1.5 + transitivePeerDependencies: + - supports-color + express@4.21.2: dependencies: accepts: 1.3.8 @@ -3548,6 +3790,8 @@ snapshots: he@1.2.0: {} + hookable@5.5.3: {} + html-minifier-terser@7.2.0: dependencies: camel-case: 4.1.2 @@ -3670,6 +3914,8 @@ snapshots: is-unicode-supported@0.1.0: {} + is-what@4.1.16: {} + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 @@ -3817,6 +4063,8 @@ snapshots: minipass@7.1.2: {} + mitt@3.0.1: {} + mkdirp-classic@0.5.3: {} mlly@1.7.4: @@ -3971,12 +4219,19 @@ snapshots: peberminta@0.9.0: {} + perfect-debounce@1.0.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} picomatch@4.0.2: {} + pinia@3.0.2(vue@3.5.13): + dependencies: + '@vue/devtools-api': 7.7.5 + vue: 3.5.13 + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -4035,12 +4290,20 @@ snapshots: punycode.js@2.3.1: {} + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.1.3: {} + qs@6.13.0: dependencies: side-channel: 1.1.0 quasar@2.18.1: {} + random-bytes@1.0.0: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -4113,6 +4376,8 @@ snapshots: restructure@3.0.2: {} + rfdc@1.4.1: {} + rollup-plugin-visualizer@5.14.0(rollup@4.40.0): dependencies: open: 8.4.2 @@ -4381,6 +4646,8 @@ snapshots: source-map@0.7.4: {} + speakingurl@14.0.1: {} + stack-trace@1.0.0-pre2: {} statuses@2.0.1: {} @@ -4422,6 +4689,10 @@ snapshots: strip-json-comments@2.0.1: {} + superjson@2.2.2: + dependencies: + copy-anything: 3.0.5 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -4510,6 +4781,10 @@ snapshots: ufo@1.6.1: {} + uid-safe@2.1.5: + dependencies: + random-bytes: 1.0.0 + undici-types@6.21.0: {} unicode-properties@1.4.1: @@ -4540,6 +4815,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@11.1.0: {} + uuid@8.3.2: {} uuid@9.0.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4bc88d5..2e0a734 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,7 @@ onlyBuiltDependencies: - '@prisma/client' + - '@prisma/engines' - better-sqlite3 - esbuild + - prisma - sqlite3 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a16bad4..b7c0042 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -99,3 +99,27 @@ model Setting { @@map("settings") // Map to the 'settings' table } + +// Added for WebAuthn +model User { + id String @id @default(uuid()) + username String @unique + authenticators Authenticator[] + + @@map("users") +} + +model Authenticator { + id String @id @default(uuid()) + credentialID String @unique @map("credential_id") // Base64URL encoded + credentialPublicKey Bytes @map("credential_public_key") + counter BigInt + credentialDeviceType String @map("credential_device_type") // 'singleDevice' or 'multiDevice' + credentialBackedUp Boolean @map("credential_backed_up") + transports String? // Comma-separated list like "internal,hybrid" + + userId String @map("user_id") + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("authenticators") +} diff --git a/public/favicon.ico b/public/favicon.ico index ae7bbdb..6438507 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/icons/favicon-128x128.png b/public/icons/favicon-128x128.png index 1401176..9b07830 100644 Binary files a/public/icons/favicon-128x128.png and b/public/icons/favicon-128x128.png differ diff --git a/public/icons/favicon-16x16.png b/public/icons/favicon-16x16.png index 679063a..c1f2f7f 100644 Binary files a/public/icons/favicon-16x16.png and b/public/icons/favicon-16x16.png differ diff --git a/public/icons/favicon-32x32.png b/public/icons/favicon-32x32.png index fd1fbc6..9888c73 100644 Binary files a/public/icons/favicon-32x32.png and b/public/icons/favicon-32x32.png differ diff --git a/public/icons/favicon-96x96.png b/public/icons/favicon-96x96.png index e93b80a..7d29588 100644 Binary files a/public/icons/favicon-96x96.png and b/public/icons/favicon-96x96.png differ diff --git a/public/stylepoint.png b/public/stylepoint.png new file mode 100644 index 0000000..b452619 Binary files /dev/null and b/public/stylepoint.png differ diff --git a/src-ssr/routes/auth.js b/src-ssr/routes/auth.js new file mode 100644 index 0000000..c867edc --- /dev/null +++ b/src-ssr/routes/auth.js @@ -0,0 +1,390 @@ +// src-ssr/routes/auth.js +import express from 'express'; +import { + generateRegistrationOptions, + verifyRegistrationResponse, + generateAuthenticationOptions, + verifyAuthenticationResponse, +} from '@simplewebauthn/server'; +import { isoBase64URL } from '@simplewebauthn/server/helpers'; // Ensure this is imported if not already +import prisma from '../database.js'; +import { rpID, rpName, origin, challengeStore } from '../server.js'; // Import RP details and challenge store + +const router = express.Router(); + +// Helper function to get user authenticators +async function getUserAuthenticators(userId) { + return prisma.authenticator.findMany({ + where: { userId }, + select: { + credentialID: true, + credentialPublicKey: true, + counter: true, + transports: true, + }, + }); +} + +// Helper function to get a user by username +async function getUserByUsername(username) { + return prisma.user.findUnique({ where: { username } }); +} + +// Helper function to get a user by ID +async function getUserById(id) { + return prisma.user.findUnique({ where: { id } }); +} + +// Helper function to get an authenticator by credential ID +async function getAuthenticatorByCredentialID(credentialID) { + return prisma.authenticator.findUnique({ where: { credentialID } }); +} + + +// Generate Registration Options +router.post('/generate-registration-options', async (req, res) => { + const { username } = req.body; + + if (!username) { + return res.status(400).json({ error: 'Username is required' }); + } + + try { + let user = await getUserByUsername(username); + + // If user doesn't exist, create one + if (!user) { + user = await prisma.user.create({ + data: { username }, + }); + } + + const userAuthenticators = await getUserAuthenticators(user.id); + + if(userAuthenticators.length > 0) { + //The user is trying to register a new authenticator, so we need to check if the user registering is the same as the one in the session + if (!req.session.loggedInUserId || req.session.loggedInUserId !== user.id) { + return res.status(403).json({ error: 'Invalid registration attempt.' }); + } + } + + const options = await generateRegistrationOptions({ + rpName, + rpID, + userName: user.username, + // Don't prompt users for additional authenticators if they've already registered some + excludeCredentials: userAuthenticators.map(auth => ({ + id: auth.credentialID, // Use isoBase64URL helper + type: 'public-key', + // Optional: Specify transports if you know them + transports: auth.transports ? auth.transports.split(',') : undefined, + })), + authenticatorSelection: { + // Defaults + residentKey: 'required', + userVerification: 'preferred', + }, + // Strong advice: Always require attestation for registration + attestationType: 'none', // Use 'none' for simplicity, 'direct' or 'indirect' recommended for production + }); + + // Store the challenge + challengeStore.set(user.id, options.challenge); + req.session.userId = user.id; // Temporarily store userId in session for verification step + + res.json(options); + } catch (error) { + console.error('Registration options error:', error); + res.status(500).json({ error: 'Failed to generate registration options' }); + } +}); + +// Verify Registration +router.post('/verify-registration', async (req, res) => { + const { registrationResponse } = req.body; + const userId = req.session.userId; // Retrieve userId stored during options generation + + if (!userId) { + return res.status(400).json({ error: 'User session not found. Please start registration again.' }); + } + + const expectedChallenge = challengeStore.get(userId); + + if (!expectedChallenge) { + return res.status(400).json({ error: 'Challenge not found or expired' }); + } + + try { + const user = await getUserById(userId); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + const verification = await verifyRegistrationResponse({ + response: registrationResponse, + expectedChallenge: expectedChallenge, + expectedOrigin: origin, + expectedRPID: rpID, + requireUserVerification: false, // Adjust based on your requirements + }); + + const { verified, registrationInfo } = verification; + + console.log(verification); + + if (verified && registrationInfo) { + const { credential, credentialDeviceType, credentialBackedUp } = registrationInfo; + + const credentialID = credential.id; + const credentialPublicKey = credential.publicKey; + const counter = credential.counter; + const transports = credential.transports || []; // Use empty array if transports are not provided + + // Check if authenticator with this ID already exists + const existingAuthenticator = await getAuthenticatorByCredentialID(isoBase64URL.fromBuffer(credentialID)); + + if (existingAuthenticator) { + return res.status(409).json({ error: 'Authenticator already registered' }); + } + + // Save the authenticator + await prisma.authenticator.create({ + data: { + credentialID, // Store as Base64URL string + credentialPublicKey: Buffer.from(credentialPublicKey), // Store as Bytes + counter: BigInt(counter), // Store as BigInt + credentialDeviceType, + credentialBackedUp, + transports: transports.join(','), // Store transports as comma-separated string + userId: user.id, + }, + }); + + // Clear the challenge and temporary userId + challengeStore.delete(userId); + delete req.session.userId; + + // Log the user in by setting the final session userId + req.session.loggedInUserId = user.id; + + res.json({ verified: true }); + } else { + res.status(400).json({ error: 'Registration verification failed' }); + } + } catch (error) { + console.error('Registration verification error:', error); + challengeStore.delete(userId); // Clean up challenge on error + delete req.session.userId; + res.status(500).json({ error: 'Failed to verify registration', details: error.message }); + } +}); + +// Generate Authentication Options +router.post('/generate-authentication-options', async (req, res) => { + const { username } = req.body; + + try { + let user; + if (username) { + user = await getUserByUsername(username); + } else if (req.session.loggedInUserId) { + // If already logged in, allow re-authentication (e.g., for step-up) + user = await getUserById(req.session.loggedInUserId); + } + + if (!user) { + 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 + allowCredentials: userAuthenticators.map(auth => ({ + id: auth.credentialID, + type: 'public-key', + transports: auth.transports ? auth.transports.split(',') : undefined, + })), + userVerification: 'preferred', + }); + + // Store the challenge associated with the user ID for verification + challengeStore.set(user.id, options.challenge); + req.session.challengeUserId = user.id; // Store user ID associated with this challenge + + res.json(options); + } catch (error) { + console.error('Authentication options error:', error); + res.status(500).json({ error: 'Failed to generate authentication options' }); + } +}); + +// Verify Authentication +router.post('/verify-authentication', async (req, res) => { + const { authenticationResponse } = req.body; + const challengeUserId = req.session.challengeUserId; // Get user ID associated with the challenge + + if (!challengeUserId) { + return res.status(400).json({ error: 'Challenge session not found. Please try logging in again.' }); + } + + const expectedChallenge = challengeStore.get(challengeUserId); + + if (!expectedChallenge) { + return res.status(400).json({ error: 'Challenge not found or expired' }); + } + + try { + const user = await getUserById(challengeUserId); + if (!user) { + return res.status(404).json({ error: 'User associated with challenge not found' }); + } + + const authenticator = await getAuthenticatorByCredentialID(authenticationResponse.id); + + if (!authenticator) { + return res.status(404).json({ error: 'Authenticator not found' }); + } + + // Ensure the authenticator belongs to the user attempting to log in + if (authenticator.userId !== user.id) { + return res.status(403).json({ error: 'Authenticator does not belong to this user' }); + } + + const verification = await verifyAuthenticationResponse({ + response: authenticationResponse, + expectedChallenge: expectedChallenge, + expectedOrigin: origin, + expectedRPID: rpID, + credential: { + id: authenticator.credentialID, + publicKey: authenticator.credentialPublicKey, + counter: authenticator.counter.toString(), // Convert BigInt to string for comparison + transports: authenticator.transports ? authenticator.transports.split(',') : undefined, + }, + requireUserVerification: false, // Enforce user verification + }); + + const { verified, authenticationInfo } = verification; + + if (verified) { + // Update the authenticator counter + await prisma.authenticator.update({ + where: { credentialID: authenticator.credentialID }, + data: { counter: BigInt(authenticationInfo.newCounter) }, // Update with the new counter + }); + + // Clear the challenge and associated user ID + challengeStore.delete(challengeUserId); + delete req.session.challengeUserId; + + // Log the user in + req.session.loggedInUserId = user.id; + + res.json({ verified: true, user: { id: user.id, username: user.username } }); + } else { + res.status(400).json({ error: 'Authentication verification failed' }); + } + } catch (error) { + console.error('Authentication verification error:', error); + challengeStore.delete(challengeUserId); // Clean up challenge on error + delete req.session.challengeUserId; + res.status(500).json({ error: 'Failed to verify authentication', details: error.message }); + } +}); + +// GET Passkeys for Logged-in User +router.get('/passkeys', async (req, res) => { + if (!req.session.loggedInUserId) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + try { + const userId = req.session.loggedInUserId; + const authenticators = await prisma.authenticator.findMany({ + where: { userId }, + select: { + credentialID: true, // Already Base64URL string + // Add other fields if needed, e.g., createdAt if you add it to the schema + // createdAt: true, + }, + }); + + // No need to convert credentialID here as it's stored as Base64URL string + res.json(authenticators); + } catch (error) { + console.error('Error fetching passkeys:', error); + res.status(500).json({ error: 'Failed to fetch passkeys' }); + } +}); + +// DELETE Passkey +router.delete('/passkeys/:credentialID', async (req, res) => { + if (!req.session.loggedInUserId) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + const { credentialID } = req.params; // This is already a Base64URL string from the client + + if (!credentialID) { + return res.status(400).json({ error: 'Credential ID is required' }); + } + + try { + const userId = req.session.loggedInUserId; + + // Find the authenticator first to ensure it belongs to the logged-in user + const authenticator = await prisma.authenticator.findUnique({ + where: { credentialID: credentialID }, // Use the Base64URL string directly + }); + + if (!authenticator) { + return res.status(404).json({ error: 'Passkey not found' }); + } + + // Security check: Ensure the passkey belongs to the user trying to delete it + if (authenticator.userId !== userId) { + return res.status(403).json({ error: 'Permission denied' }); + } + + // Delete the authenticator + await prisma.authenticator.delete({ + where: { credentialID: credentialID }, + }); + + res.json({ message: 'Passkey deleted successfully' }); + } catch (error) { + console.error('Error deleting passkey:', error); + // Handle potential Prisma errors, e.g., record not found if deleted between check and delete + if (error.code === 'P2025') { // Prisma code for record not found on delete/update + return res.status(404).json({ error: 'Passkey not found' }); + } + res.status(500).json({ error: 'Failed to delete passkey' }); + } +}); + +// Check Authentication Status +router.get('/status', (req, res) => { + if (req.session.loggedInUserId) { + return res.json({ status: 'authenticated' }); + } + res.json({ status: 'unauthenticated' }); +}); + +// Logout +router.post('/logout', (req, res) => { + req.session.destroy(err => { + if (err) { + console.error('Logout error:', err); + return res.status(500).json({ error: 'Failed to logout' }); + } + res.json({ message: 'Logged out successfully' }); + }); +}); + +export default router; \ No newline at end of file diff --git a/src-ssr/server.js b/src-ssr/server.js index 0dab099..a89d615 100644 --- a/src-ssr/server.js +++ b/src-ssr/server.js @@ -11,6 +11,8 @@ */ import express from 'express' import compression from 'compression' +import session from 'express-session'; // Added for session management +import { v4 as uuidv4 } from 'uuid'; // Added for generating session IDs import { defineSsrCreate, defineSsrListen, @@ -21,9 +23,18 @@ import { import prisma from './database.js'; // Import the prisma client instance import apiRoutes from './routes/api.js'; +import authRoutes from './routes/auth.js'; // Added for WebAuthn routes import cron from 'node-cron'; import { generateAndStoreMantisSummary } from './services/mantisSummarizer.js'; +// Define Relying Party details (Update with your actual details) +export const rpID = process.env.NODE_ENV === 'production' ? 'your-production-domain.com' : 'localhost'; +export const rpName = 'StylePoint'; +export const origin = process.env.NODE_ENV === 'production' ? `https://${rpID}` : `http://${rpID}:9100`; + +// In-memory store for challenges (Replace with a persistent store in production) +export const challengeStore = new Map(); + /** * Create your webserver and return its instance. * If needed, prepare your webserver to receive @@ -34,6 +45,19 @@ import { generateAndStoreMantisSummary } from './services/mantisSummarizer.js'; export const create = defineSsrCreate((/* { ... } */) => { const app = express() + // Session middleware configuration + app.use(session({ + genid: (req) => uuidv4(), // Use UUIDs for session IDs + secret: process.env.SESSION_SECRET || 'a-very-strong-secret-key', // Use an environment variable for the secret + resave: false, + saveUninitialized: true, + cookie: { + secure: process.env.NODE_ENV === 'production', // Use secure cookies in production + httpOnly: true, + maxAge: 1000 * 60 * 60 * 24 // 1 day + } + })); + // Initialize the database (now synchronous) try { console.log('Prisma Client is ready.'); // Log Prisma readiness @@ -72,6 +96,7 @@ export const create = defineSsrCreate((/* { ... } */) => { // Add API routes app.use('/api', apiRoutes); + app.use('/auth', authRoutes); // Added WebAuthn auth routes // place here any middlewares that // absolutely need to run before anything else diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index b048010..09fcb40 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -16,71 +16,42 @@ - - - Forms - - - - - - Forms - View existing forms - - - + - Mantis Summaries + {{ item.meta.title }} - + - Mantis Summaries - View daily summaries + {{ item.meta.title }} + {{ item.meta.caption }} + - Email Summaries + Logout - + - Email Summaries - View email summaries - - - - - - Settings - - - - - - - Settings - Manage application settings + Logout @@ -94,11 +65,55 @@ diff --git a/src/pages/LandingPage.vue b/src/pages/LandingPage.vue new file mode 100644 index 0000000..9fafacb --- /dev/null +++ b/src/pages/LandingPage.vue @@ -0,0 +1,36 @@ + + + + + Welcome to StylePoint + The all-in-one tool designed for StyleTech Developers. + + + + Features + + + + {{ feature }} + + + + + + + + \ No newline at end of file diff --git a/src/pages/LoginPage.vue b/src/pages/LoginPage.vue new file mode 100644 index 0000000..e5d0e83 --- /dev/null +++ b/src/pages/LoginPage.vue @@ -0,0 +1,103 @@ + + + + + Login + + + + + + {{ errorMessage }} + + + + + + + + + + diff --git a/src/pages/PasskeyManagementPage.vue b/src/pages/PasskeyManagementPage.vue new file mode 100644 index 0000000..05679b0 --- /dev/null +++ b/src/pages/PasskeyManagementPage.vue @@ -0,0 +1,275 @@ + + + + Passkey Management + + + + + + + + + Your Registered Passkeys + + + {{ registerSuccessMessage }} + {{ registerErrorMessage }} + + + + Passkey ID: {{ passkey.credentialID }} + + Verified just now! + + + + + + + + + + + + Loading passkeys... + You have no passkeys registered yet. + + {{ fetchErrorMessage }} + {{ deleteSuccessMessage }} + {{ deleteErrorMessage }} + {{ identifyErrorMessage }} + + + + + + + \ No newline at end of file diff --git a/src/pages/RegisterPage.vue b/src/pages/RegisterPage.vue new file mode 100644 index 0000000..fef368e --- /dev/null +++ b/src/pages/RegisterPage.vue @@ -0,0 +1,135 @@ + + + + + + {{ isLoggedIn ? 'Register New Passkey' : 'Register Passkey' }} + + + + + + {{ successMessage }} + {{ errorMessage }} + + + + + + + + + + + diff --git a/src/router/index.js b/src/router/index.js index 226eb50..86e5d93 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,6 +1,7 @@ import { defineRouter } from '#q-app/wrappers' import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router' import routes from './routes' +import { useAuthStore } from 'stores/auth'; // Import the auth store /* * If not building with SSR mode, you can @@ -11,7 +12,7 @@ import routes from './routes' * with the Router instance. */ -export default defineRouter(function (/* { store, ssrContext } */) { +export default defineRouter(function ({ store /* { store, ssrContext } */ }) { const createHistory = process.env.SERVER ? createMemoryHistory : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory) @@ -26,5 +27,45 @@ export default defineRouter(function (/* { store, ssrContext } */) { history: createHistory(process.env.VUE_ROUTER_BASE) }) + // Navigation Guard using Pinia store + Router.beforeEach(async (to, from, next) => { + const authStore = useAuthStore(store); // Get store instance + + // Ensure auth status is checked, especially on first load or refresh + // This check might be better placed in App.vue or a boot file + if (!authStore.user && !authStore.loading) { // Check only if user is not loaded and not already loading + try { + await authStore.checkAuthStatus(); + } catch (e) { + console.error("Initial auth check failed", e); + // Decide how to handle initial check failure (e.g., proceed, redirect to error page) + } + } + + const requiresAuth = to.matched.some(record => record.meta.requiresAuth); + const publicPages = ['/login', '/register']; + const isPublicPage = publicPages.includes(to.path); + const isAuthenticated = authStore.isAuthenticated; // Get status from store + + console.log('Store Auth status:', isAuthenticated); + console.log('Navigating to:', to.path); + console.log('Requires auth:', requiresAuth); + console.log('Is public page:', isPublicPage); + + if (requiresAuth && !isAuthenticated) { + // If route requires auth and user is not authenticated, redirect to login + console.log('Redirecting to login (requires auth, not authenticated)'); + next('/login'); + } else if (isPublicPage && isAuthenticated) { + // If user is authenticated and tries to access login/register, redirect to home + console.log('Redirecting to home (public page, authenticated)'); + next('/'); + } else { + // Otherwise, allow navigation + console.log('Allowing navigation'); + next(); + } + }); + return Router }) diff --git a/src/router/routes.js b/src/router/routes.js index ecb0426..837063a 100644 --- a/src/router/routes.js +++ b/src/router/routes.js @@ -3,15 +3,101 @@ const routes = [ path: '/', component: () => import('layouts/MainLayout.vue'), children: [ - { path: '', name: 'home', component: () => import('pages/FormListPage.vue') }, - { path: 'forms', name: 'formList', component: () => import('pages/FormListPage.vue') }, - { path: 'forms/new', name: 'formCreate', component: () => import('pages/FormCreatePage.vue') }, - { path: 'forms/:id/edit', name: 'formEdit', component: () => import('pages/FormEditPage.vue'), props: true }, - { path: 'forms/:id/fill', name: 'formFill', component: () => import('pages/FormFillPage.vue'), props: true }, - { path: 'forms/:id/responses', name: 'formResponses', component: () => import('pages/FormResponsesPage.vue'), props: true }, - { path: 'mantis-summaries', name: 'mantisSummaries', component: () => import('pages/MantisSummariesPage.vue') }, - { path: 'email-summaries', name: 'emailSummaries', component: () => import('pages/EmailSummariesPage.vue') }, - { path: 'settings', name: 'settings', component: () => import('pages/SettingsPage.vue') } + { + path: '', + name: 'home', + component: () => import('pages/LandingPage.vue'), + meta: { requiresAuth: false } // Keep home accessible, but don't show in nav + }, + { + path: '/login', + name: 'login', + component: () => import('pages/LoginPage.vue'), + meta: { + requiresAuth: false, + navGroup: 'noAuth', // Show only when logged out + icon: 'login', + title: 'Login', + caption: 'Access your account' + } + }, + { + path: '/register', + name: 'register', + component: () => import('pages/RegisterPage.vue'), + meta: { + requiresAuth: false, + navGroup: 'noAuth', // Show only when logged out + icon: 'person_add', + title: 'Register', + caption: 'Create an account' + } + }, + // Add a new route specifically for managing passkeys when logged in + { + path: '/passkeys', + name: 'passkeys', + 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' + } + }, + { + path: 'forms', + name: 'formList', + component: () => import('pages/FormListPage.vue'), + meta: { + requiresAuth: true, + navGroup: 'auth', // Show only when logged in + icon: 'list_alt', + title: 'Forms', + caption: 'View existing forms' + } + }, + { path: 'forms/new', name: 'formCreate', component: () => import('pages/FormCreatePage.vue'), meta: { requiresAuth: true } }, // Not in nav + { path: 'forms/:id/edit', name: 'formEdit', component: () => import('pages/FormEditPage.vue'), props: true, meta: { requiresAuth: true } }, // Not in nav + { path: 'forms/:id/fill', name: 'formFill', component: () => import('pages/FormFillPage.vue'), props: true, meta: { requiresAuth: true } }, // Not in nav + { path: 'forms/:id/responses', name: 'formResponses', component: () => import('pages/FormResponsesPage.vue'), props: true, meta: { requiresAuth: true } }, // Not in nav + { + path: 'mantis-summaries', + name: 'mantisSummaries', + component: () => import('pages/MantisSummariesPage.vue'), + meta: { + requiresAuth: true, + navGroup: 'auth', // Show only when logged in + icon: 'summarize', + title: 'Mantis Summaries', + caption: 'View daily summaries' + } + }, + { + path: 'email-summaries', + name: 'emailSummaries', + component: () => import('pages/EmailSummariesPage.vue'), + meta: { + requiresAuth: true, + navGroup: 'auth', // Show only when logged in + icon: 'email', + title: 'Email Summaries', + caption: 'View email summaries' + } + }, + { + path: 'settings', + name: 'settings', + component: () => import('pages/SettingsPage.vue'), + meta: { + requiresAuth: true, + navGroup: 'auth', // Show only when logged in + icon: 'settings', + title: 'Settings', + caption: 'Manage application settings' + } + } ] }, diff --git a/src/stores/auth.js b/src/stores/auth.js new file mode 100644 index 0000000..405c869 --- /dev/null +++ b/src/stores/auth.js @@ -0,0 +1,39 @@ +import { defineStore } from 'pinia' +import axios from 'axios'; + +export const useAuthStore = defineStore('auth', { + state: () => ({ + isAuthenticated: false, + user: null, + loading: false, // Optional: track loading state + error: null, // Optional: track errors + }), + actions: { + async checkAuthStatus() { + this.loading = true; + this.error = null; + try { + const res = await axios.get('/auth/check-auth'); + if (res.data.isAuthenticated) { + this.isAuthenticated = true; + this.user = res.data.user; + } else { + this.isAuthenticated = false; + this.user = null; + } + } catch (error) { + console.error('Failed to check authentication status:', error); + this.error = 'Could not verify login status.'; + this.isAuthenticated = false; + this.user = null; + } finally { + this.loading = false; + } + }, + // Action to manually set user as logged out (e.g., after logout) + logout() { + this.isAuthenticated = false; + this.user = null; + } + }, +}); diff --git a/src/stores/index.js b/src/stores/index.js new file mode 100644 index 0000000..91f3dd4 --- /dev/null +++ b/src/stores/index.js @@ -0,0 +1,20 @@ +import { defineStore } from '#q-app/wrappers' +import { createPinia } from 'pinia' + +/* + * If not building with SSR mode, you can + * directly export the Store instantiation; + * + * The function below can be async too; either use + * async/await or return a Promise which resolves + * with the Store instance. + */ + +export default defineStore((/* { ssrContext } */) => { + const pinia = createPinia() + + // You can add Pinia plugins here + // pinia.use(SomePiniaPlugin) + + return pinia +}) \ No newline at end of file
The all-in-one tool designed for StyleTech Developers.