Adds in authentication system and overhauls the navigation bar to be built dynamically.
|
@ -16,22 +16,29 @@
|
||||||
"@google/genai": "^0.9.0",
|
"@google/genai": "^0.9.0",
|
||||||
"@prisma/client": "^6.6.0",
|
"@prisma/client": "^6.6.0",
|
||||||
"@quasar/extras": "^1.16.4",
|
"@quasar/extras": "^1.16.4",
|
||||||
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
|
"@simplewebauthn/server": "^13.1.1",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"better-sqlite3": "^11.9.1",
|
"better-sqlite3": "^11.9.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
|
"express-session": "^1.18.1",
|
||||||
"mailparser": "^3.7.2",
|
"mailparser": "^3.7.2",
|
||||||
"marked": "^15.0.9",
|
"marked": "^15.0.9",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
"node-imap": "^0.9.6",
|
"node-imap": "^0.9.6",
|
||||||
"pdfkit": "^0.17.0",
|
"pdfkit": "^0.17.0",
|
||||||
"pdfmake": "^0.2.18",
|
"pdfmake": "^0.2.18",
|
||||||
|
"pinia": "^3.0.2",
|
||||||
"quasar": "^2.16.0",
|
"quasar": "^2.16.0",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
"vue": "^3.4.18",
|
"vue": "^3.4.18",
|
||||||
"vue-router": "^4.0.0"
|
"vue-router": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/app-vite": "^2.1.0",
|
"@quasar/app-vite": "^2.1.0",
|
||||||
|
"@types/express-session": "^1.18.1",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
"postcss": "^8.4.14",
|
"postcss": "^8.4.14",
|
||||||
"prisma": "^6.6.0"
|
"prisma": "^6.6.0"
|
||||||
|
|
281
pnpm-lock.yaml
generated
|
@ -17,6 +17,12 @@ importers:
|
||||||
'@quasar/extras':
|
'@quasar/extras':
|
||||||
specifier: ^1.16.4
|
specifier: ^1.16.4
|
||||||
version: 1.16.17
|
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:
|
axios:
|
||||||
specifier: ^1.8.4
|
specifier: ^1.8.4
|
||||||
version: 1.8.4
|
version: 1.8.4
|
||||||
|
@ -29,6 +35,9 @@ importers:
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.5.0
|
specifier: ^16.5.0
|
||||||
version: 16.5.0
|
version: 16.5.0
|
||||||
|
express-session:
|
||||||
|
specifier: ^1.18.1
|
||||||
|
version: 1.18.1
|
||||||
mailparser:
|
mailparser:
|
||||||
specifier: ^3.7.2
|
specifier: ^3.7.2
|
||||||
version: 3.7.2
|
version: 3.7.2
|
||||||
|
@ -47,9 +56,15 @@ importers:
|
||||||
pdfmake:
|
pdfmake:
|
||||||
specifier: ^0.2.18
|
specifier: ^0.2.18
|
||||||
version: 0.2.18
|
version: 0.2.18
|
||||||
|
pinia:
|
||||||
|
specifier: ^3.0.2
|
||||||
|
version: 3.0.2(vue@3.5.13)
|
||||||
quasar:
|
quasar:
|
||||||
specifier: ^2.16.0
|
specifier: ^2.16.0
|
||||||
version: 2.18.1
|
version: 2.18.1
|
||||||
|
uuid:
|
||||||
|
specifier: ^11.1.0
|
||||||
|
version: 11.1.0
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.4.18
|
specifier: ^3.4.18
|
||||||
version: 3.5.13
|
version: 3.5.13
|
||||||
|
@ -59,7 +74,13 @@ importers:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@quasar/app-vite':
|
'@quasar/app-vite':
|
||||||
specifier: ^2.1.0
|
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:
|
autoprefixer:
|
||||||
specifier: ^10.4.2
|
specifier: ^10.4.2
|
||||||
version: 10.4.21(postcss@8.5.3)
|
version: 10.4.21(postcss@8.5.3)
|
||||||
|
@ -258,6 +279,9 @@ packages:
|
||||||
resolution: {integrity: sha512-FD2RizYGInsvfjeaN6O+wQGpRnGVglS1XWrGQr8K7D04AfMmvPodDSw94U9KyFtsVLzWH9kmlPyFM+G4jbmkqg==}
|
resolution: {integrity: sha512-FD2RizYGInsvfjeaN6O+wQGpRnGVglS1XWrGQr8K7D04AfMmvPodDSw94U9KyFtsVLzWH9kmlPyFM+G4jbmkqg==}
|
||||||
engines: {node: '>=18.0.0'}
|
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':
|
'@inquirer/figures@1.0.11':
|
||||||
resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==}
|
resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
@ -287,6 +311,24 @@ packages:
|
||||||
'@jridgewell/trace-mapping@0.3.25':
|
'@jridgewell/trace-mapping@0.3.25':
|
||||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
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':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
@ -472,6 +514,13 @@ packages:
|
||||||
'@selderee/plugin-htmlparser2@0.11.0':
|
'@selderee/plugin-htmlparser2@0.11.0':
|
||||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
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':
|
'@swc/helpers@0.5.17':
|
||||||
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
||||||
|
|
||||||
|
@ -496,6 +545,9 @@ packages:
|
||||||
'@types/express-serve-static-core@4.19.6':
|
'@types/express-serve-static-core@4.19.6':
|
||||||
resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==}
|
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':
|
'@types/express@4.17.21':
|
||||||
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
|
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
|
||||||
|
|
||||||
|
@ -532,6 +584,9 @@ packages:
|
||||||
'@types/serve-static@1.15.7':
|
'@types/serve-static@1.15.7':
|
||||||
resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==}
|
resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==}
|
||||||
|
|
||||||
|
'@types/uuid@10.0.0':
|
||||||
|
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
|
||||||
|
|
||||||
'@vitejs/plugin-vue@5.2.3':
|
'@vitejs/plugin-vue@5.2.3':
|
||||||
resolution: {integrity: sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==}
|
resolution: {integrity: sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
@ -554,6 +609,15 @@ packages:
|
||||||
'@vue/devtools-api@6.6.4':
|
'@vue/devtools-api@6.6.4':
|
||||||
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
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':
|
'@vue/reactivity@3.5.13':
|
||||||
resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==}
|
resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==}
|
||||||
|
|
||||||
|
@ -623,6 +687,10 @@ packages:
|
||||||
array-flatten@1.1.1:
|
array-flatten@1.1.1:
|
||||||
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
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:
|
async@3.2.6:
|
||||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||||
|
|
||||||
|
@ -671,6 +739,9 @@ packages:
|
||||||
bindings@1.5.0:
|
bindings@1.5.0:
|
||||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||||
|
|
||||||
|
birpc@2.3.0:
|
||||||
|
resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==}
|
||||||
|
|
||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||||
|
|
||||||
|
@ -835,10 +906,21 @@ packages:
|
||||||
cookie-signature@1.0.6:
|
cookie-signature@1.0.6:
|
||||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||||
|
|
||||||
|
cookie-signature@1.0.7:
|
||||||
|
resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
|
||||||
|
|
||||||
cookie@0.7.1:
|
cookie@0.7.1:
|
||||||
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
|
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
|
||||||
engines: {node: '>= 0.6'}
|
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:
|
core-util-is@1.0.3:
|
||||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
|
|
||||||
|
@ -1068,6 +1150,10 @@ packages:
|
||||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
express-session@1.18.1:
|
||||||
|
resolution: {integrity: sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
express@4.21.2:
|
express@4.21.2:
|
||||||
resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
|
resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
|
@ -1230,6 +1316,9 @@ packages:
|
||||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
hookable@5.5.3:
|
||||||
|
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||||
|
|
||||||
html-minifier-terser@7.2.0:
|
html-minifier-terser@7.2.0:
|
||||||
resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==}
|
resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==}
|
||||||
engines: {node: ^14.13.1 || >=16.0.0}
|
engines: {node: ^14.13.1 || >=16.0.0}
|
||||||
|
@ -1341,6 +1430,10 @@ packages:
|
||||||
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
|
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
is-what@4.1.16:
|
||||||
|
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
|
||||||
|
engines: {node: '>=12.13'}
|
||||||
|
|
||||||
is-wsl@2.2.0:
|
is-wsl@2.2.0:
|
||||||
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -1492,6 +1585,9 @@ packages:
|
||||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
|
mitt@3.0.1:
|
||||||
|
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||||
|
|
||||||
mkdirp-classic@0.5.3:
|
mkdirp-classic@0.5.3:
|
||||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||||
|
|
||||||
|
@ -1653,6 +1749,9 @@ packages:
|
||||||
peberminta@0.9.0:
|
peberminta@0.9.0:
|
||||||
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
||||||
|
|
||||||
|
perfect-debounce@1.0.0:
|
||||||
|
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
|
@ -1664,6 +1763,15 @@ packages:
|
||||||
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
|
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
|
||||||
engines: {node: '>=12'}
|
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:
|
pkg-types@1.3.1:
|
||||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||||
|
|
||||||
|
@ -1713,6 +1821,13 @@ packages:
|
||||||
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||||
engines: {node: '>=6'}
|
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:
|
qs@6.13.0:
|
||||||
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
|
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
|
@ -1721,6 +1836,10 @@ packages:
|
||||||
resolution: {integrity: sha512-db/P64Mzpt1uXJ0MapaG+IYJQ9hHDb5KtTCoszwC78DR7sA+Uoj7nBW2EytwYykIExEmqavOvKrdasTvqhkgEg==}
|
resolution: {integrity: sha512-db/P64Mzpt1uXJ0MapaG+IYJQ9hHDb5KtTCoszwC78DR7sA+Uoj7nBW2EytwYykIExEmqavOvKrdasTvqhkgEg==}
|
||||||
engines: {node: '>= 10.18.1', npm: '>= 6.13.4', yarn: '>= 1.21.1'}
|
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:
|
randombytes@2.1.0:
|
||||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||||
|
|
||||||
|
@ -1773,6 +1892,9 @@ packages:
|
||||||
restructure@3.0.2:
|
restructure@3.0.2:
|
||||||
resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==}
|
resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==}
|
||||||
|
|
||||||
|
rfdc@1.4.1:
|
||||||
|
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||||
|
|
||||||
rollup-plugin-visualizer@5.14.0:
|
rollup-plugin-visualizer@5.14.0:
|
||||||
resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==}
|
resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
@ -2036,6 +2158,10 @@ packages:
|
||||||
resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
|
resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
speakingurl@14.0.1:
|
||||||
|
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
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'}
|
||||||
|
@ -2073,6 +2199,10 @@ packages:
|
||||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
superjson@2.2.2:
|
||||||
|
resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -2165,6 +2295,10 @@ packages:
|
||||||
ufo@1.6.1:
|
ufo@1.6.1:
|
||||||
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
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:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
|
@ -2198,6 +2332,10 @@ packages:
|
||||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||||
engines: {node: '>= 0.4.0'}
|
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:
|
uuid@8.3.2:
|
||||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
@ -2475,6 +2613,8 @@ snapshots:
|
||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
|
'@hexagon/base64@1.1.28': {}
|
||||||
|
|
||||||
'@inquirer/figures@1.0.11': {}
|
'@inquirer/figures@1.0.11': {}
|
||||||
|
|
||||||
'@isaacs/cliui@8.0.2':
|
'@isaacs/cliui@8.0.2':
|
||||||
|
@ -2508,6 +2648,41 @@ snapshots:
|
||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@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':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
@ -2543,7 +2718,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@prisma/debug': 6.6.0
|
'@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:
|
dependencies:
|
||||||
'@quasar/render-ssr-error': 1.0.3
|
'@quasar/render-ssr-error': 1.0.3
|
||||||
'@quasar/ssl-certificate': 1.0.0
|
'@quasar/ssl-certificate': 1.0.0
|
||||||
|
@ -2585,6 +2760,8 @@ snapshots:
|
||||||
vue: 3.5.13
|
vue: 3.5.13
|
||||||
vue-router: 4.5.0(vue@3.5.13)
|
vue-router: 4.5.0(vue@3.5.13)
|
||||||
webpack-merge: 6.0.1
|
webpack-merge: 6.0.1
|
||||||
|
optionalDependencies:
|
||||||
|
pinia: 3.0.2(vue@3.5.13)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- jiti
|
- jiti
|
||||||
|
@ -2683,6 +2860,18 @@ snapshots:
|
||||||
domhandler: 5.0.3
|
domhandler: 5.0.3
|
||||||
selderee: 0.11.0
|
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':
|
'@swc/helpers@0.5.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
@ -2716,6 +2905,10 @@ snapshots:
|
||||||
'@types/range-parser': 1.2.7
|
'@types/range-parser': 1.2.7
|
||||||
'@types/send': 0.17.4
|
'@types/send': 0.17.4
|
||||||
|
|
||||||
|
'@types/express-session@1.18.1':
|
||||||
|
dependencies:
|
||||||
|
'@types/express': 4.17.21
|
||||||
|
|
||||||
'@types/express@4.17.21':
|
'@types/express@4.17.21':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/body-parser': 1.19.5
|
'@types/body-parser': 1.19.5
|
||||||
|
@ -2758,6 +2951,8 @@ snapshots:
|
||||||
'@types/node': 22.14.1
|
'@types/node': 22.14.1
|
||||||
'@types/send': 0.17.4
|
'@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)':
|
'@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:
|
dependencies:
|
||||||
vite: 6.3.2(@types/node@22.14.1)(sass-embedded@1.87.0)(terser@5.39.0)
|
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@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':
|
'@vue/reactivity@3.5.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/shared': 3.5.13
|
'@vue/shared': 3.5.13
|
||||||
|
@ -2873,6 +3086,12 @@ snapshots:
|
||||||
|
|
||||||
array-flatten@1.1.1: {}
|
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: {}
|
async@3.2.6: {}
|
||||||
|
|
||||||
asynckit@0.4.0: {}
|
asynckit@0.4.0: {}
|
||||||
|
@ -2921,6 +3140,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
file-uri-to-path: 1.0.0
|
file-uri-to-path: 1.0.0
|
||||||
|
|
||||||
|
birpc@2.3.0: {}
|
||||||
|
|
||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer: 5.7.1
|
buffer: 5.7.1
|
||||||
|
@ -3112,8 +3333,16 @@ snapshots:
|
||||||
|
|
||||||
cookie-signature@1.0.6: {}
|
cookie-signature@1.0.6: {}
|
||||||
|
|
||||||
|
cookie-signature@1.0.7: {}
|
||||||
|
|
||||||
cookie@0.7.1: {}
|
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: {}
|
core-util-is@1.0.3: {}
|
||||||
|
|
||||||
crc-32@1.2.2: {}
|
crc-32@1.2.2: {}
|
||||||
|
@ -3330,6 +3559,19 @@ snapshots:
|
||||||
|
|
||||||
expand-template@2.0.3: {}
|
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:
|
express@4.21.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
accepts: 1.3.8
|
accepts: 1.3.8
|
||||||
|
@ -3548,6 +3790,8 @@ snapshots:
|
||||||
|
|
||||||
he@1.2.0: {}
|
he@1.2.0: {}
|
||||||
|
|
||||||
|
hookable@5.5.3: {}
|
||||||
|
|
||||||
html-minifier-terser@7.2.0:
|
html-minifier-terser@7.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
camel-case: 4.1.2
|
camel-case: 4.1.2
|
||||||
|
@ -3670,6 +3914,8 @@ snapshots:
|
||||||
|
|
||||||
is-unicode-supported@0.1.0: {}
|
is-unicode-supported@0.1.0: {}
|
||||||
|
|
||||||
|
is-what@4.1.16: {}
|
||||||
|
|
||||||
is-wsl@2.2.0:
|
is-wsl@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
|
@ -3817,6 +4063,8 @@ snapshots:
|
||||||
|
|
||||||
minipass@7.1.2: {}
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
|
mitt@3.0.1: {}
|
||||||
|
|
||||||
mkdirp-classic@0.5.3: {}
|
mkdirp-classic@0.5.3: {}
|
||||||
|
|
||||||
mlly@1.7.4:
|
mlly@1.7.4:
|
||||||
|
@ -3971,12 +4219,19 @@ snapshots:
|
||||||
|
|
||||||
peberminta@0.9.0: {}
|
peberminta@0.9.0: {}
|
||||||
|
|
||||||
|
perfect-debounce@1.0.0: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
|
|
||||||
picomatch@4.0.2: {}
|
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:
|
pkg-types@1.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
confbox: 0.1.8
|
confbox: 0.1.8
|
||||||
|
@ -4035,12 +4290,20 @@ snapshots:
|
||||||
|
|
||||||
punycode.js@2.3.1: {}
|
punycode.js@2.3.1: {}
|
||||||
|
|
||||||
|
pvtsutils@1.3.6:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
pvutils@1.1.3: {}
|
||||||
|
|
||||||
qs@6.13.0:
|
qs@6.13.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
side-channel: 1.1.0
|
side-channel: 1.1.0
|
||||||
|
|
||||||
quasar@2.18.1: {}
|
quasar@2.18.1: {}
|
||||||
|
|
||||||
|
random-bytes@1.0.0: {}
|
||||||
|
|
||||||
randombytes@2.1.0:
|
randombytes@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
@ -4113,6 +4376,8 @@ snapshots:
|
||||||
|
|
||||||
restructure@3.0.2: {}
|
restructure@3.0.2: {}
|
||||||
|
|
||||||
|
rfdc@1.4.1: {}
|
||||||
|
|
||||||
rollup-plugin-visualizer@5.14.0(rollup@4.40.0):
|
rollup-plugin-visualizer@5.14.0(rollup@4.40.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
open: 8.4.2
|
open: 8.4.2
|
||||||
|
@ -4381,6 +4646,8 @@ snapshots:
|
||||||
|
|
||||||
source-map@0.7.4: {}
|
source-map@0.7.4: {}
|
||||||
|
|
||||||
|
speakingurl@14.0.1: {}
|
||||||
|
|
||||||
stack-trace@1.0.0-pre2: {}
|
stack-trace@1.0.0-pre2: {}
|
||||||
|
|
||||||
statuses@2.0.1: {}
|
statuses@2.0.1: {}
|
||||||
|
@ -4422,6 +4689,10 @@ snapshots:
|
||||||
|
|
||||||
strip-json-comments@2.0.1: {}
|
strip-json-comments@2.0.1: {}
|
||||||
|
|
||||||
|
superjson@2.2.2:
|
||||||
|
dependencies:
|
||||||
|
copy-anything: 3.0.5
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
|
@ -4510,6 +4781,10 @@ snapshots:
|
||||||
|
|
||||||
ufo@1.6.1: {}
|
ufo@1.6.1: {}
|
||||||
|
|
||||||
|
uid-safe@2.1.5:
|
||||||
|
dependencies:
|
||||||
|
random-bytes: 1.0.0
|
||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
unicode-properties@1.4.1:
|
unicode-properties@1.4.1:
|
||||||
|
@ -4540,6 +4815,8 @@ snapshots:
|
||||||
|
|
||||||
utils-merge@1.0.1: {}
|
utils-merge@1.0.1: {}
|
||||||
|
|
||||||
|
uuid@11.1.0: {}
|
||||||
|
|
||||||
uuid@8.3.2: {}
|
uuid@8.3.2: {}
|
||||||
|
|
||||||
uuid@9.0.1: {}
|
uuid@9.0.1: {}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
onlyBuiltDependencies:
|
onlyBuiltDependencies:
|
||||||
- '@prisma/client'
|
- '@prisma/client'
|
||||||
|
- '@prisma/engines'
|
||||||
- better-sqlite3
|
- better-sqlite3
|
||||||
- esbuild
|
- esbuild
|
||||||
|
- prisma
|
||||||
- sqlite3
|
- sqlite3
|
||||||
|
|
|
@ -99,3 +99,27 @@ model Setting {
|
||||||
|
|
||||||
@@map("settings") // Map to the 'settings' table
|
@@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")
|
||||||
|
}
|
||||||
|
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 859 B After Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 5.8 KiB |
BIN
public/stylepoint.png
Normal file
After Width: | Height: | Size: 662 KiB |
390
src-ssr/routes/auth.js
Normal file
|
@ -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;
|
|
@ -11,6 +11,8 @@
|
||||||
*/
|
*/
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import compression from 'compression'
|
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 {
|
import {
|
||||||
defineSsrCreate,
|
defineSsrCreate,
|
||||||
defineSsrListen,
|
defineSsrListen,
|
||||||
|
@ -21,9 +23,18 @@ import {
|
||||||
|
|
||||||
import prisma from './database.js'; // Import the prisma client instance
|
import prisma from './database.js'; // Import the prisma client instance
|
||||||
import apiRoutes from './routes/api.js';
|
import apiRoutes from './routes/api.js';
|
||||||
|
import authRoutes from './routes/auth.js'; // Added for WebAuthn routes
|
||||||
import cron from 'node-cron';
|
import cron from 'node-cron';
|
||||||
import { generateAndStoreMantisSummary } from './services/mantisSummarizer.js';
|
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.
|
* Create your webserver and return its instance.
|
||||||
* If needed, prepare your webserver to receive
|
* If needed, prepare your webserver to receive
|
||||||
|
@ -34,6 +45,19 @@ import { generateAndStoreMantisSummary } from './services/mantisSummarizer.js';
|
||||||
export const create = defineSsrCreate((/* { ... } */) => {
|
export const create = defineSsrCreate((/* { ... } */) => {
|
||||||
const app = express()
|
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)
|
// Initialize the database (now synchronous)
|
||||||
try {
|
try {
|
||||||
console.log('Prisma Client is ready.'); // Log Prisma readiness
|
console.log('Prisma Client is ready.'); // Log Prisma readiness
|
||||||
|
@ -72,6 +96,7 @@ export const create = defineSsrCreate((/* { ... } */) => {
|
||||||
|
|
||||||
// Add API routes
|
// Add API routes
|
||||||
app.use('/api', apiRoutes);
|
app.use('/api', apiRoutes);
|
||||||
|
app.use('/auth', authRoutes); // Added WebAuthn auth routes
|
||||||
|
|
||||||
// place here any middlewares that
|
// place here any middlewares that
|
||||||
// absolutely need to run before anything else
|
// absolutely need to run before anything else
|
||||||
|
|
|
@ -16,71 +16,42 @@
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<q-item clickable v-ripple :to="{ name: 'formList' }" exact>
|
<!-- Dynamic Navigation Items -->
|
||||||
<q-tooltip anchor="center right" self="center left" >
|
|
||||||
<span>Forms</span>
|
|
||||||
</q-tooltip>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="list_alt" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Forms</q-item-label>
|
|
||||||
<q-item-label caption>View existing forms</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item
|
<q-item
|
||||||
|
v-for="item in navItems"
|
||||||
|
:key="item.name"
|
||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
:to="{ name: 'mantisSummaries' }"
|
:to="{ name: item.name }"
|
||||||
exact
|
exact
|
||||||
>
|
>
|
||||||
<q-tooltip anchor="center right" self="center left" >
|
<q-tooltip anchor="center right" self="center left" >
|
||||||
<span>Mantis Summaries</span>
|
<span>{{ item.meta.title }}</span>
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="summarize" />
|
<q-icon :name="item.meta.icon" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>Mantis Summaries</q-item-label>
|
<q-item-label>{{ item.meta.title }}</q-item-label>
|
||||||
<q-item-label caption>View daily summaries</q-item-label>
|
<q-item-label caption>{{ item.meta.caption }}</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
|
<!-- Logout Button (Conditional) -->
|
||||||
<q-item
|
<q-item
|
||||||
|
v-if="authStore.isAuthenticated"
|
||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
:to="{ name: 'emailSummaries' }"
|
@click="logout"
|
||||||
exact
|
|
||||||
>
|
>
|
||||||
<q-tooltip anchor="center right" self="center left" >
|
<q-tooltip anchor="center right" self="center left" >
|
||||||
<span>Email Summaries</span>
|
<span>Logout</span>
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="email" />
|
<q-icon name="logout" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>Email Summaries</q-item-label>
|
<q-item-label>Logout</q-item-label>
|
||||||
<q-item-label caption>View email summaries</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
clickable
|
|
||||||
to="/settings" exact
|
|
||||||
>
|
|
||||||
<q-tooltip anchor="center right" self="center left" >
|
|
||||||
<span>Settings</span>
|
|
||||||
</q-tooltip>
|
|
||||||
<q-item-section
|
|
||||||
avatar
|
|
||||||
>
|
|
||||||
<q-icon name="settings" />
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Settings</q-item-label>
|
|
||||||
<q-item-label caption>Manage application settings</q-item-label>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
|
@ -94,11 +65,55 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import axios from 'axios'
|
||||||
|
import { ref, computed } from 'vue' // Import computed
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useQuasar } from 'quasar'
|
||||||
|
import { useAuthStore } from 'stores/auth'; // Import the auth store
|
||||||
|
import routes from '../router/routes'; // Import routes
|
||||||
|
|
||||||
|
const $q = useQuasar()
|
||||||
const leftDrawerOpen = ref(false)
|
const leftDrawerOpen = ref(false)
|
||||||
|
const router = useRouter()
|
||||||
|
const authStore = useAuthStore(); // Use the auth store
|
||||||
|
|
||||||
|
// Get the child routes of the main layout
|
||||||
|
const mainLayoutRoutes = routes.find(r => r.path === '/')?.children || [];
|
||||||
|
|
||||||
|
// Compute navigation items based on auth state and route meta
|
||||||
|
const navItems = computed(() => {
|
||||||
|
const isAuthenticated = authStore.isAuthenticated;
|
||||||
|
return mainLayoutRoutes.filter(route => {
|
||||||
|
const navGroup = route.meta?.navGroup;
|
||||||
|
if (!navGroup) return false; // Only include routes with navGroup defined
|
||||||
|
|
||||||
|
if (navGroup === 'always') return true;
|
||||||
|
if (navGroup === 'auth' && isAuthenticated) return true;
|
||||||
|
if (navGroup === 'noAuth' && !isAuthenticated) return true;
|
||||||
|
|
||||||
|
return false; // Exclude otherwise
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
function toggleLeftDrawer () {
|
function toggleLeftDrawer () {
|
||||||
leftDrawerOpen.value = !leftDrawerOpen.value
|
leftDrawerOpen.value = !leftDrawerOpen.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
try {
|
||||||
|
await axios.post('/auth/logout');
|
||||||
|
authStore.logout(); // Use the store action to update state
|
||||||
|
// No need to manually push, router guard should redirect
|
||||||
|
// router.push({ name: 'login' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout failed:', error);
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
color: 'negative',
|
||||||
|
message: 'Logout failed. Please try again.',
|
||||||
|
icon: 'report_problem'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
36
src/pages/LandingPage.vue
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<template>
|
||||||
|
<q-page class="landing-page column items-center q-pa-md">
|
||||||
|
|
||||||
|
<div class="hero text-center q-pa-xl full-width">
|
||||||
|
<h1 class="text-h3 text-weight-bold text-primary q-mb-sm">Welcome to StylePoint</h1>
|
||||||
|
<p class="text-h6 text-grey-8 q-mb-lg">The all-in-one tool designed for StyleTech Developers.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="features q-mt-xl q-pa-md text-center" style="max-width: 800px; width: 100%;">
|
||||||
|
<h2 class="text-h4 text-weight-medium text-secondary q-mb-lg">Features</h2>
|
||||||
|
<q-list bordered separator class="rounded-borders">
|
||||||
|
<q-item v-for="(feature, index) in features" :key="index" class="q-pa-md">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label class="text-body1">{{ feature }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
const currentYear = ref(new Date().getFullYear());
|
||||||
|
|
||||||
|
const features = ref([
|
||||||
|
'Auatomated Daily Reports',
|
||||||
|
'Deep Mantis Integration',
|
||||||
|
'Easy Authentication',
|
||||||
|
'And more..?'
|
||||||
|
]);
|
||||||
|
|
||||||
|
</script>
|
103
src/pages/LoginPage.vue
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
<template>
|
||||||
|
<q-page class="flex flex-center">
|
||||||
|
<q-card style="width: 400px; max-width: 90vw;">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Login</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section>
|
||||||
|
<q-input
|
||||||
|
v-model="username"
|
||||||
|
label="Username"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
class="q-mb-md"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
:hint="errorMessage ? errorMessage : ''"
|
||||||
|
:rules="[val => !!val || 'Username is required']"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
label="Login with Passkey"
|
||||||
|
color="primary"
|
||||||
|
class="full-width"
|
||||||
|
@click="handleLogin"
|
||||||
|
:loading="loading"
|
||||||
|
/>
|
||||||
|
<div v-if="errorMessage" class="text-negative q-mt-md">{{ errorMessage }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions align="center">
|
||||||
|
<q-btn flat label="Don't have an account? Register" to="/register" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { startAuthentication } from '@simplewebauthn/browser';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useAuthStore } from 'stores/auth'; // Import the auth store
|
||||||
|
|
||||||
|
const username = ref('');
|
||||||
|
const loading = ref(false);
|
||||||
|
const errorMessage = ref('');
|
||||||
|
const router = useRouter();
|
||||||
|
const authStore = useAuthStore(); // Use the auth store
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
loading.value = true;
|
||||||
|
errorMessage.value = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Get options from server
|
||||||
|
const optionsRes = await axios.post('/auth/generate-authentication-options', {
|
||||||
|
username: username.value || undefined, // Send username if provided
|
||||||
|
});
|
||||||
|
const options = optionsRes.data;
|
||||||
|
|
||||||
|
// 2. Start authentication ceremony in browser
|
||||||
|
const authResp = await startAuthentication(options);
|
||||||
|
|
||||||
|
// 3. Send response to server for verification
|
||||||
|
const verificationRes = await axios.post('/auth/verify-authentication', {
|
||||||
|
authenticationResponse: authResp,
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
} else {
|
||||||
|
errorMessage.value = 'Authentication failed.';
|
||||||
|
// Optionally update store state on failure
|
||||||
|
authStore.isAuthenticated = false;
|
||||||
|
authStore.user = null;
|
||||||
|
authStore.error = 'Authentication failed.';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
const message = error.response?.data?.error || error.message || 'An unknown error occurred during login.';
|
||||||
|
// Handle specific simplewebauthn errors if needed
|
||||||
|
if (error.name === 'NotAllowedError') {
|
||||||
|
errorMessage.value = 'Authentication ceremony was cancelled or timed out.';
|
||||||
|
} else if (error.response?.status === 404 && error.response?.data?.error?.includes('User not found')) {
|
||||||
|
errorMessage.value = 'User not found. Please check your username or register.';
|
||||||
|
} else if (error.response?.status === 404 && error.response?.data?.error?.includes('Authenticator not found')) {
|
||||||
|
errorMessage.value = 'No registered passkey found for this user or device. Try registering first.';
|
||||||
|
} else {
|
||||||
|
errorMessage.value = `Login failed: ${message}`;
|
||||||
|
}
|
||||||
|
// Optionally update store state on error
|
||||||
|
authStore.isAuthenticated = false;
|
||||||
|
authStore.user = null;
|
||||||
|
authStore.error = `Login failed: ${message}`;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
275
src/pages/PasskeyManagementPage.vue
Normal file
|
@ -0,0 +1,275 @@
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 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-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>
|
||||||
|
|
||||||
|
<q-item-section side class="row no-wrap items-center">
|
||||||
|
|
||||||
|
<!-- Delete Button -->
|
||||||
|
<q-btn
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { startRegistration, startAuthentication } from '@simplewebauthn/browser'; // Import startAuthentication
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useAuthStore } from 'stores/auth';
|
||||||
|
|
||||||
|
const registerLoading = ref(false);
|
||||||
|
const registerErrorMessage = ref('');
|
||||||
|
const registerSuccessMessage = ref('');
|
||||||
|
const fetchLoading = ref(false);
|
||||||
|
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 identifyErrorMessage = ref('');
|
||||||
|
const identifiedPasskeyId = ref(null); // Store the ID of the successfully identified passkey
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const passkeys = ref([]); // To store the list of passkeys
|
||||||
|
|
||||||
|
// 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
|
||||||
|
deleteErrorMessage.value = '';
|
||||||
|
identifyErrorMessage.value = ''; // Clear identify message
|
||||||
|
identifiedPasskeyId.value = null; // Clear identified key
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/auth/passkeys');
|
||||||
|
passkeys.value = response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching passkeys:', error);
|
||||||
|
fetchErrorMessage.value = error.response?.data?.error || 'Failed to load passkeys.';
|
||||||
|
passkeys.value = []; // Clear passkeys on error
|
||||||
|
} finally {
|
||||||
|
fetchLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check auth status and fetch passkeys on component mount
|
||||||
|
onMounted(async () => {
|
||||||
|
let initialAuthError = '';
|
||||||
|
if (!authStore.isAuthenticated) {
|
||||||
|
await authStore.checkAuthStatus();
|
||||||
|
if (authStore.error) {
|
||||||
|
initialAuthError = `Authentication error: ${authStore.error}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleRegister() {
|
||||||
|
if (!isLoggedIn.value || !username.value) {
|
||||||
|
registerErrorMessage.value = 'User not authenticated.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
registerLoading.value = true;
|
||||||
|
registerErrorMessage.value = '';
|
||||||
|
registerSuccessMessage.value = '';
|
||||||
|
deleteSuccessMessage.value = ''; // Clear other messages
|
||||||
|
deleteErrorMessage.value = '';
|
||||||
|
identifyErrorMessage.value = '';
|
||||||
|
identifiedPasskeyId.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Get options from server
|
||||||
|
const optionsRes = await axios.post('/auth/generate-registration-options', {
|
||||||
|
username: username.value, // Use username from store
|
||||||
|
});
|
||||||
|
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('/auth/verify-registration', {
|
||||||
|
registrationResponse: regResp,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (verificationRes.data.verified) {
|
||||||
|
registerSuccessMessage.value = 'New passkey registered successfully!';
|
||||||
|
fetchPasskeys(); // Refresh the list of passkeys
|
||||||
|
} else {
|
||||||
|
registerErrorMessage.value = 'Passkey verification failed.';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
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.';
|
||||||
|
} else if (error.name === 'NotAllowedError') {
|
||||||
|
registerErrorMessage.value = 'Registration ceremony was cancelled or timed out.';
|
||||||
|
} else if (error.response?.status === 409) {
|
||||||
|
registerErrorMessage.value = 'This passkey seems to be registered already.';
|
||||||
|
} else {
|
||||||
|
registerErrorMessage.value = `Registration failed: ${message}`;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
registerLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
// }
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.delete(`/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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle identifying a passkey
|
||||||
|
async function handleIdentify() {
|
||||||
|
if (!isLoggedIn.value) {
|
||||||
|
identifyErrorMessage.value = 'You must be logged in.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
identifyLoading.value = true;
|
||||||
|
identifyErrorMessage.value = '';
|
||||||
|
identifiedPasskeyId.value = null; // Reset identified key
|
||||||
|
// Clear other messages
|
||||||
|
registerSuccessMessage.value = '';
|
||||||
|
registerErrorMessage.value = '';
|
||||||
|
deleteSuccessMessage.value = '';
|
||||||
|
deleteErrorMessage.value = '';
|
||||||
|
|
||||||
|
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('/auth/generate-authentication-options', {}); // Send empty body
|
||||||
|
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
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Identification error:', error);
|
||||||
|
identifiedPasskeyId.value = null;
|
||||||
|
if (error.name === 'NotAllowedError') {
|
||||||
|
identifyErrorMessage.value = 'Identification ceremony was cancelled or timed out.';
|
||||||
|
} else {
|
||||||
|
identifyErrorMessage.value = error.response?.data?.error || error.message || 'Failed to identify passkey.';
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
identifyLoading.value = null; // Clear loading state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
135
src/pages/RegisterPage.vue
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
<template>
|
||||||
|
<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 -->
|
||||||
|
<div class="text-h6">{{ isLoggedIn ? 'Register New Passkey' : 'Register Passkey' }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section>
|
||||||
|
<q-input
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
:label="isLoggedIn ? 'Register New Passkey' : 'Register Passkey'"
|
||||||
|
color="primary"
|
||||||
|
class="full-width"
|
||||||
|
@click="handleRegister"
|
||||||
|
:loading="loading"
|
||||||
|
:disable="loading || (!username && !isLoggedIn)"
|
||||||
|
/>
|
||||||
|
<div v-if="successMessage" class="text-positive q-mt-md">{{ successMessage }}</div>
|
||||||
|
<div v-if="errorMessage" class="text-negative q-mt-md">{{ errorMessage }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions align="center">
|
||||||
|
<!-- Hide login link if already logged in based on store state -->
|
||||||
|
<q-btn v-if="!isLoggedIn" flat label="Already have an account? Login" to="/login" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue'; // Import computed
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { startRegistration } from '@simplewebauthn/browser';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useAuthStore } from 'stores/auth'; // Import the auth store
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const errorMessage = ref('');
|
||||||
|
const successMessage = ref('');
|
||||||
|
const router = useRouter();
|
||||||
|
const authStore = useAuthStore(); // Use the auth store
|
||||||
|
|
||||||
|
// Computed properties to get state from the store
|
||||||
|
const isLoggedIn = computed(() => authStore.isAuthenticated);
|
||||||
|
|
||||||
|
const username = ref(''); // Local ref for username input
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleRegister() {
|
||||||
|
const currentUsername = isLoggedIn.value ? authStore.user?.username : username.value;
|
||||||
|
if (!currentUsername) {
|
||||||
|
errorMessage.value = 'Username is missing.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
errorMessage.value = '';
|
||||||
|
successMessage.value = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Get options from server
|
||||||
|
const optionsRes = await axios.post('/auth/generate-registration-options', {
|
||||||
|
username: currentUsername, // Use username from store
|
||||||
|
});
|
||||||
|
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('/auth/verify-registration', {
|
||||||
|
registrationResponse: regResp,
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorMessage.value = 'Registration failed.';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
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') {
|
||||||
|
errorMessage.value = 'Authenticator already registered. Try logging in instead.';
|
||||||
|
} else if (error.name === 'NotAllowedError') {
|
||||||
|
errorMessage.value = 'Registration ceremony was cancelled or timed out.';
|
||||||
|
} else if (error.response?.status === 409) {
|
||||||
|
errorMessage.value = 'This passkey seems to be registered already.';
|
||||||
|
} else {
|
||||||
|
errorMessage.value = `Registration failed: ${message}`;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,6 +1,7 @@
|
||||||
import { defineRouter } from '#q-app/wrappers'
|
import { defineRouter } from '#q-app/wrappers'
|
||||||
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
|
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||||
import routes from './routes'
|
import routes from './routes'
|
||||||
|
import { useAuthStore } from 'stores/auth'; // Import the auth store
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If not building with SSR mode, you can
|
* If not building with SSR mode, you can
|
||||||
|
@ -11,7 +12,7 @@ import routes from './routes'
|
||||||
* with the Router instance.
|
* with the Router instance.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default defineRouter(function (/* { store, ssrContext } */) {
|
export default defineRouter(function ({ store /* { store, ssrContext } */ }) {
|
||||||
const createHistory = process.env.SERVER
|
const createHistory = process.env.SERVER
|
||||||
? createMemoryHistory
|
? createMemoryHistory
|
||||||
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
|
: (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)
|
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
|
return Router
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,15 +3,101 @@ const routes = [
|
||||||
path: '/',
|
path: '/',
|
||||||
component: () => import('layouts/MainLayout.vue'),
|
component: () => import('layouts/MainLayout.vue'),
|
||||||
children: [
|
children: [
|
||||||
{ path: '', name: 'home', component: () => import('pages/FormListPage.vue') },
|
{
|
||||||
{ path: 'forms', name: 'formList', component: () => import('pages/FormListPage.vue') },
|
path: '',
|
||||||
{ path: 'forms/new', name: 'formCreate', component: () => import('pages/FormCreatePage.vue') },
|
name: 'home',
|
||||||
{ path: 'forms/:id/edit', name: 'formEdit', component: () => import('pages/FormEditPage.vue'), props: true },
|
component: () => import('pages/LandingPage.vue'),
|
||||||
{ path: 'forms/:id/fill', name: 'formFill', component: () => import('pages/FormFillPage.vue'), props: true },
|
meta: { requiresAuth: false } // Keep home accessible, but don't show in nav
|
||||||
{ 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: '/login',
|
||||||
{ path: 'settings', name: 'settings', component: () => import('pages/SettingsPage.vue') }
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
39
src/stores/auth.js
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
20
src/stores/index.js
Normal file
|
@ -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
|
||||||
|
})
|