Compare commits

...

10 Commits

Author SHA1 Message Date
ce72dd40a8 added cors 2023-11-18 14:12:46 +03:00
0b3a178715 changed /api/getPosts 2023-11-17 09:16:05 +03:00
d61797a748 rework of the DB structure, rewrite of the API 2023-11-17 01:17:39 +03:00
82b1a82aac remove unused column 2023-11-17 01:17:09 +03:00
5b1307a662 added options to post 2023-11-16 20:33:38 +03:00
8f1461ce2b changed icon 2023-11-13 12:55:30 +03:00
6195f80c4a changed icon 2023-11-13 12:49:47 +03:00
fce1a6aa71 changed icon 2023-11-13 12:48:59 +03:00
934f8960cd Delete unused component 2023-11-13 08:32:44 +03:00
e55b42fdac Add favicon ico 2023-11-12 10:57:01 +03:00
9 changed files with 196 additions and 87 deletions

View File

@@ -1,6 +1,6 @@
<mxfile host="Electron" modified="2023-11-04T11:34:00.376Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/22.0.2 Chrome/114.0.5735.289 Electron/25.8.4 Safari/537.36" etag="zK0hocMrlzpD75Dksgz9" version="22.0.2" type="device">
<mxfile host="Electron" modified="2023-11-16T17:33:19.287Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/22.0.2 Chrome/114.0.5735.289 Electron/25.8.4 Safari/537.36" etag="ZbU5Z7KAwFtN-KRkF2Te" version="22.0.2" type="device">
<diagram id="R2lEEEUBdFMjLlhIrx00" name="Page-1">
<mxGraphModel dx="1669" dy="479" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0" extFonts="Permanent Marker^https://fonts.googleapis.com/css?family=Permanent+Marker">
<mxGraphModel dx="1645" dy="474" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0" extFonts="Permanent Marker^https://fonts.googleapis.com/css?family=Permanent+Marker">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
@@ -62,21 +62,21 @@
<mxRectangle width="220" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="lzMxBIVNoJ9j-Thf5CWH-1" value="" style="shape=partialRectangle;collapsible=0;dropTarget=0;pointerEvents=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" vertex="1" parent="C-vyLk0tnHw3VtMMgP7b-23">
<mxCell id="lzMxBIVNoJ9j-Thf5CWH-1" value="" style="shape=partialRectangle;collapsible=0;dropTarget=0;pointerEvents=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="C-vyLk0tnHw3VtMMgP7b-23" vertex="1">
<mxGeometry y="120" width="250" height="30" as="geometry" />
</mxCell>
<mxCell id="lzMxBIVNoJ9j-Thf5CWH-2" value="" style="shape=partialRectangle;overflow=hidden;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;" vertex="1" parent="lzMxBIVNoJ9j-Thf5CWH-1">
<mxCell id="lzMxBIVNoJ9j-Thf5CWH-2" value="" style="shape=partialRectangle;overflow=hidden;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;" parent="lzMxBIVNoJ9j-Thf5CWH-1" vertex="1">
<mxGeometry width="30" height="30" as="geometry">
<mxRectangle width="30" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="lzMxBIVNoJ9j-Thf5CWH-3" value="settings JSON NOT NULL" style="shape=partialRectangle;overflow=hidden;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;" vertex="1" parent="lzMxBIVNoJ9j-Thf5CWH-1">
<mxCell id="lzMxBIVNoJ9j-Thf5CWH-3" value="settings JSON NOT NULL" style="shape=partialRectangle;overflow=hidden;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;" parent="lzMxBIVNoJ9j-Thf5CWH-1" vertex="1">
<mxGeometry x="30" width="220" height="30" as="geometry">
<mxRectangle width="220" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="IqcACfFecUyFIL0cjSVW-16" value="post" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;" parent="1" vertex="1">
<mxGeometry x="470" y="290" width="250" height="210" as="geometry" />
<mxGeometry x="470" y="282" width="250" height="240" as="geometry" />
</mxCell>
<mxCell id="IqcACfFecUyFIL0cjSVW-17" value="" style="shape=partialRectangle;collapsible=0;dropTarget=0;pointerEvents=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" parent="IqcACfFecUyFIL0cjSVW-16" vertex="1">
<mxGeometry y="30" width="250" height="30" as="geometry" />
@@ -156,6 +156,19 @@
<mxRectangle width="220" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="Z5fAuwPoposVQ_Fsqkp6-1" value="" style="shape=partialRectangle;collapsible=0;dropTarget=0;pointerEvents=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" vertex="1" parent="IqcACfFecUyFIL0cjSVW-16">
<mxGeometry y="210" width="250" height="30" as="geometry" />
</mxCell>
<mxCell id="Z5fAuwPoposVQ_Fsqkp6-2" value="" style="shape=partialRectangle;overflow=hidden;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;" vertex="1" parent="Z5fAuwPoposVQ_Fsqkp6-1">
<mxGeometry width="30" height="30" as="geometry">
<mxRectangle width="30" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="Z5fAuwPoposVQ_Fsqkp6-3" value="options VARCHAR(255)" style="shape=partialRectangle;overflow=hidden;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;" vertex="1" parent="Z5fAuwPoposVQ_Fsqkp6-1">
<mxGeometry x="30" width="220" height="30" as="geometry">
<mxRectangle width="220" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="IqcACfFecUyFIL0cjSVW-23" value="" style="edgeStyle=entityRelationEdgeStyle;fontSize=12;html=1;endArrow=ERoneToMany;rounded=0;exitX=0.998;exitY=0.676;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="IqcACfFecUyFIL0cjSVW-12" target="C-vyLk0tnHw3VtMMgP7b-3" edge="1">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="440" y="270" as="sourcePoint" />

View File

@@ -1,17 +1,18 @@
CREATE TABLE boards (
board_id VARCHAR(5) PRIMARY KEY,
board_name VARCHAR(32) NOT NULL,
threads_ids INT8[],
options JSON
);
CREATE TABLE threads (
thread_id SERIAL8 PRIMARY KEY,
board_id VARCHAR(5) NOT NULL,
thread_id BIGINT NOT NULL,
thread_name VARCHAR(32),
posts_ids BIGINT[],
is_locked boolean NOT NULL,
is_pinned boolean NOT NULL,
options VARCHAR(255)
options VARCHAR(255),
PRIMARY KEY(board_id, thread_id)
);
CREATE TABLE media (
@@ -19,12 +20,16 @@ CREATE TABLE media (
);
CREATE TABLE posts (
post_id SERIAL8 PRIMARY KEY,
board_id VARCHAR(5) NOT NULL,
thread_id BIGINT NOT NULL,
post_id BIGINT NOT NULL,
options VARCHAR(255),
content TEXT NOT NULL,
media_ids VARCHAR(22)[],
is_root BOOL NOT NULL,
timestamp TIMESTAMP NOT NULL,
user_ip CIDR NOT NULL
user_ip CIDR NOT NULL,
PRIMARY KEY(board_id, thread_id, post_id)
);
CREATE TABLE admins (

View File

@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<link rel="icon" href="/favicon.svg">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<title>Dachan</title>
</head>
<body>
<div id="app"></div>

21
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@vitejs/plugin-vue": "^4.4.1",
"axios": "^1.6.1",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"ejs": "^3.1.9",
"express": "^4.18.2",
@@ -779,6 +780,18 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/csstype": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
@@ -1531,6 +1544,14 @@
"node": ">=0.10.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",

View File

@@ -7,7 +7,8 @@
"dev": "nodemon src/index.js & vite --host",
"build": "vite build",
"preview": "vite preview",
"start": "node src/index.js & vite --host"
"start": "node src/index.js & vite --host",
"back": "nodemon src/index.js"
},
"repository": {
"type": "git",
@@ -22,6 +23,7 @@
"@vitejs/plugin-vue": "^4.4.1",
"axios": "^1.6.1",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"ejs": "^3.1.9",
"express": "^4.18.2",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

80
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -1,49 +0,0 @@
<script setup>
</script>
<template>
<header>
<div class="headerIneer">
<div class="headerLogo"><h1 class="headerLogoTitle">Dach</h1></div>
<div class="headerFaq"><a class="headerFaqTitle" href="faq.html">FAQ</a></div>
</div>
</header>
</template>
<style scoped>
header {
display: flex;
flex: 0 0 100%;
min-width: 100vh;
}
.headerIneer{
display: flex;
width: 100%;
border-radius: 1em;
background: #171515fb; /* fallback for old browsers */
}
.headerLogo{
height: 100%;
margin: 0 5%;
}
.headerLogoTitle{
font-family: 'Tilt Neon', sans-serif;
font-size: 2.5em;
color: whitesmoke;
}
.headerFaq{
margin: 2%;
min-height: 2em;
margin-left: auto;
margin-right: 5em;
}
.headerFaqTitle{
font-family: 'Tilt Neon', sans-serif;
font-size: 1.5em;
text-decoration: none;
color: whitesmoke;
}
</style>

View File

@@ -6,6 +6,7 @@ const MemoryStore = require('memorystore')(session);
const fs = require('fs');
const bcrypt = require('bcryptjs');
// const fileupload = require('express-fileupload');
const cors = require('cors');
const app = express();
@@ -50,7 +51,7 @@ let tokens = {};
app.use(express.urlencoded({ extended: true }))
app.use(express.json())
app.use(cors());
app.use(session({
secret: process.env.SESSION_SECRET,
@@ -67,51 +68,82 @@ app.post('/api/uploadMedia', async (req, res) => {
});
app.get('/api/getPosts/:boardId/:threadId', async (req, res) => {
posts = [];
(await db.query('SELECT post_id, content, timestamp, options FROM posts WHERE board_id = $1 AND thread_id = $2', [req.params.boardId, req.params.threadId])).rows
.forEach((post) => posts.push(post))
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(posts));
});
app.post('/api/post', async (req, res) => {
const { content, } = req.body;
let login = req.session.login,
token = req.session.token,
isAdmin = false;
const {options, content, threadId, boardId} = req.body;
if (!threadId || !boardId) return res.status(400).send("Не указано ID треда или доски");
if (login && token) {
if (authorize(login, token)) isAdmin = true;
else res.status(403).send("Невалидный токен");
}
let postId = Number((await db.query('SELECT post_id FROM posts WHERE board_id = $1 ORDER BY post_id DESC LIMIT 1', [boardId])).rows[0].post_id) + 1
await db.query('INSERT INTO posts(board_id, thread_id, post_id, options, content, media_ids, is_root, timestamp, user_ip) VALUES ($1, $2, $3, $4, $5, \'{}\', false, NOW(), $6)', [boardId, threadId, postId, options, content, req.socket.remoteAddress]);
await db.query('UPDATE threads SET posts_ids = ARRAY_APPEND(posts_ids, $1) WHERE thread_id = $2 AND board_id = $3', [postId, threadId, boardId]);
res.status(200).send("Пост отправлен");
});
app.post('/api/createThread', async (req, res) => {
let login, token, isLocked, isPinned
let login = req.session.login,
token = req.session.token,
isLocked,
isPinned,
isAdmin = false;
const { boardId, threadTitle, content, options} = req.body;
isLocked = isLocked? isLocked : false;
if (!boardId) return res.status(400).send("Не указано имя доски");
isLocked = isLocked? isLocked : false; // if undefined then false
isPinned = isPinned? isPinned : false;
console.log(`Board id: ${boardId}\nThread name: ${threadTitle}\nIs locked: ${isLocked}\nIs pinned: ${isPinned}\nContent: ${content}\nOptions: ${options}`);
try {
let currentSession = req.session;
token = currentSession.token;
login = currentSession.login;
} catch (err) {
console.log(err);
if (login && token) {
if (authorize(login, token)) isAdmin = true;
else res.status(403).send("Невалидный токен");
}
const boardOptions = (await db.query('SELECT options FROM boards WHERE board_id = $1', [boardId])).rows[0].options;
if (login && token && token != tokens[login]) return res.status(403).send("Невалидный токен");
let isAdmin = token? true : false;
const boardOptions = (await db.query('SELECT * FROM boards WHERE board_id = $1', [boardId])).rows[0].options
let postId = (await db.query('SELECT nextval(pg_get_serial_sequence(\'posts\', \'post_id\'))')).rows[0].nextval;
let threadId = (await db.query('SELECT nextval(pg_get_serial_sequence(\'threads\', \'thread_id\'))')).rows[0].nextval;
let threadId = (await db.query('SELECT EXISTS(SELECT FROM threads WHERE board_id = $1)', [boardId])).rows[0].exists?
Number((await db.query('SELECT thread_id FROM threads WHERE board_id = $1 ORDER BY thread_id DESC LIMIT 1', [boardId])).rows[0].thread_id) + 1
: 0
let postId = (await db.query('SELECT EXISTS(SELECT FROM posts WHERE board_id = $1)', [boardId])).rows[0].exists?
Number((await db.query('SELECT post_id FROM posts WHERE board_id = $1 ORDER BY post_id DESC LIMIT 1', [boardId])).rows[0].post_id) + 1
: 0
console.log(`ThreadId: ${threadId} postId: ${postId}`);
let validateResults = validateThread(threadTitle, isLocked,
isPinned, content, options,
boardOptions, isAdmin);
if (validateResults != "ok") return res.status(400).send(validateResults);
await db.query('INSERT INTO posts (post_id, content, is_root, timestamp, user_ip) VALUES($1, $2, $3, NOW(), $4)', [postId, content, true, req.socket.remoteAddress]);
await db.query('INSERT INTO threads (thread_id, thread_name, posts_ids, is_locked, is_pinned, options) VALUES ($1, $2, $3, $4, $5, $6)', [threadId, threadTitle, [postId], isLocked, isPinned, options]);
await db.query('UPDATE boards SET threads_ids = ARRAY_APPEND(threads_ids, $1) WHERE board_id = $2', [threadId, boardId]);
await db.query('INSERT INTO posts (board_id, thread_id, post_id, content, is_root, timestamp, user_ip) VALUES($1, $2, $3, $4, $5, NOW(), $6)', [boardId, threadId, postId, content, true, req.socket.remoteAddress]);
await db.query('INSERT INTO threads (board_id, thread_id, thread_name, posts_ids, is_locked, is_pinned, options) VALUES ($1, $2, $3, $4, $5, $6, $7)', [boardId, threadId, threadTitle, [postId], isLocked, isPinned, options]);
res.redirect(`/${boardId}/${postId}`);
});
app.get('/api/getThreads/:boardId', async (req, res) => {
let queryRes = (await db.query('SELECT * FROM boards WHERE board_id = $1', [req.params.boardId])).rows[0];
threads = [];
(await db.query('SELECT thread_id FROM threads WHERE board_id = $1', [req.params.boardId])).rows
.forEach((thread) => threads.push(thread.thread_id))
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(queryRes.threads_ids));
res.end(JSON.stringify(threads));
});
app.get('/api/getBoards', async (req, res) => {
@@ -119,6 +151,7 @@ app.get('/api/getBoards', async (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(queryRes.rows));
// res.json(JSON.stringify(queryRes.rows));
});
app.post('/api/login', async (req, res) => {
@@ -178,8 +211,12 @@ app.listen(process.env.APP_PORT, () => {
const validateThread = (threadName, isLocked, isPinned, content, options, boardOptions, isAdmin) => {
if ((isPinned || isLocked) && !isAdmin) return "Нет прав на выставление админских флагов";
if (!content) return "Нельзя создать тред без текста";
if (!content && boardOptions.requireContentForThreadCreation) return "Нельзя создать тред без текста";
//TODO: check if image is required
return 'ok'
}
};
const authorize = (login, token) => {
return tokens[login] == token? true : false;
};