diff --git a/db.psql b/db.psql index 8717c30..3acbda3 100644 --- a/db.psql +++ b/db.psql @@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS Users ( CREATE TABLE IF NOT EXISTS Chats ( ID SERIAL, - name VARCHAR(32), --chat name + name VARCHAR(128), --chat name admins INT[], -- to table Users, column ID. messages INT[] -- ref to table Messages, column ID. ); diff --git a/docker-compose.yml b/docker-compose.yml index 47089d3..c734b88 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,8 +3,10 @@ version: "3.3" services: chat: build: . + restart: always ports: - - 8080:8080 + - 8080:8080 # HTTP + - 8081:8081 # Websocket networks: ne_nuzhen: ipv4_address: 10.5.0.5 @@ -18,7 +20,8 @@ services: networks: ne_nuzhen: ipv4_address: 10.5.0.6 - + volumes: + - ./postgres:/var/lib/postgresql/data environment: POSTGRES_USER: smk # The PostgreSQL user (useful to connect to the database) POSTGRES_PASSWORD: CHANGEME # The PostgreSQL password (useful to connect to the database) @@ -30,4 +33,4 @@ networks: ipam: config: - subnet: 10.5.0.0/16 - gateway: 10.5.0.1 \ No newline at end of file + gateway: 10.5.0.1 diff --git a/package-lock.json b/package-lock.json index e919cda..28bc6b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "express": "^4.19.2", "express-session": "^1.18.0", "nodemon": "^3.1.3", - "pg": "^8.12.0" + "pg": "^8.12.0", + "ws": "^8.17.0" } }, "node_modules/@mapbox/node-pre-gyp": { @@ -201,6 +202,20 @@ "node": ">=8" } }, + "node_modules/bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1063,6 +1078,18 @@ } } }, + "node_modules/node-gyp-build": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "optional": true, + "peer": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/nodemon": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.3.tgz", @@ -1723,6 +1750,20 @@ "node": ">= 0.8" } }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -1771,6 +1812,26 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "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/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 4b62c90..ef80a81 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "express": "^4.19.2", "express-session": "^1.18.0", "nodemon": "^3.1.3", - "pg": "^8.12.0" + "pg": "^8.12.0", + "ws": "^8.17.0" } } diff --git a/src/index.js b/src/index.js index 953f142..26e6989 100644 --- a/src/index.js +++ b/src/index.js @@ -5,9 +5,11 @@ const fs = require('fs'); const bcrypt = require('bcrypt'); const cors = require("cors"); const cookieSession = require("cookie-session"); - +const WebSocket = require('ws'); const app = express(); +const PORT = 8080; + const { Client } = pg; const client = new Client({ user: "smk", @@ -15,34 +17,84 @@ const client = new Client({ host: "10.5.0.6", //defined in docker-compose.yml. port: 5432, database: "chat" +}); +// /api/getChats +// /api/getChat/?id +// websocket + +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 ws = new WebSocket.Server({ + port: PORT + 1 }) +console.log("[LOG] Socket has been started"); + +let clients = [] + +ws.on('connection', (client) => { + clients.push(client) + + client.on('message', async (msg) => { + try { + let jsonMsg = JSON.parse(msg) + switch (jsonMsg.action) { + case "sync": { + 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(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) + }) +}) + app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(require('express-session')({ name: 'smk_chat', - secret: 'PLEASE!!GENERATE!!A!!STRONG!!ONE', + secret: 'PLEASE!!GENERATE!!A!!STRONG!!ONE!!', resave: false, saveUninitialized: false })); +app.use('/js', express.static(__dirname + "/js")); -let sessions = {} +let sessions = {}; -app.get('/', (req, res) => { - if (sessions[req.session.token] == undefined) return res.redirect('/login'); +app.get('/', requireToBeLoggedIn, (req, res) => { res.sendFile('views/index.html', { root: __dirname }); }); -app.get('/registration', (req, res) => { - if (req.session.token != undefined) return res.redirect('/'); - +app.get('/registration', requireToBeNotLoggedIn, (req, res) => { res.sendFile('views/registration.html', { root: __dirname }); }); -app.get('/login', (req, res) => { - if (sessions[req.session.token] != undefined) return res.redirect('/'); +app.get('/login', requireToBeNotLoggedIn, (req, res) => { res.sendFile('views/login.html', { root: __dirname }); }); @@ -50,20 +102,89 @@ 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; } -app.get('/api/logout', (req, res) => { - if (req.session.token == undefined) return res.redirect('/login'); - sessions[req.session.token] = undefined; - res.redirect('/login'); -}) -app.post('/api/register', async (req, res) => { + +//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) => { + const { lastname, firstname, middlename } = req.body; + + return res.send(await getIdByCredentials(lastname, firstname, middlename)).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) => { + + 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(); +}); + +//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) => { + const userId = sessions[req.session.token] + let { toInviteIds } = req.body; + + toInviteIds = toInviteIds.split(" "); + + toInviteIds.forEach(async (id, index, toInviteIds) => { + if ((await client.query("SELECT ID FROM Users WHERE ID = $1", [id])).rowCount == 0) { + return res.send(`User with id ${id} does not exist.`) + } + }); + + let chatName + if (toInviteIds.length == 1) { + 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 = "Новая группа" + } + + let chatId = (await client.query("INSERT INTO Chats (name) VALUES ($1) RETURNING ID;", [chatName])).rows[0].id + + await client.query("UPDATE Chats SET admins = ARRAY_APPEND(admins, $1) WHERE ID = $2", [userId, chatId]); + + toInviteIds.forEach(async (id, index, toInviteIds) => { + await client.query("UPDATE Users SET chats = ARRAY_APPEND(chats, $1) WHERE ID = $2;", [chatId, id]); + }); + return res.send("Ok") +}); + +//IN: none. +//OUT: redirect to /login. +//Removes client's session, thus unlogging a user. +//Requires to be logged in. +app.get('/api/logout', requireToBeLoggedIn, (req, res) => { + sessions[req.session.token] = undefined; + req.session.token = undefined; + res.redirect('/login'); +}); + +//IN: lastname, firstname, middlename, password. +//OUT: redirect to /. +//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) => { try { const { lastname, firstname, middlename, password } = req.body; @@ -82,12 +203,17 @@ app.post('/api/register', async (req, res) => { sessions[req.session.token] = id; res.redirect('/'); } catch (err) { - console.log("[ERROR] in /api/register: " + err) + console.log(`[ERROR] in /api/register: ${err}`) res.status(500).send(); } }); -app.post('/api/login', async (req, res) => { +//IN: lastname, firstname, middlename, password. +//OUT: redirect to /. +//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) => { try { const { lastname, firstname, middlename, password } = req.body; @@ -106,7 +232,7 @@ app.post('/api/login', async (req, res) => { return res.status(400).send("Wrong password").end(); } } catch (err) { - console.log("[ERROR] in /api/login: " + err) + console.log(`[ERROR] in /api/login: ${err}`) res.status(500).send(); } @@ -118,14 +244,14 @@ const initDb = async () => { let db_schema = fs.readFileSync('./db.psql').toString(); try { const res = await client.query(db_schema); - console.log("Database initialized.") + console.log("[LOG] Database initialized.") } catch (err) { - console.log("Cannot initialize database. Error: " + err); + console.log(`[ERROR] Cannot initialize database: ${err}`); } } initDb().then(() => { - app.listen(8080, "0.0.0.0", () => { - console.log("Ready to use."); + 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 new file mode 100644 index 0000000..247e83e --- /dev/null +++ b/src/js/frontend.js @@ -0,0 +1,23 @@ +let socket + +const pingServer = (socket) => { + socket.send("") +} + +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}` + socket = new WebSocket(connectionString) + + socket.addEventListener('open', (e) => { + socket.send(JSON.stringify( + { + "action": "sync" + } + )); + }) + + fetch("/api/getChats", { + method: "GET" + }).then(response => response.text()) + .then((response => console.log(response))) +}) diff --git a/src/views/index.html b/src/views/index.html index 45019de..a6318f1 100644 --- a/src/views/index.html +++ b/src/views/index.html @@ -5,6 +5,13 @@ index +
+
+
+ + +
Выйти + \ No newline at end of file diff --git a/src/views/login.html b/src/views/login.html index d931ecb..dfe3012 100644 --- a/src/views/login.html +++ b/src/views/login.html @@ -17,7 +17,7 @@
- Нет аккаунта? Зарегистрируйте. + Нет аккаунта? Зарегистрируйте. \ No newline at end of file diff --git a/src/views/registration.html b/src/views/registration.html index fb47b00..4948ddf 100644 --- a/src/views/registration.html +++ b/src/views/registration.html @@ -8,14 +8,19 @@


+

+

+

+

+
Уже есть аккаунт? Войдите.