diff --git a/minecraft-to-site-chat.drawio b/minecraft-to-site-chat.drawio new file mode 100644 index 0000000..903ee89 --- /dev/null +++ b/minecraft-to-site-chat.drawio @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index da9217b..12d5a91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,11 @@ "express-session": "^1.18.1", "jimp": "^1.6.0", "jsonwebtoken": "^9.0.2", + "kafkajs": "^2.2.4", "multer": "^1.4.5-lts.1", "pg": "^8.13.1", - "pug": "^3.0.3" + "pug": "^3.0.3", + "ws": "^8.18.0" }, "devDependencies": { "nodemon": "^3.1.9" @@ -1885,6 +1887,15 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/kafkajs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", + "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -3367,6 +3378,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml-parse-from-string": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", diff --git a/package.json b/package.json index 5efe0f3..55758e3 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,11 @@ "express-session": "^1.18.1", "jimp": "^1.6.0", "jsonwebtoken": "^9.0.2", + "kafkajs": "^2.2.4", "multer": "^1.4.5-lts.1", "pg": "^8.13.1", - "pug": "^3.0.3" + "pug": "^3.0.3", + "ws": "^8.18.0" }, "devDependencies": { "nodemon": "^3.1.9" diff --git a/public/css/index.css b/public/css/index.css index 53bbef6..a974847 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -6,7 +6,7 @@ html { } body { - margin: 0; + margin: 0; padding: 0; height: 100%; width: 100%; @@ -62,7 +62,7 @@ canvas { padding-left: 3%; padding-right: 3%; margin-top: 0%; - border:solid 2px gray; + border: solid 2px gray; box-shadow: 0px 20px 150px rgba(0, 0, 0, 1); } @@ -100,3 +100,29 @@ canvas { overflow: scroll; border: black solid } + +.messages-container { + width: 100%; + height: 75%; + border: gray solid; +} + +.input-container { + width: 100%; + height: 5%; +} + +#chat-input { + width: calc(100% - 150px); + height: 100%; + margin: 0; +} + +#send-message-button { + float: right; + width: 150px; + height: 100%; + font-size: 15pt; + margin: 0; + padding: 0 +} \ No newline at end of file diff --git a/public/js/chat.js b/public/js/chat.js new file mode 100644 index 0000000..3296b9d --- /dev/null +++ b/public/js/chat.js @@ -0,0 +1,16 @@ +$(document).ready(() => { + const sendData = async () => { + alert(1); + } + + $("#send-message-button").click(sendData); + + $('#chat-input').keyup(function(e){ + if(e.keyCode == 13) + { + $(this).trigger("enterKey"); + } + }); + + $("#chat-input").bind("enterKey", sendData); +}); \ No newline at end of file diff --git a/src/controllers/user.js b/src/controllers/user.js index 1d55408..08dc5b1 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -31,7 +31,8 @@ class UserController { utils.removeFromFile('./inviteTokens.txt', req.body.inviteToken); } - req.session.jwt = jwt.sign({ username }, process.env.SECRET, {expiresIn: "1y"}); + const token = jwt.sign({ username }, process.env.SECRET, {expiresIn: "1y"}); + res.cookie("jwt", token); return res.status(200).send("Ok"); } @@ -43,12 +44,13 @@ class UserController { if (!bcrypt.compareSync(password, storedPassword)) { return res.status(403).send("Password is not correct"); } - req.session.jwt = jwt.sign({ username }, process.env.SECRET, {expiresIn: "1y"}); + const token = jwt.sign({ username }, process.env.SECRET, {expiresIn: "1y"}); + res.cookie("jwt", token); return res.status(200).send("Ok"); } async changePassword(req, res) { - const token = req.session.jwt; + const token = req.cookies["jwt"]; const username = jwt.decode(token).username; const { oldPassword, newPassword } = req.body; @@ -67,11 +69,12 @@ class UserController { async logout(req, res) { req.session.destroy(); + res.clearCookie("jwt"); return res.redirect("/login"); } async uploadSkin(req, res) { - const token = req.session.jwt; + const token = req.cookies["jwt"]; const decoded = jwt.decode(token); const tempPath = req.file.path; const targetPath = `/opt/skins/${decoded.username}.png`; @@ -93,7 +96,7 @@ class UserController { } async uploadCape(req, res) { - const token = req.session.jwt; + const token = req.cookies["jwt"]; const decoded = jwt.decode(token); const tempPath = req.file.path; const targetPath = `/opt/cloaks/${decoded.username}.png`; @@ -116,7 +119,7 @@ class UserController { } async getUsername(req, res) { - const token = req.session.jwt; + const token = req.cookies["jwt"]; return res.status(200).send(jwt.decode(token).username); } } diff --git a/src/index.js b/src/index.js index 9b3127e..16fc699 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,7 @@ import UserRouter from './routers/user.js'; const app = express(); -dotenv.config({path: ".env"}); +dotenv.config({ path: ".env" }); app.use(session({ secret: process.env.SECRET, @@ -18,7 +18,7 @@ app.use(session({ cookie: { maxAge: 1000 * 60 * 60 * 24 } })); app.use(express.static(path.join('./public'))); -app.use(express.urlencoded({extended: false})); +app.use(express.urlencoded({ extended: false })); app.use(express.json()); app.use(cookieParser()); @@ -27,6 +27,8 @@ app.set('view engine', 'pug'); app.use('/api', ApiRouter); app.use('/', UserRouter); -app.listen(process.env.PORT, () => { +const server = app.listen(process.env.PORT, () => { console.log("App has been started!"); -}); \ No newline at end of file +}); + +export default server; diff --git a/src/messages.js b/src/messages.js new file mode 100644 index 0000000..421fba8 --- /dev/null +++ b/src/messages.js @@ -0,0 +1,68 @@ +import { Kafka } from "kafkajs"; +import ws from 'ws'; +import jwt from 'jwt'; + +import server from './index.js'; + +const kafka = new Kafka({ + clientId: 'backend', + brokers: ['kafka:9092'] +}); + +const wsClients = []; + +const producer = kafka.producer(); +const consumer = kafka.consumer(); + +await producer.connect(); +await consumer.connect(); +await consumer.subscribe({ + topic: "chatMessage", + fromBeginning: true +}); + +const onMessageFromServer = async ({ topic, partition, message }) => { + wsClients.forEach(client => { + client.send({ + message + }) + }) +}; + +await consumer.run({ + eachMessage: onMessageFromServer +}); + +const wsServer = new ws.Server({ noServer: true }); +wsServer.on('connection', socket => { + wsClients.push(socket); + socket.on('message', async (message) => { + + const token = message.jwt; + if (!jwt.verify(token, process.env.secret)) { + socket.send("JWT is not valid.") + return; + } + + await producer.send({ + topic: 'chatMessage', + messages: [{ + author: message.author, + content: message.content, + date: message.date + }] + }); + }); + + socket.on('close', async () => { + wsClients = wsClients.filter(s => s !== socket); + await producer.disconnect(); + }); +}); + +server.on('upgrade', (request, socket, head) => { + wsServer.handleUpgrade(request, socket, head, socket => { + wsServer.emit('connection', socket, request); + }) +}) + diff --git a/src/middlewares/auth.js b/src/middlewares/auth.js index 6d22033..2a5671d 100644 --- a/src/middlewares/auth.js +++ b/src/middlewares/auth.js @@ -7,9 +7,10 @@ import UserService from '../services/user.js'; dotenv.config({path: ".env"}); const authenticate = async (req, res, next) => { - const token = req.session.jwt; + const token = req.cookies["jwt"]; if (!token || !jwt.verify(token, process.env.SECRET)) { req.session.destroy(); + res.clearCookie("jwt"); return res.redirect("/login"); } next(); @@ -33,7 +34,7 @@ const validateInviteToken = async (req, res, next) => { }; const canHaveCloak = async (req, res, next) => { - const { username } = jwt.decode(req.session.jwt); + const { username } = jwt.decode(req.cookies["jwt"]); if (!(await UserService.canHaveCloak(username))) return res.status(403).send("You cannot have cloak"); next(); diff --git a/src/middlewares/existance.js b/src/middlewares/existance.js index 6b0b3c3..dc387f0 100644 --- a/src/middlewares/existance.js +++ b/src/middlewares/existance.js @@ -19,11 +19,11 @@ const userExist = async (req, res, next) => { let username; if (req.body.username) { username = req.body.username; - } else if (req.session.jwt) { - if (!req.session.jwt || !jwt.verify(req.session.jwt, process.env.SECRET)) { + } else if (req.cookies["jwt"]) { + if (!req.cookies["jwt"] || !jwt.verify(req.cookies["jwt"], process.env.SECRET)) { return res.status(403).send("Unauthorized"); } - username = jwt.decode(req.session.jwt).username; + username = jwt.decode(req.cookies["jwt"]).username; } if (!(await UserService.exists(username))) { diff --git a/src/routers/user.js b/src/routers/user.js index 277318e..ee79d3a 100644 --- a/src/routers/user.js +++ b/src/routers/user.js @@ -10,7 +10,7 @@ dotenv.config({path: ".env"}); const UserRouter = new Router(); UserRouter.get('/register', async (req, res) => { - if (req.session.jwt && jwt.verify(req.session.jwt, process.env.SECRET)) + if (req.cookies["jwt"] && jwt.verify(req.cookies["jwt"], process.env.SECRET)) return res.redirect("/index"); return res.render("register.pug", { @@ -19,7 +19,7 @@ UserRouter.get('/register', async (req, res) => { }); UserRouter.get(['/', '/login'], async (req, res) => { - if(req.session.jwt && jwt.verify(req.session.jwt, process.env.SECRET)) { + if(req.cookies["jwt"] && jwt.verify(req.cookies["jwt"], process.env.SECRET)) { return res.redirect("/index"); } @@ -27,7 +27,7 @@ UserRouter.get(['/', '/login'], async (req, res) => { }); UserRouter.get(['/index', '/skin'], auth.authenticate, async (req, res) => { - const username = jwt.decode(req.session.jwt).username; + const username = jwt.decode(req.cookies["jwt"]).username; return res.render('skin.pug', { username: username, @@ -36,7 +36,7 @@ UserRouter.get(['/index', '/skin'], auth.authenticate, async (req, res) => { }); UserRouter.get('/changepassword', auth.authenticate, async (req, res) => { - const username = jwt.decode(req.session.jwt).username; + const username = jwt.decode(req.cookies["jwt"]).username; return res.render('changepassword.pug', { can_have_cloak: await UserService.canHaveCloak(username) @@ -44,7 +44,7 @@ UserRouter.get('/changepassword', auth.authenticate, async (req, res) => { }); UserRouter.get('/chat', auth.authenticate, async (req, res) => { - const username = jwt.decode(req.session.jwt).username; + const username = jwt.decode(req.cookies["jwt"]).username; return res.render('chat.pug', { can_have_cloak: await UserService.canHaveCloak(username) @@ -52,7 +52,7 @@ UserRouter.get('/chat', auth.authenticate, async (req, res) => { }); UserRouter.get('/worldmap', auth.authenticate, async (req, res) => { - const username = jwt.decode(req.session.jwt).username; + const username = jwt.decode(req.cookies["jwt"]).username; return res.render('worldmap.pug', { can_have_cloak: await UserService.canHaveCloak(username) diff --git a/views/chat.pug b/views/chat.pug index 5288954..02d52bd 100644 --- a/views/chat.pug +++ b/views/chat.pug @@ -4,6 +4,9 @@ html title Личный кабинет | Чат link(href="css/index.css" rel="stylesheet") body + script(src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer") + script(type="text/javascript" src="js/chat.js") + iframe(name="hiddenFrame" style="position:absolute; top:-1px; left:-1px; width:1px; height:1px;") include header.pug @@ -11,4 +14,8 @@ html div(class="content") h1 Чат div(class="chat-container") - p Пока что тут пусто, но скоро будет кое-что интересное! \ No newline at end of file + div(class="messages-container") + + div(class="input-container") + input(type="text" id="chat-input") + button(id="send-message-button") Отправить \ No newline at end of file