Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 40 additions & 16 deletions app/(site)/components/AuthForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const AuthForm = () => {
},
});

const onSubmit: SubmitHandler<FieldValues> = (data) => {
const onSubmit: SubmitHandler<FieldValues> = async (data) => {
setIsLoading(true);

if (!data) {
Expand All @@ -56,33 +56,57 @@ 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", {
Copy link

@cubic-dev-ai cubic-dev-ai bot Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure the LOGIN signIn call is wrapped in a try/finally (or handles rejection) so setIsLoading(false) always runs; otherwise a thrown error will leave the form stuck in a loading state.

Prompt for AI agents
Address the following comment on app/(site)/components/AuthForm.tsx at line 61:

<comment>Ensure the LOGIN signIn call is wrapped in a try/finally (or handles rejection) so setIsLoading(false) always runs; otherwise a thrown error will leave the form stuck in a loading state.</comment>

<file context>
@@ -56,33 +56,51 @@ const AuthForm = () =&gt; {
-          toast.error(&quot;Something went wrong!&quot;);
+      try {
+        await axios.post(&quot;/api/register&quot;, data);
+        const callback = await signIn(&quot;credentials&quot;, {
+          email: data.email,
+          password: data.password,
</file context>
Fix with Cubic

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!");
}
if (callback?.ok && !callback?.error) {
toast.success("Logged in!");
router.push("/users");
}
});
} finally {
setIsLoading(false);
}
return;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Login Flow Fails to Reset Loading State

The LOGIN flow's signIn call lacks try-catch error handling. If signIn throws an exception, setIsLoading(false) isn't reached, leaving the form stuck in a loading state. This differs from the REGISTER flow, which correctly uses finally to reset the loading state.

Fix in Cursor Fix in Web


setIsLoading(false);
};

const socialAction = (action: string) => {
Expand Down
4 changes: 2 additions & 2 deletions app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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");
Expand Down
5 changes: 3 additions & 2 deletions app/api/migrate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}

Expand Down
84 changes: 80 additions & 4 deletions app/api/register/route.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,44 @@
import bcrypt from "bcrypt";
import { Prisma } from "@prisma/client";

import prisma from "@/app/libs/prismadb";
import { pusherServer } from "@/app/libs/pusher";
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);
Expand All @@ -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 });
}
}
11 changes: 11 additions & 0 deletions app/libs/prismadb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
});
Expand Down