a lot of changes in back and front ;)

This commit is contained in:
leca 2024-06-21 02:33:43 +03:00
parent 6cb2bea05b
commit a500916712
7 changed files with 471 additions and 119 deletions

View File

@ -2,10 +2,10 @@ const express = require('express');
const bcrypt = require('bcrypt');
const cors = require("cors");
const WebSocket = require('ws');
const http = require('http')
const http = require('http');
const session = require('express-session');
clients = []; // websocket clients
sessions = {}; // logged in clients, there are stored their tokens.
sessions = {}; // logged in clients, there are stored objects like {"token": "some_token_here", "socket": here-is-websocket-object}
const middleware = require("./middleware");
const utils = require("./utils")
@ -20,21 +20,40 @@ const httpServer = http.createServer(app).listen(PORT, "0.0.0.0", () => {
console.log("[LOG] Ready to use.");
});
const ws = new WebSocket.Server({
server: httpServer,
host: "0.0.0.0"
const wss = new WebSocket.WebSocketServer({ clientTracking: false, noServer: true });
const onSocketError = (err) => {
console.error(err);
}
httpServer.on('upgrade', (request, socket, head) => {
socket.on('error', onSocketError);
sessionParser(request, {}, () => {
if (!request.session.token) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
socket.removeListener('error', onSocketError);
wss.handleUpgrade(request, socket, head, function (ws) {
wss.emit('connection', ws, request);
});
});
});
console.log("[LOG] Socket has been started");
wss.on('connection', function (ws, request) {
const token = request.session.token;
ws.on('connection', (wsclient) => {
clients.push(wsclient)
sessions[token].socket = ws;
ws.on('error', console.error);
/*
Package structure:
{
"action": "...", // the request
"token": "...", // the token that is the key for sessions dictionary
"content": ..., // the rest content of an action.
}
@ -42,51 +61,91 @@ ws.on('connection', (wsclient) => {
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) {
ws.on('message', async (message) => {
message = JSON.parse(message);
switch (message.action) {
case "message": {
let userId = sessions[jsonMsg.token];
console.log(`${userId} has sent a message: ${jsonMsg.content.text} to ${jsonMsg.content.chatId}`);
let chatId = message.content.chatId;
let text = message.content.text;
let userId = sessions[token].userId
let r = await client.query("SELECT ID FROM Chats WHERE ID = $1", [chatId]);
if (r.rowCount == 0) { // no such chat exist
sessions[token].socket.send(`No chat with ID ${chatId} exist.`);
break;
}
default:
console.log(`Package cannot be understood: ${msg}`)
if ((await client.query("SELECT ID FROM Users WHERE ID = $1 AND $2 = ANY(chats)", [userId, chatId])).rowCount == 0) {
ws.send("You are not a member of this chat.");
}
//Inserting a message into database.
let messageId = (await client.query("INSERT INTO Messages (author_id, time_sent, content) VALUES($1, NOW(), $2) RETURNING ID", [userId, text])).rows[0].id;
await client.query("UPDATE Chats SET messages = ARRAY_APPEND(messages, $1) WHERE ID = $2", [messageId, chatId])
// sessions[token].socket.send("successful");
//Sending a message to everyone who is in the group.
let membersIds = (await client.query("SELECT ID FROM Users WHERE $1 = ANY(chats)", [chatId])).rows
let Ids = [];
for (let mId of membersIds) {
Ids.push(mId.id)
}
// find all sessions by IDs and send to their sockets
for (const [key, value] of Object.entries(sessions)) {
let session = value;
if (session == undefined) continue
if (session.userId == userId) continue; // Skip the sender of the message
if (Ids.includes(session.userId)) {
session.socket.send(JSON.stringify({
action: "message",
content: {
chatId: chatId,
authorId: userId,
text: text
}
}));
}
}
break;
}
default: {
console.log(`[ERROR] Package cannot be understood: ${JSON.stringify(message)}`);
sessions[token].socket.send("Package cannot be understood.")
}
} catch (e) {
console.log(`[ERROR] in receiving message by websocket: ${e}`)
}
});
wsclient.on('close', () => {
clients = clients.filter(c => c !== wsclient)
})
})
ws.on('close', function () {
// sessions[token].socket = undefined
});
});
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(require('express-session')({
console.log("[LOG] Socket has been started");
const sessionParser = session({
name: 'smk_chat',
secret: 'PLEASE!!GENERATE!!A!!STRONG!!ONE!!',
resave: false,
saveUninitialized: false
});
}));
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(sessionParser);
app.use('/js', express.static(__dirname + "/js"));
//======================frontend===========================
app.get('/', middleware.requireToBeLoggedIn, (req, res) => {
res.sendFile('views/index.html', { root: __dirname });
});
@ -99,27 +158,102 @@ app.get('/login', middleware.requireToBeNotLoggedIn, (req, res) => {
res.sendFile('views/login.html', { root: __dirname });
});
app.get('/chat/:chatId', middleware.requireToBeLoggedIn, (req, res) => {
res.sendFile('views/chat.html', { root: __dirname })
});
//======================backend===========================
//IN: userId (in route)
//OUT: lastname, firstname, middlename
//Return credentials of the user whose id is userId.
//Return -1 if no such user exist.
//Requires client to be logged in.
app.get('/api/getCredentialsById/:id', middleware.requireToBeLoggedIn, async (req, res) => {
try {
const userId = req.params.id;
let user = (await client.query("SELECT lastname, firstname, middlename FROM Users WHERE ID = $1", [userId])).rows[0];
if (user == undefined) return res.send("-1").status(400).end();
return res.send(user).status(200).end();
} catch (e) {
console.log(`[ERROR] in /api/getCredentialsById/${req.params.id}}: ${e}`)
res.status(500).send();
}
});
//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', middleware.requireToBeLoggedIn, async (req, res) => {
try {
const { lastname, firstname, middlename } = req.body;
return res.send(await utils.getIdByCredentials(lastname, firstname, middlename)).end();
} catch (e) {
console.log(`[ERROR] in /api/getIdByCredentials: ${e}`)
res.status(500).send();
}
});
//IN: chatId, amount
//OUT: array of objects that represent a message, like {"author_id": ..., time_sent: ..., content: ...}
//Returns an array of last ${amount} ordered by date of described objects if everything is okay
//Returns HTTP/1.1 400 "Chat with id ${ID} does not exist." if no chat with supplied id does not exist.
//Returns HTTP/1.1 403 "You are not a member of this chat" if the statement is true ;)
//Requires client to be logged in.
app.post('/api/getMessagesFromChat/by-amount', middleware.requireToBeLoggedIn, async (req, res) => {
try {
const userId = sessions[req.session.token].userId;
const { chatId, amount } = req.body;
let messages = []
if ((await client.query("SELECT ID FROM Users WHERE ID = $1 AND $2 = ANY(Chats)", [userId, chatId]).rowCount == 0)) {
return res.status(403).send("You are not a member of this chat");
}
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 result = await client.query("SELECT messages FROM Chats WHERE ID = $1 LIMIT $2", [chatId, amount])
let messagesIds = result.rows[0].messages;
if (messagesIds == null || messagesIds == undefined) return res.send("").status(200).end();
let selectedMessagesIds = messagesIds.slice(-amount);
if (selectedMessagesIds == null || selectedMessagesIds == undefined) return res.send("").status(200).end();
for (let id of selectedMessagesIds) {
let message = (await client.query("SELECT * FROM messages WHERE ID = $1 ORDER BY time_sent DESC LIMIT $2", [id, amount])).rows[0];
messages.push(message)
}
return res.send(messages).status(200).end();
} catch (e) {
console.log(`[ERROR] in /api/getMessagesFromChat/by-amount: ${e}`)
res.status(500).send();
}
});
//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.
//Returns an array of described objects if everything is okay
//Returns HTTP/1.1 400 "Chat with id ${ID} does not exist." if no chat with supplied id does not exist.
//Returns HTTP/1.1 200 and an empty string if no messages in chat are found /shrug.
//Requires client to be logged in.
app.post('/api/getMessagesFromChat', middleware.requireToBeLoggedIn, async (req, res) => {
const userId = sessions[req.session.token];
app.post('/api/getMessagesFromChat/by-time', middleware.requireToBeLoggedIn, async (req, res) => {
try {
const userId = sessions[req.session.token].userId;
const { chatId, fromTimestamp, toTimestamp } = req.body;
let messages = []
@ -128,12 +262,20 @@ app.post('/api/getMessagesFromChat', middleware.requireToBeLoggedIn, async (req,
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;
let result = await client.query("SELECT messages FROM Chats WHERE ID = $1", [chatId])
let messagesIds = result.rows[0].messages;
if (messagesIds == null) return res.send("").status(200).end();
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];
if (message == undefined) continue
messages.push(message)
}
return res.send(messages).status(200).end();
} catch (e) {
console.log(`[ERROR] in /api/getMessagesFromChat/by-time: ${e}`);
res.status(500).send();
}
});
@ -142,25 +284,75 @@ app.post('/api/getMessagesFromChat', middleware.requireToBeLoggedIn, async (req,
//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', middleware.requireToBeLoggedIn, async (req, res) => {
const userId = sessions[req.session.token]
try {
const userId = sessions[req.session.token].userId
let chats = (await client.query("SELECT chats FROM Users WHERE ID = $1", [userId])).rows[0].chats
return res.send(chats).status(200).end();
} catch (e) {
console.log(`[ERROR] in /api/getChats: ${e}`)
res.status(500).send();
}
});
//IN: chatId (in route)
//OUT: JSON describes a chat with id:
//{name: ..., admins: ..., members: ...}
//Note: JSON does not include messages. You have to use /api/getMessagesFromChat to query messages.
//Returns aforementioned json if the chat exists and user is a member of this chat.
//Returns -1 if chat does not exist
//Return -2 if user is not a member of the chat
app.get('/api/getChatInfo/:chatId', middleware.requireToBeLoggedIn, async (req, res) => {
try {
const chatId = req.params.chatId;
if (Number(chatId) != chatId) return res.send("-1").status(400).end();
let chatRequest = await client.query("SELECT name, admins FROM Chats WHERE ID = $1::INT", [chatId]);
if (chatRequest.rowCount == 0) return res.send("-1").status(400).end();
let membersRequest = await client.query("SELECT ID FROM Users WHERE $1 = ANY(chats)", [chatId]);
let members = [];
membersRequest.rows.forEach((member, index, membersRequest) => {
members.push(member.id)
});
// console.log(members)
// console.log(sessions[req.session.token])
if (!members.includes(sessions[req.session.token].userId)) {
return res.send("-2").status(403).end();
}
let chatInfo = {
name: chatRequest.rows[0].name,
admins: chatRequest.rows[0].admins,
members: members
};
return res.send(JSON.stringify(chatInfo)).status(200).end();
} catch (e) {
console.log(`[ERROR] in /api/getChatInfo/${req.params.chatId}: ${e}`);
res.status(500).send();
}
});
//IN: UserId, array of UserIDs that are to be invited.
//OUT: "Ok" if succsessful, "User with id ${MEMBERID} does not exist."
//OUT: "Ok" if successful, "User with id ${MEMBERID} does not exist."
//Return -1 if amout of users to invite is 0.
//Requires to be logged in
app.post('/api/createChat', middleware.requireToBeLoggedIn, async (req, res) => {
const userId = sessions[req.session.token]
try {
const userId = sessions[req.session.token].userId
let { toInviteIds } = req.body;
if (toInviteIds == "" || toInviteIds == undefined || toInviteIds == null) return res.send("-1").status(400).end();
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.`)
return res.send(`User with id ${id} does not exist.`).end();
}
});
@ -177,10 +369,16 @@ app.post('/api/createChat', middleware.requireToBeLoggedIn, async (req, res) =>
await client.query("UPDATE Chats SET admins = ARRAY_APPEND(admins, $1) WHERE ID = $2", [userId, chatId]);
toInviteIds.push(userId); // we need to invite the admin, too :)
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")
return res.send("Ok");
} catch (e) {
console.log(`[ERROR] in /api/createChat: ${e}`);
res.status(500).send();
}
});
//IN: none.
@ -188,9 +386,15 @@ app.post('/api/createChat', middleware.requireToBeLoggedIn, async (req, res) =>
//Removes client's session, thus unlogging a user.
//Requires to be logged in.
app.get('/api/logout', middleware.requireToBeLoggedIn, (req, res) => {
try {
sessions[req.session.token] = undefined;
req.session.token = undefined;
res.redirect('/login');
} catch (e) {
console.log(`[ERROR] in /api/logout: ${e}`)
res.status(500).send();
}
});
//IN: lastname, firstname, middlename, password.
@ -214,7 +418,10 @@ app.post('/api/register', middleware.requireToBeNotLoggedIn, async (req, res) =>
[lastname, firstname, middlename, hash]
)).rows[0].id;
req.session.token = utils.generateRandomString();
sessions[req.session.token] = id;
sessions[req.session.token] = {
userId: id,
socket: undefined
};
res.redirect('/');
} catch (err) {
console.log(`[ERROR] in /api/register: ${err}`)
@ -240,7 +447,10 @@ app.post('/api/login', middleware.requireToBeNotLoggedIn, async (req, res) => {
if (bcrypt.compareSync(password, stored_password)) {
req.session.token = utils.generateRandomString()
sessions[req.session.token] = ID;
sessions[req.session.token] = {
userId: ID,
socket: undefined
};
return res.redirect('/');
} else {
return res.status(400).send("Wrong password").end();
@ -249,5 +459,4 @@ app.post('/api/login', middleware.requireToBeNotLoggedIn, async (req, res) => {
console.log(`[ERROR] in /api/login: ${err}`)
res.status(500).send();
}
});

70
src/js/chat.js Normal file
View File

@ -0,0 +1,70 @@
const sendMessage = (event) => {
if (event.key == "Enter") {
console.log(event);
}
}
const createMessageElement = async (message) => {
let author = JSON.parse(await fetch(`/api/getCredentialsById/${message.author_id}`).then(response => response.text()).then((response) => { return response; }))
let messageDiv = document.createElement("div");
messageDiv.id = `message-${message.id}`;
messageDiv.style = "background-color: gray; box-shadow: 1px 1px; margin: 10px; padding: 5px"
let authorName = document.createElement("b");
authorName.innerText = `${author.firstname}:`;
let messageContent = document.createElement("p");
messageContent.innerText = message.content;
messageContent.id = "content";
let messageTime = document.createElement("i");
messageTime.innerText = `@ ${message.time_sent.toString()}`;
messageTime.id = "time_sent";
messageDiv.appendChild(authorName);
messageDiv.appendChild(messageContent);
messageDiv.appendChild(messageTime);
return messageDiv
}
window.addEventListener('load', async function () {
let location = window.location.href.split("/")
const chatId = location[location.length - 1];
let chatInfo = JSON.parse(
await fetch(`/api/getChatInfo/${chatId}`).then(response => response.text()).then(response => { return response })
);
console.log(chatInfo)
let header = document.getElementById('header-text');
header.innerText = chatInfo.name;
let content = document.getElementById('content-div')
let today = new Date();
let yesterday = new Date();
yesterday.setUTCDate(today.getUTCDate() - 10);
console.log(today.toISOString(), yesterday.toISOString())
// fetching messages from yesterday to today
let messages = await fetch("/api/getMessagesFromChat/by-time", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
chatId: chatId,
fromTimestamp: yesterday.toISOString(),
toTimestamp: today.toISOString()
})
}).then(response => response.text()).then((response) => { return response; })
messages = eval(messages)
for (let message of messages) {
content.appendChild(await createMessageElement(message));
}
console.log(messages)
});

View File

@ -1,35 +0,0 @@
let socket
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}:${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": "message",
"token": getCookie("token"),
"content": {
"chatId": 1,
"text": "test"
}
}
));
})
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)))
})

84
src/js/index.js Normal file
View File

@ -0,0 +1,84 @@
let socket
window.addEventListener('load', async function () {
let connectionString = location.protocol == "https:" ? `wss://${window.location.hostname}:${window.location.port}` : `ws://${window.location.hostname}:${window.location.port}`
socket = new WebSocket(connectionString)
socket.addEventListener('open', (e) => {
socket.send(JSON.stringify(
{
"action": "message",
"content": {
"chatId": 1,
"text": "test"
}
}
));
})
socket.addEventListener('message', (m) => {
if (m.data == "successful") return;
const data = JSON.parse(m.data)
switch (data.action) {
case "message": {
console.log(`New message in chat ${data.content.chatId} from ${data.content.authorId}, which says '${data.content.text}'`)
break;
}
}
})
let chatsIds = await fetch("/api/getChats").then(response => response.text()).then((response) => {
return response
})
let chats = []; // chats with their info
chatsIds = eval(chatsIds)
for (let chat of chatsIds) {
let chatInfo = await fetch(`/api/getChatInfo/${chat}`).then(response => response.text()).then(response => { return response });
chatInfo = JSON.parse(chatInfo)
chatInfo.id = chat
let lastMessage = await fetch(`/api/getMessagesFromChat/by-amount`, {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ chatId: chat, amount: 1 })
}).then(response => response.text()).then((response) => { return response });
if (lastMessage == undefined || lastMessage == "") {
chatInfo.lastMessage = {
id: "",
author_id: "",
time_sent: "",
content: ""
}
chats.push(chatInfo)
continue
}
chatInfo.lastMessage = JSON.parse(lastMessage)[0]
chats.push(chatInfo);
}
let chatsContainer = this.document.getElementById('chats');
for (let chat of chats) {
let chatHTML = `
<div class=\"chat-entry\" name=\"${chat.name}\">
<p>
<a href=\"/chat/${chat.id}\">${chat.name}</a> | ${chat.lastMessage.author_id}: ${chat.lastMessage.content} ${chat.lastMessage.time_sent}
</p>
</div>
`;
chatsContainer.innerHTML += chatHTML;
}
fetch("/api/getMessagesFromChat/by-time", {
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)))
})

View File

@ -6,7 +6,8 @@ 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.
// host: "10.5.0.6", //defined in docker-compose.yml.
host: 'localhost',
port: 5432,
database: "chat"
});

20
src/views/chat.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Chat</title>
</head>
<body>
<div id="header-div">
<b id="header-text"></b>
</div>
<div id="content-div"></div>
<div id="typefield-div">
<input type="text" onkeypress="sendMessage(event)">
</div>
</body>
<script src="/js/chat.js"></script>
</html>

View File

@ -5,6 +5,9 @@
<title>index</title>
</head>
<body>
<div class="chats" name="chats" id="chats">
</div>
<form action="/api/createChat" method="POST">
<label for="toInviteIds">ID пользователей для приглашения через пробел</label><br/>
<input type="text" id="toInviteIds" name="toInviteIds"><br/>
@ -13,5 +16,5 @@
</form>
<a href="/api/logout">Выйти</a>
</body>
<script src="/js/frontend.js"></script>
<script src="/js/index.js"></script>
</html>