From a8e94c9d12a06b6ad2eb247faf0f8e781a08dc9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emrik=20=C3=96stling?= Date: Mon, 4 Mar 2024 17:42:00 +0100 Subject: [PATCH] solves #3 --- .dockerignore | 3 +- .gitignore | 3 +- Dockerfile | 4 +- README.md | 52 +++++++------ backend/{views => }/home.html | 30 +++++++- backend/index.js | 137 +++++++++++++++++++++++++++++++--- backend/package-lock.json | 68 ++++++++++++++++- backend/package.json | 5 +- docker-compose.yml | 29 +++++-- frontend/resources/js/app.js | 70 ++++++++--------- 10 files changed, 314 insertions(+), 87 deletions(-) rename backend/{views => }/home.html (77%) diff --git a/.dockerignore b/.dockerignore index d4d28fd..febc2cb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,5 @@ /db/ -/node_modules/ +/frontend/node_modules/ +/backend/node_modules/ /vendor/ .env \ No newline at end of file diff --git a/.gitignore b/.gitignore index a7e72c5..80a47cc 100644 --- a/.gitignore +++ b/.gitignore @@ -228,4 +228,5 @@ $RECYCLE.BIN/ # .pnp.* # End of https://www.toptal.com/developers/gitignore/api/node,macos,linux,windows,visualstudiocode,yarn,composer -/db/ \ No newline at end of file +/db/ +docker-compose.yml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 1257c80..8901bde 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ ENV NODE_ENV=production WORKDIR /app/frontend COPY frontend/package*.json ./ -RUN npm install +RUN npm install --production COPY frontend/ ./ RUN npm run production @@ -12,7 +12,7 @@ RUN npm run production WORKDIR /app/backend COPY backend/package*.json ./ -RUN npm install +RUN npm install --production COPY backend/ ./ diff --git a/README.md b/README.md index 42d4390..ce79814 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,41 @@ # CHS MC Website -This is the CHS MC website that can be found here [mc.chs.se][1] +This is the CHS MC website that can be found here [mc.chs.se](https://mc.chs.se/). -## Requirement +The source code for the old CHS MC website can be found here on [github](https://github.com/gudchalmers/chs-mc-website/tree/aa622740b57cfc073a5d3f4b9321ecb184ad7804). -* Requires a database with a `mc_stats` table -* Webserver with php, [composer][2] and [nodejs][3] - -## Setup +## Development +```shell script +cd frontend +npm install +npm run prod -Rename the `.env.example` to `.env` and modify it to the current environment. +cd .. +cd backend +npm install +npx nodemon index.js +``` -The site is served out of the `public` folder. +For frontend development you probably want a better setup. -To setup the dev run: +## Deployment -```shell script -composer install -npm install -npm run dev -# or -npm run watch -``` +Using docker-compose: -To setup the production site run: +```yml +version: '3' -```shell script -composer install --optimize-autoloader --no-dev -npm install --production -npm run prod +services: + chs-mc-website: + image: ghcr.io/gudchalmers/chs-mc-website:main + container_name: chs-mc-website + restart: unless-stopped + ports: + - "3000:3000" ``` Copy the latest version of the dynmap website files from the plugin folder to the `public/dynmap` folder. ## License -[MIT][4] - -[1]: https://mc.chs.se/ -[2]: https://getcomposer.org/ -[3]: https://nodejs.org/ -[4]: https://choosealicense.com/licenses/mit/ \ No newline at end of file +[MIT](https://choosealicense.com/licenses/mit/) \ No newline at end of file diff --git a/backend/views/home.html b/backend/home.html similarity index 77% rename from backend/views/home.html rename to backend/home.html index 9d68e09..ecedc29 100644 --- a/backend/views/home.html +++ b/backend/home.html @@ -60,7 +60,33 @@

Getting started:

To join the server you need to register your account with your Chalmers student email.

-
+
+
+ +
+ + + + +
+
+
+ +
+ + + + +
+
+
+
+ +
+
+
+
diff --git a/backend/index.js b/backend/index.js index 6396a0f..9b04646 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,23 +1,138 @@ -import express from 'express'; +import express from "express"; import mc from "minecraftstatuspinger"; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import crypto from "crypto"; +import nodemailer from "nodemailer"; +import mariadb from "mariadb"; +import "dotenv/config"; + +const port = process.env.PORT || 3000; +const dbName = process.env.DB_NAME || "mc_stats"; + +const pool = mariadb.createPool({ + host: "localhost", + user: process.env.DB_USER, + database: dbName, + password: process.env.DB_PASS, + connectionLimit: 5, +}); + +// seed db +const seed = async () => { + let conn; + try { + conn = await pool.getConnection(); + await conn.query(`CREATE DATABASE IF NOT EXISTS ${dbName}`); + await conn.query(`USE ${dbName}`); + await conn.query( + `CREATE TABLE IF NOT EXISTS users (id INT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(255), username VARCHAR(255), active BOOLEAN)` + ); + await conn.query( + `CREATE TABLE IF NOT EXISTS confirmations (id INT AUTO_INCREMENT PRIMARY KEY, user_id INT, token VARCHAR(255))` + ); + } catch (err) { + console.error(err); + } finally { + if (conn) conn.end(); + } +}; +seed(); const app = express(); -const port = 3000; const currentFilePath = fileURLToPath(import.meta.url); const currentDirPath = dirname(currentFilePath); -// use public -app.use(express.static('../frontend/public')); +app.use(express.static("../frontend/public")); // use public +app.use(express.json()); // to support JSON-encoded bodies +app.use(express.urlencoded({ extended: true })); // to support URL-encoded bodies -app.get('/', (req, res) => { - const filePath = path.join(currentDirPath, 'views', 'home.html'); +app.get("/", (req, res) => { + const filePath = path.join(currentDirPath, "home.html"); res.sendFile(filePath); }); -app.get('/ping', async (_, res) => { +const transporter = nodemailer.createTransport({ + host: process.env.MAIL_HOST, + port: process.env.MAIL_PORT, + secure: true, + auth: { + user: process.env.MAIL_USER, + pass: process.env.MAIL_PASS, + }, +}); + +app.post("/register", async (req, res) => { + let userEmail = req.body.email; + let username = req.body.username; + + let token = crypto.randomBytes(32).toString("hex"); + + // Send email with the token + let mailOptions = { + from: `"${process.env.MAIL_NAME}>" <${process.env.MAIL_FROM}>`, + to: userEmail, + subject: "Registration Confirmation for mc.chs.se", + text: + "Please confirm your registration by clicking the following link: \nhttp://" + + req.headers.host + + "/confirm/" + + token + + "\n\n" + + "If you did not request this, please ignore this email.", + }; + + transporter.sendMail(mailOptions, function (err) { + if (err) { + console.error("There was an error: ", err); + } else { + console.log("Email sent"); + } + }); + + let conn; + try { + conn = await pool.getConnection(); + let sql = "INSERT INTO users (email, username, active) VALUES (?, ?, 0)"; + await conn.query(sql, [userEmail, username]); + sql = "SELECT id FROM users WHERE email = ?"; + const rows = await conn.query(sql, [userEmail]); + let userId = rows[0].id; + sql = "INSERT INTO confirmations (user_id, token) VALUES (?, ?)"; + await conn.query(sql, [userId, token]); + } catch (err) { + console.error(err); + } finally { + if (conn) conn.end(); + } +}); + +app.get("/confirm/:token", async (req, res) => { + let token = req.params.token; + let conn; + try { + conn = await pool.getConnection(); + let sql = "SELECT user_id FROM confirmations WHERE token = ?"; + const rows = await conn.query(sql, [token]); + if (rows.length) { + // If the token exists, delete it from the database and set the user to active + sql = "DELETE FROM confirmations WHERE token = ?"; + await conn.query(sql, [token]); + sql = "UPDATE users SET active = 1 WHERE id = ?"; + await conn.query(sql, [rows[0].user_id]); + res.send("Your account has been activated."); + } else { + res.send("Invalid token."); + } + } catch (err) { + console.error(err); + } finally { + if (conn) conn.end(); + } +}); + +app.get("/ping", async (_, res) => { try { let result = await mc.lookup({ host: "mc.chs.se" }); res.send(result); @@ -28,4 +143,4 @@ app.get('/ping', async (_, res) => { app.listen(port, () => { console.log(`App listening at http://localhost:${port}`); -}); \ No newline at end of file +}); diff --git a/backend/package-lock.json b/backend/package-lock.json index a03f7af..b2c340b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,9 +9,12 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "dotenv": "^16.4.5", "express": "^4.18.2", "express-handlebars": "^7.1.2", - "minecraftstatuspinger": "^1.1.5" + "mariadb": "^3.2.3", + "minecraftstatuspinger": "^1.1.5", + "nodemailer": "^6.9.10" } }, "node_modules/@isaacs/cliui": { @@ -39,6 +42,16 @@ "node": ">=14" } }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" + }, + "node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -225,6 +238,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -242,6 +263,17 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -603,6 +635,32 @@ "node": "14 || >=16.14" } }, + "node_modules/mariadb": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.2.3.tgz", + "integrity": "sha512-Hyc1ehdUJwzvvzcLU2juZS528wJ6oE8pUlpgY0BAOdpKWcdN1motuugi5lC3jkpCkFpyNknHG7Yg66KASl3aPg==", + "dependencies": { + "@types/geojson": "^7946.0.10", + "@types/node": "^17.0.45", + "denque": "^2.1.0", + "iconv-lite": "^0.6.3", + "lru-cache": "^10.0.1" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/mariadb/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==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -710,6 +768,14 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/nodemailer": { + "version": "6.9.10", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.10.tgz", + "integrity": "sha512-qtoKfGFhvIFW5kLfrkw2R6Nm6Ur4LNUMykyqu6n9BRKJuyQrqEGwdXXUAbwWEKt33dlWUGXb7rzmJP/p4+O+CA==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", diff --git a/backend/package.json b/backend/package.json index fa138d7..aed491e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,8 +1,11 @@ { "dependencies": { + "dotenv": "^16.4.5", "express": "^4.18.2", "express-handlebars": "^7.1.2", - "minecraftstatuspinger": "^1.1.5" + "mariadb": "^3.2.3", + "minecraftstatuspinger": "^1.1.5", + "nodemailer": "^6.9.10" }, "name": "backend", "version": "1.0.0", diff --git a/docker-compose.yml b/docker-compose.yml index 30a8868..24d2085 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,16 +3,33 @@ version: '3' services: chs-mc-website: build: . - networks: - - chs-mc-website ports: - - "8080:80" + - "3000:3000" environment: + PORT: 3000 DB_USER: chs-mc-website DB_PASS: 5a7XoNwyRKgDyr9sA2rh DB_NAME: mc_stats DB_HOST: mariadb + MAIL_FROM: hello@example.com + MAIL_NAME: 'CHS Minecraft Server' + MAIL_HOST: sandbox.smtp.mailtrap.io + MAIL_PORT: 2525 + MAIL_USER: ed34e3c7508a9d + MAIL_PASS: e278e352efca4b + mariadb: + image: mariadb:11 + container_name: mariadb + volumes: + - mariadb:/var/lib/mysql + restart: unless-stopped + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: 5a7XoNwyRKgDyr9sA2rh + MYSQL_DATABASE: mc_stats + MYSQL_USER: chs-mc-website + MYSQL_PASSWORD: 5a7XoNwyRKgDyr9sA2rh -networks: - chs-mc-website: - external: true +volumes: + mariadb: diff --git a/frontend/resources/js/app.js b/frontend/resources/js/app.js index a0488c8..791d62d 100755 --- a/frontend/resources/js/app.js +++ b/frontend/resources/js/app.js @@ -1,51 +1,51 @@ -import $ from 'cash-dom' -import _ from 'lodash' -import ky from 'ky' -import Mustache from 'mustache' -import { Converter } from '@gorymoon/minecraft-text' -import tippy from 'tippy.js' +import $ from "cash-dom"; +import _ from "lodash"; +import ky from "ky"; +import Mustache from "mustache"; +import { Converter } from "@gorymoon/minecraft-text"; +import tippy from "tippy.js"; -import '@fortawesome/fontawesome-free/js/fontawesome' -import '@fortawesome/fontawesome-free/js/solid' -import '@fortawesome/fontawesome-free/js/regular' -import '@fortawesome/fontawesome-free/js/brands' -import 'tippy.js/dist/tippy.css' +import "@fortawesome/fontawesome-free/js/fontawesome"; +import "@fortawesome/fontawesome-free/js/solid"; +import "@fortawesome/fontawesome-free/js/regular"; +import "@fortawesome/fontawesome-free/js/brands"; +import "tippy.js/dist/tippy.css"; const converter = new Converter({ newline: true }); $(function () { - tippy('[data-tippy-content]'); + tippy("[data-tippy-content]"); - $('.navbar-burger').on('click', function () { - $('.navbar-burger').toggleClass('is-active'); - $('.navbar-menu').toggleClass('is-active'); + $(".navbar-burger").on("click", function () { + $(".navbar-burger").toggleClass("is-active"); + $(".navbar-menu").toggleClass("is-active"); }); - $('body').on('click', '.chs-modal-close', function (e) { - let modal = $(this).data('modal'); - $(`#${modal}`).removeClass('is-active'); + $("body").on("click", ".chs-modal-close", function (e) { + let modal = $(this).data("modal"); + $(`#${modal}`).removeClass("is-active"); }); - $('body').on('click', '.chs-modal-open', function (e) { - let modal = $(this).data('modal'); - $(`#${modal}`).addClass('is-active'); + $("body").on("click", ".chs-modal-open", function (e) { + let modal = $(this).data("modal"); + $(`#${modal}`).addClass("is-active"); }); - $('body').on('click', '.modal-background', function (e) { - $(this).parent().removeClass('is-active'); + $("body").on("click", ".modal-background", function (e) { + $(this).parent().removeClass("is-active"); }); async function updateMOTD() { try { - const response = await ky.get('/ping').json(); - const data = response['status']; - console.log(converter.toHTML(converter.parse(data['description']))); - const rendered = Mustache.render($('#motd-template-success').html(), { - current: data['players']['online'], - max: data['players']['max'], - motd: converter.toHTML(converter.parse(data['description'])) + const response = await ky.get("/ping").json(); + const data = response["status"]; + console.log(converter.toHTML(converter.parse(data["description"]))); + const rendered = Mustache.render($("#motd-template-success").html(), { + current: data["players"]["online"], + max: data["players"]["max"], + motd: converter.toHTML(converter.parse(data["description"])), }); - $('#status').html(rendered); + $("#status").html(rendered); ///// Broken for now, need to get players from the response // let players = 'No players online'; @@ -67,10 +67,10 @@ $(function () { // allowHTML: true // }); } catch (e) { - $('#status').html(Mustache.render($('#motd-template-error').html())); + $("#status").html(Mustache.render($("#motd-template-error").html())); } - }; + } updateMOTD(); - setInterval(updateMOTD, 30 * 1000) -}); \ No newline at end of file + setInterval(updateMOTD, 30 * 1000); +});