Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 10x 10x 10x 10x 10x 10x 10x 10x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 5x 10x 1x 1x 4x 4x 10x 4x 4x 4x 10x 1x 1x 3x 3x 3x 10x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | import { NextRequest, NextResponse } from "next/server";
import { Session } from "next-auth";
import { prisma } from "@/lib/prisma";
import { addSecurityHeaders } from "@/lib/security";
import { checkRateLimit, getRateLimitInfo } from "@/lib/security";
import { z } from "zod";
import bcrypt from "bcryptjs";
import { logger } from "@/lib/logging";
import { PASSWORD_CONFIG } from "@/lib/security";
import { invalidateAllUserSessions } from "@/lib/auth";
import { logAuditEvent, AuditAction } from "@/lib/audit-logger";
import {
withAuth,
withErrorHandling,
successResponse,
ApiError,
ApiSuccessResponse,
ApiErrorResponse } from "@/lib/api";
import { RouteContext } from "@/lib/api/middleware";
// Password change schema
const PasswordChangeSchema = z.object({
oldPassword: z.string().min(1, "Old password is required"),
newPassword: z.string()
.min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
.regex(/[0-9]/, "Password must contain at least one number"),
confirmPassword: z.string().min(1, "Confirm password is required")}).refine((data) => data.newPassword === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"]});
// POST /api/user/password - Change user's password
async function handlePost(
request: NextRequest,
_context: RouteContext | undefined,
session: Session
): Promise<NextResponse<ApiSuccessResponse<{ message: string; requireReauth: boolean }> | ApiErrorResponse>> {
// Rate limiting (stricter for password changes)
const ip = request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || "unknown";
if (!checkRateLimit(`api-user-password-${ip}`, 5, 900000)) { // 5 attempts per 15 minutes
//
const rateLimitInfo = getRateLimitInfo(`api-user-password-${ip}`, 5);
const response = NextResponse.json<ApiErrorResponse>(
{
success: false,
error: {
code: "RATE_LIMITED",
message: "Too many password change attempts. Please try again later."}},
{
status: 429,
headers: {
"X-RateLimit-Limit": "5",
"X-RateLimit-Remaining": rateLimitInfo.remaining.toString(),
"X-RateLimit-Reset": rateLimitInfo.resetTime}}
);
return addSecurityHeaders(response);
}
const userId = session.user.id;
const body = await request.json();
// Validate input
const validatedData = PasswordChangeSchema.parse(body);
// Get current user with password
const user = await prisma.user.findUnique({
where: { id: userId },
select: { id: true, password: true }});
if (!user) {
throw ApiError.notFound("User");
}
// OAuth users don't have passwords
if (!user.password) {
throw ApiError.badRequest("Cannot change password for OAuth accounts");
}
// Verify old password
const isValidPassword = await bcrypt.compare(validatedData.oldPassword, user.password);
if (!isValidPassword) {
throw ApiError.badRequest("Current password is incorrect");
}
// Check if new password is same as old password
const isSamePassword = await bcrypt.compare(validatedData.newPassword, user.password);
if (isSamePassword) {
throw ApiError.badRequest("New password must be different from current password");
}
// Hash new password with security config cost factor
const hashedPassword = await bcrypt.hash(validatedData.newPassword, PASSWORD_CONFIG.BCRYPT_ROUNDS);
// Update password
await prisma.user.update({
where: { id: userId },
data: { password: hashedPassword }});
// Session rotation: invalidate all other sessions after password change
// This ensures any compromised sessions are invalidated
await invalidateAllUserSessions(userId);
// Log the password change event
await logAuditEvent(AuditAction.PASSWORD_CHANGE, request, {
userId});
logger.info(`Password changed for user ${userId}`, { category: "API" });
const response = successResponse({
message: "Password changed successfully. Please sign in again.",
requireReauth: true, // Signal frontend to redirect to login
});
return addSecurityHeaders(response);
}
export const POST = withErrorHandling(withAuth(handlePost));
|