From 6cb2bea05baec113e493292ded4685b15945f1a8 Mon Sep 17 00:00:00 2001 From: leca Date: Sat, 15 Jun 2024 03:24:14 +0300 Subject: [PATCH] add new endpoint --- db.psql | 2 +- docker-compose.yml | 3 +- src/index.js | 172 ++++++++++++++++++++++----------------------- src/js/frontend.js | 28 +++++--- src/middleware.js | 10 +++ src/utils.js | 36 ++++++++++ 6 files changed, 152 insertions(+), 99 deletions(-) create mode 100644 src/middleware.js create mode 100644 src/utils.js diff --git a/db.psql b/db.psql index 196d7ad..de0f0e2 100644 --- a/db.psql +++ b/db.psql @@ -17,6 +17,6 @@ CREATE TABLE IF NOT EXISTS Chats ( CREATE TABLE IF NOT EXISTS Messages ( ID SERIAL, author_id INT, -- ref to table Users, column ID. - time_sent TIMESTAMP, + time_sent TIMESTAMP, -- yyyy.mm.dd hh:mm:ss.mmmmmm content TEXT ); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index c734b88..1d6f367 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,8 +5,7 @@ services: build: . restart: always ports: - - 8080:8080 # HTTP - - 8081:8081 # Websocket + - 8080:8080 networks: ne_nuzhen: ipv4_address: 10.5.0.5 diff --git a/src/index.js b/src/index.js index 0d7f947..cd2d22f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,73 +1,76 @@ const express = require('express'); -const pg = require('pg'); -const path = require('path'); -const fs = require('fs'); const bcrypt = require('bcrypt'); const cors = require("cors"); -const cookieSession = require("cookie-session"); const WebSocket = require('ws'); +const http = require('http') + +clients = []; // websocket clients +sessions = {}; // logged in clients, there are stored their tokens. + +const middleware = require("./middleware"); +const utils = require("./utils") + const app = express(); const PORT = 8080; -const { Client } = pg; -const client = new Client({ - user: "smk", - password: "CHANGEME", // do not forget to change it in docker-compose.yml in db section. - host: "10.5.0.6", //defined in docker-compose.yml. - port: 5432, - database: "chat" -}); -// /api/getChats -// /api/getChat/?id -// websocket +utils.initDb(); -const requireToBeLoggedIn = (req, res, next) => { - if (sessions[req.session.token] == undefined) return res.redirect('/login'); - next(); -}; -const requireToBeNotLoggedIn = (req, res, next) => { - if (req.session.token != undefined) return res.redirect('/'); - next(); -}; +const httpServer = http.createServer(app).listen(PORT, "0.0.0.0", () => { + console.log("[LOG] Ready to use."); +}); const ws = new WebSocket.Server({ - port: PORT + 1 -}) + server: httpServer, + host: "0.0.0.0" +}); + console.log("[LOG] Socket has been started"); -let clients = [] +ws.on('connection', (wsclient) => { + clients.push(wsclient) -ws.on('connection', (client) => { - clients.push(client) + /* + Package structure: + { + "action": "...", // the request + "token": "...", // the token that is the key for sessions dictionary + "content": ..., // the rest content of an action. + } - client.on('message', async (msg) => { + examples: + message: + { + "action": "message", + "token": "some_token_here", + "content": { + "chatId": 1, + "text": "Hello!" + } + } + */ + + wsclient.on('message', async (msg) => { try { let jsonMsg = JSON.parse(msg) switch (jsonMsg.action) { - case "sync": { + case "message":{ + let userId = sessions[jsonMsg.token]; + console.log(`${userId} has sent a message: ${jsonMsg.content.text} to ${jsonMsg.content.chatId}`); break; - let chats; - let query_res = await client.query(`SELECT chats FROM Users WHERE `); } - case "message": - break; default: - console.log(`Package cannot be understood: ${jsonMsg}`) + console.log(`Package cannot be understood: ${msg}`) } - console.log(jsonMsg) - client.send(JSON.stringify({ - "test": "a" - })); } catch (e) { console.log(`[ERROR] in receiving message by websocket: ${e}`) } }); - client.on('close', () => { - clients = clients.filter(c => c !== client) + wsclient.on('close', () => { + clients = clients.filter(c => c !== wsclient) }) }) @@ -84,51 +87,63 @@ app.use(require('express-session')({ })); app.use('/js', express.static(__dirname + "/js")); -let sessions = {}; - -app.get('/', requireToBeLoggedIn, (req, res) => { +app.get('/', middleware.requireToBeLoggedIn, (req, res) => { res.sendFile('views/index.html', { root: __dirname }); }); -app.get('/registration', requireToBeNotLoggedIn, (req, res) => { +app.get('/registration', middleware.requireToBeNotLoggedIn, (req, res) => { res.sendFile('views/registration.html', { root: __dirname }); }); -app.get('/login', requireToBeNotLoggedIn, (req, res) => { +app.get('/login', middleware.requireToBeNotLoggedIn, (req, res) => { res.sendFile('views/login.html', { root: __dirname }); }); -const generateRandomString = () => { - return Math.floor(Math.random() * Date.now()).toString(36); -}; - -const getIdByCredentials = async (lastname, firstname, middlename) => { - let query_res = await client.query("SELECT ID FROM Users WHERE lastname = $1::text AND firstname = $2::text AND middlename = $3::text;", [lastname, firstname, middlename]); - if (query_res.rowCount == 0) return -1; // no such user - if (query_res.rowCount == 1) return query_res.rows[0].id; -} - - //IN: lastname, firstname, middlename //OUT: UserID //Returns an ID of the user, whose lastname, firstname and middlename were passed. //Returns -1 if user does not exist. //Requires client to be logged in. -app.get('/api/getIdByCredentials', requireToBeLoggedIn, async (req, res) => { +app.get('/api/getIdByCredentials', middleware.requireToBeLoggedIn, async (req, res) => { const { lastname, firstname, middlename } = req.body; - return res.send(await getIdByCredentials(lastname, firstname, middlename)).end(); + return res.send(await utils.getIdByCredentials(lastname, firstname, middlename)).end(); }); + +//IN: chatId, fromTimestamp, toTimestamp +//OUT: array of objects that represent a message, like {"author_id": ..., time_sent: ..., content: ...} +//fromTimestamp must be lower that toTimestamp, otherwise it might return nothing. +//Returns an array of described objects. +//Return "Chat with id ${ID} does not exist." if no chat with supplied id does not exist. +//Requires client to be logged in. +app.post('/api/getMessagesFromChat', middleware.requireToBeLoggedIn, async (req, res) => { + const userId = sessions[req.session.token]; + + const { chatId, fromTimestamp, toTimestamp } = req.body; + let messages = [] + + if ((await client.query("SELECT ID FROM Chats WHERE ID = $1", [chatId])).rowCount == 0) { + return res.status(400).send(`Chat with id ${chatId} does not exist.`).end() + } + + let messagesIds = (await client.query("SELECT messages FROM Chats WHERE ID = $1", [chatId])).rows[0].messages; + for (let id of messagesIds) { + let message = (await client.query("SELECT * FROM messages WHERE ID = $1 AND time_sent <= $2::TIMESTAMP AND time_sent >= $3::TIMESTAMP", [id, toTimestamp, fromTimestamp])).rows[0]; + messages.push(message) + } + return res.send(messages).status(200).end(); +}); + + //IN: UserID //OUT: Array of chat IDs //Returs ids of chats which user with passed ID is member in. //Return empty string if user has no membership in any chat. -app.get('/api/getChats', requireToBeLoggedIn, async (req, res) => { +app.get('/api/getChats', middleware.requireToBeLoggedIn, async (req, res) => { const userId = sessions[req.session.token] - console.log(`userId: ${userId}`); let chats = (await client.query("SELECT chats FROM Users WHERE ID = $1", [userId])).rows[0].chats return res.send(chats).status(200).end(); @@ -137,7 +152,7 @@ app.get('/api/getChats', requireToBeLoggedIn, async (req, res) => { //IN: UserId, array of UserIDs that are to be invited. //OUT: "Ok" if succsessful, "User with id ${MEMBERID} does not exist." //Requires to be logged in -app.post('/api/createChat', requireToBeLoggedIn, async (req, res) => { +app.post('/api/createChat', middleware.requireToBeLoggedIn, async (req, res) => { const userId = sessions[req.session.token] let { toInviteIds } = req.body; @@ -154,7 +169,6 @@ app.post('/api/createChat', requireToBeLoggedIn, async (req, res) => { let invitedFullname = (await client.query("SELECT lastname, firstname, middlename FROM Users WHERE ID = $1;", [toInviteIds[0]])).rows[0] let invitorFullname = (await client.query("SELECT lastname, firstname, middlename FROM Users WHERE ID = $1;", [userId])).rows[0] chatName = invitedFullname.lastname + " " + invitedFullname.firstname + " " + invitedFullname.middlename + " и " + invitorFullname.lastname + " " + invitorFullname.firstname + " " + invitorFullname.middlename - console.log(`Chatname: ${chatName}`) } else { chatName = "Новая группа" } @@ -173,7 +187,7 @@ app.post('/api/createChat', requireToBeLoggedIn, async (req, res) => { //OUT: redirect to /login. //Removes client's session, thus unlogging a user. //Requires to be logged in. -app.get('/api/logout', requireToBeLoggedIn, (req, res) => { +app.get('/api/logout', middleware.requireToBeLoggedIn, (req, res) => { sessions[req.session.token] = undefined; req.session.token = undefined; res.redirect('/login'); @@ -184,14 +198,14 @@ app.get('/api/logout', requireToBeLoggedIn, (req, res) => { //Checks if user exist. If so, returns 400 with response "Such user exists.". //Otherwise, registers a user with given data. //Requires to be not logged in. -app.post('/api/register', requireToBeNotLoggedIn, async (req, res) => { +app.post('/api/register', middleware.requireToBeNotLoggedIn, async (req, res) => { try { const { lastname, firstname, middlename, password } = req.body; const salt = bcrypt.genSaltSync(10); const hash = bcrypt.hashSync(password, salt); - if (await getIdByCredentials(lastname, firstname, middlename) > -1) { + if (await utils.getIdByCredentials(lastname, firstname, middlename) > -1) { return res.status(400).send("Such user exists.").end(); } @@ -199,7 +213,7 @@ app.post('/api/register', requireToBeNotLoggedIn, async (req, res) => { "INSERT INTO Users (lastname, firstname, middlename, salty_password) VALUES ($1, $2, $3, $4) RETURNING ID;", [lastname, firstname, middlename, hash] )).rows[0].id; - req.session.token = generateRandomString(); + req.session.token = utils.generateRandomString(); sessions[req.session.token] = id; res.redirect('/'); } catch (err) { @@ -213,11 +227,11 @@ app.post('/api/register', requireToBeNotLoggedIn, async (req, res) => { //Checks if user exists. If not, returns 400 with response "No such user.". //Otherwise, compares passwords using bcrypt //If passwords match, creating session and redirects to / -app.post('/api/login', requireToBeNotLoggedIn, async (req, res) => { +app.post('/api/login', middleware.requireToBeNotLoggedIn, async (req, res) => { try { const { lastname, firstname, middlename, password } = req.body; - const ID = await getIdByCredentials(lastname, firstname, middlename) + const ID = await utils.getIdByCredentials(lastname, firstname, middlename) if (ID == -1) { return res.status(400).send("No such user.").end(); } @@ -225,7 +239,7 @@ app.post('/api/login', requireToBeNotLoggedIn, async (req, res) => { let stored_password = (await client.query("SELECT salty_password FROM Users WHERE ID = $1;", [ID])).rows[0].salty_password; if (bcrypt.compareSync(password, stored_password)) { - req.session.token = generateRandomString() + req.session.token = utils.generateRandomString() sessions[req.session.token] = ID; return res.redirect('/'); } else { @@ -237,21 +251,3 @@ app.post('/api/login', requireToBeNotLoggedIn, async (req, res) => { } }); - -const initDb = async () => { - await client.connect() - - let db_schema = fs.readFileSync('./db.psql').toString(); - try { - const res = await client.query(db_schema); - console.log("[LOG] Database initialized.") - } catch (err) { - console.log(`[ERROR] Cannot initialize database: ${err}`); - } -} - -initDb().then(() => { - app.listen(PORT, "0.0.0.0", () => { - console.log("[LOG] Ready to use."); - }); -}); \ No newline at end of file diff --git a/src/js/frontend.js b/src/js/frontend.js index 247e83e..fa9d1e8 100644 --- a/src/js/frontend.js +++ b/src/js/frontend.js @@ -1,23 +1,35 @@ let socket -const pingServer = (socket) => { - socket.send("") +function getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); } window.addEventListener('load', function () { - let connectionString = location.protocol == "https:" ? `wss://${window.location.hostname}:${Number(window.location.port) + 1}` : `ws://${window.location.hostname}:${Number(window.location.port) + 1}` + let connectionString = location.protocol == "https:" ? `wss://${window.location.hostname}:${window.location.port}` : `ws://${window.location.hostname}:${window.location.port}` socket = new WebSocket(connectionString) - + console.log(getCookie("token")) socket.addEventListener('open', (e) => { socket.send(JSON.stringify( { - "action": "sync" + "action": "message", + "token": getCookie("token"), + "content": { + "chatId": 1, + "text": "test" + } } )); }) - fetch("/api/getChats", { - method: "GET" + fetch("/api/getMessagesFromChat", { + method: "POST", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ chatId: 1, fromTimestamp: '2012.01.01 00:00:00.000000', toTimestamp: '2026.01.01 00:00:00.000000' }) }).then(response => response.text()) - .then((response => console.log(response))) + .then((response => console.log(response))) }) diff --git a/src/middleware.js b/src/middleware.js new file mode 100644 index 0000000..4b5a437 --- /dev/null +++ b/src/middleware.js @@ -0,0 +1,10 @@ +module.exports = { + requireToBeLoggedIn: (req, res, next) => { + if (global.sessions[req.session.token] == undefined) return res.redirect('/login'); + next(); + }, + requireToBeNotLoggedIn: (req, res, next) => { + if (req.session.token != undefined) return res.redirect('/'); + next(); + } +} \ No newline at end of file diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..110cd89 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,36 @@ +const pg = require('pg'); +const fs = require('fs'); + +const { Client } = pg; + +client = new Client({ + user: "smk", + password: "CHANGEME", // do not forget to change it in docker-compose.yml in db section. + host: "10.5.0.6", //defined in docker-compose.yml. + port: 5432, + database: "chat" +}); + +module.exports = { + generateRandomString: () => { + return Math.floor(Math.random() * Date.now()).toString(36); + }, + + getIdByCredentials: async (lastname, firstname, middlename) => { + let query_res = await client.query("SELECT ID FROM Users WHERE lastname = $1::text AND firstname = $2::text AND middlename = $3::text;", [lastname, firstname, middlename]); + if (query_res.rowCount == 0) return -1; // no such user + if (query_res.rowCount == 1) return query_res.rows[0].id; + }, + initDb: async () => { + await client.connect() + + let db_schema = fs.readFileSync('./db.psql').toString(); + try { + await client.query(db_schema); + console.log("[LOG] Database initialized.") + } catch (err) { + console.log(`[ERROR] Cannot initialize database: ${err}`); + } + } + +}; \ No newline at end of file