diff --git a/src/index.ts b/src/index.ts index fa61015..c153d20 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { handler as launcherLoaderUpdateDataHandler } from './routes/launcher/lo import { handler as accountForgotUsernamePostHandler } from './routes/account/forgot-username/post' import { handler as accountForgotPasswordPostHandler } from './routes/account/forgot-password/post' +import { handler as accountResetPasswordPostHandler } from './routes/account/reset-password/post' import { handler as berryDashLatestVersionGetHandler } from './routes/berrydash/latest-version/get' @@ -165,6 +166,17 @@ app.post('/account/forgot-password', accountForgotPasswordPostHandler, { verifyCode: t.String() }) }) +app.post('/account/reset-password', accountResetPasswordPostHandler, { + detail: { + description: 'The endpoint for resetting the password for an account.', + tags: ['Accounts'] + }, + body: t.Object({ + token: t.String(), + code: t.String(), + password: t.String() + }) +}) app.get('/berrydash/latest-version', berryDashLatestVersionGetHandler, { detail: { description: 'The endpoint for getting the latest berry dash version.', diff --git a/src/routes/account/reset-password/post.ts b/src/routes/account/reset-password/post.ts new file mode 100644 index 0000000..b1cb450 --- /dev/null +++ b/src/routes/account/reset-password/post.ts @@ -0,0 +1,127 @@ +import { Context } from 'elysia' +import { + getClientIp, + getDatabaseConnection, + jsonResponse, + validateTurnstile +} 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 + code: string + password: string +} + +export async function handler (context: Context) { + const dbInfo0 = getDatabaseConnection(0) + + if (!dbInfo0) + return jsonResponse( + { success: false, message: 'Failed to connect to database' }, + 500 + ) + const { connection: connection0, db: db0 } = dbInfo0 + + const body = context.body as Body + if (!body.token || !body.code || !body.password) { + connection0.end() + return jsonResponse( + { + success: false, + message: 'Token, code and password must be in POST data' + }, + 400 + ) + } + if (body.code.length != 64) { + connection0.end() + return jsonResponse( + { + success: false, + message: 'Invalid verify code (codes can only be used once)' + }, + 400 + ) + } + const ip = getClientIp(context) + if (!ip) { + connection0.end() + return jsonResponse( + { + success: false, + message: 'Failed to get required info' + }, + 400 + ) + } + + const result = await validateTurnstile(body.token, ip) + if (!result.success) { + connection0.end() + return jsonResponse( + { + success: false, + message: 'Unable to verify captcha key' + }, + 400 + ) + } + + const time = Math.floor(Date.now() / 1000) + const codeExists = await db0 + .select({ id: resetCodes.id, userId: resetCodes.userId }) + .from(resetCodes) + .where( + and( + eq(resetCodes.ip, ip), + eq(resetCodes.usedTimestamp, 0), + eq(resetCodes.code, body.code), + sql`${resetCodes.timestamp} >= UNIX_TIMESTAMP() - 600`, + eq(resetCodes.type, 0) + ) + ) + .orderBy(desc(resetCodes.id)) + .limit(1) + .execute() + if (codeExists[0]) { + await db0 + .update(resetCodes) + .set({ usedTimestamp: time }) + .where( + and( + eq(resetCodes.id, codeExists[0].id), + eq(resetCodes.ip, ip), + eq(resetCodes.usedTimestamp, 0), + eq(resetCodes.code, body.code), + eq(resetCodes.type, 0) + ) + ) + .execute() + const hashedPassword = await bcrypt.hash(body.password, 10) + await db0 + .update(users) + .set({ password: hashedPassword }) + .where(eq(users.id, codeExists[0].userId)) + .execute() + connection0.end() + return jsonResponse( + { + success: true, + message: null + }, + 200 + ) + } else { + connection0.end() + return jsonResponse( + { + success: false, + message: 'Invalid reset code (codes can only be used once)' + }, + 400 + ) + } +}