From b4309294e6bf003e4d7edae44ba381cef9634236 Mon Sep 17 00:00:00 2001 From: Lncvrt Date: Sun, 1 Feb 2026 16:09:35 -0700 Subject: [PATCH] Switch to a system where you can use either a verifyCode or a captcha token for any endpoint with one or the other --- src/index.ts | 23 ++++--- src/lib/util.ts | 56 ++++++++++++++- src/routes/account/forgot-password/post.ts | 42 +++-------- src/routes/account/forgot-username/post.ts | 44 +++--------- src/routes/account/register/post.ts | 42 +++-------- src/routes/account/reset-password/post.ts | 15 ++-- .../berrydash/icon-marketplace/upload/post.ts | 69 ++++--------------- src/routes/berrydash/splash-text/post.ts | 16 ++--- 8 files changed, 130 insertions(+), 177 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3310e7a..1990cd0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -585,10 +585,11 @@ app.post( tags: ['Accounts'] }, body: t.Object({ + token: t.Optional(t.String()), + verifyCode: t.Optional(t.String()), username: t.String(), password: t.String(), - email: t.String(), - verifyCode: t.String() + email: t.String() }), headers: t.Object({ 'x-forwarded-for': t.Optional( @@ -654,8 +655,9 @@ app.post('/account/forgot-username', accountForgotUsernamePostHandler, { tags: ['Accounts'] }, body: t.Object({ - email: t.String(), - verifyCode: t.String() + token: t.Optional(t.String()), + verifyCode: t.Optional(t.String()), + email: t.String() }), headers: t.Object({ 'x-forwarded-for': t.Optional( @@ -671,8 +673,9 @@ app.post('/account/forgot-password', accountForgotPasswordPostHandler, { tags: ['Accounts'] }, body: t.Object({ - email: t.String(), - verifyCode: t.String() + token: t.Optional(t.String()), + verifyCode: t.Optional(t.String()), + email: t.String() }), headers: t.Object({ 'x-forwarded-for': t.Optional( @@ -687,7 +690,8 @@ app.post('/account/reset-password', accountResetPasswordPostHandler, { hide: true }, body: t.Object({ - token: t.String(), + token: t.Optional(t.String()), + verifyCode: t.Optional(t.String()), code: t.String(), password: t.String() }), @@ -1087,8 +1091,8 @@ app.post( tags: ['Berry Dash', 'Icon Marketplace'] }, body: t.Object({ - verifyCode: t.Optional(t.String()), token: t.Optional(t.String()), + verifyCode: t.Optional(t.String()), price: t.String(), name: t.String(), fileContent: t.String() @@ -1233,7 +1237,8 @@ app.post('/berrydash/splash-text', berryDashSplashTextPostHandler, { hide: true }, body: t.Object({ - token: t.String(), + token: t.Optional(t.String()), + verifyCode: t.Optional(t.String()), content: t.String() }), headers: t.Object({ diff --git a/src/lib/util.ts b/src/lib/util.ts index 9d9ca25..ff57e2e 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -1,5 +1,5 @@ import mysql from 'mysql2' -import { drizzle } from 'drizzle-orm/mysql2' +import { drizzle, MySql2Database } from 'drizzle-orm/mysql2' import { allowedDatabaseVersions, allowedVersions, @@ -11,6 +11,8 @@ import axios from 'axios' import FormData from 'form-data' import nodemailer from 'nodemailer' import { createHash } from 'crypto' +import { and, eq, sql } from 'drizzle-orm' +import { verifyCodes } from './tables' export function jsonResponse (data: any, status = 200) { return new Response(JSON.stringify(data, null, 2), { @@ -123,7 +125,57 @@ export const validateTurnstile = async (token: string, remoteip: string) => { } ) - return response.data + return response.data.success +} + +export const validateVerifyCode = async ( + db0: MySql2Database, + ip: string, + verifyCode: string +): Promise => { + const time = Math.floor(Date.now() / 1000) + const codeExists = await db0 + .select({ id: verifyCodes.id }) + .from(verifyCodes) + .where( + and( + eq(verifyCodes.ip, ip), + eq(verifyCodes.usedTimestamp, 0), + eq(verifyCodes.code, verifyCode), + sql`${verifyCodes.timestamp} >= UNIX_TIMESTAMP() - 600` + ) + ) + .limit(1) + .execute() + if (codeExists[0]) { + await db0 + .update(verifyCodes) + .set({ usedTimestamp: time }) + .where( + and( + eq(verifyCodes.id, codeExists[0].id), + eq(verifyCodes.ip, ip), + eq(verifyCodes.usedTimestamp, 0), + eq(verifyCodes.code, verifyCode) + ) + ) + .execute() + return true + } else return false +} + +export const verifyTurstileOrVerifyCode = ( + token: string | null, + verifyCode: string | null, + ip: string, + db0: MySql2Database +) => { + if (token != null) { + return validateTurnstile(token, ip) + } else if (verifyCode != null) { + return validateVerifyCode(db0, ip, verifyCode) + } + return false } export const sendEmail = async (to: string, title: string, body: string) => { diff --git a/src/routes/account/forgot-password/post.ts b/src/routes/account/forgot-password/post.ts index d85e6ef..31b512c 100644 --- a/src/routes/account/forgot-password/post.ts +++ b/src/routes/account/forgot-password/post.ts @@ -3,16 +3,18 @@ import { getClientIp, getDatabaseConnection, jsonResponse, - sendEmail + sendEmail, + verifyTurstileOrVerifyCode } from '../../../lib/util' -import { resetCodes, users, verifyCodes } from '../../../lib/tables' +import { resetCodes, users } from '../../../lib/tables' import { and, desc, eq, sql } from 'drizzle-orm' import isEmail from 'validator/lib/isEmail' import { randomBytes } from 'crypto' type Body = { + token: string | null + verifyCode: string | null email: string - verifyCode: string } export async function handler (context: Context) { @@ -58,38 +60,14 @@ export async function handler (context: Context) { ) } const time = Math.floor(Date.now() / 1000) - const codeExists = await db0 - .select({ id: verifyCodes.id }) - .from(verifyCodes) - .where( - and( - eq(verifyCodes.ip, ip), - eq(verifyCodes.usedTimestamp, 0), - eq(verifyCodes.code, body.verifyCode), - sql`${verifyCodes.timestamp} >= UNIX_TIMESTAMP() - 600` - ) - ) - .orderBy(desc(verifyCodes.id)) - .limit(1) - .execute() - if (codeExists[0]) { - await db0 - .update(verifyCodes) - .set({ usedTimestamp: time }) - .where( - and( - eq(verifyCodes.id, codeExists[0].id), - eq(verifyCodes.ip, ip), - eq(verifyCodes.usedTimestamp, 0), - eq(verifyCodes.code, body.verifyCode) - ) - ) - .execute() - } else + if (!(await verifyTurstileOrVerifyCode(body.token, body.verifyCode, ip, db0))) return jsonResponse( { success: false, - message: 'Invalid verify code (codes can only be used once)' + message: + body.token != null + ? 'Invalid captcha token' + : 'Invalid verify code (codes can only be used once)' }, 400 ) diff --git a/src/routes/account/forgot-username/post.ts b/src/routes/account/forgot-username/post.ts index e3976a5..92a7c76 100644 --- a/src/routes/account/forgot-username/post.ts +++ b/src/routes/account/forgot-username/post.ts @@ -3,15 +3,17 @@ import { getClientIp, getDatabaseConnection, jsonResponse, - sendEmail + sendEmail, + verifyTurstileOrVerifyCode } from '../../../lib/util' -import { users, verifyCodes } from '../../../lib/tables' -import { and, desc, eq, sql } from 'drizzle-orm' import isEmail from 'validator/lib/isEmail' +import { users } from '../../../lib/tables' +import { eq } from 'drizzle-orm' type Body = { + token: string | null + verifyCode: string | null email: string - verifyCode: string } export async function handler (context: Context) { @@ -57,38 +59,14 @@ export async function handler (context: Context) { ) } const time = Math.floor(Date.now() / 1000) - const codeExists = await db0 - .select({ id: verifyCodes.id }) - .from(verifyCodes) - .where( - and( - eq(verifyCodes.ip, ip), - eq(verifyCodes.usedTimestamp, 0), - eq(verifyCodes.code, body.verifyCode), - sql`${verifyCodes.timestamp} >= UNIX_TIMESTAMP() - 600` - ) - ) - .orderBy(desc(verifyCodes.id)) - .limit(1) - .execute() - if (codeExists[0]) { - await db0 - .update(verifyCodes) - .set({ usedTimestamp: time }) - .where( - and( - eq(verifyCodes.id, codeExists[0].id), - eq(verifyCodes.ip, ip), - eq(verifyCodes.usedTimestamp, 0), - eq(verifyCodes.code, body.verifyCode) - ) - ) - .execute() - } else + if (!(await verifyTurstileOrVerifyCode(body.token, body.verifyCode, ip, db0))) return jsonResponse( { success: false, - message: 'Invalid verify code (codes can only be used once)' + message: + body.token != null + ? 'Invalid captcha token' + : 'Invalid verify code (codes can only be used once)' }, 400 ) diff --git a/src/routes/account/register/post.ts b/src/routes/account/register/post.ts index 652e981..be5e6ea 100644 --- a/src/routes/account/register/post.ts +++ b/src/routes/account/register/post.ts @@ -2,19 +2,20 @@ import { Context } from 'elysia' import { getClientIp, getDatabaseConnection, - jsonResponse + jsonResponse, + verifyTurstileOrVerifyCode } from '../../../lib/util' import isEmail from 'validator/lib/isEmail' -import { berryDashUserData, users, verifyCodes } from '../../../lib/tables' -import { and, desc, eq, sql } from 'drizzle-orm' +import { berryDashUserData, users } from '../../../lib/tables' import bcrypt from 'bcryptjs' import { randomBytes } from 'crypto' type Body = { + token: string | null + verifyCode: string | null username: string password: string email: string - verifyCode: string } export async function handler (context: Context) { @@ -65,37 +66,14 @@ export async function handler (context: Context) { ) } const time = Math.floor(Date.now() / 1000) - const codeExists = await db0 - .select({ id: verifyCodes.id }) - .from(verifyCodes) - .where( - and( - eq(verifyCodes.ip, ip), - eq(verifyCodes.usedTimestamp, 0), - eq(verifyCodes.code, body.verifyCode), - sql`${verifyCodes.timestamp} >= UNIX_TIMESTAMP() - 600` - ) - ) - .orderBy(desc(verifyCodes.id)) - .limit(1) - .execute() - if (codeExists[0]) { - await db0 - .update(verifyCodes) - .set({ usedTimestamp: time }) - .where( - and( - eq(verifyCodes.id, codeExists[0].id), - eq(verifyCodes.ip, ip), - eq(verifyCodes.usedTimestamp, 0), - eq(verifyCodes.code, body.verifyCode) - ) - ) - } else + if (!(await verifyTurstileOrVerifyCode(body.token, body.verifyCode, ip, db0))) return jsonResponse( { success: false, - message: 'Invalid verify code (codes can only be used once)' + message: + body.token != null + ? 'Invalid captcha token' + : 'Invalid verify code (codes can only be used once)' }, 400 ) diff --git a/src/routes/account/reset-password/post.ts b/src/routes/account/reset-password/post.ts index b1cb450..21520d1 100644 --- a/src/routes/account/reset-password/post.ts +++ b/src/routes/account/reset-password/post.ts @@ -3,14 +3,15 @@ import { getClientIp, getDatabaseConnection, jsonResponse, - validateTurnstile + verifyTurstileOrVerifyCode } from '../../../lib/util' import { resetCodes, users } from '../../../lib/tables' import { and, desc, eq, sql } from 'drizzle-orm' import bcrypt from 'bcryptjs' type Body = { - token: string + token: string | null + verifyCode: string | null code: string password: string } @@ -58,17 +59,17 @@ export async function handler (context: Context) { ) } - const result = await validateTurnstile(body.token, ip) - if (!result.success) { - connection0.end() + if (!(await verifyTurstileOrVerifyCode(body.token, body.verifyCode, ip, db0))) return jsonResponse( { success: false, - message: 'Unable to verify captcha key' + message: + body.token != null + ? 'Invalid captcha token' + : 'Invalid verify code (codes can only be used once)' }, 400 ) - } const time = Math.floor(Date.now() / 1000) const codeExists = await db0 diff --git a/src/routes/berrydash/icon-marketplace/upload/post.ts b/src/routes/berrydash/icon-marketplace/upload/post.ts index 428b822..3298d21 100644 --- a/src/routes/berrydash/icon-marketplace/upload/post.ts +++ b/src/routes/berrydash/icon-marketplace/upload/post.ts @@ -4,18 +4,17 @@ import { getDatabaseConnection, hash, jsonResponse, - validateTurnstile + verifyTurstileOrVerifyCode } from '../../../../lib/util' import { checkAuthorization } from '../../../../lib/auth' -import { berryDashMarketplaceIcons, verifyCodes } from '../../../../lib/tables' -import { and, desc, eq, sql } from 'drizzle-orm' +import { berryDashMarketplaceIcons } from '../../../../lib/tables' import { Buffer } from 'buffer' import sizeOf from 'image-size' import { Connection } from 'mysql2/typings/mysql/lib/Connection' type Body = { - verifyCode: string - token: string + token: string | null + verifyCode: string | null price: string name: string fileContent: string @@ -136,55 +135,17 @@ export async function handler (context: Context) { ) const time = Math.floor(Date.now() / 1000) - if (body.verifyCode) { - const codeExists = await db0 - .select({ id: verifyCodes.id }) - .from(verifyCodes) - .where( - and( - eq(verifyCodes.ip, ip), - eq(verifyCodes.usedTimestamp, 0), - eq(verifyCodes.code, body.verifyCode), - sql`${verifyCodes.timestamp} >= UNIX_TIMESTAMP() - 600` - ) - ) - .orderBy(desc(verifyCodes.id)) - .limit(1) - .execute() - if (codeExists[0]) { - await db0 - .update(verifyCodes) - .set({ usedTimestamp: time }) - .where( - and( - eq(verifyCodes.id, codeExists[0].id), - eq(verifyCodes.ip, ip), - eq(verifyCodes.usedTimestamp, 0), - eq(verifyCodes.code, body.verifyCode) - ) - ) - .execute() - } else - return jsonResponse( - { - success: false, - message: 'Invalid verify code (codes can only be used once)' - }, - 400 - ) - } else { - const result = await validateTurnstile(body.token, ip) - if (!result.success) { - connection0.end() - return jsonResponse( - { - success: false, - message: 'Unable to verify captcha key' - }, - 400 - ) - } - } + if (!(await verifyTurstileOrVerifyCode(body.token, body.verifyCode, ip, db0))) + return jsonResponse( + { + success: false, + message: + body.token != null + ? 'Invalid captcha token' + : 'Invalid verify code (codes can only be used once)' + }, + 400 + ) const hashResult = hash(atob(body.fileContent), 'sha512') const id = crypto.randomUUID() diff --git a/src/routes/berrydash/splash-text/post.ts b/src/routes/berrydash/splash-text/post.ts index 8a0340a..53806ae 100644 --- a/src/routes/berrydash/splash-text/post.ts +++ b/src/routes/berrydash/splash-text/post.ts @@ -3,14 +3,15 @@ import { getClientIp, getDatabaseConnection, jsonResponse, - validateTurnstile + verifyTurstileOrVerifyCode } from '../../../lib/util' import { checkAuthorization } from '../../../lib/auth' import { berryDashSplashTexts } from '../../../lib/tables' import { eq } from 'drizzle-orm' type Body = { - token: string + token: string | null + verifyCode: string | null content: string } @@ -107,18 +108,17 @@ export async function handler (context: Context) { ) } - const result = await validateTurnstile(body.token, ip) - if (!result.success) { - connection0.end() - connection1.end() + if (!(await verifyTurstileOrVerifyCode(body.token, body.verifyCode, ip, db0))) return jsonResponse( { success: false, - message: 'Unable to verify captcha key' + message: + body.token != null + ? 'Invalid captcha token' + : 'Invalid verify code (codes can only be used once)' }, 400 ) - } const time = Math.floor(Date.now() / 1000) await db1