diff --git a/app/(site)/components/AuthForm.tsx b/app/(site)/components/AuthForm.tsx index 5e33fc7..84c13d9 100644 --- a/app/(site)/components/AuthForm.tsx +++ b/app/(site)/components/AuthForm.tsx @@ -47,7 +47,7 @@ const AuthForm = () => { }, }); - const onSubmit: SubmitHandler = (data) => { + const onSubmit: SubmitHandler = async (data) => { setIsLoading(true); if (!data) { @@ -56,22 +56,45 @@ const AuthForm = () => { } if (variant === "REGISTER") { - axios - .post("/api/register", data) - .then(() => { - signIn("credentials", data); - }) - .catch((err) => { - console.log(err); - toast.error("Something went wrong!"); + try { + await axios.post("/api/register", data); + const callback = await signIn("credentials", { + email: data.email, + password: data.password, + redirect: false, }); + + if (callback?.error) { + toast.error(callback.error || "Invalid credentials!"); + } else { + toast.success("Account created!"); + router.push("/users"); + } + } catch (err: any) { + if (err?.response?.status === 409) { + toast.error("Email already registered"); + } else if (err?.response?.data) { + const errorMessage = typeof err.response.data === 'string' + ? err.response.data + : err.response.data.error || err.response.data.message || 'Registration failed'; + toast.error(errorMessage); + } else { + toast.error("Registration failed"); + } + } finally { + setIsLoading(false); + } + return; } if (variant === "LOGIN") { - signIn("credentials", { - ...data, - redirect: false, - }).then((callback) => { + try { + const callback = await signIn("credentials", { + email: data.email, + password: data.password, + redirect: false, + }); + if (callback?.error) { toast.error("Invalid credentials!"); } @@ -79,10 +102,11 @@ const AuthForm = () => { toast.success("Logged in!"); router.push("/users"); } - }); + } finally { + setIsLoading(false); + } + return; } - - setIsLoading(false); }; const socialAction = (action: string) => { diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index bec8798..f507c11 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,5 +1,5 @@ import { PrismaAdapter } from "@next-auth/prisma-adapter"; -import brcypt from "bcrypt"; +import bcrypt from "bcrypt"; import NextAuth, { AuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import GithubProvider from "next-auth/providers/github"; @@ -37,7 +37,7 @@ export const authOptions: AuthOptions = { throw new Error("Invalid credentials"); } - const isPasswordCorrect = await brcypt.compare(credentials.password, user.hashedPassword); + const isPasswordCorrect = await bcrypt.compare(credentials.password, user.hashedPassword); if (!isPasswordCorrect) { throw new Error("Invalid credentials"); diff --git a/app/api/migrate/route.ts b/app/api/migrate/route.ts index 9a81d08..3ace041 100644 --- a/app/api/migrate/route.ts +++ b/app/api/migrate/route.ts @@ -5,8 +5,9 @@ export async function GET(request: Request) { // Simple auth check - you should use a proper secret const { searchParams } = new URL(request.url); const secret = searchParams.get("secret"); - - if (secret !== "your-migration-secret-123") { + const expected = process.env.MIGRATE_SECRET || "your-migration-secret-123"; + + if (secret !== expected) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } diff --git a/app/api/register/route.ts b/app/api/register/route.ts index baae4f7..6750d13 100644 --- a/app/api/register/route.ts +++ b/app/api/register/route.ts @@ -1,4 +1,5 @@ import bcrypt from "bcrypt"; +import { Prisma } from "@prisma/client"; import prisma from "@/app/libs/prismadb"; import { pusherServer } from "@/app/libs/pusher"; @@ -6,11 +7,38 @@ import { NextResponse } from "next/server"; export async function POST(req: Request) { try { + // Basic runtime env check for DB + if (!process.env.DATABASE_URL) { + return NextResponse.json( + { error: "Database is not configured. Set DATABASE_URL env var" }, + { status: 500 } + ); + } + const body = await req.json(); const { email, name, password } = body; if (!email || !name || !password) { - return new NextResponse("Missing fields.", { status: 400 }); + return NextResponse.json({ error: "Missing fields" }, { status: 400 }); + } + + // Basic validation to catch obvious client errors early + const emailRegex = /[^@\s]+@[^@\s]+\.[^@\s]+/; + if (!emailRegex.test(email)) { + return NextResponse.json({ error: "Invalid email format" }, { status: 400 }); + } + + if (typeof password !== 'string' || password.length < 6) { + return NextResponse.json({ error: "Password must be at least 6 characters" }, { status: 400 }); + } + + // Prevent duplicate accounts + const existingUser = await prisma.user.findUnique({ + where: { email }, + }); + + if (existingUser) { + return NextResponse.json({ error: "Email already registered" }, { status: 409 }); } const hashedPassword = await bcrypt.hash(password, 12); @@ -32,8 +60,56 @@ export async function POST(req: Request) { } return NextResponse.json(user); - } catch (error) { - console.log("[REGISTRATION_ERROR]", error); - return new NextResponse("Error while registering user.", { status: 500 }); + } catch (error: any) { + // Log detailed error server-side for diagnostics + console.error("[REGISTRATION_ERROR]", { + message: error?.message, + code: error?.code, + stack: error?.stack, + }); + + // Prisma known request errors (e.g., unique constraint) + if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === "P2002") { + return NextResponse.json({ error: "Email already registered" }, { status: 409 }); + } + return NextResponse.json({ error: `Database error (${error.code})` }, { status: 500 }); + } + + // Prisma initialization/connection errors + if (typeof error?.code === 'string' && error.code.startsWith('P10')) { + // P1000..P1017 are connection/auth/timeout errors + return NextResponse.json({ + error: "Database connection error", + code: error.code, + hint: "Verify DATABASE_URL and SSL settings for Aiven MySQL (e.g., add ?sslaccept=strict)" + }, { status: 500 }); + } + + // Missing table / not migrated yet + const messageText = String(error?.message || '').toLowerCase(); + if (messageText.includes('doesn\'t exist') || messageText.includes('no such table') || messageText.includes('er_no_such_table')) { + return NextResponse.json({ + error: "Database not migrated", + hint: "Run `npx prisma migrate deploy` or open /api/migrate with the secret" + }, { status: 500 }); + } + + // Low-level network errors + if (error?.code === 'ECONNREFUSED' || messageText.includes('getaddrinfo') || messageText.includes('connect etimedout')) { + return NextResponse.json({ + error: "Cannot reach database", + hint: "Check host/port/firewall and SSL requirements for your Aiven instance" + }, { status: 500 }); + } + + // Fallback (include message in non-production for TDD diagnostics) + if (process.env.NODE_ENV !== 'production') { + return NextResponse.json( + { error: "Error while registering user", message: error?.message }, + { status: 500 } + ); + } + return NextResponse.json({ error: "Error while registering user" }, { status: 500 }); } } diff --git a/app/libs/prismadb.ts b/app/libs/prismadb.ts index b6ad834..ff6b5f0 100644 --- a/app/libs/prismadb.ts +++ b/app/libs/prismadb.ts @@ -76,6 +76,17 @@ const createClient = () => { return new MockPrismaClient() as unknown as PrismaClient; } + // Ensure Aiven MySQL connections use SSL if not explicitly provided + const originalUrl = process.env.DATABASE_URL || ""; + const needsSsl = originalUrl.startsWith("mysql://") && !/[?&](sslaccept|ssl-mode)=/i.test(originalUrl); + const urlWithSsl = needsSsl + ? originalUrl + (originalUrl.includes("?") ? "&" : "?") + "sslaccept=strict" + : originalUrl; + + if (needsSsl) { + process.env.DATABASE_URL = urlWithSsl; + } + return new PrismaClient({ log: process.env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'], });