diff --git a/.gitignore b/.gitignore index 641dc5d..c0855c5 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ dist-ssr *.sw? mongo +mysql + +.env \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 8fedd49..b2b7e22 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,10 +6,20 @@ services: ports: - "3000:3000" depends_on: - - mongodb + - mariadb + environment: + - DB_HOST=mariadb + - DB_USER=elliptical + - DB_PASS=yourpassword + - DB_NAME=elliptical - mongodb: + mariadb: restart: always - image: mongo + image: mariadb:latest + environment: + - MYSQL_ROOT_PASSWORD=rootpassword + - MYSQL_DATABASE=elliptical + - MYSQL_USER=elliptical + - MYSQL_PASSWORD=yourpassword volumes: - - ./mongo:/data/db + - ./mysql:/var/lib/mysql diff --git a/index.js b/index.js index 7f91755..ea88925 100644 --- a/index.js +++ b/index.js @@ -1,38 +1,11 @@ +import "dotenv/config" import { v4 as uuid } from "uuid" -import { build, createServer as createViteServer } from "vite" - import { createInterface } from "node:readline" -import path from "node:path" -import url from "node:url" - -import { io, app, server } from "./server/host.js" +import { io, server } from "./server/host.js" import { executeUserInput } from "./server/adminhandler.js" import { get, getroom } from "./server/functions.js" -import { rooms, adminpass, reports, initializeDB } from "./server/mongo.js" import { context } from "./server/context.js" - -// Initalize server stuff -const __dirname = url.fileURLToPath(new URL("./", import.meta.url)) - - -// frontend -if (process.argv.includes("--dev")) { - // if dev mode start vite server - const vite = await createViteServer({ - server: { middlewareMode: "html" }, - }) - app.use(vite.middlewares) - console.log("✅ Vite development server served with middleware") -} else { - // prod, serve from dist - console.log("🔁 Building for production...") - await build() - app.use(express.static("dist")) - app.use((req, res) => - res.sendFile(path.join(__dirname, "dist", "index.html")) - ) - console.log("✅ Production build served from dist") -} +import { pool } from "./server/db.js" // Handle socket.io events io.on("connection", async (socket) => { @@ -70,18 +43,9 @@ io.on("connection", async (socket) => { } else { const id = uuid() - await rooms.updateOne( - { - roomid, - }, - { - $push: { - messages: { - message, - msgid: id, - }, - }, - } + const [result] = await pool.execute( + "INSERT INTO messages (room_id, user_id, content, message_uuid) VALUES (?, ?, ?, ?)", + [roomid, socket.id, message, id] ) io.emit("message", { @@ -98,7 +62,10 @@ io.on("connection", async (socket) => { const messageIncludesBlockedTerm = context.BLOCKED.some((term) => room.title.replaceAll(" ", "").toLowerCase().includes(term) ) - const roomCount = await rooms.countDocuments({ private: false }) + const [rows] = await pool.execute( + "SELECT COUNT(*) as count FROM rooms WHERE is_private = 0" + ) + const roomCount = rows[0].count if (messageIncludesBlockedTerm) socket.emit("event", { @@ -124,17 +91,15 @@ io.on("connection", async (socket) => { else { const id = uuid() - await rooms.insertOne({ - title: room.title, - roomid: id, - private: room.private && !!room.code, - ...(room.private && room.code - ? { - code: room.code, - } - : {}), - messages: [], - }) + const [result] = await pool.execute( + "INSERT INTO rooms (room_uuid, name, is_private, access_code) VALUES (?, ?, ?, ?)", + [ + id, + room.title, + room.private && !!room.code ? 1 : 0, + room.code || null, + ] + ) if (room.private && room.code) socket.emit("room", { @@ -154,14 +119,20 @@ io.on("connection", async (socket) => { socket.on("join private", async (code) => { try { - const room = await rooms.findOne({ code }) - - room.id = room.roomid - - delete room._id - delete room.roomid + const [rows] = await pool.execute( + "SELECT * FROM rooms WHERE access_code = ?", + [code] + ) - socket.emit("room", room) + if (rows.length > 0) { + const room = rows[0] + socket.emit("room", { + title: room.name, + id: room.room_uuid, + private: true, + code: room.access_code, + }) + } } catch {} // Room does not exist }) @@ -185,20 +156,16 @@ io.on("connection", async (socket) => { socket.on("report msg", async (msg) => { try { // Check if report already exists - const existingReport = await reports.findOne({ - msgid: msg.msgid, - roomid: msg.roomid, - }) - - if (!existingReport) { - // Only insert if no existing report found - await reports.insertOne({ - msgid: msg.msgid, - roomid: msg.roomid, - message: msg.message, - timestamp: new Date(), - }) + const [existing] = await pool.execute( + "SELECT id FROM reports WHERE message_uuid = ? AND room_uuid = ?", + [msg.msgid, msg.roomid] + ) + if (existing.length === 0) { + await pool.execute( + "INSERT INTO reports (message_uuid, room_uuid, message_content, reported_at) VALUES (?, ?, ?, NOW())", + [msg.msgid, msg.roomid, msg.message] + ) console.log("📝 New report added:", msg) } @@ -215,18 +182,11 @@ io.on("connection", async (socket) => { else console.log("❌ Invalid admin password attempt: " + msg.adminpass) }) - socket.on("passchange", (msg) => { + socket.on("passchange", async (msg) => { if (msg.adminpass.includes(context.PASSWORD)) { - adminpass.updateOne( - { - id: "admin", - }, - { - $set: { - password: msg.newpass, - }, - } - ) + await pool.execute("UPDATE admin_settings SET password = ?", [ + msg.newpass, + ]) context.PASSWORD = msg.newpass socket.emit("event", { @@ -250,8 +210,6 @@ io.on("connection", async (socket) => { }) }) -await initializeDB() - // Start the server server.listen(3000, () => console.log("✅ Elliptical server running at http://localhost:3000") diff --git a/package-lock.json b/package-lock.json index 7833e26..cc72e17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,9 @@ "@vitejs/plugin-vue": "^5.2.1", "autoprefixer": "^10.4.20", "dompurify": "^3.2.4", + "dotenv": "^16.4.7", "express": "^4.21.2", - "mongodb": "^6.13.0", + "mysql2": "^3.12.0", "nodejs-base64": "^2.0.0", "postcss": "^8.5.2", "showdown": "^2.1.0", @@ -565,15 +566,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz", - "integrity": "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==", - "license": "MIT", - "dependencies": { - "sparse-bitfield": "^3.0.3" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -903,21 +895,6 @@ "license": "MIT", "optional": true }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", - "license": "MIT" - }, - "node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", - "license": "MIT", - "dependencies": { - "@types/webidl-conversions": "*" - } - }, "node_modules/@vitejs/plugin-vue": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz", @@ -1136,6 +1113,15 @@ "postcss": "^8.1.0" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1240,15 +1226,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bson": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.2.tgz", - "integrity": "sha512-5afhLTjqDSA3akH56E+/2J6kTDuSIlBxyXPdQslj9hcIgOUE378xdOfZvC/9q3LifJNI6KR/juZ+d0NRNYBwXg==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.20.1" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1654,6 +1631,15 @@ "ms": "2.0.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1694,6 +1680,18 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2133,6 +2131,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -2367,6 +2374,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2422,12 +2435,33 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", + "license": "Apache-2.0" + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/lru.min": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz", + "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -2455,12 +2489,6 @@ "node": ">= 0.6" } }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "license": "MIT" - }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -2558,68 +2586,44 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mongodb": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.13.0.tgz", - "integrity": "sha512-KeESYR5TEaFxOuwRqkOm3XOsMqCSkdeDMjaW5u2nuKfX7rqaofp7JQGoi7sVqQcNJTKuveNbzZtWMstb8ABP6Q==", - "license": "Apache-2.0", + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz", + "integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==", + "license": "MIT", "dependencies": { - "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.1", - "mongodb-connection-string-url": "^3.0.0" + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" }, "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } + "node": ">= 8.0" } }, - "node_modules/mongodb-connection-string-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", - "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", - "license": "Apache-2.0", + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^13.0.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -2631,6 +2635,27 @@ "thenify-all": "^1.0.0" } }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -2990,15 +3015,6 @@ "node": ">= 0.10" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -3259,6 +3275,11 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -3571,21 +3592,21 @@ "node": ">=0.10.0" } }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "license": "MIT", - "dependencies": { - "memory-pager": "^1.0.2" - } - }, "node_modules/spawn-command": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", "dev": true }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -3829,18 +3850,6 @@ "node": ">=0.6" } }, - "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -4051,28 +4060,6 @@ } } }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", - "license": "MIT", - "dependencies": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 3aedc12..beef6a1 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,9 @@ "@vitejs/plugin-vue": "^5.2.1", "autoprefixer": "^10.4.20", "dompurify": "^3.2.4", + "dotenv": "^16.4.7", "express": "^4.21.2", - "mongodb": "^6.13.0", + "mysql2": "^3.12.0", "nodejs-base64": "^2.0.0", "postcss": "^8.5.2", "showdown": "^2.1.0", diff --git a/server/adminhandler.js b/server/adminhandler.js index 6e1f2f4..2afc329 100644 --- a/server/adminhandler.js +++ b/server/adminhandler.js @@ -1,18 +1,18 @@ import { v4 as uuid } from "uuid" import { io } from "./host.js" import { getReports } from "./functions.js" -import { rooms } from "./mongo.js" import { context } from "./context.js" +import { pool } from "./db.js" export const executeUserInput = async (input, socket) => { try { const command = input.command - if (command.charAt(0) === "m") + if (command.charAt(0) === "m") { io.emit("event", { message: `Server: ${command.substring(2)}`, }) - else if (command == "lockall") { + } else if (command == "lockall") { io.emit("event", { message: "Chat has been locked", status: 1, @@ -29,30 +29,13 @@ export const executeUserInput = async (input, socket) => { context.LOCKED = false } else if (command == "refresh") io.emit("reload", "") else if (command == "purge") { - rooms.deleteMany({}) - + await pool.execute("DELETE FROM rooms WHERE is_private = 0") io.emit("purge") - } else if (command == "eval") { - console.log("🔁 Running eval...") - - eval(input + "()") - } else if (command.includes("opentab")) { - // Should probably remove this as it is a security risk - let message = command.substring(7) - - io.emit("opentab", message) } else if (command == "deletemsg") { - await rooms.updateOne( - { - roomid: input.roomid, - }, - { - $pull: { - messages: { - msgid: input.msgid, - }, - }, - } + console.log(input) + await pool.execute( + "DELETE FROM messages WHERE message_uuid = ? AND room_id = ?", + [input.msgid, input.roomid] ) io.to(input.roomid).emit("delete", { @@ -60,32 +43,22 @@ export const executeUserInput = async (input, socket) => { id: input.msgid, }) } else if (command == "deleteroom") { - await rooms.deleteOne({ roomid: input.roomid }) + await pool.execute("DELETE FROM rooms WHERE room_uuid = ?", [ + input.roomid, + ]) io.to("home").emit("delete", { type: "room", id: input.roomid, }) } else if (command == "highlight") { - // Highlight messages in a room if (!input.roomid) return if (input.message) { const id = uuid() - - await rooms.updateOne( - { - roomid: input.roomid, - }, - { - $push: { - messages: { - message: input.message, - msgid: id, - highlight: true, - }, - }, - } + await pool.execute( + "INSERT INTO messages (message_uuid, room_id, user_id, content, is_highlighted) VALUES (?, ?, ?, ?, true)", + [id, input.roomid, "system", input.message] ) io.to(input.roomid).emit("message", { @@ -94,29 +67,32 @@ export const executeUserInput = async (input, socket) => { highlight: true, }) } else { - await rooms.updateOne( - { roomid: input.roomid }, - { - $set: { - highlight: true, - }, - } + await pool.execute( + "UPDATE rooms SET is_highlighted = true WHERE room_uuid = ?", + [input.roomid] ) - const room = await rooms.findOne({ roomid: input.roomid }) + const [rows] = await pool.execute( + "SELECT room_uuid, name as title FROM rooms WHERE room_uuid = ?", + [input.roomid] + ) - io.to("home").emit("room", { - title: room.title, - id: room.roomid, - highlight: true, - update: true, - }) + if (rows.length > 0) { + io.to("home").emit("room", { + title: rows[0].title, + id: rows[0].room_uuid, + highlight: true, + update: true, + }) + } } - } else if(command == "joinreports") { + } else if (command == "joinreports") { socket.emit("joined", "reports") getReports(socket) - } else console.log("❌ An invalid command was provided:", command) + } else { + console.log("❌ An invalid command was provided:", command) + } } catch (error) { console.warn("❌ Error!", error) } -} \ No newline at end of file +} diff --git a/server/db.js b/server/db.js new file mode 100644 index 0000000..a25e9dd --- /dev/null +++ b/server/db.js @@ -0,0 +1,103 @@ +import mysql from "mysql2/promise" + +/** + * Ensures that the database and required tables are created if they do not exist. + * Connects to the MySQL database using the provided environment variables. + * Creates the database and tables if they do not exist. + * Logs the status of the operations. + * Closes the connection after the operations are complete. + * + * @returns {Promise} A promise that resolves when the operation is complete. + */ + +const db_config = { + host: process.env.DB_HOST || "localhost", + user: process.env.DB_USER || "root", + password: process.env.DB_PASS || "", + database: process.env.DB_NAME || "elliptical".replace(/[^a-zA-Z0-9_]/g, ""), +} + +export async function ensuredb() { + let connection + try { + connection = await mysql.createConnection({ + host: db_config.host, + user: db_config.user, + password: db_config.password, + }) + await connection.query("CREATE DATABASE IF NOT EXISTS ??", [ + db_config.database, + ]) + await connection.query("USE ??", [db_config.database]) + console.log("Database ensured.") + + // await connection.query(` + // CREATE TABLE IF NOT EXISTS accounts ( + // id INT AUTO_INCREMENT PRIMARY KEY, + // username VARCHAR(255) NOT NULL UNIQUE, + // hashed_password VARCHAR(255) NOT NULL + // ); + // `); + // console.log("Table 'accounts' ensured."); + // Create rooms table + + await connection.query(` + CREATE TABLE IF NOT EXISTS rooms ( + id INT AUTO_INCREMENT PRIMARY KEY, + room_uuid VARCHAR(36) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + is_private BOOLEAN DEFAULT FALSE, + is_highlighted BOOLEAN DEFAULT FALSE, + access_code VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `) + console.log("Table 'rooms' ensured.") + + // Create messages table + await connection.query(` + CREATE TABLE IF NOT EXISTS messages ( + id INT AUTO_INCREMENT PRIMARY KEY, + message_uuid VARCHAR(36) NOT NULL UNIQUE, + room_id VARCHAR(36) NOT NULL, + user_id VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + is_highlighted BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (room_id) REFERENCES rooms(room_uuid) ON DELETE CASCADE + ); + `); + console.log("Table 'messages' ensured.") + + await connection.query(` + CREATE TABLE IF NOT EXISTS reports ( + id INT AUTO_INCREMENT PRIMARY KEY, + message_uuid VARCHAR(36) NOT NULL, + room_uuid VARCHAR(36) NOT NULL, + message_content TEXT NOT NULL, + reported_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + + await connection.query(` + CREATE TABLE IF NOT EXISTS admin_settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + password VARCHAR(255) NOT NULL UNIQUE + ); + `); + } catch (err) { + console.error("Error ensuring database and table:", err) + } finally { + if (connection) await connection.end() + } +} + +export const pool = mysql.createPool({ + host: db_config.host, + user: db_config.user, + password: db_config.password, + database: db_config.database, + waitForConnections: true, + connectionLimit: 20, + queueLimit: 40, +}) diff --git a/server/functions.js b/server/functions.js index f5dbc5e..461f183 100644 --- a/server/functions.js +++ b/server/functions.js @@ -1,57 +1,80 @@ -import { adminpass, rooms, reports } from "./mongo.js" import { context } from "./context.js" +import { pool } from "./db.js" export const password = async () => { - const result = await adminpass.findOne({ id: "admin" }) - - if (result) return (context.PASSWORD = result.password) + try { + // Check if admin password exists + const [rows] = await pool.execute( + "SELECT password FROM admin_settings WHERE id = 1" + ) - adminpass.insertOne({ - id: "admin", - password: context.PASSWORD, - }) + if (rows.length > 0) { + context.PASSWORD = rows[0].password + } else { + // If no password exists, insert the default one from context + await pool.execute("INSERT INTO admin_settings (password) VALUES (?)", [ + context.PASSWORD, + ]) + } + } catch (error) { + console.warn("❌ Error managing admin password:", error) + } } export const getroom = async (socket) => { - const result = rooms.find({ - private: false, - }) - - for await (const room of result) { - if (room.data === "room") socket.emit("room", room) - else - socket.emit("room", { - highlight: room.highlight, - title: room.title, - id: room.roomid, - }) + const [rooms] = await pool.execute( + "SELECT room_uuid as id, name as title FROM rooms WHERE is_private = 0" + ) + + for (const room of rooms) { + socket.emit("room", { + title: room.title, + id: room.id, + }) } } export const get = async (socket, id) => { try { - const room = await rooms.findOne({ roomid: id }) - - if (!room || !room.messages) return + const [messages] = await pool.execute( + "SELECT message_uuid as id, content as message, is_highlighted FROM messages WHERE room_id = ? ORDER BY created_at ASC", + [id] + ) - for (const message of room.messages) { - socket.emit("message", message) + for (const message of messages) { + console.log(message) + socket.emit("message", { + msgid: message.id, + message: message.message, + highlight: message.is_highlighted, + }) } } catch (error) { - console.warn("❌ Error! " + error) + console.warn("❌ Error fetching messages:", error) } } export const getReports = async (socket) => { try { - const reportsCursor = reports.find({}) - - // Send each report to the client - for await (const report of reportsCursor) { - const message ={msgid: report.msgid, roomid: report.roomid, message: report.message, time: report.timestamp} - socket.emit("report", message) + const [reports] = await pool.execute( + `SELECT + message_uuid as msgid, + room_uuid as roomid, + message_content as message, + reported_at as time + FROM reports + ORDER BY reported_at DESC` + ) + + for (const report of reports) { + socket.emit("report", { + msgid: report.msgid, + roomid: report.roomid, + message: report.message, + time: report.time, + }) } } catch (error) { - console.warn("❌ Error! " + error) + console.warn("❌ Error fetching reports:", error) } -} \ No newline at end of file +} diff --git a/server/host.js b/server/host.js index a3ea183..be41b87 100644 --- a/server/host.js +++ b/server/host.js @@ -1,7 +1,37 @@ import http from "node:http" import express from "express" import { Server } from "socket.io" +import { build, createServer as createViteServer } from "vite" +import path from "node:path" +import url from "node:url" +import { ensuredb } from "./db.js" +import { password } from "./functions.js" -export const app = express() +await ensuredb() +await password() + +const app = express() export const server = http.createServer(app) export const io = new Server(server) + +// Initalize server stuff +const __dirname = url.fileURLToPath(new URL("./", import.meta.url)) + +// frontend +if (process.argv.includes("--dev")) { + // if dev mode start vite server + const vite = await createViteServer({ + server: { middlewareMode: "html" }, + }) + app.use(vite.middlewares) + console.log("✅ Vite development server served with middleware") +} else { + // prod, serve from dist + console.log("🔁 Building for production...") + await build() + app.use(express.static("dist")) + app.use((req, res) => + res.sendFile(path.join(__dirname, "..", "dist", "index.html")) + ) + console.log("✅ Production build served from dist") +} diff --git a/server/mongo.js b/server/mongo.js deleted file mode 100644 index e59ca1e..0000000 --- a/server/mongo.js +++ /dev/null @@ -1,25 +0,0 @@ -import { MongoClient } from "mongodb" -import { password } from "./functions.js" - -const client = new MongoClient( - process.argv[2] == "Docker" - ? "mongodb://mongodb:27017" - : "mongodb://127.0.0.1:27017" -) - -const db = client.db("elliptical") -export const rooms = db.collection("rooms") -export const adminpass = db.collection("admin-password") -export const reports = db.collection("reports") - -// Connect to the database -export async function initializeDB() { - try { - await client.connect() - console.log("✅ Connected to MongoDB") - // Import password function only after DB connection - await password() - } catch (error) { - console.error("❌ Error connecting to MongoDB", error) - } -} diff --git a/src/assets/code.js b/src/assets/code.js index 86d776f..998d7b4 100644 --- a/src/assets/code.js +++ b/src/assets/code.js @@ -72,9 +72,7 @@ export const joinReports = () => { export const gotomsg = (roomid, msgid) => { joinRoom({ id: roomid }) - setTimeout(() => { - document.getElementById(msgid)?.scrollIntoView({ behavior: "smooth" }) - }, 800) + context.flag = msgid } export const reportmsg = (msg) => { diff --git a/src/assets/store.js b/src/assets/store.js index fb7dd01..1cb3bd9 100644 --- a/src/assets/store.js +++ b/src/assets/store.js @@ -32,6 +32,7 @@ export const context = reactive({ text: "", id: "", }, + flag: "", }) export const showMain = ref(false); diff --git a/src/components/Messages.vue b/src/components/Messages.vue index 6253299..4b7f2bb 100644 --- a/src/components/Messages.vue +++ b/src/components/Messages.vue @@ -29,8 +29,10 @@ const currentRoomTitle = computed(() => {

Welcome to #{{ currentRoomTitle }}