Files
beenvoice-web/src/app/api/auth/reset-password/route.ts
T

122 lines
3.2 KiB
TypeScript

import { type NextRequest, NextResponse } from "next/server";
import { eq, and, gt } from "drizzle-orm";
import bcrypt from "bcryptjs";
import { hashPasswordResetToken } from "~/lib/reset-token";
import { revokeUserSessions } from "~/lib/session-security";
import { rateLimitKey, requireRateLimit } from "~/lib/rate-limit";
import { db } from "~/server/db";
import { accounts, users } from "~/server/db/schema";
export async function POST(request: NextRequest) {
try {
const ipRateLimit = requireRateLimit(rateLimitKey(request, "auth:reset"), {
windowMs: 60 * 1000,
max: 10,
});
if (ipRateLimit) return ipRateLimit;
const { token, password } = (await request.json()) as {
token: string;
password: string;
};
if (!token || typeof token !== "string") {
return NextResponse.json({ error: "Token is required" }, { status: 400 });
}
if (!password || typeof password !== "string") {
return NextResponse.json(
{ error: "Password is required" },
{ status: 400 },
);
}
if (password.length < 8) {
return NextResponse.json(
{ error: "Password must be at least 8 characters long" },
{ status: 400 },
);
}
const tokenRateLimit = requireRateLimit(
rateLimitKey(request, "auth:reset-token", token),
{
windowMs: 60 * 60 * 1000,
max: 5,
},
);
if (tokenRateLimit) return tokenRateLimit;
const tokenHash = hashPasswordResetToken(token);
// Find user with valid reset token that hasn't expired
const user = await db.query.users.findFirst({
where: and(
eq(users.resetToken, tokenHash),
gt(users.resetTokenExpiry, new Date()),
),
});
if (!user) {
return NextResponse.json(
{ error: "Invalid or expired token" },
{ status: 400 },
);
}
// Hash the new password
const hashedPassword = await bcrypt.hash(password, 12);
await db.transaction(async (tx) => {
await tx
.update(users)
.set({
password: hashedPassword,
resetToken: null,
resetTokenExpiry: null,
})
.where(eq(users.id, user.id));
const credentialAccount = await tx.query.accounts.findFirst({
where: and(
eq(accounts.userId, user.id),
eq(accounts.providerId, "credential"),
),
});
if (credentialAccount) {
await tx
.update(accounts)
.set({
password: hashedPassword,
updatedAt: new Date(),
})
.where(eq(accounts.id, credentialAccount.id));
} else {
await tx.insert(accounts).values({
userId: user.id,
accountId: user.id,
providerId: "credential",
password: hashedPassword,
});
}
});
await revokeUserSessions(user.id);
return NextResponse.json(
{
success: true,
message: "Password has been reset successfully",
},
{ status: 200 },
);
} catch (error) {
console.error("Password reset error:", error);
return NextResponse.json(
{ error: "An error occurred while resetting your password" },
{ status: 500 },
);
}
}