diff --git a/bun.lock b/bun.lock index c0fda45..7e3ab74 100644 --- a/bun.lock +++ b/bun.lock @@ -7,12 +7,16 @@ "dependencies": { "@elysiajs/cors": "1.4.1", "@elysiajs/swagger": "1.3.1", + "bcryptjs": "3.0.3", + "crypto": "1.0.1", "dotenv": "17.2.3", "drizzle-orm": "0.45.1", "elysia": "1.4.22", "mysql2": "3.16.1", + "validator": "13.15.26", }, "devDependencies": { + "@types/validator": "13.15.10", "bun-types": "1.3.6", }, }, @@ -36,16 +40,22 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], + "@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="], + + "@types/validator": ["@types/validator@13.15.10", "", {}, "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA=="], "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], + "bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="], + "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "crypto": ["crypto@1.0.1", "", {}, "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], @@ -106,6 +116,8 @@ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "validator": ["validator@13.15.26", "", {}, "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA=="], + "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], diff --git a/package.json b/package.json index 72a290b..ecb35cb 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,16 @@ "dependencies": { "@elysiajs/cors": "1.4.1", "@elysiajs/swagger": "1.3.1", + "bcryptjs": "3.0.3", + "crypto": "1.0.1", "dotenv": "17.2.3", "drizzle-orm": "0.45.1", "elysia": "1.4.22", - "mysql2": "3.16.1" + "mysql2": "3.16.1", + "validator": "13.15.26" }, "devDependencies": { + "@types/validator": "13.15.10", "bun-types": "1.3.6" }, "module": "src/index.js" diff --git a/src/index.ts b/src/index.ts index 8762b13..a58c3f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,8 @@ import { handler as berrydashProfilePostsPutHandler } from './routes/berrydash/p import { handler as berryDashIconMarketplacePostHandler } from './routes/berrydash/icon-marketplace/post' +import { handler as berryDashAccountLoginPostHandler } from './routes/berrydash/account/login/post' +import { handler as berryDashAccountRegisterPostHandler } from './routes/berrydash/account/register/post' import { handler as berryDashAccountSaveGetHandler } from './routes/berrydash/account/save/get' import { handler as berryDashAccountSavePostHandler } from './routes/berrydash/account/save/post' @@ -348,6 +350,36 @@ app.post( }) } ) +app.post( + '/berrydash/account/login', + context => berryDashAccountLoginPostHandler(context), + { + detail: { + description: + 'The endpoint for logging into an account. This is also the endpoint for refreshing login.', + tags: ['Berry Dash', 'Accounts'] + }, + body: t.Object({ + username: t.String(), + password: t.String() + }) + } +) +app.post( + '/berrydash/account/register', + context => berryDashAccountRegisterPostHandler(context), + { + detail: { + description: 'The endpoint for registering an account.', + tags: ['Berry Dash', 'Accounts'] + }, + body: t.Object({ + username: t.String(), + password: t.String(), + email: t.String() + }) + } +) app.all('*', () => jsonResponse( { diff --git a/src/routes/berrydash/account/login/post.ts b/src/routes/berrydash/account/login/post.ts new file mode 100644 index 0000000..dd489a5 --- /dev/null +++ b/src/routes/berrydash/account/login/post.ts @@ -0,0 +1,101 @@ +import { Context } from 'elysia' +import { getDatabaseConnection, jsonResponse } from '../../../../lib/util' +import { berryDashUserData, users } from '../../../../lib/tables' +import { eq } from 'drizzle-orm' +import bcrypt from 'bcryptjs' + +type Body = { + username: string + password: string +} + +export async function handler (context: Context) { + const dbInfo0 = getDatabaseConnection(0) + const dbInfo1 = getDatabaseConnection(1) + + if (!dbInfo0 || !dbInfo1) + return jsonResponse( + { success: false, message: 'Failed to connect to database', data: null }, + 500 + ) + const { connection: connection0, db: db0 } = dbInfo0 + const { connection: connection1, db: db1 } = dbInfo1 + + const body = context.body as Body + if (!body.username || !body.password) { + connection0.end() + connection1.end() + return jsonResponse( + { + success: false, + message: 'Username and password must be in POST data', + data: null + }, + 400 + ) + } + + const user = await db0 + .select({ + id: users.id, + username: users.username, + password: users.password + }) + .from(users) + .where(eq(users.username, body.username)) + .limit(1) + .execute() + if (!user[0]) { + connection0.end() + connection1.end() + return jsonResponse( + { + success: false, + message: 'Invalid username or password', + data: null + }, + 401 + ) + } + if (!(await bcrypt.compare(body.password, user[0].password))) { + connection0.end() + connection1.end() + return jsonResponse( + { + success: false, + message: 'Invalid username or password', + data: null + }, + 401 + ) + } + + const user2 = await db1 + .select({ token: berryDashUserData.token }) + .from(berryDashUserData) + .where(eq(berryDashUserData.id, user[0].id)) + .limit(1) + .execute() + if (!user2[0]) { + connection0.end() + connection1.end() + return jsonResponse( + { + success: false, + message: 'Invalid username or password', + data: null + }, + 401 + ) + } + + return jsonResponse({ + success: true, + message: null, + data: { + session: user2[0].token, + username: user[0].username, + id: user[0].id + } + }) +} diff --git a/src/routes/berrydash/account/register/post.ts b/src/routes/berrydash/account/register/post.ts new file mode 100644 index 0000000..790fdb6 --- /dev/null +++ b/src/routes/berrydash/account/register/post.ts @@ -0,0 +1,150 @@ +import { Context } from 'elysia' +import { + getClientIp, + getDatabaseConnection, + jsonResponse +} from '../../../../lib/util' +import isEmail from 'validator/lib/isEmail' +import { berryDashUserData, users } from '../../../../lib/tables' +import { eq, or } from 'drizzle-orm' +import bcrypt from 'bcryptjs' +import { randomBytes } from 'crypto' + +type Body = { + username: string + password: string + email: string +} + +export async function handler (context: Context) { + const dbInfo0 = getDatabaseConnection(0) + const dbInfo1 = getDatabaseConnection(1) + + if (!dbInfo0 || !dbInfo1) + return jsonResponse( + { success: false, message: 'Failed to connect to database', data: null }, + 500 + ) + const { connection: connection0, db: db0 } = dbInfo0 + const { connection: connection1, db: db1 } = dbInfo1 + + const body = context.body as Body + if (!body.username || !body.password || !body.email) { + connection0.end() + connection1.end() + return jsonResponse( + { + success: false, + message: 'Username, password and email must be in POST data', + data: null + }, + 400 + ) + } + + if (!/^[a-zA-Z0-9]{3,16}$/.test(body.username)) { + connection0.end() + connection1.end() + return jsonResponse( + { + success: false, + message: 'Username must be 3-16 characters, letters and numbers only', + data: null + }, + 400 + ) + } + + if (!isEmail(body.email)) { + connection0.end() + connection1.end() + return jsonResponse( + { + success: false, + message: 'Email is invalid', + data: null + }, + 400 + ) + } + + if ( + !/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d!@#$%^&*()_\-+=]{8,}$/.test(body.password) + ) { + connection0.end() + connection1.end() + return jsonResponse( + { + success: false, + message: + 'Password must be at least 8 characters with at least one letter and one number', + data: null + }, + 400 + ) + } + + const existingCheck = await db0 + .select({ id: users.id }) + .from(users) + .where(or(eq(users.username, body.email), eq(users.email, body.email))) + .limit(1) + .execute() + if (existingCheck[0]) { + connection0.end() + connection1.end() + return jsonResponse( + { + success: false, + message: 'Username or email is already taken', + data: null + }, + 409 + ) + } + + const hashedPassword = await bcrypt.hash(body.password, 10) + const token = randomBytes(256).toString('hex') + const ip = getClientIp(context) + const time = Math.floor(Date.now() / 1000) + if (!ip) { + connection0.end() + connection1.end() + return jsonResponse( + { + success: false, + message: 'Failed to get required info', + data: null + }, + 400 + ) + } + + const result = await db0 + .insert(users) + .values({ + username: body.username, + password: hashedPassword, + email: body.email, + registerTime: time, + latestIp: ip + }) + .execute() + + await db1 + .insert(berryDashUserData) + .values({ + id: result[0].insertId, + token + }) + .execute() + + return jsonResponse( + { + success: true, + message: null, + data: null + }, + 200 + ) +}