Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
679 changes: 280 additions & 399 deletions app/(app)/settings/_client.tsx

Large diffs are not rendered by default.

102 changes: 77 additions & 25 deletions app/api/verify-email/route.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,96 @@
import { getServerAuthSession } from "@/server/auth";
import { db } from "@/server/db";
import {
deleteTokenFromDb,
getTokenFromDb,
updateEmail,
} from "@/utils/emailToken";
import { NextRequest, NextResponse } from "next/server";
emailChangeRequest,
user,
emailChangeHistory,
} from "@/server/db/schema";
import { and, eq, gte } from "drizzle-orm";
import { type NextRequest } from "next/server";

export async function GET(req: NextRequest, res: NextResponse) {
export async function GET(req: NextRequest) {
try {
const token = req.nextUrl.searchParams.get("token");

if (!token)
return NextResponse.json({ message: "Invalid request" }, { status: 400 });
if (!token) {
return new Response(JSON.stringify({ message: "Invalid request" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}

const session = await getServerAuthSession();

if (!session || !session.user)
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
if (!session || !session.user) {
return new Response(JSON.stringify({ message: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}

const tokenFromDb = await getTokenFromDb(token, session.user.id);
const userId = session.user.id;

if (!tokenFromDb || !tokenFromDb.length)
return NextResponse.json({ message: "Invalid token" }, { status: 400 });
const request = await db.query.emailChangeRequest.findFirst({
where: and(
eq(emailChangeRequest.token, token),
eq(emailChangeRequest.userId, userId),
gte(emailChangeRequest.expiresAt, new Date()),
),
});

const { userId, expiresAt, email } = tokenFromDb[0];
if (expiresAt < new Date())
return NextResponse.json({ message: "Token expired" }, { status: 400 });
if (!request) {
return new Response(
JSON.stringify({ message: "Invalid or expired token" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}

await updateEmail(userId, email);
const currentUser = await db.query.user.findFirst({
where: eq(user.id, userId),
});

await deleteTokenFromDb(token);
if (!currentUser) {
return new Response(JSON.stringify({ message: "User not found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}

return NextResponse.json(
{ message: "Email successfully verified" },
{ status: 200 },
await db
.update(user)
.set({
email: request.newEmail,
emailVerified: new Date().toISOString(),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure correct data type for emailVerified field

The emailVerified field is being set using new Date().toISOString(), which returns a string. If the database schema expects a Date object, consider passing new Date() instead to ensure type consistency.

Apply this diff to fix the issue:

- emailVerified: new Date().toISOString(),
+ emailVerified: new Date(),
📝 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
emailVerified: new Date().toISOString(),
emailVerified: new Date(),

})
.where(eq(user.id, userId));

await db.insert(emailChangeHistory).values({
userId,
oldEmail: currentUser.email ?? "",
newEmail: request.newEmail,
ipAddress: req.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? "",
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

IP Address Extraction Validation Needs Improvement

The current implementation directly accesses the x-forwarded-for header without ensuring its presence or handling potential malformed values. Additionally, there is no indication of using trusted proxies or middleware to obtain the client's IP address reliably.

  • File: app/api/verify-email/route.ts at line 74

🔗 Analysis chain

Validate client IP address extraction

When extracting the client's IP address from the x-forwarded-for header, you're assuming the header is present and contains a comma-separated list of IPs. To enhance reliability:

  • Ensure the header exists before splitting and trimming.
  • Consider using a trusted proxy or middleware to obtain the client's IP.
  • Handle scenarios where the header might be missing or malformed.

You can verify the presence of the header with the following script:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for usage of 'x-forwarded-for' header in codebase

# Test: Search for instances where 'x-forwarded-for' is used
rg --type typescript '"x-forwarded-for"'

Length of output: 77


Script:

#!/bin/bash
# Description: Check for usage of 'x-forwarded-for' header in TypeScript codebase

# Test: Search for instances where 'x-forwarded-for' is used
rg --type ts '"x-forwarded-for"'

Length of output: 145


Script:

#!/bin/bash
# Description: Check for trusted proxy or middleware configurations handling client IP extraction in TypeScript codebase

# Test: Search for usage of 'trust proxy' or related middleware configurations
rg --type ts 'trust proxy|middleware'

Length of output: 589

userAgent: req.headers.get("user-agent") ?? "",
});

await db
.delete(emailChangeRequest)
.where(eq(emailChangeRequest.id, request.id));

return new Response(
JSON.stringify({ message: "Email updated successfully" }),
{
status: 200,
headers: { "Content-Type": "application/json" },
},
);
} catch (error) {
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
console.error("Error verifying email:", error);
return new Response(JSON.stringify({ error: "Internal server error" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}
39 changes: 39 additions & 0 deletions drizzle/0010_email-tokens-and-indexes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
CREATE TABLE IF NOT EXISTS "EmailChangeHistory" (
"id" serial PRIMARY KEY NOT NULL,
"userId" text NOT NULL,
"oldEmail" text NOT NULL,
"newEmail" text NOT NULL,
"changedAt" timestamp DEFAULT now() NOT NULL,
"ipAddress" text,
"userAgent" text
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "EmailChangeRequest" (
"id" serial PRIMARY KEY NOT NULL,
"userId" text NOT NULL,
"newEmail" text NOT NULL,
"token" text NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"expiresAt" timestamp NOT NULL,
CONSTRAINT "EmailChangeRequest_token_unique" UNIQUE("token")
);
--> statement-breakpoint
DROP TABLE "EmailVerificationToken";--> statement-breakpoint
DROP TABLE "verificationToken";--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "EmailChangeHistory" ADD CONSTRAINT "EmailChangeHistory_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "EmailChangeRequest" ADD CONSTRAINT "EmailChangeRequest_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "Comment_postId_index" ON "Comment" USING btree ("postId");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "Notification_userId_index" ON "Notification" USING btree ("userId");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "Post_slug_index" ON "Post" USING btree ("slug");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "Post_userId_index" ON "Post" USING btree ("userId");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "User_username_index" ON "user" USING btree ("username");
Loading