basic registration/login/session

This commit is contained in:
leca 2024-06-11 17:22:27 +03:00
parent 44510096d1
commit f3cae4b90f
11 changed files with 2171 additions and 0 deletions

14
Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM node:latest
WORKDIR /usr/src/app
COPY package*.json ./
#RUN apk update && apk upgrade # && apk add --update alpine-sdk && apk add --update python
RUN npm install
# If you are building your code for production
# RUN npm ci --omit=dev
COPY . .
EXPOSE 8080
CMD [ "node", "src/index.js" ]

23
db.psql Normal file
View File

@ -0,0 +1,23 @@
CREATE TABLE IF NOT EXISTS Users (
ID SERIAL,
lastname VARCHAR(32),
firstname VARCHAR(32),
middlename VARCHAR(32),
password_hash CHAR(60), --nodejs bcrypt.
salt CHAR(29), -- nodejs bcrypt.
chats INT[] -- to table Chats, column ID.
);
CREATE TABLE IF NOT EXISTS Chats (
ID SERIAL,
name VARCHAR(32), --chat name
admins INT[], -- to table Users, column ID.
messages INT[] -- ref to table Messages, column ID.
);
CREATE TABLE IF NOT EXISTS Messages (
ID SERIAL,
author_id INT, -- ref to table Users, column ID.
time_sent TIMESTAMP,
content TEXT
);

33
docker-compose.yml Normal file
View File

@ -0,0 +1,33 @@
version: "3.3"
services:
chat:
build: .
ports:
- 8080:8080
networks:
ne_nuzhen:
ipv4_address: 10.5.0.5
depends_on:
- db
db:
image: 'postgres:15'
ports:
- 5432:5432
networks:
ne_nuzhen:
ipv4_address: 10.5.0.6
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)
POSTGRES_DB: chat # The PostgreSQL default database (automatically created at first launch)
networks:
ne_nuzhen:
driver: bridge
ipam:
config:
- subnet: 10.5.0.0/16
gateway: 10.5.0.1

1788
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "chat",
"version": "1.0.0",
"description": "",
"main": "test.js",
"scripts": {
"test": "nodemon test.js",
"dev": "nodemon src/index.js"
},
"repository": {
"type": "git",
"url": "https://git.foxarmy.org/leca/smk-chat"
},
"author": "leca",
"license": "WTFPL",
"dependencies": {
"bcrypt": "^5.1.1",
"cookie-session": "^2.1.0",
"cors": "^2.8.5",
"express": "^4.19.2",
"express-session": "^1.18.0",
"nodemon": "^3.1.3",
"pg": "^8.12.0"
}
}

88
schema.drawio Normal file
View File

@ -0,0 +1,88 @@
<mxfile host="app.diagrams.net" modified="2024-06-11T11:00:11.476Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" etag="D0Ei79GTjtOxs9i7zUn2" version="24.5.2" type="device">
<diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">
<mxGraphModel dx="1279" dy="748" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="WIyWlLk6GJQsqaUBKTNV-0" />
<mxCell id="WIyWlLk6GJQsqaUBKTNV-1" parent="WIyWlLk6GJQsqaUBKTNV-0" />
<mxCell id="GfNOIcLhO2e9G-0w8RP9-0" value="Users" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry y="80" width="240" height="240" as="geometry" />
</mxCell>
<mxCell id="GfNOIcLhO2e9G-0w8RP9-1" value="ID SERIAL" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" parent="GfNOIcLhO2e9G-0w8RP9-0" vertex="1">
<mxGeometry y="30" width="240" height="30" as="geometry" />
</mxCell>
<mxCell id="GfNOIcLhO2e9G-0w8RP9-2" value="lastname VARCHAR(32)" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" parent="GfNOIcLhO2e9G-0w8RP9-0" vertex="1">
<mxGeometry y="60" width="240" height="30" as="geometry" />
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-1" value="firstname VARCHAR(32)" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="GfNOIcLhO2e9G-0w8RP9-0">
<mxGeometry y="90" width="240" height="30" as="geometry" />
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-2" value="middlename VARCHAR(32)" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="GfNOIcLhO2e9G-0w8RP9-0">
<mxGeometry y="120" width="240" height="30" as="geometry" />
</mxCell>
<mxCell id="GfNOIcLhO2e9G-0w8RP9-3" value="password_hash CHAR(60)" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" parent="GfNOIcLhO2e9G-0w8RP9-0" vertex="1">
<mxGeometry y="150" width="240" height="30" as="geometry" />
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-0" value="salt CHAR(29)" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="GfNOIcLhO2e9G-0w8RP9-0">
<mxGeometry y="180" width="240" height="30" as="geometry" />
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-4" value="chats INT[]" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="GfNOIcLhO2e9G-0w8RP9-0">
<mxGeometry y="210" width="240" height="30" as="geometry" />
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-5" value="Chats" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" vertex="1" parent="WIyWlLk6GJQsqaUBKTNV-1">
<mxGeometry x="687" y="110" width="140" height="150" as="geometry" />
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-6" value="ID SERIAL" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="5QsmdMTZOQ-KcOoPIG_9-5">
<mxGeometry y="30" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-7" value="name VARCHAR(32)" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="5QsmdMTZOQ-KcOoPIG_9-5">
<mxGeometry y="60" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-21" value="admins&lt;span style=&quot;background-color: initial;&quot;&gt;&amp;nbsp;INT[]&lt;/span&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="5QsmdMTZOQ-KcOoPIG_9-5">
<mxGeometry y="90" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-8" value="messages INT[]" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="5QsmdMTZOQ-KcOoPIG_9-5">
<mxGeometry y="120" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-9" value="Messages" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" vertex="1" parent="WIyWlLk6GJQsqaUBKTNV-1">
<mxGeometry x="360" y="360" width="240" height="150" as="geometry" />
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-10" value="ID SERIAL" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="5QsmdMTZOQ-KcOoPIG_9-9">
<mxGeometry y="30" width="240" height="30" as="geometry" />
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-11" value="author_id INT" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="5QsmdMTZOQ-KcOoPIG_9-9">
<mxGeometry y="60" width="240" height="30" as="geometry" />
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-12" value="time_sent TIMESTAMP" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="5QsmdMTZOQ-KcOoPIG_9-9">
<mxGeometry y="90" width="240" height="30" as="geometry" />
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-23" value="content TEXT" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="5QsmdMTZOQ-KcOoPIG_9-9">
<mxGeometry y="120" width="240" height="30" as="geometry" />
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-17" value="" style="edgeStyle=entityRelationEdgeStyle;fontSize=12;html=1;endArrow=ERzeroToMany;startArrow=ERmandOne;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="WIyWlLk6GJQsqaUBKTNV-1" source="5QsmdMTZOQ-KcOoPIG_9-4" target="5QsmdMTZOQ-KcOoPIG_9-6">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="400" y="370" as="sourcePoint" />
<mxPoint x="500" y="270" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-20" value="" style="edgeStyle=entityRelationEdgeStyle;fontSize=12;html=1;endArrow=ERzeroToOne;startArrow=ERmandOne;rounded=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="WIyWlLk6GJQsqaUBKTNV-1" source="5QsmdMTZOQ-KcOoPIG_9-8" target="5QsmdMTZOQ-KcOoPIG_9-10">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="160" y="500" as="sourcePoint" />
<mxPoint x="260" y="400" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-22" value="" style="edgeStyle=entityRelationEdgeStyle;fontSize=12;html=1;endArrow=ERzeroToMany;startArrow=ERmandOne;rounded=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="WIyWlLk6GJQsqaUBKTNV-1" source="5QsmdMTZOQ-KcOoPIG_9-21" target="GfNOIcLhO2e9G-0w8RP9-1">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="505" y="550" as="sourcePoint" />
<mxPoint x="815" y="440" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="5QsmdMTZOQ-KcOoPIG_9-24" value="" style="edgeStyle=entityRelationEdgeStyle;fontSize=12;html=1;endArrow=ERzeroToOne;startArrow=ERmandOne;rounded=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="WIyWlLk6GJQsqaUBKTNV-1" source="5QsmdMTZOQ-KcOoPIG_9-11" target="GfNOIcLhO2e9G-0w8RP9-1">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="150" y="480" as="sourcePoint" />
<mxPoint x="250" y="380" as="targetPoint" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

131
src/index.js Normal file
View File

@ -0,0 +1,131 @@
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 app = express();
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"
})
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',
resave: false,
saveUninitialized: false
}));
let sessions = {}
app.get('/', (req, res) => {
if (sessions[req.session.token] == undefined) return res.redirect('/login');
res.sendFile('views/index.html', { root: __dirname });
});
app.get('/registration', (req, res) => {
if (req.session.token != undefined) return res.redirect('/');
res.sendFile('views/registration.html', { root: __dirname });
});
app.get('/login', (req, res) => {
if (sessions[req.session.token] != undefined) return res.redirect('/');
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;
}
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) => {
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) {
return res.status(400).send("Such user exists.").end();
}
let id = (await client.query(
"INSERT INTO Users (lastname, firstname, middlename, password_hash, salt) VALUES ($1, $2, $3, $4, $5) RETURNING ID;",
[lastname, firstname, middlename, hash, salt]
)).rows[0].id;
req.session.token = generateRandomString();
sessions[req.session.token] = id;
res.redirect('/');
} catch (err) {
console.log("[ERROR] in /api/register: " + err)
res.status(500).send();
}
});
app.post('/api/login', async (req, res) => {
try {
const { lastname, firstname, middlename, password } = req.body;
const ID = await getIdByCredentials(lastname, firstname, middlename)
if (ID == -1) {
return res.status(400).send("No such user.").end();
}
let stored_password = (await client.query("SELECT password_hash FROM Users WHERE ID = $1;", [ID])).rows[0].password_hash;
if (bcrypt.compareSync(password, stored_password)) {
req.session.token = generateRandomString()
sessions[req.session.token] = ID;
return res.redirect('/');
} else {
return res.status(400).send("Wrong password").end();
}
} catch (err) {
console.log("[ERROR] in /api/login: " + err)
res.status(500).send();
}
});
const initDb = async () => {
await client.connect()
let db_schema = fs.readFileSync('./db.psql').toString();
try {
const res = await client.query(db_schema);
console.log("Database initialized.")
} catch (err) {
console.log("Cannot initialize database. Error: " + err);
}
}
initDb().then(() => {
app.listen(8080, "0.0.0.0", () => {
console.log("Ready to use.");
});
});

10
src/views/index.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<a href="/api/logout">Выйти</a>
</body>
</html>

23
src/views/login.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<body>
<form method="post" action="/api/login">
<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/>
<input type="submit" value="Войти">
</form>
Нет аккаунта? <a href="/register">Зарегистрируйте.</a>
</body>
</body>
</html>

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>registration</title>
</head>
<body>
<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>
</body>
<script>
function checkPassword(form) {
const password = form.password.value;
const confirmPassword = form.confirmPassword.value;
if (password != confirmPassword) {
alert("Error! Password did not match.");
return false;
}
return true;
}
</script>
</html>

0
test.js Normal file
View File