diff --git a/client/captcha.js b/client/captcha.js new file mode 100644 index 0000000..cd6b07b --- /dev/null +++ b/client/captcha.js @@ -0,0 +1,52 @@ +/* https://raw.githubusercontent.com/napa3um/node-captcha/refs/heads/master/captcha.js */ + +import Route from "../route.js"; +import initDb from "../db.js"; +import Canvas from "canvas"; +import crypto from "node:crypto"; + +let db = await initDb(); + +// TODO: rewrite +let main = new Route([], async function (req, res, input) { + const canvas = new Canvas.Canvas(250, 100) + const ctx = canvas.getContext('2d') + + ctx.antialias = 'gray' + ctx.fillStyle = 'black' + ctx.fillRect(0, 0, 250, 100) + ctx.fillStyle = 'red' + ctx.lineWidth = 3 + ctx.strokeStyle = 'red' + ctx.font = `40px sans` + + // draw two curve lines: + for (var i = 0; i < 2; i++) { + ctx.moveTo(Math.floor(0.08 * 250), Math.random() * 100) + ctx.bezierCurveTo(Math.floor(0.32 * 250), Math.random() * 100, Math.floor(1.07 * 100), Math.random() * 100, Math.floor(0.92 * 250), Math.random() * 100) + ctx.stroke() + } + + // draw text: + const text = ('' + crypto.randomBytes(4).readUIntBE(0, 4)).substring(2, 10) + text.split('').forEach((char, i) => { + ctx.setTransform(Math.random() * 0.5 + 1, Math.random() * 0.4, Math.random() * 0.4, Math.random() * 0.5 + 1, Math.floor(0.375 * 50) * i + Math.floor(0.25 * 10), Math.floor(1.25 * 40)) + ctx.fillText(char, 0, 0) + }) + + let ck = crypto.randomUUID(); + + res.cookie('captcha', ck); + await db.run('INSERT INTO captcha (key, solution) VALUES (?,?)', [ + ck, + text + ]) + + // send image: + res.type('jpg') + res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0') + res.header('Expires', 'Sun, 19 May 1984 02:00:00 GMT') + canvas.jpegStream().pipe(res) +}); + +export default main; \ No newline at end of file diff --git a/db.js b/db.js index 66c1cd8..8d5ccfc 100644 --- a/db.js +++ b/db.js @@ -20,6 +20,7 @@ async function initDb() { await db.run('CREATE TABLE IF NOT EXISTS follow (username TEXT, target TEXT);'); await db.run(`CREATE TABLE IF NOT EXISTS user (username TEXT, bio TEXT);`); + await db.run(`CREATE TABLE IF NOT EXISTS captcha (key TEXT, solution TEXT);`); return db; } diff --git a/form/captcha.js b/form/captcha.js new file mode 100644 index 0000000..14c3a45 --- /dev/null +++ b/form/captcha.js @@ -0,0 +1,26 @@ +import Route from "../route.js"; +import initDb from "../db.js"; + +let db = await initDb(); + +let main = new Route([], async function (req, res, input) { + let body = { ...req.cookies, ...req.body }; + + let { captcha, challenger } = body; + + let match = await db.all('SELECT * FROM captcha WHERE key = ? AND solution = ?', [ + captcha, + challenger + ]); + + await db.all('DELETE FROM captcha WHERE key = ? AND solution = ?', [ + captcha, + challenger + ]); + + return { + captchaMatch: (match.length > 0) + }; +}); + +export default main; \ No newline at end of file diff --git a/form/comment.js b/form/comment.js index 0be0bfe..f062b97 100644 --- a/form/comment.js +++ b/form/comment.js @@ -2,11 +2,16 @@ import Route from "../route.js"; import initDb from "../db.js"; import { randomUUID } from 'node:crypto'; import auth from "../form/auth.js"; +import captcha from "./captcha.js"; let db = await initDb(); // TODO: rewrite -let main = new Route([auth], async function (req, res, input) { +let main = new Route([auth,captcha], async function (req, res, input) { + let { captchaMatch} = input; + + if (!captchaMatch) return { 'success': false, 'message': 'Captcha is incorrect' }; + let { username } = input; let id = randomUUID(); diff --git a/form/login.js b/form/login.js index 0ccd0b3..3ca74bc 100644 --- a/form/login.js +++ b/form/login.js @@ -3,12 +3,16 @@ import Route from "../route.js"; import initDb from "../db.js"; import { compare } from "bcrypt"; import { randomBytes } from 'node:crypto'; +import captcha from "./captcha.js"; let db = await initDb(); // TODO: rewrite -let main = new Route([], async function (req, res, input) { +let main = new Route([captcha], async function (req, res, input) { let { user, pass } = req.body; + let { captchaMatch} = input; + + if (!captchaMatch) return { 'success': false, 'message': 'Captcha is incorrect' }; if (!pass || !user) return { 'success': false, 'message': 'Some fields are missing' }; diff --git a/form/register.js b/form/register.js index a68d7c5..ecb6607 100644 --- a/form/register.js +++ b/form/register.js @@ -2,6 +2,7 @@ import Route from "../route.js"; import initDb from "../db.js"; import { hash } from "bcrypt"; +import captcha from "./captcha.js"; const minChar = 1; const maxChar = 32; @@ -13,8 +14,11 @@ function isValid(user) { } // TODO: rewrite -let main = new Route([], async function (req, res, input) { +let main = new Route([captcha], async function (req, res, input) { let { user, pass, pass2 } = req.body; + let { captchaMatch} = input; + + if (!captchaMatch) return { 'success': false, 'message': 'Captcha is incorrect' }; if (pass != pass2) return { 'success': false, 'message': 'Passwords do not match' }; diff --git a/form/settings.js b/form/settings.js index b92927b..56e3958 100644 --- a/form/settings.js +++ b/form/settings.js @@ -3,13 +3,17 @@ import initDb from "../db.js"; import auth from "../form/auth.js"; import {exec} from 'node:child_process'; import { promisify } from "node:util"; +import captcha from "./captcha.js"; const execP = promisify(exec); let db = await initDb(); // TODO: rewrite -let main = new Route([auth], async function (req, res, input) { +let main = new Route([auth,captcha], async function (req, res, input) { + let { captchaMatch} = input; + + if (!captchaMatch) return { 'success': false, 'message': 'Captcha is incorrect' }; let { path } = req.file; let { username } = input; diff --git a/form/upload.js b/form/upload.js index 20ec71b..a9f5807 100644 --- a/form/upload.js +++ b/form/upload.js @@ -4,13 +4,17 @@ import { randomUUID } from 'node:crypto'; import {exec} from 'node:child_process'; import { promisify } from "node:util"; import auth from "../form/auth.js"; +import captcha from "./captcha.js"; const execP = promisify(exec); let db = await initDb(); // TODO: rewrite -let main = new Route([auth], async function (req, res, input) { +let main = new Route([auth,captcha], async function (req, res, input) { + let { captchaMatch} = input; + + if (!captchaMatch) return { 'success': false, 'message': 'Captcha is incorrect' }; let { username } = input; let id = randomUUID(); diff --git a/package.json b/package.json index aa6c923..887f1c6 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "type": "module", "dependencies": { "bcrypt": "^5.1.1", + "canvas": "^2.11.2", "cookie-parser": "^1.4.6", "ejs": "^3.1.10", "express": "^4.19.2", diff --git a/routes.js b/routes.js index f73affd..c74467e 100644 --- a/routes.js +++ b/routes.js @@ -7,6 +7,7 @@ import user from "./client/user.js"; import settings from "./client/settings.js"; import e404 from "./client/e404.js"; import tou from "./client/tou.js"; +import captcha from './client/captcha.js'; import loginB from "./form/login.js"; import registerB from "./form/register.js"; @@ -33,7 +34,8 @@ routes.client = { user, settings, e404, - tou + tou, + captcha } routes.get = { diff --git a/static/main.css b/static/main.css index 77ca73a..d7b02f5 100644 --- a/static/main.css +++ b/static/main.css @@ -270,5 +270,6 @@ img { } pre { - white-space: pre-wrap + white-space: pre-wrap; + font-family: var(--font) !important; } \ No newline at end of file diff --git a/views/form_bare.ejs b/views/form_bare.ejs index 860ba57..9db2eec 100644 --- a/views/form_bare.ejs +++ b/views/form_bare.ejs @@ -3,6 +3,10 @@ <%= title %>
+ + + + <% for (let elem of data) { %> <%= elem.label %>