backend without proxy

This commit is contained in:
leca 2025-04-25 22:25:41 +03:00
parent c8c347b342
commit ae8cd6e1c1
17 changed files with 2010 additions and 0 deletions

1
.gitignore vendored
View File

@ -130,3 +130,4 @@ dist
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
data

8
Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM node:22-bullseye
ARG APP_PORT
ENV APP_PORT $APP_PORT
COPY . .
RUN npm i
EXPOSE $APP_PORT
ENTRYPOINT ["node", "./src/index.js"]

5
db_schema.psql Normal file
View File

@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS captchas(
id SERIAL PRIMARY KEY,
hash CHAR(32),
solution CHAR(6)
);

25
docker-compose.yml Normal file
View File

@ -0,0 +1,25 @@
services:
captchas:
build: .
env_file: .env
ports:
- ${APP_PORT}:${APP_PORT}
depends_on:
- database
restart: on-failure
volumes:
- ./data/temp:/temp
- ./data/uploads:/uploads
database:
image: 'postgres:15'
volumes:
- ./data/db:/var/lib/postgresql/data
env_file: .env
ports:
- 5432:5432
environment:
POSTGRES_USER: ${DBUSER}
POSTGRES_PASSWORD: ${DBPASS}
POSTGRES_DB: ${DBNAME}
healthcheck:
test: ["CMD", "pg_isready", "-U", "${DBUSER}"]

1725
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": "captcha_agregator",
"version": "1.0.0",
"description": "Site for aggregating captchas and give them to users to solve",
"repository": {
"type": "git",
"url": "https://git.foxarmy.org/leca/captcha_agregator"
},
"license": "GPL-3.0-or-later",
"author": "leca",
"type": "module",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js"
},
"dependencies": {
"dotenv": "^16.5.0",
"express": "^5.1.0",
"pg": "^8.15.5",
"pug": "^3.0.3"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}

0
public/css/index.css Normal file
View File

0
public/js/index.js Normal file
View File

26
public/views/index.pug Normal file
View File

@ -0,0 +1,26 @@
html
head
title Captcha Aggregator
meta(charset="utf-8")
meta(name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no")
meta(name="description" content="")
link(href="css/index.css" rel="stylesheet")
body
div(id="tsparticles")
main(class="box")
h2 Вход
form(id="loginForm" target="hiddenFrame")
div(class="inputBox")
label(for="username") Ник
input(type="text" name="username" id="username" placeholder="ваш ник на сервере" required=true)
div(class="inputBox")
label(for="password") Пароль
input(type="password" name="password" id="password" placeholder="ваш пароль" required=true)
div
button(type="submit" name="" style="float: left;") Войти
a(class="button" href="register" style="float: left;") Регистрация
script(type="text/javascript" src="js/index.js")

11
sample.env Normal file
View File

@ -0,0 +1,11 @@
#Databse
DBUSER=captchas
DBHOST=localhost
DBNAME=captchas
DBPORT=5432
DBPASS=GENERATE_A_STRONG_PASSWORD_HERE
#General
APP_PORT=3000
CAPTCHA_SOURCE_URL=https://example.com
DATA_DIR=/opt/captcha_aggregator

20
src/config.js Normal file
View File

@ -0,0 +1,20 @@
import dotenv from 'dotenv';
dotenv.config({ path: ".env" });
const getBoolean = value => { return value === 'true'? true : false }
const config = {
dbuser: process.env.DBUSER,
dbhost: process.env.DBHOST,
dbname: process.env.DBNAME,
dbport: Number.parseInt(process.env.DBPORT),
dbpass: process.env.DBPASS,
app_port: Number.parseInt(process.env.APP_PORT),
captcha_source_url: process.env.CAPTCHA_SOURCE_URL,
data_dir: process.env.DATA_DIR
}
export default config;

38
src/controllers/api.js Normal file
View File

@ -0,0 +1,38 @@
import CaptchaService from "../services/captcha.js";
import config from '../config.js';
import fs from 'fs/promises';
class ApiController {
async new(req, res) {
const id = await CaptchaService.new();
return res.status(200).send({"id": id});
}
async get(req, res) {
const id = req.params.id;
const hash = await CaptchaService.get(id);
if (hash == undefined) {
return res.status(404).send({"message": "captcha not found"});
}
const image = await fs.readFile(`${config.data_dir}/${hash}.jpeg`);
return res.status(200).send(image)
}
async solve (req, res) {
const id = req.params.id;
const solution = req.body["solution"];
if (solution == undefined || solution.length != 6) {
return res.status(400).send({"message": 'please, send a valid solution. Example: {"solution":"123456"}'});
}
if (!await CaptchaService.check_and_save_solution(id, solution))
return res.status(409).send({"message": "Solution is not correct"});
return res.status(200).send({"message": "Successful"});
}
}
export default new ApiController();

21
src/db.js Normal file
View File

@ -0,0 +1,21 @@
import pg from 'pg';
import fs from 'fs';
import config from './config.js';
const { Pool } = pg;
console.log("Connecting to PostgreSQL database");
const pool = new Pool({
user: config.dbuser,
host: config.dbhost,
database: config.dbname,
password: config.dbpass,
port: config.dbport
});
pool.query(fs.readFileSync('./db_schema.psql').toString());
export default pool;

24
src/index.js Normal file
View File

@ -0,0 +1,24 @@
import express from 'express';
import path from 'path';
import ApiRouter from './routers/api.js';
import FrontendRouter from './routers/frontend.js';
import config from './config.js';
const app = express();
app.use(express.static(path.join('./public')));
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.set('view engine', 'pug');
app.use('/api', ApiRouter);
app.use('/', FrontendRouter);
const server = app.listen(config.app_port, () => {
console.log("App has been started!");
});
export default server;

11
src/routers/api.js Normal file
View File

@ -0,0 +1,11 @@
import { Router } from 'express';
import ApiController from '../controllers/api.js';
const ApiRouter = new Router();
ApiRouter.post('/captcha/new', ApiController.new);
ApiRouter.get('/captcha/:id', ApiController.get);
ApiRouter.patch('/captcha/:id', ApiController.solve);
export default ApiRouter;

9
src/routers/frontend.js Normal file
View File

@ -0,0 +1,9 @@
import { Router } from 'express';
const FrontendRouter = new Router();
FrontendRouter.get('/', async (req, res) => {
return res.render("index.pug");
});
export default FrontendRouter;

61
src/services/captcha.js Normal file
View File

@ -0,0 +1,61 @@
import db from '../db.js';
import fs from 'fs/promises';
import config from '../config.js';
import { createHash } from 'crypto';
class CaptchaService {
async new() {
try {
const captcha = await (await fetch(config.captcha_source_url)).blob();
const buffer = Buffer.from(await captcha.arrayBuffer());
const hash = createHash('md5').update(buffer).digest('hex');
await fs.writeFile(`${config.data_dir}/${hash}.jpeg`, buffer);
const id = (await db.query("INSERT INTO captchas (hash) VALUES ($1) RETURNING id", [hash])).rows[0].id;
return id
} catch(e) {
console.log(e);
}
}
async check_solution(solution) {
const body = {
"TotalSum": "78278",
"FnNumber": "9960440301173139",
"ReceiptOperationType": "1",
"DocNumber": "35704",
"DocFiscalSign": "4149689833",
"Captcha": solution,
"DocDateTime": "2022-09-21T20:28:00.000Z"
}
const result = await fetch("https://check.ofd.ru/Document/FetchReceiptFromFns", {
"headers": {
"Content-Type": "application/json;charset=utf-8"
},
"body": JSON.stringify(body),
"method": "POST",
});
return result.status != 400;
}
async get (id) {
let result = await db.query("SELECT hash FROM captchas WHERE id = $1", [id])
if (result.rows[0] == undefined) {
return undefined
}
return result.rows[0].hash;
}
async check_and_save_solution (id, solution) {
if (!await this.check_solution(solution)) {
return false;
}
await db.query("UPDATE captchas SET solution = $1 WHERE id = $2", [solution, id]);
return true;
}
}
export default new CaptchaService();