Switch to a system where you can use either a verifyCode or a captcha token for any endpoint with one or the other

This commit is contained in:
2026-02-01 16:09:35 -07:00
parent 3f3d6325d6
commit b4309294e6
8 changed files with 130 additions and 177 deletions

View File

@@ -585,10 +585,11 @@ app.post(
tags: ['Accounts'] tags: ['Accounts']
}, },
body: t.Object({ body: t.Object({
token: t.Optional(t.String()),
verifyCode: t.Optional(t.String()),
username: t.String(), username: t.String(),
password: t.String(), password: t.String(),
email: t.String(), email: t.String()
verifyCode: t.String()
}), }),
headers: t.Object({ headers: t.Object({
'x-forwarded-for': t.Optional( 'x-forwarded-for': t.Optional(
@@ -654,8 +655,9 @@ app.post('/account/forgot-username', accountForgotUsernamePostHandler, {
tags: ['Accounts'] tags: ['Accounts']
}, },
body: t.Object({ body: t.Object({
email: t.String(), token: t.Optional(t.String()),
verifyCode: t.String() verifyCode: t.Optional(t.String()),
email: t.String()
}), }),
headers: t.Object({ headers: t.Object({
'x-forwarded-for': t.Optional( 'x-forwarded-for': t.Optional(
@@ -671,8 +673,9 @@ app.post('/account/forgot-password', accountForgotPasswordPostHandler, {
tags: ['Accounts'] tags: ['Accounts']
}, },
body: t.Object({ body: t.Object({
email: t.String(), token: t.Optional(t.String()),
verifyCode: t.String() verifyCode: t.Optional(t.String()),
email: t.String()
}), }),
headers: t.Object({ headers: t.Object({
'x-forwarded-for': t.Optional( 'x-forwarded-for': t.Optional(
@@ -687,7 +690,8 @@ app.post('/account/reset-password', accountResetPasswordPostHandler, {
hide: true hide: true
}, },
body: t.Object({ body: t.Object({
token: t.String(), token: t.Optional(t.String()),
verifyCode: t.Optional(t.String()),
code: t.String(), code: t.String(),
password: t.String() password: t.String()
}), }),
@@ -1087,8 +1091,8 @@ app.post(
tags: ['Berry Dash', 'Icon Marketplace'] tags: ['Berry Dash', 'Icon Marketplace']
}, },
body: t.Object({ body: t.Object({
verifyCode: t.Optional(t.String()),
token: t.Optional(t.String()), token: t.Optional(t.String()),
verifyCode: t.Optional(t.String()),
price: t.String(), price: t.String(),
name: t.String(), name: t.String(),
fileContent: t.String() fileContent: t.String()
@@ -1233,7 +1237,8 @@ app.post('/berrydash/splash-text', berryDashSplashTextPostHandler, {
hide: true hide: true
}, },
body: t.Object({ body: t.Object({
token: t.String(), token: t.Optional(t.String()),
verifyCode: t.Optional(t.String()),
content: t.String() content: t.String()
}), }),
headers: t.Object({ headers: t.Object({

View File

@@ -1,5 +1,5 @@
import mysql from 'mysql2' import mysql from 'mysql2'
import { drizzle } from 'drizzle-orm/mysql2' import { drizzle, MySql2Database } from 'drizzle-orm/mysql2'
import { import {
allowedDatabaseVersions, allowedDatabaseVersions,
allowedVersions, allowedVersions,
@@ -11,6 +11,8 @@ import axios from 'axios'
import FormData from 'form-data' import FormData from 'form-data'
import nodemailer from 'nodemailer' import nodemailer from 'nodemailer'
import { createHash } from 'crypto' import { createHash } from 'crypto'
import { and, eq, sql } from 'drizzle-orm'
import { verifyCodes } from './tables'
export function jsonResponse (data: any, status = 200) { export function jsonResponse (data: any, status = 200) {
return new Response(JSON.stringify(data, null, 2), { 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<boolean> => {
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) => { export const sendEmail = async (to: string, title: string, body: string) => {

View File

@@ -3,16 +3,18 @@ import {
getClientIp, getClientIp,
getDatabaseConnection, getDatabaseConnection,
jsonResponse, jsonResponse,
sendEmail sendEmail,
verifyTurstileOrVerifyCode
} from '../../../lib/util' } 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 { and, desc, eq, sql } from 'drizzle-orm'
import isEmail from 'validator/lib/isEmail' import isEmail from 'validator/lib/isEmail'
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
type Body = { type Body = {
token: string | null
verifyCode: string | null
email: string email: string
verifyCode: string
} }
export async function handler (context: Context) { export async function handler (context: Context) {
@@ -58,38 +60,14 @@ export async function handler (context: Context) {
) )
} }
const time = Math.floor(Date.now() / 1000) const time = Math.floor(Date.now() / 1000)
const codeExists = await db0 if (!(await verifyTurstileOrVerifyCode(body.token, body.verifyCode, ip, 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( return jsonResponse(
{ {
success: false, 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 400
) )

View File

@@ -3,15 +3,17 @@ import {
getClientIp, getClientIp,
getDatabaseConnection, getDatabaseConnection,
jsonResponse, jsonResponse,
sendEmail sendEmail,
verifyTurstileOrVerifyCode
} from '../../../lib/util' } from '../../../lib/util'
import { users, verifyCodes } from '../../../lib/tables'
import { and, desc, eq, sql } from 'drizzle-orm'
import isEmail from 'validator/lib/isEmail' import isEmail from 'validator/lib/isEmail'
import { users } from '../../../lib/tables'
import { eq } from 'drizzle-orm'
type Body = { type Body = {
token: string | null
verifyCode: string | null
email: string email: string
verifyCode: string
} }
export async function handler (context: Context) { export async function handler (context: Context) {
@@ -57,38 +59,14 @@ export async function handler (context: Context) {
) )
} }
const time = Math.floor(Date.now() / 1000) const time = Math.floor(Date.now() / 1000)
const codeExists = await db0 if (!(await verifyTurstileOrVerifyCode(body.token, body.verifyCode, ip, 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( return jsonResponse(
{ {
success: false, 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 400
) )

View File

@@ -2,19 +2,20 @@ import { Context } from 'elysia'
import { import {
getClientIp, getClientIp,
getDatabaseConnection, getDatabaseConnection,
jsonResponse jsonResponse,
verifyTurstileOrVerifyCode
} from '../../../lib/util' } from '../../../lib/util'
import isEmail from 'validator/lib/isEmail' import isEmail from 'validator/lib/isEmail'
import { berryDashUserData, users, verifyCodes } from '../../../lib/tables' import { berryDashUserData, users } from '../../../lib/tables'
import { and, desc, eq, sql } from 'drizzle-orm'
import bcrypt from 'bcryptjs' import bcrypt from 'bcryptjs'
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
type Body = { type Body = {
token: string | null
verifyCode: string | null
username: string username: string
password: string password: string
email: string email: string
verifyCode: string
} }
export async function handler (context: Context) { export async function handler (context: Context) {
@@ -65,37 +66,14 @@ export async function handler (context: Context) {
) )
} }
const time = Math.floor(Date.now() / 1000) const time = Math.floor(Date.now() / 1000)
const codeExists = await db0 if (!(await verifyTurstileOrVerifyCode(body.token, body.verifyCode, ip, 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
return jsonResponse( return jsonResponse(
{ {
success: false, 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 400
) )

View File

@@ -3,14 +3,15 @@ import {
getClientIp, getClientIp,
getDatabaseConnection, getDatabaseConnection,
jsonResponse, jsonResponse,
validateTurnstile verifyTurstileOrVerifyCode
} from '../../../lib/util' } from '../../../lib/util'
import { resetCodes, users } from '../../../lib/tables' import { resetCodes, users } from '../../../lib/tables'
import { and, desc, eq, sql } from 'drizzle-orm' import { and, desc, eq, sql } from 'drizzle-orm'
import bcrypt from 'bcryptjs' import bcrypt from 'bcryptjs'
type Body = { type Body = {
token: string token: string | null
verifyCode: string | null
code: string code: string
password: string password: string
} }
@@ -58,17 +59,17 @@ export async function handler (context: Context) {
) )
} }
const result = await validateTurnstile(body.token, ip) if (!(await verifyTurstileOrVerifyCode(body.token, body.verifyCode, ip, db0)))
if (!result.success) {
connection0.end()
return jsonResponse( return jsonResponse(
{ {
success: false, 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 400
) )
}
const time = Math.floor(Date.now() / 1000) const time = Math.floor(Date.now() / 1000)
const codeExists = await db0 const codeExists = await db0

View File

@@ -4,18 +4,17 @@ import {
getDatabaseConnection, getDatabaseConnection,
hash, hash,
jsonResponse, jsonResponse,
validateTurnstile verifyTurstileOrVerifyCode
} from '../../../../lib/util' } from '../../../../lib/util'
import { checkAuthorization } from '../../../../lib/auth' import { checkAuthorization } from '../../../../lib/auth'
import { berryDashMarketplaceIcons, verifyCodes } from '../../../../lib/tables' import { berryDashMarketplaceIcons } from '../../../../lib/tables'
import { and, desc, eq, sql } from 'drizzle-orm'
import { Buffer } from 'buffer' import { Buffer } from 'buffer'
import sizeOf from 'image-size' import sizeOf from 'image-size'
import { Connection } from 'mysql2/typings/mysql/lib/Connection' import { Connection } from 'mysql2/typings/mysql/lib/Connection'
type Body = { type Body = {
verifyCode: string token: string | null
token: string verifyCode: string | null
price: string price: string
name: string name: string
fileContent: string fileContent: string
@@ -136,55 +135,17 @@ export async function handler (context: Context) {
) )
const time = Math.floor(Date.now() / 1000) const time = Math.floor(Date.now() / 1000)
if (body.verifyCode) { if (!(await verifyTurstileOrVerifyCode(body.token, body.verifyCode, ip, db0)))
const codeExists = await db0 return jsonResponse(
.select({ id: verifyCodes.id }) {
.from(verifyCodes) success: false,
.where( message:
and( body.token != null
eq(verifyCodes.ip, ip), ? 'Invalid captcha token'
eq(verifyCodes.usedTimestamp, 0), : 'Invalid verify code (codes can only be used once)'
eq(verifyCodes.code, body.verifyCode), },
sql`${verifyCodes.timestamp} >= UNIX_TIMESTAMP() - 600` 400
) )
)
.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
)
}
}
const hashResult = hash(atob(body.fileContent), 'sha512') const hashResult = hash(atob(body.fileContent), 'sha512')
const id = crypto.randomUUID() const id = crypto.randomUUID()

View File

@@ -3,14 +3,15 @@ import {
getClientIp, getClientIp,
getDatabaseConnection, getDatabaseConnection,
jsonResponse, jsonResponse,
validateTurnstile verifyTurstileOrVerifyCode
} from '../../../lib/util' } from '../../../lib/util'
import { checkAuthorization } from '../../../lib/auth' import { checkAuthorization } from '../../../lib/auth'
import { berryDashSplashTexts } from '../../../lib/tables' import { berryDashSplashTexts } from '../../../lib/tables'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
type Body = { type Body = {
token: string token: string | null
verifyCode: string | null
content: string content: string
} }
@@ -107,18 +108,17 @@ export async function handler (context: Context) {
) )
} }
const result = await validateTurnstile(body.token, ip) if (!(await verifyTurstileOrVerifyCode(body.token, body.verifyCode, ip, db0)))
if (!result.success) {
connection0.end()
connection1.end()
return jsonResponse( return jsonResponse(
{ {
success: false, 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 400
) )
}
const time = Math.floor(Date.now() / 1000) const time = Math.floor(Date.now() / 1000)
await db1 await db1