Moved away from SSR to regular Node API server.
This commit is contained in:
parent
9aea69c7be
commit
83d93aefc0
30 changed files with 939 additions and 1024 deletions
|
@ -43,7 +43,7 @@ docker compose up -d
|
||||||
pnpm run dev
|
pnpm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start the PostgreSQL server on `localhost:5432` and the application server accessible at [http://localhost:9100](http://localhost:9100).
|
This will start the PostgreSQL server on `localhost:5432` the API server on `localhost:8000` (shouldn't need to directly work with this) and the application server accessible at [http://localhost:9000](http://localhost:9000).
|
||||||
|
|
||||||
I recommend using [Postico](https://eggerapps.io/postico/) for Mac or [pgAdmin](https://www.pgadmin.org/) for Windows to manage the PostgreSQL database.
|
I recommend using [Postico](https://eggerapps.io/postico/) for Mac or [pgAdmin](https://www.pgadmin.org/) for Windows to manage the PostgreSQL database.
|
||||||
|
|
||||||
|
@ -53,6 +53,4 @@ To create a production build of the application, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run build
|
pnpm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
This command compiles and optimizes the application for deployment. The output files are usually placed in the `dist` directory.
|
|
|
@ -8,8 +8,8 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"No test specified\" && exit 0",
|
"test": "echo \"No test specified\" && exit 0",
|
||||||
"dev": "pnpm prisma migrate dev && quasar dev -m ssr",
|
"dev": "pnpm i && pnpm prisma migrate dev && concurrently \"quasar dev -m spa\" \"nodemon src-server/server.js\"",
|
||||||
"build": "quasar build -m ssr",
|
"build": "quasar build -m spa",
|
||||||
"postinstall": "quasar prepare"
|
"postinstall": "quasar prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -43,9 +43,11 @@
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
|
"concurrently": "^9.1.2",
|
||||||
"eslint": "^9.25.1",
|
"eslint": "^9.25.1",
|
||||||
"eslint-plugin-vue": "^10.0.0",
|
"eslint-plugin-vue": "^10.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
|
"nodemon": "^3.1.10",
|
||||||
"postcss": "^8.4.14",
|
"postcss": "^8.4.14",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prisma": "^6.6.0",
|
"prisma": "^6.6.0",
|
||||||
|
|
112
pnpm-lock.yaml
generated
112
pnpm-lock.yaml
generated
|
@ -93,6 +93,9 @@ importers:
|
||||||
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)
|
||||||
|
concurrently:
|
||||||
|
specifier: ^9.1.2
|
||||||
|
version: 9.1.2
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.25.1
|
specifier: ^9.25.1
|
||||||
version: 9.25.1
|
version: 9.25.1
|
||||||
|
@ -102,6 +105,9 @@ importers:
|
||||||
globals:
|
globals:
|
||||||
specifier: ^16.0.0
|
specifier: ^16.0.0
|
||||||
version: 16.0.0
|
version: 16.0.0
|
||||||
|
nodemon:
|
||||||
|
specifier: ^3.1.10
|
||||||
|
version: 3.1.10
|
||||||
postcss:
|
postcss:
|
||||||
specifier: ^8.4.14
|
specifier: ^8.4.14
|
||||||
version: 8.5.3
|
version: 8.5.3
|
||||||
|
@ -1065,6 +1071,11 @@ packages:
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
|
concurrently@9.1.2:
|
||||||
|
resolution: {integrity: sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
confbox@0.1.8:
|
confbox@0.1.8:
|
||||||
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
|
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
|
||||||
|
|
||||||
|
@ -1590,6 +1601,10 @@ packages:
|
||||||
resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==}
|
resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
has-flag@3.0.0:
|
||||||
|
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
has-flag@4.0.0:
|
has-flag@4.0.0:
|
||||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -1647,6 +1662,9 @@ packages:
|
||||||
ieee754@1.2.1:
|
ieee754@1.2.1:
|
||||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
|
|
||||||
|
ignore-by-default@1.0.1:
|
||||||
|
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
|
||||||
|
|
||||||
ignore@5.3.2:
|
ignore@5.3.2:
|
||||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
@ -2008,6 +2026,11 @@ packages:
|
||||||
resolution: {integrity: sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==}
|
resolution: {integrity: sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
nodemon@3.1.10:
|
||||||
|
resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
normalize-path@3.0.0:
|
normalize-path@3.0.0:
|
||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -2215,6 +2238,9 @@ packages:
|
||||||
proxy-from-env@1.1.0:
|
proxy-from-env@1.1.0:
|
||||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||||
|
|
||||||
|
pstree.remy@1.1.8:
|
||||||
|
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
|
||||||
|
|
||||||
pump@3.0.2:
|
pump@3.0.2:
|
||||||
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
||||||
|
|
||||||
|
@ -2537,6 +2563,10 @@ packages:
|
||||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
shell-quote@1.8.2:
|
||||||
|
resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
side-channel-list@1.0.0:
|
side-channel-list@1.0.0:
|
||||||
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
|
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
@ -2566,6 +2596,10 @@ packages:
|
||||||
simple-get@4.0.1:
|
simple-get@4.0.1:
|
||||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||||
|
|
||||||
|
simple-update-notifier@2.0.0:
|
||||||
|
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -2630,6 +2664,10 @@ packages:
|
||||||
resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
|
resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
supports-color@5.5.0:
|
||||||
|
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
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'}
|
||||||
|
@ -2694,9 +2732,17 @@ packages:
|
||||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
|
|
||||||
|
touch@3.1.1:
|
||||||
|
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
tr46@0.0.3:
|
tr46@0.0.3:
|
||||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||||
|
|
||||||
|
tree-kill@1.2.2:
|
||||||
|
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
ts-api-utils@2.1.0:
|
ts-api-utils@2.1.0:
|
||||||
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
|
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
|
||||||
engines: {node: '>=18.12'}
|
engines: {node: '>=18.12'}
|
||||||
|
@ -2748,6 +2794,9 @@ packages:
|
||||||
resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==}
|
resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
undefsafe@2.0.5:
|
||||||
|
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
||||||
|
|
||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
|
@ -3102,7 +3151,7 @@ snapshots:
|
||||||
'@eslint/config-array@0.20.0':
|
'@eslint/config-array@0.20.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/object-schema': 2.1.6
|
'@eslint/object-schema': 2.1.6
|
||||||
debug: 4.4.0
|
debug: 4.4.0(supports-color@5.5.0)
|
||||||
minimatch: 3.1.2
|
minimatch: 3.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -3116,7 +3165,7 @@ snapshots:
|
||||||
'@eslint/eslintrc@3.3.1':
|
'@eslint/eslintrc@3.3.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
debug: 4.4.0
|
debug: 4.4.0(supports-color@5.5.0)
|
||||||
espree: 10.3.0
|
espree: 10.3.0
|
||||||
globals: 14.0.0
|
globals: 14.0.0
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
|
@ -3575,7 +3624,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.31.0
|
'@typescript-eslint/types': 8.31.0
|
||||||
'@typescript-eslint/visitor-keys': 8.31.0
|
'@typescript-eslint/visitor-keys': 8.31.0
|
||||||
debug: 4.4.0
|
debug: 4.4.0(supports-color@5.5.0)
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
|
@ -4008,6 +4057,16 @@ snapshots:
|
||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
|
concurrently@9.1.2:
|
||||||
|
dependencies:
|
||||||
|
chalk: 4.1.2
|
||||||
|
lodash: 4.17.21
|
||||||
|
rxjs: 7.8.2
|
||||||
|
shell-quote: 1.8.2
|
||||||
|
supports-color: 8.1.1
|
||||||
|
tree-kill: 1.2.2
|
||||||
|
yargs: 17.7.2
|
||||||
|
|
||||||
confbox@0.1.8: {}
|
confbox@0.1.8: {}
|
||||||
|
|
||||||
content-disposition@0.5.4:
|
content-disposition@0.5.4:
|
||||||
|
@ -4055,9 +4114,11 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.0.0
|
ms: 2.0.0
|
||||||
|
|
||||||
debug@4.4.0:
|
debug@4.4.0(supports-color@5.5.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
optionalDependencies:
|
||||||
|
supports-color: 5.5.0
|
||||||
|
|
||||||
decompress-response@6.0.0:
|
decompress-response@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4201,7 +4262,7 @@ snapshots:
|
||||||
|
|
||||||
esbuild-register@3.6.0(esbuild@0.25.3):
|
esbuild-register@3.6.0(esbuild@0.25.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.0
|
debug: 4.4.0(supports-color@5.5.0)
|
||||||
esbuild: 0.25.3
|
esbuild: 0.25.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -4292,7 +4353,7 @@ snapshots:
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
debug: 4.4.0
|
debug: 4.4.0(supports-color@5.5.0)
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
eslint-scope: 8.3.0
|
eslint-scope: 8.3.0
|
||||||
eslint-visitor-keys: 4.2.0
|
eslint-visitor-keys: 4.2.0
|
||||||
|
@ -4597,6 +4658,8 @@ snapshots:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
has-flag@3.0.0: {}
|
||||||
|
|
||||||
has-flag@4.0.0: {}
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
has-property-descriptors@1.0.2:
|
has-property-descriptors@1.0.2:
|
||||||
|
@ -4653,7 +4716,7 @@ snapshots:
|
||||||
https-proxy-agent@7.0.6:
|
https-proxy-agent@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.3
|
agent-base: 7.1.3
|
||||||
debug: 4.4.0
|
debug: 4.4.0(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
@ -4667,6 +4730,8 @@ snapshots:
|
||||||
|
|
||||||
ieee754@1.2.1: {}
|
ieee754@1.2.1: {}
|
||||||
|
|
||||||
|
ignore-by-default@1.0.1: {}
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
|
|
||||||
immutable@5.1.1: {}
|
immutable@5.1.1: {}
|
||||||
|
@ -4990,6 +5055,19 @@ snapshots:
|
||||||
|
|
||||||
nodemailer@6.9.16: {}
|
nodemailer@6.9.16: {}
|
||||||
|
|
||||||
|
nodemon@3.1.10:
|
||||||
|
dependencies:
|
||||||
|
chokidar: 3.6.0
|
||||||
|
debug: 4.4.0(supports-color@5.5.0)
|
||||||
|
ignore-by-default: 1.0.1
|
||||||
|
minimatch: 3.1.2
|
||||||
|
pstree.remy: 1.1.8
|
||||||
|
semver: 7.7.1
|
||||||
|
simple-update-notifier: 2.0.0
|
||||||
|
supports-color: 5.5.0
|
||||||
|
touch: 3.1.1
|
||||||
|
undefsafe: 2.0.5
|
||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
normalize-range@0.1.2: {}
|
normalize-range@0.1.2: {}
|
||||||
|
@ -5207,6 +5285,8 @@ snapshots:
|
||||||
|
|
||||||
proxy-from-env@1.1.0: {}
|
proxy-from-env@1.1.0: {}
|
||||||
|
|
||||||
|
pstree.remy@1.1.8: {}
|
||||||
|
|
||||||
pump@3.0.2:
|
pump@3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
end-of-stream: 1.4.4
|
end-of-stream: 1.4.4
|
||||||
|
@ -5533,6 +5613,8 @@ snapshots:
|
||||||
|
|
||||||
shebang-regex@3.0.0: {}
|
shebang-regex@3.0.0: {}
|
||||||
|
|
||||||
|
shell-quote@1.8.2: {}
|
||||||
|
|
||||||
side-channel-list@1.0.0:
|
side-channel-list@1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
|
@ -5573,6 +5655,10 @@ snapshots:
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
simple-concat: 1.0.1
|
simple-concat: 1.0.1
|
||||||
|
|
||||||
|
simple-update-notifier@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
semver: 7.7.1
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
source-map-support@0.5.21:
|
source-map-support@0.5.21:
|
||||||
|
@ -5633,6 +5719,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
copy-anything: 3.0.5
|
copy-anything: 3.0.5
|
||||||
|
|
||||||
|
supports-color@5.5.0:
|
||||||
|
dependencies:
|
||||||
|
has-flag: 3.0.0
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
|
@ -5705,8 +5795,12 @@ snapshots:
|
||||||
|
|
||||||
toidentifier@1.0.1: {}
|
toidentifier@1.0.1: {}
|
||||||
|
|
||||||
|
touch@3.1.1: {}
|
||||||
|
|
||||||
tr46@0.0.3: {}
|
tr46@0.0.3: {}
|
||||||
|
|
||||||
|
tree-kill@1.2.2: {}
|
||||||
|
|
||||||
ts-api-utils@2.1.0(typescript@5.8.3):
|
ts-api-utils@2.1.0(typescript@5.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
|
@ -5744,6 +5838,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
random-bytes: 1.0.0
|
random-bytes: 1.0.0
|
||||||
|
|
||||||
|
undefsafe@2.0.5: {}
|
||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
unicode-properties@1.4.1:
|
unicode-properties@1.4.1:
|
||||||
|
@ -5825,7 +5921,7 @@ snapshots:
|
||||||
|
|
||||||
vue-eslint-parser@10.1.3(eslint@9.25.1):
|
vue-eslint-parser@10.1.3(eslint@9.25.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.0
|
debug: 4.4.0(supports-color@5.5.0)
|
||||||
eslint: 9.25.1
|
eslint: 9.25.1
|
||||||
eslint-scope: 8.3.0
|
eslint-scope: 8.3.0
|
||||||
eslint-visitor-keys: 4.2.0
|
eslint-visitor-keys: 4.2.0
|
||||||
|
|
|
@ -76,7 +76,15 @@ export default defineConfig((/* ctx */) =>
|
||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver
|
||||||
devServer: {
|
devServer: {
|
||||||
// https: true,
|
// https: true,
|
||||||
open: true // opens browser window automatically
|
open: true, // opens browser window automatically
|
||||||
|
|
||||||
|
//Add a proxy from /api to the backend server for dev usage
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target : 'http://localhost:8000',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { rpID, rpName, origin, challengeStore } from '../server.js'; // Import R
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Helper function to get user authenticators
|
// Helper function to get user authenticators
|
||||||
async function getUserAuthenticators(userId)
|
async function getUserAuthenticators(userId)
|
||||||
{
|
{
|
||||||
return prisma.authenticator.findMany({
|
return prisma.authenticator.findMany({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
|
@ -27,40 +27,40 @@ async function getUserAuthenticators(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get a user by username
|
// Helper function to get a user by username
|
||||||
async function getUserByUsername(username)
|
async function getUserByUsername(username)
|
||||||
{
|
{
|
||||||
return prisma.user.findUnique({ where: { username } });
|
return prisma.user.findUnique({ where: { username } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get a user by ID
|
// Helper function to get a user by ID
|
||||||
async function getUserById(id)
|
async function getUserById(id)
|
||||||
{
|
{
|
||||||
return prisma.user.findUnique({ where: { id } });
|
return prisma.user.findUnique({ where: { id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get an authenticator by credential ID
|
// Helper function to get an authenticator by credential ID
|
||||||
async function getAuthenticatorByCredentialID(credentialID)
|
async function getAuthenticatorByCredentialID(credentialID)
|
||||||
{
|
{
|
||||||
return prisma.authenticator.findUnique({ where: { credentialID } });
|
return prisma.authenticator.findUnique({ where: { credentialID } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Generate Registration Options
|
// Generate Registration Options
|
||||||
router.post('/generate-registration-options', async(req, res) =>
|
router.post('/generate-registration-options', async(req, res) =>
|
||||||
{
|
{
|
||||||
const { username } = req.body;
|
const { username } = req.body;
|
||||||
|
|
||||||
if (!username)
|
if (!username)
|
||||||
{
|
{
|
||||||
return res.status(400).json({ error: 'Username is required' });
|
return res.status(400).json({ error: 'Username is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
let user = await getUserByUsername(username);
|
let user = await getUserByUsername(username);
|
||||||
|
|
||||||
// If user doesn't exist, create one
|
// If user doesn't exist, create one
|
||||||
if (!user)
|
if (!user)
|
||||||
{
|
{
|
||||||
user = await prisma.user.create({
|
user = await prisma.user.create({
|
||||||
data: { username },
|
data: { username },
|
||||||
|
@ -69,10 +69,10 @@ router.post('/generate-registration-options', async(req, res) =>
|
||||||
|
|
||||||
const userAuthenticators = await getUserAuthenticators(user.id);
|
const userAuthenticators = await getUserAuthenticators(user.id);
|
||||||
|
|
||||||
if(userAuthenticators.length > 0)
|
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
|
//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)
|
if (!req.session.loggedInUserId || req.session.loggedInUserId !== user.id)
|
||||||
{
|
{
|
||||||
return res.status(403).json({ error: 'Invalid registration attempt.' });
|
return res.status(403).json({ error: 'Invalid registration attempt.' });
|
||||||
}
|
}
|
||||||
|
@ -103,8 +103,8 @@ router.post('/generate-registration-options', async(req, res) =>
|
||||||
req.session.userId = user.id; // Temporarily store userId in session for verification step
|
req.session.userId = user.id; // Temporarily store userId in session for verification step
|
||||||
|
|
||||||
res.json(options);
|
res.json(options);
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Registration options error:', error);
|
console.error('Registration options error:', error);
|
||||||
res.status(500).json({ error: 'Failed to generate registration options' });
|
res.status(500).json({ error: 'Failed to generate registration options' });
|
||||||
|
@ -112,27 +112,27 @@ router.post('/generate-registration-options', async(req, res) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verify Registration
|
// Verify Registration
|
||||||
router.post('/verify-registration', async(req, res) =>
|
router.post('/verify-registration', async(req, res) =>
|
||||||
{
|
{
|
||||||
const { registrationResponse } = req.body;
|
const { registrationResponse } = req.body;
|
||||||
const userId = req.session.userId; // Retrieve userId stored during options generation
|
const userId = req.session.userId; // Retrieve userId stored during options generation
|
||||||
|
|
||||||
if (!userId)
|
if (!userId)
|
||||||
{
|
{
|
||||||
return res.status(400).json({ error: 'User session not found. Please start registration again.' });
|
return res.status(400).json({ error: 'User session not found. Please start registration again.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedChallenge = challengeStore.get(userId);
|
const expectedChallenge = challengeStore.get(userId);
|
||||||
|
|
||||||
if (!expectedChallenge)
|
if (!expectedChallenge)
|
||||||
{
|
{
|
||||||
return res.status(400).json({ error: 'Challenge not found or expired' });
|
return res.status(400).json({ error: 'Challenge not found or expired' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const user = await getUserById(userId);
|
const user = await getUserById(userId);
|
||||||
if (!user)
|
if (!user)
|
||||||
{
|
{
|
||||||
return res.status(404).json({ error: 'User not found' });
|
return res.status(404).json({ error: 'User not found' });
|
||||||
}
|
}
|
||||||
|
@ -149,7 +149,7 @@ router.post('/verify-registration', async(req, res) =>
|
||||||
|
|
||||||
console.log(verification);
|
console.log(verification);
|
||||||
|
|
||||||
if (verified && registrationInfo)
|
if (verified && registrationInfo)
|
||||||
{
|
{
|
||||||
const { credential, credentialDeviceType, credentialBackedUp } = registrationInfo;
|
const { credential, credentialDeviceType, credentialBackedUp } = registrationInfo;
|
||||||
|
|
||||||
|
@ -161,7 +161,7 @@ router.post('/verify-registration', async(req, res) =>
|
||||||
// Check if authenticator with this ID already exists
|
// Check if authenticator with this ID already exists
|
||||||
const existingAuthenticator = await getAuthenticatorByCredentialID(isoBase64URL.fromBuffer(credentialID));
|
const existingAuthenticator = await getAuthenticatorByCredentialID(isoBase64URL.fromBuffer(credentialID));
|
||||||
|
|
||||||
if (existingAuthenticator)
|
if (existingAuthenticator)
|
||||||
{
|
{
|
||||||
return res.status(409).json({ error: 'Authenticator already registered' });
|
return res.status(409).json({ error: 'Authenticator already registered' });
|
||||||
}
|
}
|
||||||
|
@ -187,13 +187,13 @@ router.post('/verify-registration', async(req, res) =>
|
||||||
req.session.loggedInUserId = user.id;
|
req.session.loggedInUserId = user.id;
|
||||||
|
|
||||||
res.json({ verified: true });
|
res.json({ verified: true });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
res.status(400).json({ error: 'Registration verification failed' });
|
res.status(400).json({ error: 'Registration verification failed' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Registration verification error:', error);
|
console.error('Registration verification error:', error);
|
||||||
challengeStore.delete(userId); // Clean up challenge on error
|
challengeStore.delete(userId); // Clean up challenge on error
|
||||||
|
@ -203,24 +203,24 @@ router.post('/verify-registration', async(req, res) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate Authentication Options
|
// Generate Authentication Options
|
||||||
router.post('/generate-authentication-options', async(req, res) =>
|
router.post('/generate-authentication-options', async(req, res) =>
|
||||||
{
|
{
|
||||||
const { username } = req.body;
|
const { username } = req.body;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
let user;
|
let user;
|
||||||
if (username)
|
if (username)
|
||||||
{
|
{
|
||||||
user = await getUserByUsername(username);
|
user = await getUserByUsername(username);
|
||||||
}
|
}
|
||||||
else if (req.session.loggedInUserId)
|
else if (req.session.loggedInUserId)
|
||||||
{
|
{
|
||||||
// If already logged in, allow re-authentication (e.g., for step-up)
|
// If already logged in, allow re-authentication (e.g., for step-up)
|
||||||
user = await getUserById(req.session.loggedInUserId);
|
user = await getUserById(req.session.loggedInUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user)
|
if (!user)
|
||||||
{
|
{
|
||||||
return res.status(404).json({ error: 'User not found' });
|
return res.status(404).json({ error: 'User not found' });
|
||||||
}
|
}
|
||||||
|
@ -247,8 +247,8 @@ router.post('/generate-authentication-options', async(req, res) =>
|
||||||
req.session.challengeUserId = user.id; // Store user ID associated with this challenge
|
req.session.challengeUserId = user.id; // Store user ID associated with this challenge
|
||||||
|
|
||||||
res.json(options);
|
res.json(options);
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Authentication options error:', error);
|
console.error('Authentication options error:', error);
|
||||||
res.status(500).json({ error: 'Failed to generate authentication options' });
|
res.status(500).json({ error: 'Failed to generate authentication options' });
|
||||||
|
@ -256,40 +256,40 @@ router.post('/generate-authentication-options', async(req, res) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verify Authentication
|
// Verify Authentication
|
||||||
router.post('/verify-authentication', async(req, res) =>
|
router.post('/verify-authentication', async(req, res) =>
|
||||||
{
|
{
|
||||||
const { authenticationResponse } = req.body;
|
const { authenticationResponse } = req.body;
|
||||||
const challengeUserId = req.session.challengeUserId; // Get user ID associated with the challenge
|
const challengeUserId = req.session.challengeUserId; // Get user ID associated with the challenge
|
||||||
|
|
||||||
if (!challengeUserId)
|
if (!challengeUserId)
|
||||||
{
|
{
|
||||||
return res.status(400).json({ error: 'Challenge session not found. Please try logging in again.' });
|
return res.status(400).json({ error: 'Challenge session not found. Please try logging in again.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedChallenge = challengeStore.get(challengeUserId);
|
const expectedChallenge = challengeStore.get(challengeUserId);
|
||||||
|
|
||||||
if (!expectedChallenge)
|
if (!expectedChallenge)
|
||||||
{
|
{
|
||||||
return res.status(400).json({ error: 'Challenge not found or expired' });
|
return res.status(400).json({ error: 'Challenge not found or expired' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const user = await getUserById(challengeUserId);
|
const user = await getUserById(challengeUserId);
|
||||||
if (!user)
|
if (!user)
|
||||||
{
|
{
|
||||||
return res.status(404).json({ error: 'User associated with challenge not found' });
|
return res.status(404).json({ error: 'User associated with challenge not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const authenticator = await getAuthenticatorByCredentialID(authenticationResponse.id);
|
const authenticator = await getAuthenticatorByCredentialID(authenticationResponse.id);
|
||||||
|
|
||||||
if (!authenticator)
|
if (!authenticator)
|
||||||
{
|
{
|
||||||
return res.status(404).json({ error: 'Authenticator not found' });
|
return res.status(404).json({ error: 'Authenticator not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the authenticator belongs to the user attempting to log in
|
// Ensure the authenticator belongs to the user attempting to log in
|
||||||
if (authenticator.userId !== user.id)
|
if (authenticator.userId !== user.id)
|
||||||
{
|
{
|
||||||
return res.status(403).json({ error: 'Authenticator does not belong to this user' });
|
return res.status(403).json({ error: 'Authenticator does not belong to this user' });
|
||||||
}
|
}
|
||||||
|
@ -310,7 +310,7 @@ router.post('/verify-authentication', async(req, res) =>
|
||||||
|
|
||||||
const { verified, authenticationInfo } = verification;
|
const { verified, authenticationInfo } = verification;
|
||||||
|
|
||||||
if (verified)
|
if (verified)
|
||||||
{
|
{
|
||||||
// Update the authenticator counter
|
// Update the authenticator counter
|
||||||
await prisma.authenticator.update({
|
await prisma.authenticator.update({
|
||||||
|
@ -326,13 +326,13 @@ router.post('/verify-authentication', async(req, res) =>
|
||||||
req.session.loggedInUserId = user.id;
|
req.session.loggedInUserId = user.id;
|
||||||
|
|
||||||
res.json({ verified: true, user: { id: user.id, username: user.username } });
|
res.json({ verified: true, user: { id: user.id, username: user.username } });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
res.status(400).json({ error: 'Authentication verification failed' });
|
res.status(400).json({ error: 'Authentication verification failed' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Authentication verification error:', error);
|
console.error('Authentication verification error:', error);
|
||||||
challengeStore.delete(challengeUserId); // Clean up challenge on error
|
challengeStore.delete(challengeUserId); // Clean up challenge on error
|
||||||
|
@ -342,14 +342,14 @@ router.post('/verify-authentication', async(req, res) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET Passkeys for Logged-in User
|
// GET Passkeys for Logged-in User
|
||||||
router.get('/passkeys', async(req, res) =>
|
router.get('/passkeys', async(req, res) =>
|
||||||
{
|
{
|
||||||
if (!req.session.loggedInUserId)
|
if (!req.session.loggedInUserId)
|
||||||
{
|
{
|
||||||
return res.status(401).json({ error: 'Not authenticated' });
|
return res.status(401).json({ error: 'Not authenticated' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const userId = req.session.loggedInUserId;
|
const userId = req.session.loggedInUserId;
|
||||||
const authenticators = await prisma.authenticator.findMany({
|
const authenticators = await prisma.authenticator.findMany({
|
||||||
|
@ -363,8 +363,8 @@ router.get('/passkeys', async(req, res) =>
|
||||||
|
|
||||||
// No need to convert credentialID here as it's stored as Base64URL string
|
// No need to convert credentialID here as it's stored as Base64URL string
|
||||||
res.json(authenticators);
|
res.json(authenticators);
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Error fetching passkeys:', error);
|
console.error('Error fetching passkeys:', error);
|
||||||
res.status(500).json({ error: 'Failed to fetch passkeys' });
|
res.status(500).json({ error: 'Failed to fetch passkeys' });
|
||||||
|
@ -372,21 +372,21 @@ router.get('/passkeys', async(req, res) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
// DELETE Passkey
|
// DELETE Passkey
|
||||||
router.delete('/passkeys/:credentialID', async(req, res) =>
|
router.delete('/passkeys/:credentialID', async(req, res) =>
|
||||||
{
|
{
|
||||||
if (!req.session.loggedInUserId)
|
if (!req.session.loggedInUserId)
|
||||||
{
|
{
|
||||||
return res.status(401).json({ error: 'Not authenticated' });
|
return res.status(401).json({ error: 'Not authenticated' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { credentialID } = req.params; // This is already a Base64URL string from the client
|
const { credentialID } = req.params; // This is already a Base64URL string from the client
|
||||||
|
|
||||||
if (!credentialID)
|
if (!credentialID)
|
||||||
{
|
{
|
||||||
return res.status(400).json({ error: 'Credential ID is required' });
|
return res.status(400).json({ error: 'Credential ID is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const userId = req.session.loggedInUserId;
|
const userId = req.session.loggedInUserId;
|
||||||
|
|
||||||
|
@ -395,13 +395,13 @@ router.delete('/passkeys/:credentialID', async(req, res) =>
|
||||||
where: { credentialID: credentialID }, // Use the Base64URL string directly
|
where: { credentialID: credentialID }, // Use the Base64URL string directly
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!authenticator)
|
if (!authenticator)
|
||||||
{
|
{
|
||||||
return res.status(404).json({ error: 'Passkey not found' });
|
return res.status(404).json({ error: 'Passkey not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Security check: Ensure the passkey belongs to the user trying to delete it
|
// Security check: Ensure the passkey belongs to the user trying to delete it
|
||||||
if (authenticator.userId !== userId)
|
if (authenticator.userId !== userId)
|
||||||
{
|
{
|
||||||
return res.status(403).json({ error: 'Permission denied' });
|
return res.status(403).json({ error: 'Permission denied' });
|
||||||
}
|
}
|
||||||
|
@ -412,12 +412,12 @@ router.delete('/passkeys/:credentialID', async(req, res) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ message: 'Passkey deleted successfully' });
|
res.json({ message: 'Passkey deleted successfully' });
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Error deleting passkey:', error);
|
console.error('Error deleting passkey:', error);
|
||||||
// Handle potential Prisma errors, e.g., record not found if deleted between check and delete
|
// Handle potential Prisma errors, e.g., record not found if deleted between check and delete
|
||||||
if (error.code === 'P2025')
|
if (error.code === 'P2025')
|
||||||
{ // Prisma code for record not found on delete/update
|
{ // Prisma code for record not found on delete/update
|
||||||
return res.status(404).json({ error: 'Passkey not found' });
|
return res.status(404).json({ error: 'Passkey not found' });
|
||||||
}
|
}
|
||||||
|
@ -426,21 +426,28 @@ router.delete('/passkeys/:credentialID', async(req, res) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check Authentication Status
|
// Check Authentication Status
|
||||||
router.get('/status', (req, res) =>
|
router.get('/status', async(req, res) =>
|
||||||
{
|
{
|
||||||
if (req.session.loggedInUserId)
|
if (req.session.loggedInUserId)
|
||||||
{
|
{
|
||||||
return res.json({ status: 'authenticated' });
|
const user = await getUserById(req.session.loggedInUserId);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
req.session.destroy(err =>
|
||||||
|
{});
|
||||||
|
return res.status(401).json({ status: 'unauthenticated' });
|
||||||
|
}
|
||||||
|
return res.json({ status: 'authenticated', user: { id: user.id, username: user.username, email: user.email } });
|
||||||
}
|
}
|
||||||
res.json({ status: 'unauthenticated' });
|
res.json({ status: 'unauthenticated' });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Logout
|
// Logout
|
||||||
router.post('/logout', (req, res) =>
|
router.post('/logout', (req, res) =>
|
||||||
{
|
{
|
||||||
req.session.destroy(err =>
|
req.session.destroy(err =>
|
||||||
{
|
{
|
||||||
if (err)
|
if (err)
|
||||||
{
|
{
|
||||||
console.error('Logout error:', err);
|
console.error('Logout error:', err);
|
||||||
return res.status(500).json({ error: 'Failed to logout' });
|
return res.status(500).json({ error: 'Failed to logout' });
|
100
src-server/server.js
Normal file
100
src-server/server.js
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
/**
|
||||||
|
* More info about this file:
|
||||||
|
* https://v2.quasar.dev/quasar-cli-vite/developing-ssr/ssr-webserver
|
||||||
|
*
|
||||||
|
* Runs in Node context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure to yarn add / npm install (in your project root)
|
||||||
|
* anything you import here (except for express and compression).
|
||||||
|
*/
|
||||||
|
import express from 'express';
|
||||||
|
import compression from 'compression';
|
||||||
|
import session from 'express-session'; // Added for session management
|
||||||
|
import { v4 as uuidv4 } from 'uuid'; // Added for generating session IDs
|
||||||
|
import apiRoutes from './routes/api.js';
|
||||||
|
import authRoutes from './routes/auth.js'; // Added for WebAuthn routes
|
||||||
|
import chatRoutes from './routes/chat.js'; // Added for Chat routes
|
||||||
|
import cron from 'node-cron';
|
||||||
|
import { generateAndStoreMantisSummary } from './services/mantisSummarizer.js';
|
||||||
|
|
||||||
|
// Define Relying Party details (Update with your actual details)
|
||||||
|
export const rpID = process.env.NODE_ENV === 'production' ? 'your-production-domain.com' : 'localhost';
|
||||||
|
export const rpName = 'StylePoint';
|
||||||
|
export const origin = process.env.NODE_ENV === 'production' ? `https://${rpID}` : `http://${rpID}:9000`;
|
||||||
|
|
||||||
|
// In-memory store for challenges (Replace with a persistent store in production)
|
||||||
|
export const challengeStore = new Map();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Session middleware configuration
|
||||||
|
app.use(session({
|
||||||
|
genid: (req) => uuidv4(), // Use UUIDs for session IDs
|
||||||
|
secret: process.env.SESSION_SECRET || 'a-very-strong-secret-key', // Use an environment variable for the secret
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: true,
|
||||||
|
cookie: {
|
||||||
|
secure: process.env.NODE_ENV === 'production', // Use secure cookies in production
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 // 1 day
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Initialize the database (now synchronous)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
console.log('Prisma Client is ready.'); // Log Prisma readiness
|
||||||
|
|
||||||
|
// Schedule the Mantis summary task after DB initialization
|
||||||
|
// Run daily at 1:00 AM server time (adjust as needed)
|
||||||
|
cron.schedule('0 1 * * *', async() =>
|
||||||
|
{
|
||||||
|
console.log('Running scheduled Mantis summary task...');
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await generateAndStoreMantisSummary();
|
||||||
|
console.log('Scheduled Mantis summary task completed.');
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error('Error running scheduled Mantis summary task:', error);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
scheduled: true,
|
||||||
|
timezone: 'Europe/London' // Example: Set to your server's timezone
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error('Error during server setup:', error);
|
||||||
|
// Optionally handle the error more gracefully, e.g., prevent server start
|
||||||
|
process.exit(1); // Exit if setup fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// attackers can use this header to detect apps running Express
|
||||||
|
// and then launch specifically-targeted attacks
|
||||||
|
app.disable('x-powered-by');
|
||||||
|
|
||||||
|
// Add JSON body parsing middleware
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Add API routes
|
||||||
|
app.use('/api', apiRoutes);
|
||||||
|
app.use('/api/auth', authRoutes);
|
||||||
|
app.use('/api/chat', chatRoutes);
|
||||||
|
|
||||||
|
// place here any middlewares that
|
||||||
|
// absolutely need to run before anything else
|
||||||
|
if (process.env.PROD)
|
||||||
|
{
|
||||||
|
app.use(compression());
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(express.static('public', { index: false }));
|
||||||
|
|
||||||
|
app.listen(8000, () =>
|
||||||
|
{
|
||||||
|
console.log('Server is running on http://localhost:8000');
|
||||||
|
});
|
|
@ -9,6 +9,8 @@ export async function askGemini(content)
|
||||||
{
|
{
|
||||||
|
|
||||||
const GOOGLE_API_KEY = await getSetting('GEMINI_API_KEY');
|
const GOOGLE_API_KEY = await getSetting('GEMINI_API_KEY');
|
||||||
|
|
||||||
|
console.log('Google API Key:', GOOGLE_API_KEY); // Debugging line to check the key
|
||||||
|
|
||||||
if (!GOOGLE_API_KEY)
|
if (!GOOGLE_API_KEY)
|
||||||
{
|
{
|
||||||
|
@ -62,18 +64,8 @@ export async function askGeminiChat(threadId, content)
|
||||||
messages = messages.slice(0, -1);
|
messages = messages.slice(0, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const setting = await prisma.setting.findUnique({
|
const GOOGLE_API_KEY = await getSetting('GEMINI_API_KEY');
|
||||||
where: { key: 'GEMINI_API_KEY' },
|
|
||||||
select: { value: true }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!setting)
|
|
||||||
{
|
|
||||||
throw new Error('Google API key is not set in the database.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const GOOGLE_API_KEY = setting.value;
|
|
||||||
|
|
||||||
const ai = GOOGLE_API_KEY ? new GoogleGenAI({
|
const ai = GOOGLE_API_KEY ? new GoogleGenAI({
|
||||||
apiKey: GOOGLE_API_KEY,
|
apiKey: GOOGLE_API_KEY,
|
||||||
}) : null;
|
}) : null;
|
|
@ -1,71 +0,0 @@
|
||||||
import { defineSsrMiddleware } from '#q-app/wrappers';
|
|
||||||
|
|
||||||
// This middleware should execute as last one
|
|
||||||
// since it captures everything and tries to
|
|
||||||
// render the page with Vue
|
|
||||||
|
|
||||||
export default defineSsrMiddleware(({ app, resolve, render, serve }) =>
|
|
||||||
{
|
|
||||||
// we capture any other Express route and hand it
|
|
||||||
// over to Vue and Vue Router to render our page
|
|
||||||
app.get(resolve.urlPath('*'), (req, res) =>
|
|
||||||
{
|
|
||||||
res.setHeader('Content-Type', 'text/html');
|
|
||||||
|
|
||||||
render(/* the ssrContext: */ { req, res })
|
|
||||||
.then(html =>
|
|
||||||
{
|
|
||||||
// now let's send the rendered html to the client
|
|
||||||
res.send(html);
|
|
||||||
})
|
|
||||||
.catch(err =>
|
|
||||||
{
|
|
||||||
// oops, we had an error while rendering the page
|
|
||||||
|
|
||||||
// we were told to redirect to another URL
|
|
||||||
if (err.url)
|
|
||||||
{
|
|
||||||
if (err.code)
|
|
||||||
{
|
|
||||||
res.redirect(err.code, err.url);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
res.redirect(err.url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (err.code === 404)
|
|
||||||
{
|
|
||||||
// Should reach here only if no "catch-all" route
|
|
||||||
// is defined in /src/routes
|
|
||||||
res.status(404).send('404 | Page Not Found');
|
|
||||||
}
|
|
||||||
else if (process.env.DEV)
|
|
||||||
{
|
|
||||||
// well, we treat any other code as error;
|
|
||||||
// if we're in dev mode, then we can use Quasar CLI
|
|
||||||
// to display a nice error page that contains the stack
|
|
||||||
// and other useful information
|
|
||||||
|
|
||||||
// serve.error is available on dev only
|
|
||||||
serve.error({ err, req, res });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// we're in production, so we should have another method
|
|
||||||
// to display something to the client when we encounter an error
|
|
||||||
// (for security reasons, it's not ok to display the same wealth
|
|
||||||
// of information as we do in development)
|
|
||||||
|
|
||||||
// Render Error Page on production or
|
|
||||||
// create a route (/src/routes) for an error page and redirect to it
|
|
||||||
res.status(500).send('500 | Internal Server Error');
|
|
||||||
|
|
||||||
if (process.env.DEBUGGING)
|
|
||||||
{
|
|
||||||
console.error(err.stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,241 +0,0 @@
|
||||||
/**
|
|
||||||
* More info about this file:
|
|
||||||
* https://v2.quasar.dev/quasar-cli-vite/developing-ssr/ssr-webserver
|
|
||||||
*
|
|
||||||
* Runs in Node context.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make sure to yarn add / npm install (in your project root)
|
|
||||||
* anything you import here (except for express and compression).
|
|
||||||
*/
|
|
||||||
import express from 'express';
|
|
||||||
import compression from 'compression';
|
|
||||||
import session from 'express-session'; // Added for session management
|
|
||||||
import { v4 as uuidv4 } from 'uuid'; // Added for generating session IDs
|
|
||||||
import {
|
|
||||||
defineSsrCreate,
|
|
||||||
defineSsrListen,
|
|
||||||
defineSsrClose,
|
|
||||||
defineSsrServeStaticContent,
|
|
||||||
defineSsrRenderPreloadTag
|
|
||||||
} from '#q-app/wrappers';
|
|
||||||
|
|
||||||
import prisma from './database.js'; // Import the prisma client instance
|
|
||||||
import apiRoutes from './routes/api.js';
|
|
||||||
import authRoutes from './routes/auth.js'; // Added for WebAuthn routes
|
|
||||||
import chatRoutes from './routes/chat.js'; // Added for Chat routes
|
|
||||||
import cron from 'node-cron';
|
|
||||||
import { generateAndStoreMantisSummary } from './services/mantisSummarizer.js';
|
|
||||||
|
|
||||||
// Define Relying Party details (Update with your actual details)
|
|
||||||
export const rpID = process.env.NODE_ENV === 'production' ? 'your-production-domain.com' : 'localhost';
|
|
||||||
export const rpName = 'StylePoint';
|
|
||||||
export const origin = process.env.NODE_ENV === 'production' ? `https://${rpID}` : `http://${rpID}:9100`;
|
|
||||||
|
|
||||||
// In-memory store for challenges (Replace with a persistent store in production)
|
|
||||||
export const challengeStore = new Map();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create your webserver and return its instance.
|
|
||||||
* If needed, prepare your webserver to receive
|
|
||||||
* connect-like middlewares.
|
|
||||||
*
|
|
||||||
* Can be async: defineSsrCreate(async ({ ... }) => { ... })
|
|
||||||
*/
|
|
||||||
export const create = defineSsrCreate((/* { ... } */) =>
|
|
||||||
{
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
// Session middleware configuration
|
|
||||||
app.use(session({
|
|
||||||
genid: (req) => uuidv4(), // Use UUIDs for session IDs
|
|
||||||
secret: process.env.SESSION_SECRET || 'a-very-strong-secret-key', // Use an environment variable for the secret
|
|
||||||
resave: false,
|
|
||||||
saveUninitialized: true,
|
|
||||||
cookie: {
|
|
||||||
secure: process.env.NODE_ENV === 'production', // Use secure cookies in production
|
|
||||||
httpOnly: true,
|
|
||||||
maxAge: 1000 * 60 * 60 * 24 // 1 day
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Initialize the database (now synchronous)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
console.log('Prisma Client is ready.'); // Log Prisma readiness
|
|
||||||
|
|
||||||
// Schedule the Mantis summary task after DB initialization
|
|
||||||
// Run daily at 1:00 AM server time (adjust as needed)
|
|
||||||
cron.schedule('0 1 * * *', async() =>
|
|
||||||
{
|
|
||||||
console.log('Running scheduled Mantis summary task...');
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await generateAndStoreMantisSummary();
|
|
||||||
console.log('Scheduled Mantis summary task completed.');
|
|
||||||
}
|
|
||||||
catch (error)
|
|
||||||
{
|
|
||||||
console.error('Error running scheduled Mantis summary task:', error);
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
scheduled: true,
|
|
||||||
timezone: 'Europe/London' // Example: Set to your server's timezone
|
|
||||||
});
|
|
||||||
console.log('Mantis summary cron job scheduled.');
|
|
||||||
|
|
||||||
// Optional: Run once immediately on server start if needed
|
|
||||||
generateAndStoreMantisSummary().catch(err => console.error('Initial Mantis summary failed:', err));
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (error)
|
|
||||||
{
|
|
||||||
console.error('Error during server setup:', error);
|
|
||||||
// Optionally handle the error more gracefully, e.g., prevent server start
|
|
||||||
process.exit(1); // Exit if setup fails
|
|
||||||
}
|
|
||||||
|
|
||||||
// attackers can use this header to detect apps running Express
|
|
||||||
// and then launch specifically-targeted attacks
|
|
||||||
app.disable('x-powered-by');
|
|
||||||
|
|
||||||
// Add JSON body parsing middleware
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
// Add API routes
|
|
||||||
app.use('/api', apiRoutes);
|
|
||||||
app.use('/auth', authRoutes); // Added WebAuthn auth routes
|
|
||||||
app.use('/api/chat', chatRoutes); // Added Chat routes
|
|
||||||
|
|
||||||
// place here any middlewares that
|
|
||||||
// absolutely need to run before anything else
|
|
||||||
if (process.env.PROD)
|
|
||||||
{
|
|
||||||
app.use(compression());
|
|
||||||
}
|
|
||||||
|
|
||||||
return app;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* You need to make the server listen to the indicated port
|
|
||||||
* and return the listening instance or whatever you need to
|
|
||||||
* close the server with.
|
|
||||||
*
|
|
||||||
* The "listenResult" param for the "close()" definition below
|
|
||||||
* is what you return here.
|
|
||||||
*
|
|
||||||
* For production, you can instead export your
|
|
||||||
* handler for serverless use or whatever else fits your needs.
|
|
||||||
*
|
|
||||||
* Can be async: defineSsrListen(async ({ app, devHttpsApp, port }) => { ... })
|
|
||||||
*/
|
|
||||||
export const listen = defineSsrListen(({ app, devHttpsApp, port }) =>
|
|
||||||
{
|
|
||||||
const server = devHttpsApp || app;
|
|
||||||
return server.listen(port, () =>
|
|
||||||
{
|
|
||||||
if (process.env.PROD)
|
|
||||||
{
|
|
||||||
console.log('Server listening at port ' + port);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should close the server and free up any resources.
|
|
||||||
* Will be used on development mode when the server needs
|
|
||||||
* to be restarted, or when the application shuts down.
|
|
||||||
*
|
|
||||||
* Can be async: defineSsrClose(async ({ ... }) => { ... })
|
|
||||||
*/
|
|
||||||
export const close = defineSsrClose(async({ listenResult }) =>
|
|
||||||
{
|
|
||||||
// Close the database connection when the server shuts down
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await prisma.$disconnect();
|
|
||||||
console.log('Prisma Client disconnected.');
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
console.error('Error disconnecting Prisma Client:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return listenResult.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
const maxAge = process.env.DEV
|
|
||||||
? 0
|
|
||||||
: 1000 * 60 * 60 * 24 * 30;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should return a function that will be used to configure the webserver
|
|
||||||
* to serve static content at "urlPath" from "pathToServe" folder/file.
|
|
||||||
*
|
|
||||||
* Notice resolve.urlPath(urlPath) and resolve.public(pathToServe) usages.
|
|
||||||
*
|
|
||||||
* Can be async: defineSsrServeStaticContent(async ({ app, resolve }) => {
|
|
||||||
* Can return an async function: return async ({ urlPath = '/', pathToServe = '.', opts = {} }) => {
|
|
||||||
*/
|
|
||||||
export const serveStaticContent = defineSsrServeStaticContent(({ app, resolve }) =>
|
|
||||||
{
|
|
||||||
return ({ urlPath = '/', pathToServe = '.', opts = {} }) =>
|
|
||||||
{
|
|
||||||
const serveFn = express.static(resolve.public(pathToServe), { maxAge, ...opts });
|
|
||||||
app.use(resolve.urlPath(urlPath), serveFn);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const jsRE = /\.js$/;
|
|
||||||
const cssRE = /\.css$/;
|
|
||||||
const woffRE = /\.woff$/;
|
|
||||||
const woff2RE = /\.woff2$/;
|
|
||||||
const gifRE = /\.gif$/;
|
|
||||||
const jpgRE = /\.jpe?g$/;
|
|
||||||
const pngRE = /\.png$/;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should return a String with HTML output
|
|
||||||
* (if any) for preloading indicated file
|
|
||||||
*/
|
|
||||||
export const renderPreloadTag = defineSsrRenderPreloadTag((file/* , { ssrContext } */) =>
|
|
||||||
{
|
|
||||||
if (jsRE.test(file) === true)
|
|
||||||
{
|
|
||||||
return `<link rel="modulepreload" href="${file}" crossorigin>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cssRE.test(file) === true)
|
|
||||||
{
|
|
||||||
return `<link rel="stylesheet" href="${file}" crossorigin>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (woffRE.test(file) === true)
|
|
||||||
{
|
|
||||||
return `<link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (woff2RE.test(file) === true)
|
|
||||||
{
|
|
||||||
return `<link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gifRE.test(file) === true)
|
|
||||||
{
|
|
||||||
return `<link rel="preload" href="${file}" as="image" type="image/gif" crossorigin>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jpgRE.test(file) === true)
|
|
||||||
{
|
|
||||||
return `<link rel="preload" href="${file}" as="image" type="image/jpeg" crossorigin>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pngRE.test(file) === true)
|
|
||||||
{
|
|
||||||
return `<link rel="preload" href="${file}" as="image" type="image/png" crossorigin>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
});
|
|
10
src/App.vue
10
src/App.vue
|
@ -3,5 +3,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
//
|
import { useAuthStore } from './stores/auth';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
preFetch()
|
||||||
|
{
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
return authStore.checkAuthStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
14
src/boot/axios.js
Normal file
14
src/boot/axios.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { boot } from 'quasar/wrappers';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// Be careful when using SSR for cross-request state pollution
|
||||||
|
// due to creating a Singleton instance here;
|
||||||
|
// If any client changes this (global) instance, it might be a
|
||||||
|
// good idea to move this instance creation inside of the
|
||||||
|
// "export default () => {}" function below (which runs individually
|
||||||
|
// for each client)
|
||||||
|
|
||||||
|
axios.defaults.withCredentials = true; // Enable sending cookies with requests
|
||||||
|
|
||||||
|
// Export the API instance so you can import it easily elsewhere, e.g. stores
|
||||||
|
export default axios;
|
|
@ -7,17 +7,17 @@
|
||||||
:model-value="true"
|
:model-value="true"
|
||||||
>
|
>
|
||||||
<q-list>
|
<q-list>
|
||||||
<q-item
|
<q-item
|
||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
@click="toggleLeftDrawer"
|
@click="toggleLeftDrawer"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="menu" />
|
<q-icon name="menu" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label class="text-h6">
|
<q-item-label class="text-h6">
|
||||||
StylePoint
|
StylePoint
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
@ -31,9 +31,9 @@
|
||||||
:to="{ name: item.name }"
|
:to="{ name: item.name }"
|
||||||
exact
|
exact
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
anchor="center right"
|
anchor="center right"
|
||||||
self="center left"
|
self="center left"
|
||||||
>
|
>
|
||||||
<span>{{ item.meta.title }}</span>
|
<span>{{ item.meta.title }}</span>
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
|
@ -42,8 +42,8 @@
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>{{ item.meta.title }}</q-item-label>
|
<q-item-label>{{ item.meta.title }}</q-item-label>
|
||||||
<q-item-label caption>
|
<q-item-label caption>
|
||||||
{{ item.meta.caption }}
|
{{ item.meta.caption }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
@ -55,9 +55,9 @@
|
||||||
v-ripple
|
v-ripple
|
||||||
@click="logout"
|
@click="logout"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
anchor="center right"
|
anchor="center right"
|
||||||
self="center left"
|
self="center left"
|
||||||
>
|
>
|
||||||
<span>Logout</span>
|
<span>Logout</span>
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
|
@ -67,7 +67,7 @@
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>Logout</q-item-label>
|
<q-item-label>Logout</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-drawer>
|
</q-drawer>
|
||||||
|
|
||||||
|
@ -76,10 +76,10 @@
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
|
|
||||||
<!-- Chat FAB -->
|
<!-- Chat FAB -->
|
||||||
<q-page-sticky
|
<q-page-sticky
|
||||||
v-if="isAuthenticated"
|
v-if="isAuthenticated"
|
||||||
position="bottom-right"
|
position="bottom-right"
|
||||||
:offset="[18, 18]"
|
:offset="[18, 18]"
|
||||||
>
|
>
|
||||||
<q-fab
|
<q-fab
|
||||||
v-model="fabOpen"
|
v-model="fabOpen"
|
||||||
|
@ -92,28 +92,28 @@
|
||||||
</q-page-sticky>
|
</q-page-sticky>
|
||||||
|
|
||||||
<!-- Chat Window Dialog -->
|
<!-- Chat Window Dialog -->
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="isChatVisible"
|
v-model="isChatVisible"
|
||||||
:maximized="$q.screen.lt.sm"
|
:maximized="$q.screen.lt.sm"
|
||||||
fixed
|
fixed
|
||||||
persistent
|
persistent
|
||||||
style="width: max(400px, 25%);"
|
style="width: max(400px, 25%);"
|
||||||
>
|
>
|
||||||
<q-card style="width: max(400px, 25%); height: 600px; max-height: 80vh;">
|
<q-card style="width: max(400px, 25%); height: 600px; max-height: 80vh;">
|
||||||
<q-bar class="bg-primary text-white">
|
<q-bar class="bg-primary text-white">
|
||||||
<div>Chat</div>
|
<div>Chat</div>
|
||||||
<q-space />
|
<q-space />
|
||||||
<q-btn
|
<q-btn
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
icon="close"
|
icon="close"
|
||||||
@click="toggleChat"
|
@click="toggleChat"
|
||||||
/>
|
/>
|
||||||
</q-bar>
|
</q-bar>
|
||||||
|
|
||||||
<q-card-section
|
<q-card-section
|
||||||
class="q-pa-none"
|
class="q-pa-none"
|
||||||
style="height: calc(100% - 50px);"
|
style="height: calc(100% - 50px);"
|
||||||
>
|
>
|
||||||
<ChatInterface
|
<ChatInterface
|
||||||
:messages="chatMessages"
|
:messages="chatMessages"
|
||||||
|
@ -121,23 +121,23 @@
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-inner-loading :showing="isLoading">
|
<q-inner-loading :showing="isLoading">
|
||||||
<q-spinner-gears
|
<q-spinner-gears
|
||||||
size="50px"
|
size="50px"
|
||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
</q-inner-loading>
|
</q-inner-loading>
|
||||||
<q-banner
|
<q-banner
|
||||||
v-if="chatError"
|
v-if="chatError"
|
||||||
inline-actions
|
inline-actions
|
||||||
class="text-white bg-red"
|
class="text-white bg-red"
|
||||||
>
|
>
|
||||||
{{ chatError }}
|
{{ chatError }}
|
||||||
<template #action>
|
<template #action>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
color="white"
|
color="white"
|
||||||
label="Dismiss"
|
label="Dismiss"
|
||||||
@click="clearError"
|
@click="clearError"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</q-banner>
|
</q-banner>
|
||||||
|
@ -147,7 +147,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import axios from 'axios';
|
import axios from 'boot/axios';
|
||||||
import { ref, computed } from 'vue'; // Import computed
|
import { ref, computed } from 'vue'; // Import computed
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
|
@ -175,9 +175,9 @@ const isAuthenticated = computed(() => authStore.isAuthenticated); // Get auth s
|
||||||
const mainLayoutRoutes = routes.find(r => r.path === '/')?.children || [];
|
const mainLayoutRoutes = routes.find(r => r.path === '/')?.children || [];
|
||||||
|
|
||||||
// Compute navigation items based on auth state and route meta
|
// Compute navigation items based on auth state and route meta
|
||||||
const navItems = computed(() =>
|
const navItems = computed(() =>
|
||||||
{
|
{
|
||||||
return mainLayoutRoutes.filter(route =>
|
return mainLayoutRoutes.filter(route =>
|
||||||
{
|
{
|
||||||
const navGroup = route.meta?.navGroup;
|
const navGroup = route.meta?.navGroup;
|
||||||
if (!navGroup) return false; // Only include routes with navGroup defined
|
if (!navGroup) return false; // Only include routes with navGroup defined
|
||||||
|
@ -192,41 +192,41 @@ const navItems = computed(() =>
|
||||||
|
|
||||||
|
|
||||||
// Method to toggle chat visibility via the store action
|
// Method to toggle chat visibility via the store action
|
||||||
const toggleChat = () =>
|
const toggleChat = () =>
|
||||||
{
|
{
|
||||||
// Optional: Add an extra check here if needed, though hiding the button is primary
|
// Optional: Add an extra check here if needed, though hiding the button is primary
|
||||||
if (isAuthenticated.value)
|
if (isAuthenticated.value)
|
||||||
{
|
{
|
||||||
chatStore.toggleChat();
|
chatStore.toggleChat();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Method to send a message via the store action
|
// Method to send a message via the store action
|
||||||
const handleSendMessage = (messageContent) =>
|
const handleSendMessage = (messageContent) =>
|
||||||
{
|
{
|
||||||
chatStore.sendMessage(messageContent);
|
chatStore.sendMessage(messageContent);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Method to clear errors in the store (optional)
|
// Method to clear errors in the store (optional)
|
||||||
const clearError = () =>
|
const clearError = () =>
|
||||||
{
|
{
|
||||||
chatStore.error = null; // Directly setting ref or add an action in store
|
chatStore.error = null; // Directly setting ref or add an action in store
|
||||||
};
|
};
|
||||||
function toggleLeftDrawer()
|
function toggleLeftDrawer()
|
||||||
{
|
{
|
||||||
leftDrawerOpen.value = !leftDrawerOpen.value;
|
leftDrawerOpen.value = !leftDrawerOpen.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout()
|
async function logout()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await axios.post('/auth/logout');
|
await axios.post('/api/auth/logout');
|
||||||
authStore.logout(); // Use the store action to update state
|
authStore.logout(); // Use the store action to update state
|
||||||
// No need to manually push, router guard should redirect
|
// No need to manually push, router guard should redirect
|
||||||
// router.push({ name: 'login' });
|
// router.push({ name: 'login' });
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Logout failed:', error);
|
console.error('Logout failed:', error);
|
||||||
|
|
||||||
|
|
|
@ -1,136 +1,136 @@
|
||||||
<template>
|
<template>
|
||||||
<q-page padding>
|
<q-page padding>
|
||||||
<div class="text-h4 q-mb-md">
|
<div class="text-h4 q-mb-md">
|
||||||
Create New Form
|
Create New Form
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-form
|
<q-form
|
||||||
@submit.prevent="createForm"
|
@submit.prevent="createForm"
|
||||||
class="q-gutter-md"
|
class="q-gutter-md"
|
||||||
>
|
>
|
||||||
<q-input
|
<q-input
|
||||||
outlined
|
outlined
|
||||||
v-model="form.title"
|
v-model="form.title"
|
||||||
label="Form Title *"
|
label="Form Title *"
|
||||||
lazy-rules
|
lazy-rules
|
||||||
:rules="[val => val && val.length > 0 || 'Please enter a title']"
|
:rules="[val => val && val.length > 0 || 'Please enter a title']"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
outlined
|
outlined
|
||||||
v-model="form.description"
|
v-model="form.description"
|
||||||
label="Form Description"
|
label="Form Description"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
autogrow
|
autogrow
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-separator class="q-my-lg" />
|
<q-separator class="q-my-lg" />
|
||||||
|
|
||||||
<div class="text-h6 q-mb-sm">
|
<div class="text-h6 q-mb-sm">
|
||||||
Categories & Fields
|
Categories & Fields
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="(category, catIndex) in form.categories"
|
v-for="(category, catIndex) in form.categories"
|
||||||
:key="catIndex"
|
:key="catIndex"
|
||||||
class="q-mb-lg q-pa-md bordered rounded-borders"
|
class="q-mb-lg q-pa-md bordered rounded-borders"
|
||||||
>
|
>
|
||||||
<div class="row items-center q-mb-sm">
|
<div class="row items-center q-mb-sm">
|
||||||
<q-input
|
<q-input
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
v-model="category.name"
|
v-model="category.name"
|
||||||
:label="`Category ${catIndex + 1} Name *`"
|
:label="`Category ${catIndex + 1} Name *`"
|
||||||
class="col q-mr-sm"
|
class="col q-mr-sm"
|
||||||
lazy-rules
|
lazy-rules
|
||||||
:rules="[val => val && val.length > 0 || 'Category name required']"
|
:rules="[val => val && val.length > 0 || 'Category name required']"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
dense
|
dense
|
||||||
icon="delete"
|
icon="delete"
|
||||||
color="negative"
|
color="negative"
|
||||||
@click="removeCategory(catIndex)"
|
@click="removeCategory(catIndex)"
|
||||||
title="Remove Category"
|
title="Remove Category"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="(field, fieldIndex) in category.fields"
|
v-for="(field, fieldIndex) in category.fields"
|
||||||
:key="fieldIndex"
|
:key="fieldIndex"
|
||||||
class="q-ml-md q-mb-sm field-item"
|
class="q-ml-md q-mb-sm field-item"
|
||||||
>
|
>
|
||||||
<div class="row items-center q-gutter-sm">
|
<div class="row items-center q-gutter-sm">
|
||||||
<q-input
|
<q-input
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
v-model="field.label"
|
v-model="field.label"
|
||||||
label="Field Label *"
|
label="Field Label *"
|
||||||
class="col"
|
class="col"
|
||||||
lazy-rules
|
lazy-rules
|
||||||
:rules="[val => val && val.length > 0 || 'Field label required']"
|
:rules="[val => val && val.length > 0 || 'Field label required']"
|
||||||
/>
|
/>
|
||||||
<q-select
|
<q-select
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
v-model="field.type"
|
v-model="field.type"
|
||||||
:options="fieldTypes"
|
:options="fieldTypes"
|
||||||
label="Field Type *"
|
label="Field Type *"
|
||||||
class="col-auto"
|
class="col-auto"
|
||||||
style="min-width: 150px;"
|
style="min-width: 150px;"
|
||||||
lazy-rules
|
lazy-rules
|
||||||
:rules="[val => !!val || 'Field type required']"
|
:rules="[val => !!val || 'Field type required']"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
dense
|
dense
|
||||||
icon="delete"
|
icon="delete"
|
||||||
color="negative"
|
color="negative"
|
||||||
@click="removeField(catIndex, fieldIndex)"
|
@click="removeField(catIndex, fieldIndex)"
|
||||||
title="Remove Field"
|
title="Remove Field"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="field.description"
|
v-model="field.description"
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
label="Field Description (Optional)"
|
label="Field Description (Optional)"
|
||||||
autogrow
|
autogrow
|
||||||
class="q-mt-xs q-mb-xl"
|
class="q-mt-xs q-mb-xl"
|
||||||
hint="This description will appear below the field label on the form."
|
hint="This description will appear below the field label on the form."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-btn
|
<q-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
label="Add Field"
|
label="Add Field"
|
||||||
@click="addField(catIndex)"
|
@click="addField(catIndex)"
|
||||||
class="q-ml-md q-mt-sm"
|
class="q-ml-md q-mt-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
color="secondary"
|
color="secondary"
|
||||||
label="Add Category"
|
label="Add Category"
|
||||||
@click="addCategory"
|
@click="addCategory"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-separator class="q-my-lg" />
|
<q-separator class="q-my-lg" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<q-btn
|
<q-btn
|
||||||
label="Create Form"
|
label="Create Form"
|
||||||
type="submit"
|
type="submit"
|
||||||
color="primary"
|
color="primary"
|
||||||
:loading="submitting"
|
:loading="submitting"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
label="Cancel"
|
label="Cancel"
|
||||||
type="reset"
|
type="reset"
|
||||||
color="warning"
|
color="warning"
|
||||||
class="q-ml-sm"
|
class="q-ml-sm"
|
||||||
:to="{ name: 'formList' }"
|
:to="{ name: 'formList' }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
|
@ -139,7 +139,7 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'boot/axios';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
@ -157,30 +157,30 @@ const form = ref({
|
||||||
const fieldTypes = ref(['text', 'number', 'date', 'textarea', 'boolean']);
|
const fieldTypes = ref(['text', 'number', 'date', 'textarea', 'boolean']);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
|
|
||||||
function addCategory()
|
function addCategory()
|
||||||
{
|
{
|
||||||
form.value.categories.push({ name: `Category ${form.value.categories.length + 1}`, fields: [{ label: '', type: null, description: '' }] });
|
form.value.categories.push({ name: `Category ${form.value.categories.length + 1}`, fields: [{ label: '', type: null, description: '' }] });
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeCategory(index)
|
function removeCategory(index)
|
||||||
{
|
{
|
||||||
form.value.categories.splice(index, 1);
|
form.value.categories.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addField(catIndex)
|
function addField(catIndex)
|
||||||
{
|
{
|
||||||
form.value.categories[catIndex].fields.push({ label: '', type: 'text', description: '' });
|
form.value.categories[catIndex].fields.push({ label: '', type: 'text', description: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeField(catIndex, fieldIndex)
|
function removeField(catIndex, fieldIndex)
|
||||||
{
|
{
|
||||||
form.value.categories[catIndex].fields.splice(fieldIndex, 1);
|
form.value.categories[catIndex].fields.splice(fieldIndex, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createForm()
|
async function createForm()
|
||||||
{
|
{
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const response = await axios.post('/api/forms', form.value);
|
const response = await axios.post('/api/forms', form.value);
|
||||||
$q.notify({
|
$q.notify({
|
||||||
|
@ -190,8 +190,8 @@ async function createForm()
|
||||||
icon: 'check_circle'
|
icon: 'check_circle'
|
||||||
});
|
});
|
||||||
router.push({ name: 'formList' });
|
router.push({ name: 'formList' });
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Error creating form:', error);
|
console.error('Error creating form:', error);
|
||||||
const message = error.response?.data?.error || 'Failed to create form. Please check the details and try again.';
|
const message = error.response?.data?.error || 'Failed to create form. Please check the details and try again.';
|
||||||
|
@ -201,8 +201,8 @@ async function createForm()
|
||||||
message: message,
|
message: message,
|
||||||
icon: 'report_problem'
|
icon: 'report_problem'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<q-page padding>
|
<q-page padding>
|
||||||
<div class="text-h4 q-mb-md">
|
<div class="text-h4 q-mb-md">
|
||||||
Edit Form
|
Edit Form
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-form
|
<q-form
|
||||||
v-if="!loading && form"
|
v-if="!loading && form"
|
||||||
@submit.prevent="updateForm"
|
@submit.prevent="updateForm"
|
||||||
class="q-gutter-md"
|
class="q-gutter-md"
|
||||||
>
|
>
|
||||||
<q-input
|
<q-input
|
||||||
outlined
|
outlined
|
||||||
|
@ -27,18 +27,18 @@
|
||||||
|
|
||||||
<q-separator class="q-my-lg" />
|
<q-separator class="q-my-lg" />
|
||||||
|
|
||||||
<div class="text-h6 q-mb-sm">
|
<div class="text-h6 q-mb-sm">
|
||||||
Categories & Fields
|
Categories & Fields
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="(category, catIndex) in form.categories"
|
v-for="(category, catIndex) in form.categories"
|
||||||
:key="category.id || catIndex"
|
:key="category.id || catIndex"
|
||||||
class="q-mb-lg q-pa-md bordered rounded-borders"
|
class="q-mb-lg q-pa-md bordered rounded-borders"
|
||||||
>
|
>
|
||||||
<div class="row items-center q-mb-sm">
|
<div class="row items-center q-mb-sm">
|
||||||
<q-input
|
<q-input
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
v-model="category.name"
|
v-model="category.name"
|
||||||
:label="`Category ${catIndex + 1} Name *`"
|
:label="`Category ${catIndex + 1} Name *`"
|
||||||
|
@ -46,25 +46,25 @@
|
||||||
lazy-rules
|
lazy-rules
|
||||||
:rules="[ val => val && val.length > 0 || 'Category name required']"
|
:rules="[ val => val && val.length > 0 || 'Category name required']"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
dense
|
dense
|
||||||
icon="delete"
|
icon="delete"
|
||||||
color="negative"
|
color="negative"
|
||||||
@click="removeCategory(catIndex)"
|
@click="removeCategory(catIndex)"
|
||||||
title="Remove Category"
|
title="Remove Category"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="(field, fieldIndex) in category.fields"
|
v-for="(field, fieldIndex) in category.fields"
|
||||||
:key="field.id || fieldIndex"
|
:key="field.id || fieldIndex"
|
||||||
class="q-ml-md q-mb-sm"
|
class="q-ml-md q-mb-sm"
|
||||||
>
|
>
|
||||||
<div class="row items-center q-gutter-sm">
|
<div class="row items-center q-gutter-sm">
|
||||||
<q-input
|
<q-input
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
v-model="field.label"
|
v-model="field.label"
|
||||||
label="Field Label *"
|
label="Field Label *"
|
||||||
|
@ -73,7 +73,7 @@
|
||||||
:rules="[ val => val && val.length > 0 || 'Field label required']"
|
:rules="[ val => val && val.length > 0 || 'Field label required']"
|
||||||
/>
|
/>
|
||||||
<q-select
|
<q-select
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
v-model="field.type"
|
v-model="field.type"
|
||||||
:options="fieldTypes"
|
:options="fieldTypes"
|
||||||
|
@ -83,14 +83,14 @@
|
||||||
lazy-rules
|
lazy-rules
|
||||||
:rules="[ val => !!val || 'Field type required']"
|
:rules="[ val => !!val || 'Field type required']"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
dense
|
dense
|
||||||
icon="delete"
|
icon="delete"
|
||||||
color="negative"
|
color="negative"
|
||||||
@click="removeField(catIndex, fieldIndex)"
|
@click="removeField(catIndex, fieldIndex)"
|
||||||
title="Remove Field"
|
title="Remove Field"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-input
|
<q-input
|
||||||
|
@ -103,52 +103,52 @@
|
||||||
hint="This description will appear below the field label on the form."
|
hint="This description will appear below the field label on the form."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
color="primary"
|
color="primary"
|
||||||
label="Add Field"
|
label="Add Field"
|
||||||
@click="addField(catIndex)"
|
@click="addField(catIndex)"
|
||||||
class="q-ml-md q-mt-sm"
|
class="q-ml-md q-mt-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
color="secondary"
|
color="secondary"
|
||||||
label="Add Category"
|
label="Add Category"
|
||||||
@click="addCategory"
|
@click="addCategory"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-separator class="q-my-lg" />
|
<q-separator class="q-my-lg" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
label="Update Form"
|
label="Update Form"
|
||||||
type="submit"
|
type="submit"
|
||||||
color="primary"
|
color="primary"
|
||||||
:loading="submitting"
|
:loading="submitting"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
label="Cancel"
|
label="Cancel"
|
||||||
type="reset"
|
type="reset"
|
||||||
color="warning"
|
color="warning"
|
||||||
class="q-ml-sm"
|
class="q-ml-sm"
|
||||||
:to="{ name: 'formList' }"
|
:to="{ name: 'formList' }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
<div v-else-if="loading">
|
<div v-else-if="loading">
|
||||||
<q-spinner-dots
|
<q-spinner-dots
|
||||||
color="primary"
|
color="primary"
|
||||||
size="40px"
|
size="40px"
|
||||||
/>
|
/>
|
||||||
Loading form details...
|
Loading form details...
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="text-negative"
|
class="text-negative"
|
||||||
>
|
>
|
||||||
Failed to load form details.
|
Failed to load form details.
|
||||||
</div>
|
</div>
|
||||||
|
@ -157,7 +157,7 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'boot/axios';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
@ -177,21 +177,21 @@ const loading = ref(true);
|
||||||
const fieldTypes = ref(['text', 'number', 'date', 'textarea', 'boolean']);
|
const fieldTypes = ref(['text', 'number', 'date', 'textarea', 'boolean']);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
|
|
||||||
async function fetchForm()
|
async function fetchForm()
|
||||||
{
|
{
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const response = await axios.get(`/api/forms/${props.id}`);
|
const response = await axios.get(`/api/forms/${props.id}`);
|
||||||
// Ensure categories and fields exist, even if empty
|
// Ensure categories and fields exist, even if empty
|
||||||
response.data.categories = response.data.categories || [];
|
response.data.categories = response.data.categories || [];
|
||||||
response.data.categories.forEach(cat =>
|
response.data.categories.forEach(cat =>
|
||||||
{
|
{
|
||||||
cat.fields = cat.fields || [];
|
cat.fields = cat.fields || [];
|
||||||
});
|
});
|
||||||
form.value = response.data;
|
form.value = response.data;
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Error fetching form details:', error);
|
console.error('Error fetching form details:', error);
|
||||||
$q.notify({
|
$q.notify({
|
||||||
|
@ -201,8 +201,8 @@ async function fetchForm()
|
||||||
icon: 'report_problem'
|
icon: 'report_problem'
|
||||||
});
|
});
|
||||||
form.value = null; // Indicate failure
|
form.value = null; // Indicate failure
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
@ -210,38 +210,38 @@ async function fetchForm()
|
||||||
|
|
||||||
onMounted(fetchForm);
|
onMounted(fetchForm);
|
||||||
|
|
||||||
function addCategory()
|
function addCategory()
|
||||||
{
|
{
|
||||||
if (!form.value.categories)
|
if (!form.value.categories)
|
||||||
{
|
{
|
||||||
form.value.categories = [];
|
form.value.categories = [];
|
||||||
}
|
}
|
||||||
form.value.categories.push({ name: `Category ${form.value.categories.length + 1}`, fields: [{ label: '', type: 'text', description: '' }] });
|
form.value.categories.push({ name: `Category ${form.value.categories.length + 1}`, fields: [{ label: '', type: 'text', description: '' }] });
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeCategory(index)
|
function removeCategory(index)
|
||||||
{
|
{
|
||||||
form.value.categories.splice(index, 1);
|
form.value.categories.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addField(catIndex)
|
function addField(catIndex)
|
||||||
{
|
{
|
||||||
if (!form.value.categories[catIndex].fields)
|
if (!form.value.categories[catIndex].fields)
|
||||||
{
|
{
|
||||||
form.value.categories[catIndex].fields = [];
|
form.value.categories[catIndex].fields = [];
|
||||||
}
|
}
|
||||||
form.value.categories[catIndex].fields.push({ label: '', type: 'text', description: '' });
|
form.value.categories[catIndex].fields.push({ label: '', type: 'text', description: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeField(catIndex, fieldIndex)
|
function removeField(catIndex, fieldIndex)
|
||||||
{
|
{
|
||||||
form.value.categories[catIndex].fields.splice(fieldIndex, 1);
|
form.value.categories[catIndex].fields.splice(fieldIndex, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateForm()
|
async function updateForm()
|
||||||
{
|
{
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Prepare payload, potentially removing temporary IDs if any were added client-side
|
// Prepare payload, potentially removing temporary IDs if any were added client-side
|
||||||
const payload = JSON.parse(JSON.stringify(form.value));
|
const payload = JSON.parse(JSON.stringify(form.value));
|
||||||
|
@ -256,8 +256,8 @@ async function updateForm()
|
||||||
icon: 'check_circle'
|
icon: 'check_circle'
|
||||||
});
|
});
|
||||||
router.push({ name: 'formList' }); // Or maybe back to the form details/responses page
|
router.push({ name: 'formList' }); // Or maybe back to the form details/responses page
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Error updating form:', error);
|
console.error('Error updating form:', error);
|
||||||
const message = error.response?.data?.error || 'Failed to update form. Please check the details and try again.';
|
const message = error.response?.data?.error || 'Failed to update form. Please check the details and try again.';
|
||||||
|
@ -267,8 +267,8 @@ async function updateForm()
|
||||||
message: message,
|
message: message,
|
||||||
icon: 'report_problem'
|
icon: 'report_problem'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,46 +1,46 @@
|
||||||
<template>
|
<template>
|
||||||
<q-page padding>
|
<q-page padding>
|
||||||
<q-inner-loading :showing="loading">
|
<q-inner-loading :showing="loading">
|
||||||
<q-spinner-gears
|
<q-spinner-gears
|
||||||
size="50px"
|
size="50px"
|
||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
</q-inner-loading>
|
</q-inner-loading>
|
||||||
|
|
||||||
<div v-if="!loading && form">
|
<div v-if="!loading && form">
|
||||||
<div class="text-h4 q-mb-xs">
|
<div class="text-h4 q-mb-xs">
|
||||||
{{ form.title }}
|
{{ form.title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-subtitle1 text-grey q-mb-lg">
|
<div class="text-subtitle1 text-grey q-mb-lg">
|
||||||
{{ form.description }}
|
{{ form.description }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-form
|
<q-form
|
||||||
@submit.prevent="submitResponse"
|
@submit.prevent="submitResponse"
|
||||||
class="q-gutter-md"
|
class="q-gutter-md"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="category in form.categories"
|
v-for="category in form.categories"
|
||||||
:key="category.id"
|
:key="category.id"
|
||||||
class="q-mb-lg"
|
class="q-mb-lg"
|
||||||
>
|
>
|
||||||
<div class="text-h6 q-mb-sm">
|
<div class="text-h6 q-mb-sm">
|
||||||
{{ category.name }}
|
{{ category.name }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="field in category.fields"
|
v-for="field in category.fields"
|
||||||
:key="field.id"
|
:key="field.id"
|
||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
>
|
>
|
||||||
<q-item-label class="q-mb-xs">
|
<q-item-label class="q-mb-xs">
|
||||||
{{ field.label }}
|
{{ field.label }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
caption
|
caption
|
||||||
v-if="field.description"
|
v-if="field.description"
|
||||||
class="q-mb-xs text-grey-7"
|
class="q-mb-xs text-grey-7"
|
||||||
>
|
>
|
||||||
{{ field.description }}
|
{{ field.description }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-input
|
<q-input
|
||||||
v-if="field.type === 'text'"
|
v-if="field.type === 'text'"
|
||||||
|
@ -85,38 +85,38 @@
|
||||||
<q-separator class="q-my-lg" />
|
<q-separator class="q-my-lg" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
label="Submit Response"
|
label="Submit Response"
|
||||||
type="submit"
|
type="submit"
|
||||||
color="primary"
|
color="primary"
|
||||||
:loading="submitting"
|
:loading="submitting"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
label="Cancel"
|
label="Cancel"
|
||||||
type="reset"
|
type="reset"
|
||||||
color="default"
|
color="default"
|
||||||
class="q-ml-sm"
|
class="q-ml-sm"
|
||||||
:to="{ name: 'formList' }"
|
:to="{ name: 'formList' }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
</div>
|
</div>
|
||||||
<q-banner
|
<q-banner
|
||||||
v-else-if="!loading && !form"
|
v-else-if="!loading && !form"
|
||||||
class="bg-negative text-white"
|
class="bg-negative text-white"
|
||||||
>
|
>
|
||||||
<template #avatar>
|
<template #avatar>
|
||||||
<q-icon name="error" />
|
<q-icon name="error" />
|
||||||
</template>
|
</template>
|
||||||
Form not found or could not be loaded.
|
Form not found or could not be loaded.
|
||||||
<template #action>
|
<template #action>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
color="white"
|
color="white"
|
||||||
label="Back to Forms"
|
label="Back to Forms"
|
||||||
:to="{ name: 'formList' }"
|
:to="{ name: 'formList' }"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</q-banner>
|
</q-banner>
|
||||||
|
@ -125,7 +125,7 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, reactive } from 'vue';
|
import { ref, onMounted, reactive } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'boot/axios';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
@ -144,24 +144,24 @@ const responses = reactive({}); // Use reactive for dynamic properties
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
|
|
||||||
async function fetchFormDetails()
|
async function fetchFormDetails()
|
||||||
{
|
{
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
form.value = null; // Reset form data
|
form.value = null; // Reset form data
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const response = await axios.get(`/api/forms/${props.id}`);
|
const response = await axios.get(`/api/forms/${props.id}`);
|
||||||
form.value = response.data;
|
form.value = response.data;
|
||||||
// Initialize responses object based on fields
|
// Initialize responses object based on fields
|
||||||
form.value.categories.forEach(cat =>
|
form.value.categories.forEach(cat =>
|
||||||
{
|
{
|
||||||
cat.fields.forEach(field =>
|
cat.fields.forEach(field =>
|
||||||
{
|
{
|
||||||
responses[field.id] = null; // Initialize all fields to null or default
|
responses[field.id] = null; // Initialize all fields to null or default
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error(`Error fetching form ${props.id}:`, error);
|
console.error(`Error fetching form ${props.id}:`, error);
|
||||||
$q.notify({
|
$q.notify({
|
||||||
|
@ -170,17 +170,17 @@ async function fetchFormDetails()
|
||||||
message: 'Failed to load form details.',
|
message: 'Failed to load form details.',
|
||||||
icon: 'report_problem'
|
icon: 'report_problem'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitResponse()
|
async function submitResponse()
|
||||||
{
|
{
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Basic check if any response is provided (optional)
|
// Basic check if any response is provided (optional)
|
||||||
// const hasResponse = Object.values(responses).some(val => val !== null && val !== '');
|
// const hasResponse = Object.values(responses).some(val => val !== null && val !== '');
|
||||||
|
@ -201,8 +201,8 @@ async function submitResponse()
|
||||||
// Or clear the form:
|
// Or clear the form:
|
||||||
// Object.keys(responses).forEach(key => { responses[key] = null; });
|
// Object.keys(responses).forEach(key => { responses[key] = null; });
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Error submitting response:', error);
|
console.error('Error submitting response:', error);
|
||||||
const message = error.response?.data?.error || 'Failed to submit response.';
|
const message = error.response?.data?.error || 'Failed to submit response.';
|
||||||
|
@ -212,8 +212,8 @@ async function submitResponse()
|
||||||
message: message,
|
message: message,
|
||||||
icon: 'report_problem'
|
icon: 'report_problem'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,117 +1,117 @@
|
||||||
<template>
|
<template>
|
||||||
<q-page padding>
|
<q-page padding>
|
||||||
<div class="q-mb-md row justify-between items-center">
|
<div class="q-mb-md row justify-between items-center">
|
||||||
<div class="text-h4">
|
<div class="text-h4">
|
||||||
Forms
|
Forms
|
||||||
</div>
|
</div>
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
label="Create New Form"
|
label="Create New Form"
|
||||||
color="primary"
|
color="primary"
|
||||||
:to="{ name: 'formCreate' }"
|
:to="{ name: 'formCreate' }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-list
|
<q-list
|
||||||
bordered
|
bordered
|
||||||
separator
|
separator
|
||||||
v-if="forms.length > 0"
|
v-if="forms.length > 0"
|
||||||
>
|
>
|
||||||
<q-item
|
<q-item
|
||||||
v-for="form in forms"
|
v-for="form in forms"
|
||||||
:key="form.id"
|
:key="form.id"
|
||||||
>
|
>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>{{ form.title }}</q-item-label>
|
<q-item-label>{{ form.title }}</q-item-label>
|
||||||
<q-item-label caption>
|
<q-item-label caption>
|
||||||
{{ form.description || 'No description' }}
|
{{ form.description || 'No description' }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label caption>
|
<q-item-label caption>
|
||||||
Created: {{ formatDate(form.createdAt) }}
|
Created: {{ formatDate(form.createdAt) }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<div class="q-gutter-sm">
|
<div class="q-gutter-sm">
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
dense
|
dense
|
||||||
icon="edit_note"
|
icon="edit_note"
|
||||||
color="info"
|
color="info"
|
||||||
:to="{ name: 'formFill', params: { id: form.id } }"
|
:to="{ name: 'formFill', params: { id: form.id } }"
|
||||||
title="Fill Form"
|
title="Fill Form"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
dense
|
dense
|
||||||
icon="visibility"
|
icon="visibility"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
:to="{ name: 'formResponses', params: { id: form.id } }"
|
:to="{ name: 'formResponses', params: { id: form.id } }"
|
||||||
title="View Responses"
|
title="View Responses"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
dense
|
dense
|
||||||
icon="edit"
|
icon="edit"
|
||||||
color="warning"
|
color="warning"
|
||||||
:to="{ name: 'formEdit', params: { id: form.id } }"
|
:to="{ name: 'formEdit', params: { id: form.id } }"
|
||||||
title="Edit Form"
|
title="Edit Form"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
dense
|
dense
|
||||||
icon="delete"
|
icon="delete"
|
||||||
color="negative"
|
color="negative"
|
||||||
@click.stop="confirmDeleteForm(form.id)"
|
@click.stop="confirmDeleteForm(form.id)"
|
||||||
title="Delete Form"
|
title="Delete Form"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
<q-banner
|
<q-banner
|
||||||
v-else
|
v-else
|
||||||
class="bg-info text-white"
|
class="bg-info text-white"
|
||||||
>
|
>
|
||||||
<template #avatar>
|
<template #avatar>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="info"
|
name="info"
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
No forms created yet. Click the button above to create your first form.
|
No forms created yet. Click the button above to create your first form.
|
||||||
</q-banner>
|
</q-banner>
|
||||||
|
|
||||||
<q-inner-loading :showing="loading">
|
<q-inner-loading :showing="loading">
|
||||||
<q-spinner-gears
|
<q-spinner-gears
|
||||||
size="50px"
|
size="50px"
|
||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
</q-inner-loading>
|
</q-inner-loading>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'boot/axios';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const forms = ref([]);
|
const forms = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
async function fetchForms()
|
async function fetchForms()
|
||||||
{
|
{
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const response = await axios.get('/api/forms');
|
const response = await axios.get('/api/forms');
|
||||||
forms.value = response.data;
|
forms.value = response.data;
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Error fetching forms:', error);
|
console.error('Error fetching forms:', error);
|
||||||
$q.notify({
|
$q.notify({
|
||||||
|
@ -120,15 +120,15 @@ async function fetchForms()
|
||||||
message: 'Failed to load forms. Please try again later.',
|
message: 'Failed to load forms. Please try again later.',
|
||||||
icon: 'report_problem'
|
icon: 'report_problem'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add function to handle delete confirmation
|
// Add function to handle delete confirmation
|
||||||
function confirmDeleteForm(id)
|
function confirmDeleteForm(id)
|
||||||
{
|
{
|
||||||
$q.dialog({
|
$q.dialog({
|
||||||
title: 'Confirm Delete',
|
title: 'Confirm Delete',
|
||||||
|
@ -144,16 +144,16 @@ function confirmDeleteForm(id)
|
||||||
label: 'Cancel',
|
label: 'Cancel',
|
||||||
flat: true
|
flat: true
|
||||||
}
|
}
|
||||||
}).onOk(() =>
|
}).onOk(() =>
|
||||||
{
|
{
|
||||||
deleteForm(id);
|
deleteForm(id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add function to call the delete API
|
// Add function to call the delete API
|
||||||
async function deleteForm(id)
|
async function deleteForm(id)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await axios.delete(`/api/forms/${id}`);
|
await axios.delete(`/api/forms/${id}`);
|
||||||
forms.value = forms.value.filter(form => form.id !== id);
|
forms.value = forms.value.filter(form => form.id !== id);
|
||||||
|
@ -163,8 +163,8 @@ async function deleteForm(id)
|
||||||
message: 'Form deleted successfully.',
|
message: 'Form deleted successfully.',
|
||||||
icon: 'check_circle'
|
icon: 'check_circle'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error(`Error deleting form ${id}:`, error);
|
console.error(`Error deleting form ${id}:`, error);
|
||||||
const errorMessage = error.response?.data?.error || 'Failed to delete form. Please try again.';
|
const errorMessage = error.response?.data?.error || 'Failed to delete form. Please try again.';
|
||||||
|
@ -178,7 +178,7 @@ async function deleteForm(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add function to format date
|
// Add function to format date
|
||||||
function formatDate(date)
|
function formatDate(date)
|
||||||
{
|
{
|
||||||
return new Date(date).toLocaleString();
|
return new Date(date).toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'boot/axios';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
const componentProps = defineProps({
|
const componentProps = defineProps({
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { startAuthentication } from '@simplewebauthn/browser';
|
import { startAuthentication } from '@simplewebauthn/browser';
|
||||||
import axios from 'axios';
|
import axios from 'boot/axios';
|
||||||
import { useAuthStore } from 'stores/auth'; // Import the auth store
|
import { useAuthStore } from 'stores/auth'; // Import the auth store
|
||||||
|
|
||||||
const username = ref('');
|
const username = ref('');
|
||||||
|
@ -65,7 +65,7 @@ async function handleLogin()
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 1. Get options from server
|
// 1. Get options from server
|
||||||
const optionsRes = await axios.post('/auth/generate-authentication-options', {
|
const optionsRes = await axios.post('/api/auth/generate-authentication-options', {
|
||||||
username: username.value || undefined, // Send username if provided
|
username: username.value || undefined, // Send username if provided
|
||||||
});
|
});
|
||||||
const options = optionsRes.data;
|
const options = optionsRes.data;
|
||||||
|
@ -74,7 +74,7 @@ async function handleLogin()
|
||||||
const authResp = await startAuthentication(options);
|
const authResp = await startAuthentication(options);
|
||||||
|
|
||||||
// 3. Send response to server for verification
|
// 3. Send response to server for verification
|
||||||
const verificationRes = await axios.post('/auth/verify-authentication', {
|
const verificationRes = await axios.post('/api/auth/verify-authentication', {
|
||||||
authenticationResponse: authResp,
|
authenticationResponse: authResp,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
<q-item-label
|
<q-item-label
|
||||||
class="q-mt-sm markdown-content"
|
class="q-mt-sm markdown-content"
|
||||||
>
|
>
|
||||||
<div v-html="parseMarkdown(summary.content)" />
|
<div v-html="parseMarkdown(summary.summaryText)" />
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
@ -102,7 +102,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { date, useQuasar } from 'quasar'; // Import useQuasar
|
import { date, useQuasar } from 'quasar'; // Import useQuasar
|
||||||
import axios from 'axios';
|
import axios from 'boot/axios';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
|
||||||
const $q = useQuasar(); // Initialize Quasar plugin usage
|
const $q = useQuasar(); // Initialize Quasar plugin usage
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<q-page padding>
|
<q-page padding>
|
||||||
<div class="q-mb-md row justify-between items-center">
|
<div class="q-mb-md row justify-between items-center">
|
||||||
<div class="text-h4">
|
<div class="text-h4">
|
||||||
Passkey Management
|
Passkey Management
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<q-btn
|
<q-btn
|
||||||
|
@ -29,23 +29,23 @@
|
||||||
<!-- Passkey List Section -->
|
<!-- Passkey List Section -->
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h5>Your Registered Passkeys</h5>
|
<h5>Your Registered Passkeys</h5>
|
||||||
<q-list
|
<q-list
|
||||||
bordered
|
bordered
|
||||||
separator
|
separator
|
||||||
v-if="passkeys.length > 0 && !fetchLoading"
|
v-if="passkeys.length > 0 && !fetchLoading"
|
||||||
>
|
>
|
||||||
<q-item v-if="registerSuccessMessage || registerErrorMessage">
|
<q-item v-if="registerSuccessMessage || registerErrorMessage">
|
||||||
<div
|
<div
|
||||||
v-if="registerSuccessMessage"
|
v-if="registerSuccessMessage"
|
||||||
class="text-positive q-mt-md"
|
class="text-positive q-mt-md"
|
||||||
>
|
>
|
||||||
{{ registerSuccessMessage }}
|
{{ registerSuccessMessage }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="registerErrorMessage"
|
v-if="registerErrorMessage"
|
||||||
class="text-negative q-mt-md"
|
class="text-negative q-mt-md"
|
||||||
>
|
>
|
||||||
{{ registerErrorMessage }}
|
{{ registerErrorMessage }}
|
||||||
</div>
|
</div>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
|
@ -55,19 +55,19 @@
|
||||||
>
|
>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>Passkey ID: {{ passkey.credentialID }} </q-item-label>
|
<q-item-label>Passkey ID: {{ passkey.credentialID }} </q-item-label>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
caption
|
caption
|
||||||
v-if="identifiedPasskeyId === passkey.credentialID"
|
v-if="identifiedPasskeyId === passkey.credentialID"
|
||||||
>
|
>
|
||||||
Verified just now!
|
Verified just now!
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<!-- <q-item-label caption>Registered: {{ new Date(passkey.createdAt).toLocaleDateString() }}</q-item-label> -->
|
<!-- <q-item-label caption>Registered: {{ new Date(passkey.createdAt).toLocaleDateString() }}</q-item-label> -->
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section
|
<q-item-section
|
||||||
side
|
side
|
||||||
class="row no-wrap items-center"
|
class="row no-wrap items-center"
|
||||||
>
|
>
|
||||||
<!-- Delete Button -->
|
<!-- Delete Button -->
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
|
@ -82,51 +82,51 @@
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
<div
|
<div
|
||||||
v-else-if="fetchLoading"
|
v-else-if="fetchLoading"
|
||||||
class="q-mt-md"
|
class="q-mt-md"
|
||||||
>
|
>
|
||||||
Loading passkeys...
|
Loading passkeys...
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="q-mt-md"
|
class="q-mt-md"
|
||||||
>
|
>
|
||||||
You have no passkeys registered yet.
|
You have no passkeys registered yet.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="fetchErrorMessage"
|
v-if="fetchErrorMessage"
|
||||||
class="text-negative q-mt-md"
|
class="text-negative q-mt-md"
|
||||||
>
|
>
|
||||||
{{ fetchErrorMessage }}
|
{{ fetchErrorMessage }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="deleteSuccessMessage"
|
v-if="deleteSuccessMessage"
|
||||||
class="text-positive q-mt-md"
|
class="text-positive q-mt-md"
|
||||||
>
|
>
|
||||||
{{ deleteSuccessMessage }}
|
{{ deleteSuccessMessage }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="deleteErrorMessage"
|
v-if="deleteErrorMessage"
|
||||||
class="text-negative q-mt-md"
|
class="text-negative q-mt-md"
|
||||||
>
|
>
|
||||||
{{ deleteErrorMessage }}
|
{{ deleteErrorMessage }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="identifyErrorMessage"
|
v-if="identifyErrorMessage"
|
||||||
class="text-negative q-mt-md"
|
class="text-negative q-mt-md"
|
||||||
>
|
>
|
||||||
{{ identifyErrorMessage }}
|
{{ identifyErrorMessage }}
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { startRegistration, startAuthentication } from '@simplewebauthn/browser'; // Import startAuthentication
|
import { startRegistration, startAuthentication } from '@simplewebauthn/browser'; // Import startAuthentication
|
||||||
import axios from 'axios';
|
import axios from 'boot/axios';
|
||||||
import { useAuthStore } from 'stores/auth';
|
import { useAuthStore } from 'stores/auth';
|
||||||
|
|
||||||
const registerLoading = ref(false);
|
const registerLoading = ref(false);
|
||||||
|
@ -149,7 +149,7 @@ const isLoggedIn = computed(() => authStore.isAuthenticated);
|
||||||
const username = computed(() => authStore.user?.username);
|
const username = computed(() => authStore.user?.username);
|
||||||
|
|
||||||
// Fetch existing passkeys
|
// Fetch existing passkeys
|
||||||
async function fetchPasskeys()
|
async function fetchPasskeys()
|
||||||
{
|
{
|
||||||
if (!isLoggedIn.value) return;
|
if (!isLoggedIn.value) return;
|
||||||
fetchLoading.value = true;
|
fetchLoading.value = true;
|
||||||
|
@ -158,49 +158,49 @@ async function fetchPasskeys()
|
||||||
deleteErrorMessage.value = '';
|
deleteErrorMessage.value = '';
|
||||||
identifyErrorMessage.value = ''; // Clear identify message
|
identifyErrorMessage.value = ''; // Clear identify message
|
||||||
identifiedPasskeyId.value = null; // Clear identified key
|
identifiedPasskeyId.value = null; // Clear identified key
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const response = await axios.get('/auth/passkeys');
|
const response = await axios.get('/api/auth/passkeys');
|
||||||
passkeys.value = response.data || [];
|
passkeys.value = response.data || [];
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Error fetching passkeys:', error);
|
console.error('Error fetching passkeys:', error);
|
||||||
fetchErrorMessage.value = error.response?.data?.error || 'Failed to load passkeys.';
|
fetchErrorMessage.value = error.response?.data?.error || 'Failed to load passkeys.';
|
||||||
passkeys.value = []; // Clear passkeys on error
|
passkeys.value = []; // Clear passkeys on error
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
fetchLoading.value = false;
|
fetchLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check auth status and fetch passkeys on component mount
|
// Check auth status and fetch passkeys on component mount
|
||||||
onMounted(async() =>
|
onMounted(async() =>
|
||||||
{
|
{
|
||||||
let initialAuthError = '';
|
let initialAuthError = '';
|
||||||
if (!authStore.isAuthenticated)
|
if (!authStore.isAuthenticated)
|
||||||
{
|
{
|
||||||
await authStore.checkAuthStatus();
|
await authStore.checkAuthStatus();
|
||||||
if (authStore.error)
|
if (authStore.error)
|
||||||
{
|
{
|
||||||
initialAuthError = `Authentication error: ${authStore.error}`;
|
initialAuthError = `Authentication error: ${authStore.error}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isLoggedIn.value)
|
if (!isLoggedIn.value)
|
||||||
{
|
{
|
||||||
// Use register error message ref for consistency if login is required first
|
// Use register error message ref for consistency if login is required first
|
||||||
registerErrorMessage.value = initialAuthError || 'You must be logged in to manage passkeys.';
|
registerErrorMessage.value = initialAuthError || 'You must be logged in to manage passkeys.';
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
fetchPasskeys(); // Fetch passkeys if logged in
|
fetchPasskeys(); // Fetch passkeys if logged in
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleRegister()
|
async function handleRegister()
|
||||||
{
|
{
|
||||||
if (!isLoggedIn.value || !username.value)
|
if (!isLoggedIn.value || !username.value)
|
||||||
{
|
{
|
||||||
registerErrorMessage.value = 'User not authenticated.';
|
registerErrorMessage.value = 'User not authenticated.';
|
||||||
return;
|
return;
|
||||||
|
@ -213,10 +213,10 @@ async function handleRegister()
|
||||||
identifyErrorMessage.value = '';
|
identifyErrorMessage.value = '';
|
||||||
identifiedPasskeyId.value = null;
|
identifiedPasskeyId.value = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 1. Get options from server
|
// 1. Get options from server
|
||||||
const optionsRes = await axios.post('/auth/generate-registration-options', {
|
const optionsRes = await axios.post('/api/auth/generate-registration-options', {
|
||||||
username: username.value, // Use username from store
|
username: username.value, // Use username from store
|
||||||
});
|
});
|
||||||
const options = optionsRes.data;
|
const options = optionsRes.data;
|
||||||
|
@ -225,43 +225,43 @@ async function handleRegister()
|
||||||
const regResp = await startRegistration(options);
|
const regResp = await startRegistration(options);
|
||||||
|
|
||||||
// 3. Send response to server for verification
|
// 3. Send response to server for verification
|
||||||
const verificationRes = await axios.post('/auth/verify-registration', {
|
const verificationRes = await axios.post('/api/auth/verify-registration', {
|
||||||
registrationResponse: regResp,
|
registrationResponse: regResp,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (verificationRes.data.verified)
|
if (verificationRes.data.verified)
|
||||||
{
|
{
|
||||||
registerSuccessMessage.value = 'New passkey registered successfully!';
|
registerSuccessMessage.value = 'New passkey registered successfully!';
|
||||||
fetchPasskeys(); // Refresh the list of passkeys
|
fetchPasskeys(); // Refresh the list of passkeys
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
registerErrorMessage.value = 'Passkey verification failed.';
|
registerErrorMessage.value = 'Passkey verification failed.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Registration error:', error);
|
console.error('Registration error:', error);
|
||||||
const message = error.response?.data?.error || error.message || 'An unknown error occurred during registration.';
|
const message = error.response?.data?.error || error.message || 'An unknown error occurred during registration.';
|
||||||
// Handle specific simplewebauthn errors
|
// Handle specific simplewebauthn errors
|
||||||
if (error.name === 'InvalidStateError')
|
if (error.name === 'InvalidStateError')
|
||||||
{
|
{
|
||||||
registerErrorMessage.value = 'Authenticator may already be registered.';
|
registerErrorMessage.value = 'Authenticator may already be registered.';
|
||||||
}
|
}
|
||||||
else if (error.name === 'NotAllowedError')
|
else if (error.name === 'NotAllowedError')
|
||||||
{
|
{
|
||||||
registerErrorMessage.value = 'Registration ceremony was cancelled or timed out.';
|
registerErrorMessage.value = 'Registration ceremony was cancelled or timed out.';
|
||||||
}
|
}
|
||||||
else if (error.response?.status === 409)
|
else if (error.response?.status === 409)
|
||||||
{
|
{
|
||||||
registerErrorMessage.value = 'This passkey seems to be registered already.';
|
registerErrorMessage.value = 'This passkey seems to be registered already.';
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
registerErrorMessage.value = `Registration failed: ${message}`;
|
registerErrorMessage.value = `Registration failed: ${message}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
registerLoading.value = false;
|
registerLoading.value = false;
|
||||||
}
|
}
|
||||||
|
@ -269,7 +269,7 @@ async function handleRegister()
|
||||||
|
|
||||||
|
|
||||||
// Handle deleting a passkey
|
// Handle deleting a passkey
|
||||||
async function handleDelete(credentialID)
|
async function handleDelete(credentialID)
|
||||||
{
|
{
|
||||||
if (!credentialID) return;
|
if (!credentialID) return;
|
||||||
|
|
||||||
|
@ -286,27 +286,27 @@ async function handleDelete(credentialID)
|
||||||
identifyErrorMessage.value = '';
|
identifyErrorMessage.value = '';
|
||||||
identifiedPasskeyId.value = null;
|
identifiedPasskeyId.value = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await axios.delete(`/auth/passkeys/${credentialID}`);
|
await axios.delete(`/api/auth/passkeys/${credentialID}`);
|
||||||
deleteSuccessMessage.value = 'Passkey deleted successfully.';
|
deleteSuccessMessage.value = 'Passkey deleted successfully.';
|
||||||
fetchPasskeys(); // Refresh the list
|
fetchPasskeys(); // Refresh the list
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Error deleting passkey:', error);
|
console.error('Error deleting passkey:', error);
|
||||||
deleteErrorMessage.value = error.response?.data?.error || 'Failed to delete passkey.';
|
deleteErrorMessage.value = error.response?.data?.error || 'Failed to delete passkey.';
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
deleteLoading.value = null; // Clear loading state
|
deleteLoading.value = null; // Clear loading state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle identifying a passkey
|
// Handle identifying a passkey
|
||||||
async function handleIdentify()
|
async function handleIdentify()
|
||||||
{
|
{
|
||||||
if (!isLoggedIn.value)
|
if (!isLoggedIn.value)
|
||||||
{
|
{
|
||||||
identifyErrorMessage.value = 'You must be logged in.';
|
identifyErrorMessage.value = 'You must be logged in.';
|
||||||
return;
|
return;
|
||||||
|
@ -321,11 +321,11 @@ async function handleIdentify()
|
||||||
deleteSuccessMessage.value = '';
|
deleteSuccessMessage.value = '';
|
||||||
deleteErrorMessage.value = '';
|
deleteErrorMessage.value = '';
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 1. Get authentication options from the server
|
// 1. Get authentication options from the server
|
||||||
// We don't need to send username as the server should use the session
|
// 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 optionsRes = await axios.post('/api/auth/generate-authentication-options', {}); // Send empty body
|
||||||
const options = optionsRes.data;
|
const options = optionsRes.data;
|
||||||
|
|
||||||
// Optionally filter options to only allow the specific key if needed, but usually not necessary for identification
|
// Optionally filter options to only allow the specific key if needed, but usually not necessary for identification
|
||||||
|
@ -339,30 +339,30 @@ async function handleIdentify()
|
||||||
console.log('Identified Passkey ID:', identifiedPasskeyId.value);
|
console.log('Identified Passkey ID:', identifiedPasskeyId.value);
|
||||||
|
|
||||||
// Optional: Add a small delay before clearing the highlight
|
// Optional: Add a small delay before clearing the highlight
|
||||||
setTimeout(() =>
|
setTimeout(() =>
|
||||||
{
|
{
|
||||||
// Only clear if it's still the same identified key
|
// Only clear if it's still the same identified key
|
||||||
if (identifiedPasskeyId.value === authResp.id)
|
if (identifiedPasskeyId.value === authResp.id)
|
||||||
{
|
{
|
||||||
identifiedPasskeyId.value = null;
|
identifiedPasskeyId.value = null;
|
||||||
}
|
}
|
||||||
}, 5000); // Clear highlight after 5 seconds
|
}, 5000); // Clear highlight after 5 seconds
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Identification error:', error);
|
console.error('Identification error:', error);
|
||||||
identifiedPasskeyId.value = null;
|
identifiedPasskeyId.value = null;
|
||||||
if (error.name === 'NotAllowedError')
|
if (error.name === 'NotAllowedError')
|
||||||
{
|
{
|
||||||
identifyErrorMessage.value = 'Identification ceremony was cancelled or timed out.';
|
identifyErrorMessage.value = 'Identification ceremony was cancelled or timed out.';
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
identifyErrorMessage.value = error.response?.data?.error || error.message || 'Failed to identify passkey.';
|
identifyErrorMessage.value = error.response?.data?.error || error.message || 'Failed to identify passkey.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
identifyLoading.value = null; // Clear loading state
|
identifyLoading.value = null; // Clear loading state
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
<q-card style="width: 400px; max-width: 90vw;">
|
<q-card style="width: 400px; max-width: 90vw;">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<!-- Update title based on login status from store -->
|
<!-- Update title based on login status from store -->
|
||||||
<div class="text-h6">
|
<div class="text-h6">
|
||||||
{{ isLoggedIn ? 'Register New Passkey' : 'Register Passkey' }}
|
{{ isLoggedIn ? 'Register New Passkey' : 'Register Passkey' }}
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
|
@ -29,27 +29,27 @@
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:disable="loading || (!username && !isLoggedIn)"
|
:disable="loading || (!username && !isLoggedIn)"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="successMessage"
|
v-if="successMessage"
|
||||||
class="text-positive q-mt-md"
|
class="text-positive q-mt-md"
|
||||||
>
|
>
|
||||||
{{ successMessage }}
|
{{ successMessage }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="errorMessage"
|
v-if="errorMessage"
|
||||||
class="text-negative q-mt-md"
|
class="text-negative q-mt-md"
|
||||||
>
|
>
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-actions align="center">
|
<q-card-actions align="center">
|
||||||
<!-- Hide login link if already logged in based on store state -->
|
<!-- Hide login link if already logged in based on store state -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="!isLoggedIn"
|
v-if="!isLoggedIn"
|
||||||
flat
|
flat
|
||||||
label="Already have an account? Login"
|
label="Already have an account? Login"
|
||||||
to="/login"
|
to="/login"
|
||||||
/>
|
/>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
import { ref, onMounted, computed } from 'vue'; // Import computed
|
import { ref, onMounted, computed } from 'vue'; // Import computed
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { startRegistration } from '@simplewebauthn/browser';
|
import { startRegistration } from '@simplewebauthn/browser';
|
||||||
import axios from 'axios';
|
import axios from 'boot/axios';
|
||||||
import { useAuthStore } from 'stores/auth'; // Import the auth store
|
import { useAuthStore } from 'stores/auth'; // Import the auth store
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
@ -75,31 +75,31 @@ const isLoggedIn = computed(() => authStore.isAuthenticated);
|
||||||
const username = ref(''); // Local ref for username input
|
const username = ref(''); // Local ref for username input
|
||||||
|
|
||||||
// Check auth status on component mount using the store action
|
// Check auth status on component mount using the store action
|
||||||
onMounted(async() =>
|
onMounted(async() =>
|
||||||
{
|
{
|
||||||
if (!authStore.isAuthenticated)
|
if (!authStore.isAuthenticated)
|
||||||
{
|
{
|
||||||
await authStore.checkAuthStatus();
|
await authStore.checkAuthStatus();
|
||||||
if (authStore.error)
|
if (authStore.error)
|
||||||
{
|
{
|
||||||
errorMessage.value = authStore.error;
|
errorMessage.value = authStore.error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoggedIn.value)
|
if (!isLoggedIn.value)
|
||||||
{
|
{
|
||||||
username.value = ''; // Clear username if not logged in
|
username.value = ''; // Clear username if not logged in
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
username.value = authStore.user?.username || ''; // Use username from store if logged in
|
username.value = authStore.user?.username || ''; // Use username from store if logged in
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleRegister()
|
async function handleRegister()
|
||||||
{
|
{
|
||||||
const currentUsername = isLoggedIn.value ? authStore.user?.username : username.value;
|
const currentUsername = isLoggedIn.value ? authStore.user?.username : username.value;
|
||||||
if (!currentUsername)
|
if (!currentUsername)
|
||||||
{
|
{
|
||||||
errorMessage.value = 'Username is missing.';
|
errorMessage.value = 'Username is missing.';
|
||||||
return;
|
return;
|
||||||
|
@ -108,10 +108,10 @@ async function handleRegister()
|
||||||
errorMessage.value = '';
|
errorMessage.value = '';
|
||||||
successMessage.value = '';
|
successMessage.value = '';
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 1. Get options from server
|
// 1. Get options from server
|
||||||
const optionsRes = await axios.post('/auth/generate-registration-options', {
|
const optionsRes = await axios.post('/api/auth/generate-registration-options', {
|
||||||
username: currentUsername, // Use username from store
|
username: currentUsername, // Use username from store
|
||||||
});
|
});
|
||||||
const options = optionsRes.data;
|
const options = optionsRes.data;
|
||||||
|
@ -120,58 +120,58 @@ async function handleRegister()
|
||||||
const regResp = await startRegistration(options);
|
const regResp = await startRegistration(options);
|
||||||
|
|
||||||
// 3. Send response to server for verification
|
// 3. Send response to server for verification
|
||||||
const verificationRes = await axios.post('/auth/verify-registration', {
|
const verificationRes = await axios.post('/api/auth/verify-registration', {
|
||||||
registrationResponse: regResp,
|
registrationResponse: regResp,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (verificationRes.data.verified)
|
if (verificationRes.data.verified)
|
||||||
{
|
{
|
||||||
// Adjust success message based on login state
|
// Adjust success message based on login state
|
||||||
successMessage.value = isLoggedIn.value
|
successMessage.value = isLoggedIn.value
|
||||||
? 'New passkey registered successfully!'
|
? 'New passkey registered successfully!'
|
||||||
: 'Registration successful! Redirecting to login...';
|
: 'Registration successful! Redirecting to login...';
|
||||||
if (!isLoggedIn.value)
|
if (!isLoggedIn.value)
|
||||||
{
|
{
|
||||||
// Redirect to login page only if they weren't logged in
|
// Redirect to login page only if they weren't logged in
|
||||||
setTimeout(() =>
|
setTimeout(() =>
|
||||||
{
|
{
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Maybe redirect to a profile page or dashboard if already logged in
|
// Maybe redirect to a profile page or dashboard if already logged in
|
||||||
// setTimeout(() => { router.push('/dashboard'); }, 2000);
|
// setTimeout(() => { router.push('/dashboard'); }, 2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
errorMessage.value = 'Registration failed.';
|
errorMessage.value = 'Registration failed.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Registration error:', error);
|
console.error('Registration error:', error);
|
||||||
const message = error.response?.data?.error || error.message || 'An unknown error occurred during registration.';
|
const message = error.response?.data?.error || error.message || 'An unknown error occurred during registration.';
|
||||||
// Handle specific simplewebauthn errors
|
// Handle specific simplewebauthn errors
|
||||||
if (error.name === 'InvalidStateError')
|
if (error.name === 'InvalidStateError')
|
||||||
{
|
{
|
||||||
errorMessage.value = 'Authenticator already registered. Try logging in instead.';
|
errorMessage.value = 'Authenticator already registered. Try logging in instead.';
|
||||||
}
|
}
|
||||||
else if (error.name === 'NotAllowedError')
|
else if (error.name === 'NotAllowedError')
|
||||||
{
|
{
|
||||||
errorMessage.value = 'Registration ceremony was cancelled or timed out.';
|
errorMessage.value = 'Registration ceremony was cancelled or timed out.';
|
||||||
}
|
}
|
||||||
else if (error.response?.status === 409)
|
else if (error.response?.status === 409)
|
||||||
{
|
{
|
||||||
errorMessage.value = 'This passkey seems to be registered already.';
|
errorMessage.value = 'This passkey seems to be registered already.';
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
errorMessage.value = `Registration failed: ${message}`;
|
errorMessage.value = `Registration failed: ${message}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<q-page padding>
|
<q-page padding>
|
||||||
<div
|
<div
|
||||||
class="q-gutter-md"
|
class="q-gutter-md"
|
||||||
style="max-width: 800px; margin: auto;"
|
style="max-width: 800px; margin: auto;"
|
||||||
>
|
>
|
||||||
<h5 class="q-mt-none q-mb-md">
|
<h5 class="q-mt-none q-mb-md">
|
||||||
Settings
|
Settings
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
<q-card
|
<q-card
|
||||||
flat
|
flat
|
||||||
bordered
|
bordered
|
||||||
>
|
>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-h6">
|
<div class="text-h6">
|
||||||
Mantis Summary Prompt
|
Mantis Summary Prompt
|
||||||
</div>
|
</div>
|
||||||
<div class="text-caption text-grey q-mb-sm">
|
<div class="text-caption text-grey q-mb-sm">
|
||||||
Edit the prompt used to generate Mantis summaries. Use $DATE and $MANTIS_TICKETS as placeholders.
|
Edit the prompt used to generate Mantis summaries. Use $DATE and $MANTIS_TICKETS as placeholders.
|
||||||
|
@ -40,13 +40,13 @@
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
<q-card
|
<q-card
|
||||||
flat
|
flat
|
||||||
bordered
|
bordered
|
||||||
>
|
>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-h6">
|
<div class="text-h6">
|
||||||
Email Summary Prompt
|
Email Summary Prompt
|
||||||
</div>
|
</div>
|
||||||
<div class="text-caption text-grey q-mb-sm">
|
<div class="text-caption text-grey q-mb-sm">
|
||||||
Edit the prompt used to generate Email summaries. Use $EMAIL_DATA as a placeholder for the JSON email array.
|
Edit the prompt used to generate Email summaries. Use $EMAIL_DATA as a placeholder for the JSON email array.
|
||||||
|
@ -70,7 +70,7 @@
|
||||||
:disable="!emailPrompt || loadingEmailPrompt"
|
:disable="!emailPrompt || loadingEmailPrompt"
|
||||||
/>
|
/>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
@ -78,7 +78,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import axios from 'axios';
|
import axios from 'boot/axios';
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
@ -86,15 +86,15 @@ const mantisPrompt = ref('');
|
||||||
const loadingPrompt = ref(false);
|
const loadingPrompt = ref(false);
|
||||||
const savingPrompt = ref(false);
|
const savingPrompt = ref(false);
|
||||||
|
|
||||||
const fetchMantisPrompt = async() =>
|
const fetchMantisPrompt = async() =>
|
||||||
{
|
{
|
||||||
loadingPrompt.value = true;
|
loadingPrompt.value = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const response = await axios.get('/api/settings/mantisPrompt');
|
const response = await axios.get('/api/settings/mantisPrompt');
|
||||||
mantisPrompt.value = response.data.value || ''; // Handle case where setting might not exist yet
|
mantisPrompt.value = response.data.value || ''; // Handle case where setting might not exist yet
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Error fetching Mantis prompt:', error);
|
console.error('Error fetching Mantis prompt:', error);
|
||||||
$q.notify({
|
$q.notify({
|
||||||
|
@ -102,17 +102,17 @@ const fetchMantisPrompt = async() =>
|
||||||
message: 'Failed to load Mantis prompt setting.',
|
message: 'Failed to load Mantis prompt setting.',
|
||||||
icon: 'report_problem'
|
icon: 'report_problem'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
loadingPrompt.value = false;
|
loadingPrompt.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveMantisPrompt = async() =>
|
const saveMantisPrompt = async() =>
|
||||||
{
|
{
|
||||||
savingPrompt.value = true;
|
savingPrompt.value = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await axios.put('/api/settings/mantisPrompt', { value: mantisPrompt.value });
|
await axios.put('/api/settings/mantisPrompt', { value: mantisPrompt.value });
|
||||||
$q.notify({
|
$q.notify({
|
||||||
|
@ -120,8 +120,8 @@ const saveMantisPrompt = async() =>
|
||||||
message: 'Mantis prompt updated successfully.',
|
message: 'Mantis prompt updated successfully.',
|
||||||
icon: 'check_circle'
|
icon: 'check_circle'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Error saving Mantis prompt:', error);
|
console.error('Error saving Mantis prompt:', error);
|
||||||
$q.notify({
|
$q.notify({
|
||||||
|
@ -129,8 +129,8 @@ const saveMantisPrompt = async() =>
|
||||||
message: 'Failed to save Mantis prompt setting.',
|
message: 'Failed to save Mantis prompt setting.',
|
||||||
icon: 'report_problem'
|
icon: 'report_problem'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
savingPrompt.value = false;
|
savingPrompt.value = false;
|
||||||
}
|
}
|
||||||
|
@ -140,15 +140,15 @@ const emailPrompt = ref('');
|
||||||
const loadingEmailPrompt = ref(false);
|
const loadingEmailPrompt = ref(false);
|
||||||
const savingEmailPrompt = ref(false);
|
const savingEmailPrompt = ref(false);
|
||||||
|
|
||||||
const fetchEmailPrompt = async() =>
|
const fetchEmailPrompt = async() =>
|
||||||
{
|
{
|
||||||
loadingEmailPrompt.value = true;
|
loadingEmailPrompt.value = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const response = await axios.get('/api/settings/emailPrompt');
|
const response = await axios.get('/api/settings/emailPrompt');
|
||||||
emailPrompt.value = response.data.value || ''; // Handle case where setting might not exist yet
|
emailPrompt.value = response.data.value || ''; // Handle case where setting might not exist yet
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Error fetching Email prompt:', error);
|
console.error('Error fetching Email prompt:', error);
|
||||||
$q.notify({
|
$q.notify({
|
||||||
|
@ -156,17 +156,17 @@ const fetchEmailPrompt = async() =>
|
||||||
message: 'Failed to load Email prompt setting.',
|
message: 'Failed to load Email prompt setting.',
|
||||||
icon: 'report_problem'
|
icon: 'report_problem'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
loadingEmailPrompt.value = false;
|
loadingEmailPrompt.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveEmailPrompt = async() =>
|
const saveEmailPrompt = async() =>
|
||||||
{
|
{
|
||||||
savingEmailPrompt.value = true;
|
savingEmailPrompt.value = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await axios.put('/api/settings/emailPrompt', { value: emailPrompt.value });
|
await axios.put('/api/settings/emailPrompt', { value: emailPrompt.value });
|
||||||
$q.notify({
|
$q.notify({
|
||||||
|
@ -174,8 +174,8 @@ const saveEmailPrompt = async() =>
|
||||||
message: 'Email prompt updated successfully.',
|
message: 'Email prompt updated successfully.',
|
||||||
icon: 'check_circle'
|
icon: 'check_circle'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
console.error('Error saving Email prompt:', error);
|
console.error('Error saving Email prompt:', error);
|
||||||
$q.notify({
|
$q.notify({
|
||||||
|
@ -183,14 +183,14 @@ const saveEmailPrompt = async() =>
|
||||||
message: 'Failed to save Email prompt setting.',
|
message: 'Failed to save Email prompt setting.',
|
||||||
icon: 'report_problem'
|
icon: 'report_problem'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
savingEmailPrompt.value = false;
|
savingEmailPrompt.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() =>
|
onMounted(() =>
|
||||||
{
|
{
|
||||||
fetchMantisPrompt();
|
fetchMantisPrompt();
|
||||||
fetchEmailPrompt();
|
fetchEmailPrompt();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import axios from 'axios';
|
import axios from 'boot/axios';
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
export const useAuthStore = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
@ -15,8 +15,10 @@ export const useAuthStore = defineStore('auth', {
|
||||||
this.error = null;
|
this.error = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const res = await axios.get('/auth/check-auth');
|
const res = await axios.get('/api/auth/status', {
|
||||||
if (res.data.isAuthenticated)
|
withCredentials: true, // Ensure cookies are sent with the request
|
||||||
|
});
|
||||||
|
if (res.data.status === 'authenticated')
|
||||||
{
|
{
|
||||||
this.isAuthenticated = true;
|
this.isAuthenticated = true;
|
||||||
this.user = res.data.user;
|
this.user = res.data.user;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref, computed, watch } from 'vue'; // Import watch
|
import { ref, computed, watch } from 'vue'; // Import watch
|
||||||
import axios from 'axios';
|
import axios from 'boot/axios';
|
||||||
|
|
||||||
export const useChatStore = defineStore('chat', () =>
|
export const useChatStore = defineStore('chat', () =>
|
||||||
{
|
{
|
||||||
const isVisible = ref(false);
|
const isVisible = ref(false);
|
||||||
const currentThreadId = ref(null);
|
const currentThreadId = ref(null);
|
||||||
|
@ -19,13 +19,13 @@ export const useChatStore = defineStore('chat', () =>
|
||||||
// --- Actions ---
|
// --- Actions ---
|
||||||
|
|
||||||
// New action to create a thread if it doesn't exist
|
// New action to create a thread if it doesn't exist
|
||||||
async function createThreadIfNotExists()
|
async function createThreadIfNotExists()
|
||||||
{
|
{
|
||||||
if (currentThreadId.value) return; // Already have a thread
|
if (currentThreadId.value) return; // Already have a thread
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Call the endpoint without content to just create the thread
|
// Call the endpoint without content to just create the thread
|
||||||
const response = await axios.post('/api/chat/threads', {});
|
const response = await axios.post('/api/chat/threads', {});
|
||||||
|
@ -34,50 +34,50 @@ export const useChatStore = defineStore('chat', () =>
|
||||||
console.log('Created new chat thread:', currentThreadId.value);
|
console.log('Created new chat thread:', currentThreadId.value);
|
||||||
// Start polling now that we have a thread ID
|
// Start polling now that we have a thread ID
|
||||||
startPolling();
|
startPolling();
|
||||||
}
|
}
|
||||||
catch (err)
|
catch (err)
|
||||||
{
|
{
|
||||||
console.error('Error creating chat thread:', err);
|
console.error('Error creating chat thread:', err);
|
||||||
error.value = 'Failed to start chat.';
|
error.value = 'Failed to start chat.';
|
||||||
// Don't set isVisible to false, let the user see the error
|
// Don't set isVisible to false, let the user see the error
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleChat()
|
function toggleChat()
|
||||||
{
|
{
|
||||||
isVisible.value = !isVisible.value;
|
isVisible.value = !isVisible.value;
|
||||||
|
|
||||||
if (isVisible.value)
|
if (isVisible.value)
|
||||||
{
|
{
|
||||||
if (!currentThreadId.value)
|
if (!currentThreadId.value)
|
||||||
{
|
{
|
||||||
// If opening and no thread exists, create one
|
// If opening and no thread exists, create one
|
||||||
createThreadIfNotExists();
|
createThreadIfNotExists();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// If opening and thread exists, fetch messages if empty and start polling
|
// If opening and thread exists, fetch messages if empty and start polling
|
||||||
if (messages.value.length === 0)
|
if (messages.value.length === 0)
|
||||||
{
|
{
|
||||||
fetchMessages();
|
fetchMessages();
|
||||||
}
|
}
|
||||||
startPolling();
|
startPolling();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// If closing, stop polling
|
// If closing, stop polling
|
||||||
stopPolling();
|
stopPolling();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchMessages()
|
async function fetchMessages()
|
||||||
{
|
{
|
||||||
if (!currentThreadId.value)
|
if (!currentThreadId.value)
|
||||||
{
|
{
|
||||||
console.log('No active thread to fetch messages for.');
|
console.log('No active thread to fetch messages for.');
|
||||||
// Don't try to fetch if no thread ID yet. createThreadIfNotExists handles the initial state.
|
// Don't try to fetch if no thread ID yet. createThreadIfNotExists handles the initial state.
|
||||||
|
@ -86,7 +86,7 @@ export const useChatStore = defineStore('chat', () =>
|
||||||
// Avoid setting isLoading if polling, maybe use a different flag? For now, keep it simple.
|
// Avoid setting isLoading if polling, maybe use a different flag? For now, keep it simple.
|
||||||
// isLoading.value = true; // Might cause flickering during polling
|
// isLoading.value = true; // Might cause flickering during polling
|
||||||
error.value = null; // Clear previous errors on fetch attempt
|
error.value = null; // Clear previous errors on fetch attempt
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const response = await axios.get(`/api/chat/threads/${currentThreadId.value}/messages`);
|
const response = await axios.get(`/api/chat/threads/${currentThreadId.value}/messages`);
|
||||||
const newMessages = response.data.map(msg => ({
|
const newMessages = response.data.map(msg => ({
|
||||||
|
@ -97,28 +97,28 @@ export const useChatStore = defineStore('chat', () =>
|
||||||
})).sort((a, b) => a.createdAt - b.createdAt);
|
})).sort((a, b) => a.createdAt - b.createdAt);
|
||||||
|
|
||||||
// Only update if messages have actually changed to prevent unnecessary re-renders
|
// Only update if messages have actually changed to prevent unnecessary re-renders
|
||||||
if (JSON.stringify(messages.value) !== JSON.stringify(newMessages))
|
if (JSON.stringify(messages.value) !== JSON.stringify(newMessages))
|
||||||
{
|
{
|
||||||
messages.value = newMessages;
|
messages.value = newMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (err)
|
catch (err)
|
||||||
{
|
{
|
||||||
console.error('Error fetching messages:', err);
|
console.error('Error fetching messages:', err);
|
||||||
error.value = 'Failed to load messages.';
|
error.value = 'Failed to load messages.';
|
||||||
// Don't clear messages on polling error, keep the last known state
|
// Don't clear messages on polling error, keep the last known state
|
||||||
// messages.value = [];
|
// messages.value = [];
|
||||||
stopPolling(); // Stop polling if there's an error fetching
|
stopPolling(); // Stop polling if there's an error fetching
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// isLoading.value = false;
|
// isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to start polling
|
// Function to start polling
|
||||||
function startPolling()
|
function startPolling()
|
||||||
{
|
{
|
||||||
if (pollingIntervalId.value) return; // Already polling
|
if (pollingIntervalId.value) return; // Already polling
|
||||||
if (!currentThreadId.value) return; // No thread to poll for
|
if (!currentThreadId.value) return; // No thread to poll for
|
||||||
|
@ -128,9 +128,9 @@ export const useChatStore = defineStore('chat', () =>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to stop polling
|
// Function to stop polling
|
||||||
function stopPolling()
|
function stopPolling()
|
||||||
{
|
{
|
||||||
if (pollingIntervalId.value)
|
if (pollingIntervalId.value)
|
||||||
{
|
{
|
||||||
console.log('Stopping chat polling.');
|
console.log('Stopping chat polling.');
|
||||||
clearInterval(pollingIntervalId.value);
|
clearInterval(pollingIntervalId.value);
|
||||||
|
@ -139,10 +139,10 @@ export const useChatStore = defineStore('chat', () =>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function sendMessage(content)
|
async function sendMessage(content)
|
||||||
{
|
{
|
||||||
if (!content.trim()) return;
|
if (!content.trim()) return;
|
||||||
if (!currentThreadId.value)
|
if (!currentThreadId.value)
|
||||||
{
|
{
|
||||||
error.value = 'Cannot send message: No active chat thread.';
|
error.value = 'Cannot send message: No active chat thread.';
|
||||||
console.error('Attempted to send message without a thread ID.');
|
console.error('Attempted to send message without a thread ID.');
|
||||||
|
@ -165,7 +165,7 @@ export const useChatStore = defineStore('chat', () =>
|
||||||
isLoading.value = true; // Indicate activity
|
isLoading.value = true; // Indicate activity
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const payload = { content: userMessage.content };
|
const payload = { content: userMessage.content };
|
||||||
// Always post to the existing thread once it's created
|
// Always post to the existing thread once it's created
|
||||||
|
@ -180,8 +180,8 @@ export const useChatStore = defineStore('chat', () =>
|
||||||
// Immediately fetch messages after sending to get the updated list
|
// Immediately fetch messages after sending to get the updated list
|
||||||
await fetchMessages();
|
await fetchMessages();
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (err)
|
catch (err)
|
||||||
{
|
{
|
||||||
console.error('Error sending message:', err);
|
console.error('Error sending message:', err);
|
||||||
error.value = 'Failed to send message.';
|
error.value = 'Failed to send message.';
|
||||||
|
@ -190,8 +190,8 @@ export const useChatStore = defineStore('chat', () =>
|
||||||
// Optionally add an error message to the chat
|
// Optionally add an error message to the chat
|
||||||
// Ensure the object is correctly formatted
|
// Ensure the object is correctly formatted
|
||||||
messages.value.push({ sender: 'bot', content: "Sorry, I couldn't send that message.", createdAt: new Date() });
|
messages.value.push({ sender: 'bot', content: "Sorry, I couldn't send that message.", createdAt: new Date() });
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
// Restart polling after sending attempt is complete
|
// Restart polling after sending attempt is complete
|
||||||
|
@ -200,7 +200,7 @@ export const useChatStore = defineStore('chat', () =>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call this when the user logs out or the app closes if you want to clear state
|
// Call this when the user logs out or the app closes if you want to clear state
|
||||||
function resetChat()
|
function resetChat()
|
||||||
{
|
{
|
||||||
stopPolling(); // Ensure polling stops on reset
|
stopPolling(); // Ensure polling stops on reset
|
||||||
isVisible.value = false;
|
isVisible.value = false;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue