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 @@