Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
26 changes: 17 additions & 9 deletions app/api/auth/register/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import crypto from "crypto";
import prisma from "@/lib/prisma";
import { checkRateLimit } from "@/lib/rateLimit";
import { sendVerificationEmail } from "@/lib/email";
import { signupSchema } from "@/lib/validations/auth";

const REGISTER_LIMIT = 5;
const REGISTER_WINDOW_MS = 60 * 60 * 1000;

const registerSchema = signupSchema.pick({ email: true, password: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate the name field to maintain consistency with the shared schema.

By picking only email and password, the API bypasses the name validation defined in signupSchema (which requires at least 2 characters). This means the API could accept registrations with an empty name even though the frontend enforces it.

Consider using signupSchema directly to validate all fields, ensuring the API and frontend rules remain perfectly aligned.

♻️ Proposed fix
-const registerSchema = signupSchema.pick({ email: true, password: true });
+const registerSchema = signupSchema;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const registerSchema = signupSchema.pick({ email: true, password: true });
const registerSchema = signupSchema;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/auth/register/route.ts` at line 12, Update registerSchema to use
signupSchema directly instead of picking only email and password, so
registration validates name alongside the other shared signup fields and remains
aligned with frontend validation.


export async function POST(req: Request) {
const ip =
req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
Expand All @@ -29,10 +32,15 @@ export async function POST(req: Request) {
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
}

const normalizedEmail = email.toLowerCase().trim();
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$/;

if (!passwordRegex.test(password)) {
const result = registerSchema.safeParse({ email, password });
if (!result.success) {
const fieldErrors = result.error.flatten().fieldErrors;
if (fieldErrors.email) {
return NextResponse.json(
{ error: "Invalid email address" },
{ status: 400 },
);
}
return NextResponse.json(
{ error: "Password does not meet requirements" },
{ status: 400 },
Expand All @@ -46,7 +54,7 @@ export async function POST(req: Request) {
user = await prisma.user.create({
data: {
name,
email: normalizedEmail,
email: result.data.email,
password: hashedPassword,
// emailVerified intentionally left null — set only after verification
},
Expand All @@ -69,18 +77,18 @@ export async function POST(req: Request) {

await prisma.verificationToken.create({
data: {
identifier: normalizedEmail,
identifier: result.data.email,
token,
expires,
},
});

// Send verification email — non-blocking in dev if SMTP not configured
try {
await sendVerificationEmail(normalizedEmail, token);
await sendVerificationEmail(result.data.email, token);
} catch {
// Email sending failure should not block registration
console.error("Failed to send verification email to", normalizedEmail);
console.error("Failed to send verification email to", result.data.email);
}
Comment on lines 86 to 92

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid logging sensitive user data (PII).

Logging the user's email address in plain text creates a privacy risk and violates best practices for log management. Consider masking the email or removing it entirely from the log message, as the sendVerificationEmail function already logs relevant error details.

🛡️ Proposed fix
         // Send verification email — non-blocking in dev if SMTP not configured
         try {
             await sendVerificationEmail(result.data.email, token);
         } catch {
             // Email sending failure should not block registration
-            console.error("Failed to send verification email to", result.data.email);
+            console.error("Failed to send verification email");
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Send verification email — non-blocking in dev if SMTP not configured
try {
await sendVerificationEmail(normalizedEmail, token);
await sendVerificationEmail(result.data.email, token);
} catch {
// Email sending failure should not block registration
console.error("Failed to send verification email to", normalizedEmail);
console.error("Failed to send verification email to", result.data.email);
}
// Send verification email — non-blocking in dev if SMTP not configured
try {
await sendVerificationEmail(result.data.email, token);
} catch {
// Email sending failure should not block registration
console.error("Failed to send verification email");
}
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 90-90: Avoid logging sensitive data
Context: console.error("Failed to send verification email to", result.data.email)
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/auth/register/route.ts` around lines 86 - 92, Update the
sendVerificationEmail error handler in the registration route to stop logging
result.data.email in plaintext. Remove the email from the console.error call,
while preserving the non-blocking registration behavior and existing failure
message.

Source: Linters/SAST tools


return NextResponse.json(
Expand All @@ -95,4 +103,4 @@ export async function POST(req: Request) {
{ status: 500 },
);
}
}
}
11 changes: 1 addition & 10 deletions app/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Spinner } from "@/components/ui/spinner";
import { getCsrfToken } from "@/lib/csrfClient";
import { useCsrf } from "@/lib/useCsrf";
import { PLATFORMS } from "@/lib/constants";
import { getPasswordError } from "@/lib/validations/auth";

import { Navbar } from "../components/Navbar";

Expand All @@ -28,16 +29,6 @@ export default function RegisterPage() {
const [googleLoading, setGoogleLoading] = useState(false);
const [githubLoading, setGithubLoading] = useState(false);

const getPasswordError = (value: string) => {
if (value.length < 8) return "Must be at least 8 characters";
if (!/[A-Z]/.test(value)) return "Must include one uppercase letter";
if (!/[a-z]/.test(value)) return "Must include one lowercase letter";
if (!/\d/.test(value)) return "Must include one number";
if (!/[@$!%*?&]/.test(value)) return "Must include one special character";

return null;
};

const error = getPasswordError(password);

async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
Expand Down
103 changes: 103 additions & 0 deletions lib/validations/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import assert from "node:assert/strict";
import test from "node:test";

import { signupSchema, getPasswordError } from "@/lib/validations/auth";

test("signupSchema accepts valid email and password", () => {
const result = signupSchema.safeParse({
name: "John Doe",
email: "john@example.com",
password: "Secure@123",
});

assert.equal(result.success, true);
});

test("signupSchema rejects invalid email", () => {
const result = signupSchema.safeParse({
name: "John Doe",
email: "not-an-email",
password: "Secure@123",
});

assert.equal(result.success, false);
if (!result.success) {
const emailErrors = result.error.flatten().fieldErrors.email;
assert.ok(emailErrors?.some((e) => e.includes("Invalid email")));
}
});

test("signupSchema rejects empty email", () => {
const result = signupSchema.safeParse({
name: "John Doe",
email: "",
password: "Secure@123",
});

assert.equal(result.success, false);
});

test("signupSchema accepts valid password", () => {
const result = signupSchema.safeParse({
name: "John Doe",
email: "john@example.com",
password: "Abcdef1@",
});

assert.equal(result.success, true);
});

test("signupSchema rejects password below minimum length", () => {
const result = signupSchema.safeParse({
name: "John Doe",
email: "john@example.com",
password: "Ab1@",
});

assert.equal(result.success, false);
if (!result.success) {
const passwordErrors = result.error.flatten().fieldErrors.password;
assert.ok(passwordErrors?.some((e) => e.includes("at least 8")));
}
});

test("signupSchema rejects missing required fields", () => {
const result = signupSchema.safeParse({});

assert.equal(result.success, false);
if (!result.success) {
const errors = result.error.flatten().fieldErrors;
assert.ok(errors.name);
assert.ok(errors.email);
assert.ok(errors.password);
}
});

test("getPasswordError returns null for valid password", () => {
assert.equal(getPasswordError("Secure@123"), null);
});

test("getPasswordError returns error for password below minimum length", () => {
const error = getPasswordError("Ab1@");
assert.ok(error?.includes("at least 8 characters"));
});

test("getPasswordError returns error for missing uppercase", () => {
const error = getPasswordError("secure@123");
assert.ok(error?.includes("uppercase"));
});

test("getPasswordError returns error for missing lowercase", () => {
const error = getPasswordError("SECURE@123");
assert.ok(error?.includes("lowercase"));
});

test("getPasswordError returns error for missing number", () => {
const error = getPasswordError("Secure@abc");
assert.ok(error?.includes("number"));
});

test("getPasswordError returns error for missing special character", () => {
const error = getPasswordError("Secure123");
assert.ok(error?.includes("special character"));
});
15 changes: 12 additions & 3 deletions lib/validations/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@ import { z } from 'zod';

export const signupSchema = z.object({
name: z.string().min(2, 'Name is required'),
email: z.email('Invalid email address'),
email: z.string().email('Invalid email address'),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain at least 1 uppercase letter')
.regex(/[a-z]/, 'Password must contain at least 1 lowercase letter')
.regex(/[0-9]/, 'Password must contain at least 1 number')
.regex(/[!@#$%^&*(),.?":{}|<>]/, 'Password must contain at least 1 special character'),
});
.regex(/[@$!%*?&]/, 'Password must contain at least 1 special character'),
});

export function getPasswordError(password: string): string | null {
if (password.length < 8) return 'Must be at least 8 characters';
if (!/[A-Z]/.test(password)) return 'Must include one uppercase letter';
if (!/[a-z]/.test(password)) return 'Must include one lowercase letter';
if (!/\d/.test(password)) return 'Must include one number';
if (!/[@$!%*?&]/.test(password)) return 'Must include one special character';
return null;
}