fix: resolve critical bugs and security issues across the project

Security:
- Disable SSL verification only in development (lib/req.js)
- Wrap all JSON.parse(cookie) calls in try-catch via safeJsonParse utility
- Sanitize dangerouslySetInnerHTML in blog/clinic with sanitizeHtml utility
- Fix open redirect in payment page — validate URL origin before redirect
- Fix cookie cleanup on 401 — use js-cookie with correct domain scope

Performance:
- Wrap ItemDoctor with React.memo to prevent unnecessary re-renders
- Replace <img> with Next.js <Image> in blog Caption component

Functionality:
- Fix memory leak in Recode.js — store intervals in refs, cleanup on unmount
- Add null guard on retryIcon.current before classList manipulation
- Fix getParsedUserInfo in helper to handle malformed cookie gracefully

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-06-07 09:19:20 +03:30
co-authored by Claude Sonnet 4.6
parent 1aa9f82d2a
commit 59e0a0fe4f
13 changed files with 104 additions and 33 deletions
+3 -2
View File
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import { request } from "@/services/response";
import Cookies from "js-cookie";
import Container from "./Container";
import { safeJsonParse } from "@/lib/sanitize";
const defaultData = {
phone: { value: "", isEdit: false },
@@ -39,7 +40,7 @@ function AppointmentPage({ doctor, disabledDates, matchedCity }) {
setIsLoading(true);
const userInfo = Cookies.get("userInfo");
const parsedData = userInfo && JSON.parse(userInfo);
const parsedData = safeJsonParse(userInfo);
const usernameFromCookie = parsedData?.username || "";
// ابتدا شماره موبایل را از Cookie ست می‌کنیم
@@ -112,7 +113,7 @@ function AppointmentPage({ doctor, disabledDates, matchedCity }) {
const refetchUserData = async () => {
if (step === 3) {
const userInfo = Cookies.get("userInfo");
const parsedData = userInfo && JSON.parse(userInfo);
const parsedData = safeJsonParse(userInfo);
const usernameFromCookie = parsedData?.username || "";
if (userInfo && parsedData && !data.uuid) {
+11 -6
View File
@@ -1,4 +1,6 @@
import React from "react";
import Image from "next/image";
import { sanitizeHtml } from "@/lib/sanitize";
function Caption({ data }) {
const imageUrl = data?.images?.[0]?.url;
@@ -6,16 +8,19 @@ function Caption({ data }) {
return (
<>
{imageUrl && imageUrl.trim() !== "" && (
<img
className="rounded-[8px] overflow-hidden w-full object-cover"
src={imageUrl}
alt={data?.title || "blog cover"}
/>
<div className="relative w-full aspect-video rounded-[8px] overflow-hidden">
<Image
src={imageUrl}
alt={data?.title || "blog cover"}
fill
className="object-cover"
/>
</div>
)}
{data?.body?.value && (
<div
className="my-[12px] sm:my-[16px] md:my-[20px] lg:my-[24px] text-[#525252] text-[14px] md:text-[15px] lg:text-[16px] font-normal leading-[26px] sm:leading-[28px] md:leading-[30px] lg:leading-[32px]"
dangerouslySetInnerHTML={{ __html: data.body.value }}
dangerouslySetInnerHTML={{ __html: sanitizeHtml(data.body.value) }}
/>
)}
</>
@@ -1,6 +1,7 @@
import { useState } from "react";
import { Button } from "@mui/material";
import MultilineLoading from "@/app/component/loading/Multiline";
import { sanitizeHtml } from "@/lib/sanitize";
function TextDetail({ data }) {
const [isMore, setIsMore] = useState(false);
@@ -20,7 +21,7 @@ function TextDetail({ data }) {
"after:bg-[linear-gradient(transparent,#FAFAFA)] max-h-[200px] after:absolute"
}
`}
dangerouslySetInnerHTML={{ __html: data?.caption }}
dangerouslySetInnerHTML={{ __html: sanitizeHtml(data?.caption) }}
></p>
</MultilineLoading>
{data?.caption?.length > 180 && (
@@ -5,6 +5,7 @@ import List from "./List";
import Head from "./Head";
import { request } from "@/services/response";
import Cookies from "js-cookie";
import { safeJsonParse } from "@/lib/sanitize";
function Transactions({ user, loading }) {
const [payments, setPayments] = useState([]);
@@ -23,7 +24,8 @@ function Transactions({ user, loading }) {
return;
}
const parsedData = JSON.parse(userInfo);
const parsedData = safeJsonParse(userInfo);
if (!parsedData) { setIsLoading(false); return; }
const userId = parsedData.id || parsedData.uuid;
if (!userId) {
+28 -8
View File
@@ -2,45 +2,66 @@ import RetryLogin from "@/components/icons/RetryLogin";
import { changeNumToDefault } from "@/helper";
import { request } from "@/services/response";
import { Button } from "@mui/material";
import { useEffect, useRef } from "react";
const time_resend_code = 120;
function Recode({ retryIcon, timer, setUuid, num, setTimer }) {
const timerIntervalRef = useRef(null);
const animIntervalRef = useRef(null);
useEffect(() => {
return () => {
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
if (animIntervalRef.current) clearInterval(animIntervalRef.current);
};
}, []);
const handleTimer = () => {
setTimer("loading");
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
setTimeout(() => {
setTimer(time_resend_code);
let timePresent = time_resend_code;
const timerInterval = setInterval(() => {
timerIntervalRef.current = setInterval(() => {
timePresent--;
setTimer(timePresent);
!timePresent && clearInterval(timerInterval);
if (!timePresent) {
clearInterval(timerIntervalRef.current);
timerIntervalRef.current = null;
}
}, 1000);
}, 1000);
};
const rotateIcon = () => {
if (!retryIcon.current) return;
retryIcon.current.classList.add("retry-icon");
setTimeout(() => {
retryIcon.current && retryIcon.current.classList.remove("retry-icon");
retryIcon.current?.classList.remove("retry-icon");
}, 1000);
};
const handleReq = () => {
rotateIcon();
const interval = setInterval(() => {
if (animIntervalRef.current) clearInterval(animIntervalRef.current);
animIntervalRef.current = setInterval(() => {
rotateIcon();
}, 1200);
request
.sendCode(changeNumToDefault(num), "")
.then((response) => {
clearInterval(interval);
clearInterval(animIntervalRef.current);
animIntervalRef.current = null;
if (response.uuid) {
setUuid(response.uuid);
handleTimer();
}
})
.catch(() => clearInterval(interval));
.catch(() => {
clearInterval(animIntervalRef.current);
animIntervalRef.current = null;
});
};
return (
@@ -49,7 +70,6 @@ function Recode({ retryIcon, timer, setUuid, num, setTimer }) {
disabled={typeof timer === "number" && timer !== 0}
onClick={() => {
if (!timer && timer !== "loading") {
// handleTimer();
handleReq();
}
}}