reworks, docs and new api
This commit is contained in:
parent
c4e4b9121e
commit
4dee438cf7
2
db.psql
2
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.
|
||||
);
|
||||
|
|
|
@ -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
|
||||
gateway: 10.5.0.1
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
174
src/index.js
174
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.");
|
||||
});
|
||||
});
|
|
@ -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)))
|
||||
})
|
|
@ -5,6 +5,13 @@
|
|||
<title>index</title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="/api/createChat" method="POST">
|
||||
<label for="toInviteIds">ID пользователей для приглашения через пробел</label><br/>
|
||||
<input type="text" id="toInviteIds" name="toInviteIds"><br/>
|
||||
|
||||
<input type="submit" value="Создать чат">
|
||||
</form>
|
||||
<a href="/api/logout">Выйти</a>
|
||||
</body>
|
||||
<script src="/js/frontend.js"></script>
|
||||
</html>
|
|
@ -17,7 +17,7 @@
|
|||
<input type="text" id="password" name="password"><br/>
|
||||
<input type="submit" value="Войти">
|
||||
</form>
|
||||
Нет аккаунта? <a href="/register">Зарегистрируйте.</a>
|
||||
Нет аккаунта? <a href="/registration">Зарегистрируйте.</a>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
|
@ -8,14 +8,19 @@
|
|||
<form onSubmit="return checkPassword(this)" method="post" action="/api/register">
|
||||
<label for="lastname">Фамилия:</label><br/>
|
||||
<input type="text" id="lastname" name="lastname"><br/>
|
||||
|
||||
<label for="firstname">Имя:</label><br/>
|
||||
<input type="text" id="firstname" name="firstname"><br/>
|
||||
|
||||
<label for="middlename">Отчество:</label><br/>
|
||||
<input type="text" id="middlename" name="middlename"><br/>
|
||||
|
||||
<label for="password">Пароль:</label><br/>
|
||||
<input type="text" id="password" name="password"><br/>
|
||||
|
||||
<label for="confirmPassword">Повтор пароля:</label><br/>
|
||||
<input type="text" id="confirmPassword" name="confirmPassword"><br/>
|
||||
|
||||
<input type="submit" value="Зарегистрироваться">
|
||||
</form>
|
||||
Уже есть аккаунт? <a href="/login">Войдите.</a>
|
||||
|
|
Loading…
Reference in New Issue