Skip to content
Merged
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
39 changes: 1 addition & 38 deletions packages/auth/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,49 +11,12 @@ function createAdapter(): Adapter | undefined {
}

try {
const baseAdapter = DrizzleAdapter(db, {
return DrizzleAdapter(db, {
usersTable: users,
accountsTable: accounts,
sessionsTable: sessions,
verificationTokensTable: verificationTokens,
});

// DEBUG: Wrap token methods with logging
const wrappedAdapter: Adapter = {
...baseAdapter,
async createVerificationToken(data) {
console.log("[AUTH-DEBUG] createVerificationToken called:", {
identifier: data.identifier,
tokenHash: data.token?.substring(0, 20) + "...",
expires: data.expires,
});
const result = await baseAdapter.createVerificationToken!(data);
console.log("[AUTH-DEBUG] createVerificationToken result:", result ? "SUCCESS" : "NULL");
return result;
},
async useVerificationToken(params) {
console.log("[AUTH-DEBUG] useVerificationToken called:", {
identifier: params.identifier,
tokenHash: params.token?.substring(0, 20) + "...",
});
const result = await baseAdapter.useVerificationToken!(params);
console.log("[AUTH-DEBUG] useVerificationToken result:", result ? "FOUND" : "NOT_FOUND (THIS CAUSES VERIFICATION_FAILED)");
if (!result) {
// Extra debug: check if any tokens exist for this identifier
const { eq } = await import("drizzle-orm");
const existing = await db.select().from(verificationTokens).where(eq(verificationTokens.identifier, params.identifier));
console.log("[AUTH-DEBUG] tokens for this identifier:", existing.length);
if (existing.length > 0) {
console.log("[AUTH-DEBUG] stored token starts with:", existing[0].token?.substring(0, 20));
console.log("[AUTH-DEBUG] lookup token starts with:", params.token?.substring(0, 20));
console.log("[AUTH-DEBUG] tokens match:", existing[0].token === params.token);
}
}
return result;
},
};

return wrappedAdapter;
} catch (error) {
console.error("Auth adapter: Failed to create Drizzle adapter:", error);
return undefined;
Expand Down
13 changes: 8 additions & 5 deletions packages/auth/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,19 @@ export const authConfig: NextAuthConfig = {
const { createTransport } = await import("nodemailer");
const transport = createTransport(provider.server);

// Extract the raw token from NextAuth's callback URL
const parsedUrl = new URL(url);
const host = parsedUrl.host;
const token = parsedUrl.searchParams.get("token") || "";
const callbackUrl = parsedUrl.searchParams.get("callbackUrl") || "/dashboard";

// Build an intermediate /verify URL that prevents email scanners
// from consuming the one-time token via pre-fetch GET requests.
// We pass the ENTIRE original callback URL encoded to avoid
// dropping any parameters NextAuth needs internally.
// Build /verify URL — user clicks a button on this page to complete sign-in.
// This prevents email scanners from consuming the one-time token.
// The verify page redirects to our custom /api/auth/verify-email endpoint.
const verifyUrl = new URL("/verify", parsedUrl.origin);
verifyUrl.searchParams.set("callback", Buffer.from(url).toString("base64"));
verifyUrl.searchParams.set("token", token);
verifyUrl.searchParams.set("email", identifier);
verifyUrl.searchParams.set("callbackUrl", callbackUrl);
const safeUrl = verifyUrl.toString();

const result = await transport.sendMail({
Expand Down
102 changes: 0 additions & 102 deletions packages/db/scripts/diagnose-auth.ts

This file was deleted.

119 changes: 119 additions & 0 deletions sites/mainweb/app/(portal)/api/auth/verify-email/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { NextRequest, NextResponse } from "next/server";
import { db, verificationTokens, users, sessions } from "@query/db";
import { eq, and } from "drizzle-orm";

/**
* Custom email verification endpoint that bypasses NextAuth's callback.
*
* NextAuth's built-in email callback fails silently in this deployment.
* This endpoint replicates the exact same logic:
* 1. Hash the raw token with AUTH_SECRET (Web Crypto SHA-256)
* 2. Look up the hashed token + identifier in the DB
* 3. Delete the token (one-time use)
* 4. Create a database session for the user
* 5. Set the session cookie and redirect to dashboard
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const rawToken = searchParams.get("token");
const email = searchParams.get("email");
const callbackUrl = searchParams.get("callbackUrl") || "/dashboard";

if (!rawToken || !email) {
return NextResponse.redirect(new URL("/auth/error?error=Configuration", request.url));
}

const secret = process.env.AUTH_SECRET || process.env.NEXTAUTH_SECRET || "";

try {
// Hash the token the same way @auth/core does (Web Crypto SHA-256)
const data = new TextEncoder().encode(`${rawToken}${secret}`);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashedToken = Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");

// Look up and delete the token (one-time use)
if (!db) {
return NextResponse.redirect(new URL("/auth/error?error=Configuration", request.url));
}

const result = await db
.delete(verificationTokens)
.where(
and(
eq(verificationTokens.identifier, email),
eq(verificationTokens.token, hashedToken)
)
)
.returning();

if (result.length === 0) {
// Token not found — expired, already used, or hash mismatch
return NextResponse.redirect(new URL("/auth/error?error=Verification", request.url));
}

const invite = result[0];

// Check expiry
if (new Date(invite.expires) < new Date()) {
return NextResponse.redirect(new URL("/auth/error?error=Verification", request.url));
}

// Find or create user
let user = await db
.select()
.from(users)
.where(eq(users.email, email))
.then((r) => r[0] ?? null);

if (!user) {
// Create new user
const newId = crypto.randomUUID();
const inserted = await db
.insert(users)
.values({ id: newId, email, emailVerified: new Date() })
.returning();
user = inserted[0];
} else if (!user.emailVerified) {
// Mark email as verified
await db
.update(users)
.set({ emailVerified: new Date() })
.where(eq(users.id, user.id));
}

// Create a database session
const sessionToken = crypto.randomUUID();
const sessionExpires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days

await db.insert(sessions).values({
sessionToken,
userId: user.id,
expires: sessionExpires,
});

// Build redirect response with session cookie
const redirectUrl = new URL(callbackUrl, request.url);
const response = NextResponse.redirect(redirectUrl);

// Set the session cookie (same name NextAuth uses)
const isProduction = process.env.NODE_ENV === "production";
const cookieName = isProduction
? "__Secure-authjs.session-token"
: "authjs.session-token";

response.cookies.set(cookieName, sessionToken, {
httpOnly: true,
sameSite: "lax",
secure: isProduction,
path: "/",
expires: sessionExpires,
});

return response;
} catch (error) {
console.error("[verify-email] Error:", error);
return NextResponse.redirect(new URL("/auth/error?error=Verification", request.url));
}
}
76 changes: 0 additions & 76 deletions sites/mainweb/app/(portal)/api/debug-auth/route.ts

This file was deleted.

Loading
Loading