229 lines
5.7 KiB
TypeScript
229 lines
5.7 KiB
TypeScript
import { echo } from "@atums/echo";
|
|
import {
|
|
errorMessages,
|
|
httpStatus,
|
|
passwordHashing,
|
|
successMessages,
|
|
} from "#environment/constants";
|
|
import { sessionManager } from "#lib/auth";
|
|
import { cassandra } from "#lib/database";
|
|
import { isValidPassword } from "#lib/validation";
|
|
|
|
import type {
|
|
ExtendedRequest,
|
|
RouteDef,
|
|
UpdatePasswordRequest,
|
|
UpdatePasswordResponse,
|
|
UserRow,
|
|
} from "#types/server";
|
|
|
|
const routeDef: RouteDef = {
|
|
method: ["PUT", "PATCH"],
|
|
accepts: "application/json",
|
|
returns: "application/json",
|
|
needsBody: "json",
|
|
};
|
|
|
|
async function handler(
|
|
request: ExtendedRequest,
|
|
requestBody: unknown,
|
|
): Promise<Response> {
|
|
try {
|
|
const { session } = request;
|
|
|
|
if (!session) {
|
|
const response: UpdatePasswordResponse = {
|
|
code: httpStatus.UNAUTHORIZED,
|
|
success: false,
|
|
error: errorMessages.NOT_AUTHENTICATED,
|
|
};
|
|
return Response.json(response, { status: httpStatus.UNAUTHORIZED });
|
|
}
|
|
|
|
const { currentPassword, newPassword, logoutAllSessions } =
|
|
requestBody as UpdatePasswordRequest;
|
|
|
|
if (!currentPassword || !newPassword) {
|
|
const response: UpdatePasswordResponse = {
|
|
code: httpStatus.BAD_REQUEST,
|
|
success: false,
|
|
error: "Both currentPassword and newPassword are required",
|
|
};
|
|
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
|
}
|
|
|
|
const passwordValidation = isValidPassword(newPassword);
|
|
if (!passwordValidation.valid) {
|
|
const response: UpdatePasswordResponse = {
|
|
code: httpStatus.BAD_REQUEST,
|
|
success: false,
|
|
error: passwordValidation.error || "Invalid new password",
|
|
};
|
|
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
|
}
|
|
|
|
if (currentPassword === newPassword) {
|
|
const response: UpdatePasswordResponse = {
|
|
code: httpStatus.BAD_REQUEST,
|
|
success: false,
|
|
error: errorMessages.PASSWORD_SAME_AS_CURRENT,
|
|
};
|
|
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
|
}
|
|
|
|
const userQuery = `
|
|
SELECT id, username, email, password, is_verified, created_at, updated_at
|
|
FROM users WHERE id = ? LIMIT 1
|
|
`;
|
|
|
|
const userResult = (await cassandra.execute(userQuery, [session.id])) as {
|
|
rows: UserRow[];
|
|
};
|
|
|
|
if (!userResult?.rows || userResult.rows.length === 0) {
|
|
await sessionManager.invalidateSession(request);
|
|
|
|
const response: UpdatePasswordResponse = {
|
|
code: httpStatus.NOT_FOUND,
|
|
success: false,
|
|
error: errorMessages.USER_NOT_FOUND,
|
|
};
|
|
return Response.json(response, { status: httpStatus.NOT_FOUND });
|
|
}
|
|
|
|
const user = userResult.rows[0];
|
|
if (!user) {
|
|
const response: UpdatePasswordResponse = {
|
|
code: httpStatus.NOT_FOUND,
|
|
success: false,
|
|
error: errorMessages.USER_NOT_FOUND,
|
|
};
|
|
return Response.json(response, { status: httpStatus.NOT_FOUND });
|
|
}
|
|
|
|
const isCurrentPasswordValid = await Bun.password.verify(
|
|
currentPassword,
|
|
user.password,
|
|
);
|
|
|
|
if (!isCurrentPasswordValid) {
|
|
const response: UpdatePasswordResponse = {
|
|
code: httpStatus.UNAUTHORIZED,
|
|
success: false,
|
|
error: errorMessages.CURRENT_PASSWORD_INCORRECT,
|
|
};
|
|
return Response.json(response, { status: httpStatus.UNAUTHORIZED });
|
|
}
|
|
|
|
const hashedNewPassword = await Bun.password.hash(
|
|
newPassword,
|
|
passwordHashing,
|
|
);
|
|
|
|
const updateQuery = `
|
|
UPDATE users
|
|
SET password = ?, updated_at = ?
|
|
WHERE id = ?
|
|
`;
|
|
|
|
await cassandra.execute(updateQuery, [
|
|
hashedNewPassword,
|
|
new Date(),
|
|
session.id,
|
|
]);
|
|
|
|
if (logoutAllSessions === true) {
|
|
const invalidatedCount =
|
|
await sessionManager.invalidateAllSessionsForUser(session.id);
|
|
|
|
const baseMessage = successMessages.PASSWORD_UPDATED;
|
|
const sessionMessage = ` Logged out from ${invalidatedCount} session(s).`;
|
|
|
|
const response: UpdatePasswordResponse = {
|
|
code: httpStatus.OK,
|
|
success: true,
|
|
message: baseMessage + sessionMessage,
|
|
loggedOutSessions: invalidatedCount,
|
|
};
|
|
|
|
return Response.json(response, {
|
|
status: httpStatus.OK,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Set-Cookie": "session=; Path=/; Max-Age=0; HttpOnly",
|
|
},
|
|
});
|
|
}
|
|
|
|
const allSessions = await sessionManager.getActiveSessionsForUser(
|
|
session.id,
|
|
);
|
|
const currentToken = request.headers
|
|
.get("Cookie")
|
|
?.match(/session=([^;]+)/)?.[1];
|
|
|
|
let invalidatedCount = 0;
|
|
if (currentToken) {
|
|
for (const token of allSessions) {
|
|
if (token !== currentToken) {
|
|
await sessionManager.invalidateSessionByToken(token);
|
|
invalidatedCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
const userAgent = request.headers.get("User-Agent") || "Unknown";
|
|
const updatedSessionPayload = {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
isVerified: user.is_verified,
|
|
displayName: user.display_name,
|
|
createdAt: user.created_at.toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
const sessionCookie = await sessionManager.updateSession(
|
|
request,
|
|
updatedSessionPayload,
|
|
userAgent,
|
|
);
|
|
|
|
const baseMessage = successMessages.PASSWORD_UPDATED;
|
|
const sessionMessage =
|
|
invalidatedCount > 0
|
|
? ` Logged out from ${invalidatedCount} other session(s).`
|
|
: ".";
|
|
|
|
const response: UpdatePasswordResponse = {
|
|
code: httpStatus.OK,
|
|
success: true,
|
|
message: baseMessage + sessionMessage,
|
|
loggedOutSessions: invalidatedCount,
|
|
};
|
|
|
|
return Response.json(response, {
|
|
status: httpStatus.OK,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Set-Cookie": sessionCookie,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
echo.error({
|
|
message: "Error updating user password",
|
|
error,
|
|
});
|
|
|
|
const response: UpdatePasswordResponse = {
|
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
|
success: false,
|
|
error: errorMessages.INTERNAL_SERVER_ERROR,
|
|
};
|
|
return Response.json(response, {
|
|
status: httpStatus.INTERNAL_SERVER_ERROR,
|
|
});
|
|
}
|
|
}
|
|
|
|
export { handler, routeDef };
|