Add forgot username/password endpoints that send emails, reset password endpoint will be done next
This commit is contained in:
23
src/index.ts
23
src/index.ts
@@ -14,6 +14,9 @@ import { handler as launcherLatestHandler } from './routes/launcher/latest'
|
||||
import { handler as launcherLoaderLatestHandler } from './routes/launcher/loader/latest'
|
||||
import { handler as launcherLoaderUpdateDataHandler } from './routes/launcher/loader/update-data'
|
||||
|
||||
import { handler as accountForgotUsernamePostHandler } from './routes/account/forgot-username/post'
|
||||
import { handler as accountForgotPasswordPostHandler } from './routes/account/forgot-password/post'
|
||||
|
||||
import { handler as berryDashLatestVersionGetHandler } from './routes/berrydash/latest-version/get'
|
||||
|
||||
import { handler as berrydashLeaderboardGetHandler } from './routes/berrydash/leaderboard/get'
|
||||
@@ -142,6 +145,26 @@ app.get(
|
||||
})
|
||||
}
|
||||
)
|
||||
app.post('/account/forgot-username', accountForgotUsernamePostHandler, {
|
||||
detail: {
|
||||
description: 'The endpoint for retreiving the username for an account.',
|
||||
tags: ['Accounts']
|
||||
},
|
||||
body: t.Object({
|
||||
email: t.String(),
|
||||
verifyCode: t.String()
|
||||
})
|
||||
})
|
||||
app.post('/account/forgot-password', accountForgotPasswordPostHandler, {
|
||||
detail: {
|
||||
description: 'The endpoint for retreiving the password for an account.',
|
||||
tags: ['Accounts']
|
||||
},
|
||||
body: t.Object({
|
||||
email: t.String(),
|
||||
verifyCode: t.String()
|
||||
})
|
||||
})
|
||||
app.get('/berrydash/latest-version', berryDashLatestVersionGetHandler, {
|
||||
detail: {
|
||||
description: 'The endpoint for getting the latest berry dash version.',
|
||||
|
||||
@@ -74,6 +74,16 @@ export const verifyCodes = mysqlTable('verifycodes', {
|
||||
.notNull()
|
||||
})
|
||||
|
||||
export const resetCodes = mysqlTable('resetcodes', {
|
||||
id: bigint('id', { mode: 'number' }).primaryKey().autoincrement().notNull(),
|
||||
code: varchar('code', { length: 64 }).notNull(),
|
||||
ip: varchar('ip', { length: 255 }),
|
||||
timestamp: bigint('timestamp', { mode: 'number' }).notNull(),
|
||||
usedTimestamp: bigint('usedTimestamp', { mode: 'number' })
|
||||
.default(0)
|
||||
.notNull()
|
||||
})
|
||||
|
||||
// berrydashdatabase
|
||||
|
||||
export const berryDashUserData = mysqlTable('userdata', {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { Context } from 'elysia'
|
||||
import axios from 'axios'
|
||||
import FormData from 'form-data'
|
||||
import nodemailer from 'nodemailer'
|
||||
|
||||
export function jsonResponse (data: any, status = 200) {
|
||||
return new Response(JSON.stringify(data, null, 2), {
|
||||
@@ -123,3 +124,24 @@ export const validateTurnstile = async (token: string, remoteip: string) => {
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const sendEmail = async (to: string, title: string, body: string) => {
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: process.env.GMAIL_USERNAME ?? '',
|
||||
pass: process.env.GMAIL_APP_PASSWORD ?? ''
|
||||
}
|
||||
})
|
||||
|
||||
const mailOptions = {
|
||||
from: `"Lncvrt Games" <${process.env.GMAIL_USERNAME ?? ''}>`,
|
||||
to: to,
|
||||
subject: title,
|
||||
text:
|
||||
body +
|
||||
`\n\nPlease contact ${process.env.GMAIL_USERNAME} if you have any questions or need assistance.`
|
||||
}
|
||||
|
||||
return await transporter.sendMail(mailOptions)
|
||||
}
|
||||
|
||||
149
src/routes/account/forgot-password/post.ts
Normal file
149
src/routes/account/forgot-password/post.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { Context } from 'elysia'
|
||||
import {
|
||||
getClientIp,
|
||||
getDatabaseConnection,
|
||||
jsonResponse,
|
||||
sendEmail
|
||||
} from '../../../lib/util'
|
||||
import { resetCodes, users, verifyCodes } from '../../../lib/tables'
|
||||
import { and, desc, eq, sql } from 'drizzle-orm'
|
||||
import isEmail from 'validator/lib/isEmail'
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
type Body = {
|
||||
email: string
|
||||
verifyCode: 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.email || !body.verifyCode) {
|
||||
connection0.end()
|
||||
return jsonResponse(
|
||||
{
|
||||
success: false,
|
||||
message: 'Email and verifyCode must be in POST data'
|
||||
},
|
||||
400
|
||||
)
|
||||
}
|
||||
if (body.verifyCode.length != 16) {
|
||||
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 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
|
||||
return jsonResponse(
|
||||
{
|
||||
success: false,
|
||||
message: 'Invalid verify code (codes can only be used once)'
|
||||
},
|
||||
400
|
||||
)
|
||||
|
||||
const notFound = `You requested information about your account. Unfortunately, we were unable to find your account associated with this email. This is caused by either an incorrect email provided during signup, or this email not owning a Lncvrt Games account.`
|
||||
|
||||
if (!isEmail(body.email)) {
|
||||
connection0.end()
|
||||
sendEmail(body.email, 'User information reset - Password', notFound)
|
||||
}
|
||||
|
||||
const result = await db0
|
||||
.select({ id: users.id, username: users.username })
|
||||
.from(users)
|
||||
.where(eq(users.email, body.email))
|
||||
.execute()
|
||||
|
||||
if (!result[0]) {
|
||||
connection0.end()
|
||||
sendEmail(body.email, 'User information reset - Password', notFound)
|
||||
}
|
||||
|
||||
let code = randomBytes(32).toString('hex')
|
||||
const resetCodeExists = await db0
|
||||
.select({ code: resetCodes.code })
|
||||
.from(resetCodes)
|
||||
.where(
|
||||
and(
|
||||
eq(resetCodes.ip, ip),
|
||||
eq(resetCodes.usedTimestamp, 0),
|
||||
sql`${resetCodes.timestamp} >= UNIX_TIMESTAMP() - 600`
|
||||
)
|
||||
)
|
||||
.orderBy(desc(resetCodes.id))
|
||||
.limit(1)
|
||||
.execute()
|
||||
if (resetCodeExists[0]) {
|
||||
code = resetCodeExists[0].code
|
||||
} else {
|
||||
await db0.insert(resetCodes).values({ code, ip, timestamp: time })
|
||||
}
|
||||
|
||||
sendEmail(
|
||||
body.email,
|
||||
'User information request - Password',
|
||||
`You have requested a password reset for your Lncvrt Games account.\n\nYour account information:\nUsername: ${result[0].username}\n\nPlease click on the link below to reset your password:\nhttps://games.lncvrt.xyz/account/reset-password?code=${code}\n\nNote: This password reset link expires in 10 minutes or until used.`
|
||||
)
|
||||
|
||||
connection0.end()
|
||||
|
||||
return jsonResponse(
|
||||
{
|
||||
success: true,
|
||||
message: null
|
||||
},
|
||||
400
|
||||
)
|
||||
}
|
||||
128
src/routes/account/forgot-username/post.ts
Normal file
128
src/routes/account/forgot-username/post.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Context } from 'elysia'
|
||||
import {
|
||||
getClientIp,
|
||||
getDatabaseConnection,
|
||||
jsonResponse,
|
||||
sendEmail
|
||||
} from '../../../lib/util'
|
||||
import { users, verifyCodes } from '../../../lib/tables'
|
||||
import { and, desc, eq, sql } from 'drizzle-orm'
|
||||
import isEmail from 'validator/lib/isEmail'
|
||||
|
||||
type Body = {
|
||||
email: string
|
||||
verifyCode: 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.email || !body.verifyCode) {
|
||||
connection0.end()
|
||||
return jsonResponse(
|
||||
{
|
||||
success: false,
|
||||
message: 'Email and verifyCode must be in POST data'
|
||||
},
|
||||
400
|
||||
)
|
||||
}
|
||||
if (body.verifyCode.length != 16) {
|
||||
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 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
|
||||
return jsonResponse(
|
||||
{
|
||||
success: false,
|
||||
message: 'Invalid verify code (codes can only be used once)'
|
||||
},
|
||||
400
|
||||
)
|
||||
|
||||
const notFound = `You requested information about your account, your username\n\nUnfortunately, we were unable to find your username associated with this email. This is caused by either an incorrect email provided during signup, or this email not owning a Lncvrt Games account.`
|
||||
|
||||
if (!isEmail(body.email)) {
|
||||
connection0.end()
|
||||
sendEmail(body.email, 'User information request - Username', notFound)
|
||||
}
|
||||
|
||||
const result = await db0
|
||||
.select({ username: users.username })
|
||||
.from(users)
|
||||
.where(eq(users.email, body.email))
|
||||
.execute()
|
||||
|
||||
if (!result[0]) {
|
||||
connection0.end()
|
||||
sendEmail(body.email, 'User information request - Username', notFound)
|
||||
}
|
||||
|
||||
sendEmail(
|
||||
body.email,
|
||||
'User information request - Username',
|
||||
`You have requested information about your Lncvrt Games account.\n\nYour account information:\nUsername: ${result[0].username}`
|
||||
)
|
||||
|
||||
connection0.end()
|
||||
|
||||
return jsonResponse(
|
||||
{
|
||||
success: true,
|
||||
message: null
|
||||
},
|
||||
400
|
||||
)
|
||||
}
|
||||
@@ -74,6 +74,8 @@ export async function handler (context: Context) {
|
||||
|
||||
await db0.insert(verifyCodes).values({ code, ip, timestamp: time })
|
||||
|
||||
connection0.end()
|
||||
|
||||
return jsonResponse(
|
||||
{
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user