From 2d09a4a92c0190f3dd9f1ad223279f134546bf2d Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 26 Aug 2026 14:28:28 +0200 Subject: [PATCH] Add missing migrations --- .../migrations/0022_add_terms_age_consent.sql | 4 ++ scripts/e2e-register-test.ts | 67 +++++++++++++++++++ src/actions/register.ts | 22 +++++- 3 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 drizzle/migrations/0022_add_terms_age_consent.sql create mode 100644 scripts/e2e-register-test.ts diff --git a/drizzle/migrations/0022_add_terms_age_consent.sql b/drizzle/migrations/0022_add_terms_age_consent.sql new file mode 100644 index 00000000..202e0109 --- /dev/null +++ b/drizzle/migrations/0022_add_terms_age_consent.sql @@ -0,0 +1,4 @@ +-- Add terms/age consent columns expected by the users schema (register flow). +ALTER TABLE `users` + ADD COLUMN `terms_accepted` TINYINT(1) NOT NULL DEFAULT 0, + ADD COLUMN `age_verified` TINYINT(1) NOT NULL DEFAULT 0; diff --git a/scripts/e2e-register-test.ts b/scripts/e2e-register-test.ts new file mode 100644 index 00000000..e9c1bec8 --- /dev/null +++ b/scripts/e2e-register-test.ts @@ -0,0 +1,67 @@ +import { randomBytes } from "node:crypto"; +import { eq } from "drizzle-orm"; +import { drizzle } from "drizzle-orm/mysql2"; +import { bcrypt, bcryptVerify } from "hash-wasm"; +import mysql from "mysql2/promise"; +import * as schema from "@/db/schema"; + +const url = new URL( + "mysql://cms:HeelM%3Coeikgsdgsccusygcsugy@localhost:3306/habbo", +); +const pool = mysql.createPool({ + host: url.hostname, + port: Number(url.port) || 3306, + user: decodeURIComponent(url.username), + password: decodeURIComponent(url.password), + database: "habbo", + charset: "utf8mb4", +}); + +const db = drizzle(pool, { schema, mode: "default" }); + +async function main() { + const hash = await bcrypt({ + password: "Test1234", + salt: randomBytes(16), + costFactor: 12, + outputType: "encoded", + }); + const uname = "__e2e_test_" + Date.now(); + try { + await db.insert(schema.User).values({ + username: uname, + password: hash, + mail: null, + accountCreated: Math.floor(Date.now() / 1000), + ipRegister: "127.0.0.1", + ipCurrent: "127.0.0.1", + look: "hr-100-.hd-180-1.ch-255-66.lg-280-110.sh-305-62", + }); + console.log("INSERT OK"); + + // Simuleer login-verificatie + const [user] = await db + .select() + .from(schema.User) + .where(eq(schema.User.username, uname)) + .limit(1); + if (!user) throw new Error("User not found after insert"); + const ok = await bcryptVerify({ + password: "Test1234", + hash: user.password, + }); + console.log("LOGIN VERIFY:", ok ? "OK" : "FAILED"); + } catch (err) { + console.error("FAILED:", (err as Error).message); + if ((err as { cause?: Error }).cause) + console.error("CAUSE:", (err as { cause?: Error }).cause?.message); + process.exitCode = 1; + } finally { + await pool.execute( + "DELETE FROM users WHERE username LIKE '\\_\\_e2e\\_test\\_%'", + ); + await pool.end(); + } +} + +main(); diff --git a/src/actions/register.ts b/src/actions/register.ts index 5e4f9c51..eccc4fa2 100644 --- a/src/actions/register.ts +++ b/src/actions/register.ts @@ -29,6 +29,7 @@ const registerSchema = z.object({ .regex(/[A-Z]/, "Password must contain at least one uppercase letter") .regex(/[a-z]/, "Password must contain at least one lowercase letter") .regex(/[0-9]/, "Password must contain at least one digit"), + passwordConfirmation: z.string(), look: z.string().optional(), }); @@ -48,6 +49,9 @@ export async function register( .trim() .toLowerCase(), password: String(formData.get("password") ?? "").normalize("NFC"), + passwordConfirmation: String( + formData.get("password_confirmation") ?? "", + ).normalize("NFC"), look: String(formData.get("look") ?? "") .normalize("NFC") @@ -60,6 +64,10 @@ export async function register( return parsed.error.issues[0]?.message ?? "Invalid input"; } + if (parsed.data.password !== parsed.data.passwordConfirmation) { + return "Passwords do not match"; + } + const { username, mail, password, look } = parsed.data; const hasEmail = !!mail; const ip = await clientIp(); @@ -124,6 +132,7 @@ export async function register( ipRegister: ip, ipCurrent: ip, look, + termsAccepted: raw.termsAccepted, }); if (hasEmail) { @@ -133,9 +142,16 @@ export async function register( logger.warn("Failed to send verification email after registration"); } } - } catch { - logger.warn("Account creation failed"); - return "Could not create the account (is the username unique?)"; + } catch (err) { + const code = (err as { cause?: { code?: string } }).cause?.code; + if (code === "ER_DUP_ENTRY") { + return "That username is already taken"; + } + logger.error("Account creation failed", { + code, + message: err instanceof Error ? err.message : String(err), + }); + return "Could not create the account. Please try again or contact staff."; } redirect("/login?registered=1");