diff --git a/README.md b/README.md
index 7762104..1c1584b 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ docker compose up -d
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.
@@ -53,6 +53,4 @@ To create a production build of the application, run:
```bash
pnpm run build
-```
-
-This command compiles and optimizes the application for deployment. The output files are usually placed in the `dist` directory.
\ No newline at end of file
+```
\ No newline at end of file
diff --git a/package.json b/package.json
index 1233ffe..ad6070b 100644
--- a/package.json
+++ b/package.json
@@ -8,8 +8,8 @@
"private": true,
"scripts": {
"test": "echo \"No test specified\" && exit 0",
- "dev": "pnpm prisma migrate dev && quasar dev -m ssr",
- "build": "quasar build -m ssr",
+ "dev": "pnpm i && pnpm prisma migrate dev && concurrently \"quasar dev -m spa\" \"nodemon src-server/server.js\"",
+ "build": "quasar build -m spa",
"postinstall": "quasar prepare"
},
"dependencies": {
@@ -43,9 +43,11 @@
"@types/uuid": "^10.0.0",
"@vue/eslint-config-prettier": "^10.2.0",
"autoprefixer": "^10.4.2",
+ "concurrently": "^9.1.2",
"eslint": "^9.25.1",
"eslint-plugin-vue": "^10.0.0",
"globals": "^16.0.0",
+ "nodemon": "^3.1.10",
"postcss": "^8.4.14",
"prettier": "^3.5.3",
"prisma": "^6.6.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 66b1fcb..9c0ac2a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -93,6 +93,9 @@ importers:
autoprefixer:
specifier: ^10.4.2
version: 10.4.21(postcss@8.5.3)
+ concurrently:
+ specifier: ^9.1.2
+ version: 9.1.2
eslint:
specifier: ^9.25.1
version: 9.25.1
@@ -102,6 +105,9 @@ importers:
globals:
specifier: ^16.0.0
version: 16.0.0
+ nodemon:
+ specifier: ^3.1.10
+ version: 3.1.10
postcss:
specifier: ^8.4.14
version: 8.5.3
@@ -1065,6 +1071,11 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ concurrently@9.1.2:
+ resolution: {integrity: sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
confbox@0.1.8:
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
@@ -1590,6 +1601,10 @@ packages:
resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==}
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:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
@@ -1647,6 +1662,9 @@ packages:
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+ ignore-by-default@1.0.1:
+ resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
+
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -2008,6 +2026,11 @@ packages:
resolution: {integrity: sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==}
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:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
@@ -2215,6 +2238,9 @@ packages:
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+ pstree.remy@1.1.8:
+ resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
+
pump@3.0.2:
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
@@ -2537,6 +2563,10 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
+ shell-quote@1.8.2:
+ resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==}
+ engines: {node: '>= 0.4'}
+
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
@@ -2566,6 +2596,10 @@ packages:
simple-get@4.0.1:
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:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -2630,6 +2664,10 @@ packages:
resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
engines: {node: '>=16'}
+ supports-color@5.5.0:
+ resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
+ engines: {node: '>=4'}
+
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -2694,9 +2732,17 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
+ touch@3.1.1:
+ resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
+ hasBin: true
+
tr46@0.0.3:
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:
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
engines: {node: '>=18.12'}
@@ -2748,6 +2794,9 @@ packages:
resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==}
engines: {node: '>= 0.8'}
+ undefsafe@2.0.5:
+ resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
+
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@@ -3102,7 +3151,7 @@ snapshots:
'@eslint/config-array@0.20.0':
dependencies:
'@eslint/object-schema': 2.1.6
- debug: 4.4.0
+ debug: 4.4.0(supports-color@5.5.0)
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@@ -3116,7 +3165,7 @@ snapshots:
'@eslint/eslintrc@3.3.1':
dependencies:
ajv: 6.12.6
- debug: 4.4.0
+ debug: 4.4.0(supports-color@5.5.0)
espree: 10.3.0
globals: 14.0.0
ignore: 5.3.2
@@ -3575,7 +3624,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 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
is-glob: 4.0.3
minimatch: 9.0.5
@@ -4008,6 +4057,16 @@ snapshots:
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: {}
content-disposition@0.5.4:
@@ -4055,9 +4114,11 @@ snapshots:
dependencies:
ms: 2.0.0
- debug@4.4.0:
+ debug@4.4.0(supports-color@5.5.0):
dependencies:
ms: 2.1.3
+ optionalDependencies:
+ supports-color: 5.5.0
decompress-response@6.0.0:
dependencies:
@@ -4201,7 +4262,7 @@ snapshots:
esbuild-register@3.6.0(esbuild@0.25.3):
dependencies:
- debug: 4.4.0
+ debug: 4.4.0(supports-color@5.5.0)
esbuild: 0.25.3
transitivePeerDependencies:
- supports-color
@@ -4292,7 +4353,7 @@ snapshots:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
- debug: 4.4.0
+ debug: 4.4.0(supports-color@5.5.0)
escape-string-regexp: 4.0.0
eslint-scope: 8.3.0
eslint-visitor-keys: 4.2.0
@@ -4597,6 +4658,8 @@ snapshots:
- encoding
- supports-color
+ has-flag@3.0.0: {}
+
has-flag@4.0.0: {}
has-property-descriptors@1.0.2:
@@ -4653,7 +4716,7 @@ snapshots:
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.3
- debug: 4.4.0
+ debug: 4.4.0(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@@ -4667,6 +4730,8 @@ snapshots:
ieee754@1.2.1: {}
+ ignore-by-default@1.0.1: {}
+
ignore@5.3.2: {}
immutable@5.1.1: {}
@@ -4990,6 +5055,19 @@ snapshots:
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-range@0.1.2: {}
@@ -5207,6 +5285,8 @@ snapshots:
proxy-from-env@1.1.0: {}
+ pstree.remy@1.1.8: {}
+
pump@3.0.2:
dependencies:
end-of-stream: 1.4.4
@@ -5533,6 +5613,8 @@ snapshots:
shebang-regex@3.0.0: {}
+ shell-quote@1.8.2: {}
+
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
@@ -5573,6 +5655,10 @@ snapshots:
once: 1.4.0
simple-concat: 1.0.1
+ simple-update-notifier@2.0.0:
+ dependencies:
+ semver: 7.7.1
+
source-map-js@1.2.1: {}
source-map-support@0.5.21:
@@ -5633,6 +5719,10 @@ snapshots:
dependencies:
copy-anything: 3.0.5
+ supports-color@5.5.0:
+ dependencies:
+ has-flag: 3.0.0
+
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@@ -5705,8 +5795,12 @@ snapshots:
toidentifier@1.0.1: {}
+ touch@3.1.1: {}
+
tr46@0.0.3: {}
+ tree-kill@1.2.2: {}
+
ts-api-utils@2.1.0(typescript@5.8.3):
dependencies:
typescript: 5.8.3
@@ -5744,6 +5838,8 @@ snapshots:
dependencies:
random-bytes: 1.0.0
+ undefsafe@2.0.5: {}
+
undici-types@6.21.0: {}
unicode-properties@1.4.1:
@@ -5825,7 +5921,7 @@ snapshots:
vue-eslint-parser@10.1.3(eslint@9.25.1):
dependencies:
- debug: 4.4.0
+ debug: 4.4.0(supports-color@5.5.0)
eslint: 9.25.1
eslint-scope: 8.3.0
eslint-visitor-keys: 4.2.0
diff --git a/quasar.config.js b/quasar.config.js
index c3988e1..22e6777 100644
--- a/quasar.config.js
+++ b/quasar.config.js
@@ -76,7 +76,15 @@ export default defineConfig((/* ctx */) =>
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver
devServer: {
// 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
diff --git a/src-ssr/database.js b/src-server/database.js
similarity index 100%
rename from src-ssr/database.js
rename to src-server/database.js
diff --git a/src-ssr/middlewares/authMiddleware.js b/src-server/middlewares/authMiddleware.js
similarity index 100%
rename from src-ssr/middlewares/authMiddleware.js
rename to src-server/middlewares/authMiddleware.js
diff --git a/src-ssr/routes/api.js b/src-server/routes/api.js
similarity index 100%
rename from src-ssr/routes/api.js
rename to src-server/routes/api.js
diff --git a/src-ssr/routes/auth.js b/src-server/routes/auth.js
similarity index 94%
rename from src-ssr/routes/auth.js
rename to src-server/routes/auth.js
index 77ef853..2087afb 100644
--- a/src-ssr/routes/auth.js
+++ b/src-server/routes/auth.js
@@ -13,7 +13,7 @@ import { rpID, rpName, origin, challengeStore } from '../server.js'; // Import R
const router = express.Router();
// Helper function to get user authenticators
-async function getUserAuthenticators(userId)
+async function getUserAuthenticators(userId)
{
return prisma.authenticator.findMany({
where: { userId },
@@ -27,40 +27,40 @@ async function getUserAuthenticators(userId)
}
// Helper function to get a user by username
-async function getUserByUsername(username)
+async function getUserByUsername(username)
{
return prisma.user.findUnique({ where: { username } });
}
// Helper function to get a user by ID
-async function getUserById(id)
+async function getUserById(id)
{
return prisma.user.findUnique({ where: { id } });
}
// Helper function to get an authenticator by credential ID
-async function getAuthenticatorByCredentialID(credentialID)
+async function getAuthenticatorByCredentialID(credentialID)
{
return prisma.authenticator.findUnique({ where: { credentialID } });
}
// Generate Registration Options
-router.post('/generate-registration-options', async(req, res) =>
+router.post('/generate-registration-options', async(req, res) =>
{
const { username } = req.body;
- if (!username)
+ if (!username)
{
return res.status(400).json({ error: 'Username is required' });
}
- try
+ try
{
let user = await getUserByUsername(username);
// If user doesn't exist, create one
- if (!user)
+ if (!user)
{
user = await prisma.user.create({
data: { username },
@@ -69,10 +69,10 @@ router.post('/generate-registration-options', async(req, res) =>
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
- 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.' });
}
@@ -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
res.json(options);
- }
- catch (error)
+ }
+ catch (error)
{
console.error('Registration options error:', error);
res.status(500).json({ error: 'Failed to generate registration options' });
@@ -112,27 +112,27 @@ router.post('/generate-registration-options', async(req, res) =>
});
// Verify Registration
-router.post('/verify-registration', async(req, res) =>
+router.post('/verify-registration', async(req, res) =>
{
const { registrationResponse } = req.body;
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.' });
}
const expectedChallenge = challengeStore.get(userId);
- if (!expectedChallenge)
+ if (!expectedChallenge)
{
return res.status(400).json({ error: 'Challenge not found or expired' });
}
- try
+ try
{
const user = await getUserById(userId);
- if (!user)
+ if (!user)
{
return res.status(404).json({ error: 'User not found' });
}
@@ -149,7 +149,7 @@ router.post('/verify-registration', async(req, res) =>
console.log(verification);
- if (verified && registrationInfo)
+ if (verified && 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
const existingAuthenticator = await getAuthenticatorByCredentialID(isoBase64URL.fromBuffer(credentialID));
- if (existingAuthenticator)
+ if (existingAuthenticator)
{
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;
res.json({ verified: true });
- }
- else
+ }
+ else
{
res.status(400).json({ error: 'Registration verification failed' });
}
- }
- catch (error)
+ }
+ catch (error)
{
console.error('Registration verification error:', error);
challengeStore.delete(userId); // Clean up challenge on error
@@ -203,24 +203,24 @@ router.post('/verify-registration', async(req, res) =>
});
// Generate Authentication Options
-router.post('/generate-authentication-options', async(req, res) =>
+router.post('/generate-authentication-options', async(req, res) =>
{
const { username } = req.body;
- try
+ try
{
let user;
- if (username)
+ if (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)
user = await getUserById(req.session.loggedInUserId);
}
- if (!user)
+ if (!user)
{
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
res.json(options);
- }
- catch (error)
+ }
+ catch (error)
{
console.error('Authentication options error:', error);
res.status(500).json({ error: 'Failed to generate authentication options' });
@@ -256,40 +256,40 @@ router.post('/generate-authentication-options', async(req, res) =>
});
// Verify Authentication
-router.post('/verify-authentication', async(req, res) =>
+router.post('/verify-authentication', async(req, res) =>
{
const { authenticationResponse } = req.body;
const challengeUserId = req.session.challengeUserId; // Get user ID associated with the challenge
- if (!challengeUserId)
+ if (!challengeUserId)
{
return res.status(400).json({ error: 'Challenge session not found. Please try logging in again.' });
}
const expectedChallenge = challengeStore.get(challengeUserId);
- if (!expectedChallenge)
+ if (!expectedChallenge)
{
return res.status(400).json({ error: 'Challenge not found or expired' });
}
- try
+ try
{
const user = await getUserById(challengeUserId);
- if (!user)
+ if (!user)
{
return res.status(404).json({ error: 'User associated with challenge not found' });
}
const authenticator = await getAuthenticatorByCredentialID(authenticationResponse.id);
- if (!authenticator)
+ if (!authenticator)
{
return res.status(404).json({ error: 'Authenticator not found' });
}
// Ensure the authenticator belongs to the user attempting to log in
- if (authenticator.userId !== user.id)
+ if (authenticator.userId !== user.id)
{
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;
- if (verified)
+ if (verified)
{
// Update the authenticator counter
await prisma.authenticator.update({
@@ -326,13 +326,13 @@ router.post('/verify-authentication', async(req, res) =>
req.session.loggedInUserId = user.id;
res.json({ verified: true, user: { id: user.id, username: user.username } });
- }
- else
+ }
+ else
{
res.status(400).json({ error: 'Authentication verification failed' });
}
- }
- catch (error)
+ }
+ catch (error)
{
console.error('Authentication verification error:', 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
-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' });
}
- try
+ try
{
const userId = req.session.loggedInUserId;
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
res.json(authenticators);
- }
- catch (error)
+ }
+ catch (error)
{
console.error('Error fetching passkeys:', error);
res.status(500).json({ error: 'Failed to fetch passkeys' });
@@ -372,21 +372,21 @@ router.get('/passkeys', async(req, res) =>
});
// 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' });
}
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' });
}
- try
+ try
{
const userId = req.session.loggedInUserId;
@@ -395,13 +395,13 @@ router.delete('/passkeys/:credentialID', async(req, res) =>
where: { credentialID: credentialID }, // Use the Base64URL string directly
});
- if (!authenticator)
+ if (!authenticator)
{
return res.status(404).json({ error: 'Passkey not found' });
}
// Security check: Ensure the passkey belongs to the user trying to delete it
- if (authenticator.userId !== userId)
+ if (authenticator.userId !== userId)
{
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' });
- }
- catch (error)
+ }
+ catch (error)
{
console.error('Error deleting passkey:', error);
// Handle potential Prisma errors, e.g., record not found if deleted between check and delete
- if (error.code === 'P2025')
+ if (error.code === 'P2025')
{ // Prisma code for record not found on delete/update
return res.status(404).json({ error: 'Passkey not found' });
}
@@ -426,21 +426,28 @@ router.delete('/passkeys/:credentialID', async(req, res) =>
});
// 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' });
});
// 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);
return res.status(500).json({ error: 'Failed to logout' });
diff --git a/src-ssr/routes/chat.js b/src-server/routes/chat.js
similarity index 100%
rename from src-ssr/routes/chat.js
rename to src-server/routes/chat.js
diff --git a/src-server/server.js b/src-server/server.js
new file mode 100644
index 0000000..fe40e40
--- /dev/null
+++ b/src-server/server.js
@@ -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');
+});
\ No newline at end of file
diff --git a/src-ssr/services/mantisSummarizer.js b/src-server/services/mantisSummarizer.js
similarity index 100%
rename from src-ssr/services/mantisSummarizer.js
rename to src-server/services/mantisSummarizer.js
diff --git a/src-ssr/utils/gemini.js b/src-server/utils/gemini.js
similarity index 89%
rename from src-ssr/utils/gemini.js
rename to src-server/utils/gemini.js
index 20fd5ac..248ca62 100644
--- a/src-ssr/utils/gemini.js
+++ b/src-server/utils/gemini.js
@@ -9,6 +9,8 @@ export async function askGemini(content)
{
const GOOGLE_API_KEY = await getSetting('GEMINI_API_KEY');
+
+ console.log('Google API Key:', GOOGLE_API_KEY); // Debugging line to check the key
if (!GOOGLE_API_KEY)
{
@@ -62,18 +64,8 @@ export async function askGeminiChat(threadId, content)
messages = messages.slice(0, -1);
}
- const setting = await prisma.setting.findUnique({
- 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 GOOGLE_API_KEY = await getSetting('GEMINI_API_KEY');
+
const ai = GOOGLE_API_KEY ? new GoogleGenAI({
apiKey: GOOGLE_API_KEY,
}) : null;
diff --git a/src-ssr/utils/settings.js b/src-server/utils/settings.js
similarity index 100%
rename from src-ssr/utils/settings.js
rename to src-server/utils/settings.js
diff --git a/src-ssr/middlewares/render.js b/src-ssr/middlewares/render.js
deleted file mode 100644
index dedd3b5..0000000
--- a/src-ssr/middlewares/render.js
+++ /dev/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);
- }
- }
- });
- });
-});
diff --git a/src-ssr/server.js b/src-ssr/server.js
deleted file mode 100644
index 2ace7eb..0000000
--- a/src-ssr/server.js
+++ /dev/null
@@ -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 ``;
- }
-
- if (cssRE.test(file) === true)
- {
- return ``;
- }
-
- if (woffRE.test(file) === true)
- {
- return ``;
- }
-
- if (woff2RE.test(file) === true)
- {
- return ``;
- }
-
- if (gifRE.test(file) === true)
- {
- return ``;
- }
-
- if (jpgRE.test(file) === true)
- {
- return ``;
- }
-
- if (pngRE.test(file) === true)
- {
- return ``;
- }
-
- return '';
-});
diff --git a/src/App.vue b/src/App.vue
index b22f395..760a1d4 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -3,5 +3,13 @@
diff --git a/src/boot/axios.js b/src/boot/axios.js
new file mode 100644
index 0000000..0051fae
--- /dev/null
+++ b/src/boot/axios.js
@@ -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;
\ No newline at end of file
diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue
index 1956825..b6cc747 100644
--- a/src/layouts/MainLayout.vue
+++ b/src/layouts/MainLayout.vue
@@ -7,17 +7,17 @@
:model-value="true"
>
-
-
- StylePoint
+
+ StylePoint
@@ -31,9 +31,9 @@
:to="{ name: item.name }"
exact
>
-
{{ item.meta.title }}
@@ -42,8 +42,8 @@
{{ item.meta.title }}
-
- {{ item.meta.caption }}
+
+ {{ item.meta.caption }}
@@ -55,9 +55,9 @@
v-ripple
@click="logout"
>
-
Logout
@@ -67,7 +67,7 @@
Logout
-
+
@@ -76,10 +76,10 @@
-
-
Chat
-
-
-
-
{{ chatError }}
-
@@ -147,7 +147,7 @@