Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m30s
All checks were successful
Build frontend / build (push) Successful in 1m30s
This commit is contained in:
@ -78,7 +78,7 @@ export default function ClientLayout({ children }) {
|
|||||||
name: authUser.name || authUser.email,
|
name: authUser.name || authUser.email,
|
||||||
email: authUser.email,
|
email: authUser.email,
|
||||||
phone: authUser.phone,
|
phone: authUser.phone,
|
||||||
role: AuthService.isOwner() ? UserRole.OWNER : UserRole.CUSTOMER,
|
role: AuthService.isOwner() ? UserRole.OWNER : AuthService.isAgent() ? UserRole.AGENT : UserRole.CUSTOMER,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
@ -135,6 +135,8 @@ export default function ClientLayout({ children }) {
|
|||||||
const isProfilePage = pathname === "/profile";
|
const isProfilePage = pathname === "/profile";
|
||||||
|
|
||||||
const isOwner = user?.role === UserRole.OWNER;
|
const isOwner = user?.role === UserRole.OWNER;
|
||||||
|
const isAgent = user?.role === UserRole.AGENT;
|
||||||
|
const isOwnerOrAgent = isOwner || isAgent;
|
||||||
const isCustomer = user?.role === UserRole.CUSTOMER;
|
const isCustomer = user?.role === UserRole.CUSTOMER;
|
||||||
const isAuthenticated = !!user;
|
const isAuthenticated = !!user;
|
||||||
|
|
||||||
@ -718,7 +720,7 @@ export default function ClientLayout({ children }) {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
{isAuthenticated && !isAuthPage && (
|
{isAuthenticated && !isAuthPage && (
|
||||||
<BottomNav isOwner={isOwner} />
|
<BottomNav isOwner={isOwner} isOwnerOrAgent={isOwnerOrAgent} />
|
||||||
)}
|
)}
|
||||||
</FavoritesProvider>
|
</FavoritesProvider>
|
||||||
</NotificationsProvider>
|
</NotificationsProvider>
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Home, Building, Calendar, Heart, Bell, Settings, CreditCard } from "lucide-react";
|
import { usePathname } from "next/navigation";
|
||||||
|
import { Home, Building, Calendar, Heart, Bell, Settings, CreditCard, Briefcase } from "lucide-react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useNotifications } from "@/app/contexts/NotificationsContext";
|
import { useNotifications } from "@/app/contexts/NotificationsContext";
|
||||||
|
|
||||||
export default function BottomNav({ isOwner }) {
|
export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
||||||
|
const pathname = usePathname();
|
||||||
const { unreadCount } = useNotifications();
|
const { unreadCount } = useNotifications();
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
const bookingsHref = isOwner ? "/owner/reservations" : "/reservations";
|
const bookingsHref = isOwner ? "/owner/reservations" : "/reservations";
|
||||||
@ -17,6 +19,7 @@ export default function BottomNav({ isOwner }) {
|
|||||||
const items = [
|
const items = [
|
||||||
{ href: "/", label: "الرئيسية", icon: Home },
|
{ href: "/", label: "الرئيسية", icon: Home },
|
||||||
{ href: "/properties", label: "عقاراتنا", icon: Building },
|
{ href: "/properties", label: "عقاراتنا", icon: Building },
|
||||||
|
...(isOwnerOrAgent ? [{ href: "/owner/properties", label: "عقاراتي", icon: Briefcase }] : []),
|
||||||
{ href: bookingsHref, label: "الحجوزات", icon: Calendar },
|
{ href: bookingsHref, label: "الحجوزات", icon: Calendar },
|
||||||
{ href: "/favorites", label: "المفضلة", icon: Heart },
|
{ href: "/favorites", label: "المفضلة", icon: Heart },
|
||||||
{ href: "/payments", label: "المدفوعات", icon: CreditCard },
|
{ href: "/payments", label: "المدفوعات", icon: CreditCard },
|
||||||
@ -24,16 +27,22 @@ export default function BottomNav({ isOwner }) {
|
|||||||
{ href: "/settings", label: "الإعدادات", icon: Settings },
|
{ href: "/settings", label: "الإعدادات", icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const isActive = (href) => {
|
||||||
|
if (href === "/") return pathname === "/";
|
||||||
|
return pathname.startsWith(href);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50">
|
<nav className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50">
|
||||||
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-3xl shadow-lg px-2 py-2 flex items-center gap-3">
|
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-3xl shadow-lg px-2 py-2 flex items-center gap-3">
|
||||||
{items.map((it) => {
|
{items.map((it) => {
|
||||||
const Icon = it.icon;
|
const Icon = it.icon;
|
||||||
|
const active = isActive(it.href);
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={it.href}
|
key={it.href}
|
||||||
href={it.href}
|
href={it.href}
|
||||||
className="relative group flex flex-col items-center justify-center px-2.5 py-2 text-gray-700 hover:text-amber-600 transition-colors"
|
className={`relative group flex flex-col items-center justify-center px-2.5 py-2 transition-colors ${active ? "text-amber-600" : "text-gray-700 hover:text-amber-600"}`}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Icon className="w-6 h-6" />
|
<Icon className="w-6 h-6" />
|
||||||
|
|||||||
@ -7,18 +7,21 @@ const UserRole = Object.freeze({
|
|||||||
GUEST: 'guest',
|
GUEST: 'guest',
|
||||||
CUSTOMER: 'customer',
|
CUSTOMER: 'customer',
|
||||||
OWNER: 'owner',
|
OWNER: 'owner',
|
||||||
|
AGENT: 'agent',
|
||||||
});
|
});
|
||||||
|
|
||||||
const UserRoleLabels = Object.freeze({
|
const UserRoleLabels = Object.freeze({
|
||||||
[UserRole.GUEST]: 'زائر',
|
[UserRole.GUEST]: 'زائر',
|
||||||
[UserRole.CUSTOMER]: 'مستأجر',
|
[UserRole.CUSTOMER]: 'مستأجر',
|
||||||
[UserRole.OWNER]: 'مالك عقار',
|
[UserRole.OWNER]: 'مالك عقار',
|
||||||
|
[UserRole.AGENT]: 'وسيط عقاري',
|
||||||
});
|
});
|
||||||
|
|
||||||
const UserRoleColors = Object.freeze({
|
const UserRoleColors = Object.freeze({
|
||||||
[UserRole.GUEST]: 'gray',
|
[UserRole.GUEST]: 'gray',
|
||||||
[UserRole.CUSTOMER]: 'blue',
|
[UserRole.CUSTOMER]: 'blue',
|
||||||
[UserRole.OWNER]: 'amber',
|
[UserRole.OWNER]: 'amber',
|
||||||
|
[UserRole.AGENT]: 'purple',
|
||||||
});
|
});
|
||||||
|
|
||||||
export { UserRole, UserRoleLabels, UserRoleColors };
|
export { UserRole, UserRoleLabels, UserRoleColors };
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
import AuthService from '@/app/services/AuthService';
|
import AuthService from '@/app/services/AuthService';
|
||||||
import { payDeposit } from '@/app/utils/api';
|
import { payDeposit, getMyTransaction } from '@/app/utils/api';
|
||||||
|
|
||||||
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
|
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
|
||||||
|
|
||||||
@ -87,35 +87,10 @@ export default function PaymentsPage() {
|
|||||||
const [isGuest, setIsGuest] = useState(null);
|
const [isGuest, setIsGuest] = useState(null);
|
||||||
const [selectedPayment, setSelectedPayment] = useState('cash');
|
const [selectedPayment, setSelectedPayment] = useState('cash');
|
||||||
|
|
||||||
const getAuthToken = () => {
|
|
||||||
if (typeof window === 'undefined') return '';
|
|
||||||
return (
|
|
||||||
AuthService?.getToken?.() ||
|
|
||||||
AuthService?.getAccessToken?.() ||
|
|
||||||
localStorage.getItem('token') ||
|
|
||||||
localStorage.getItem('accessToken') ||
|
|
||||||
localStorage.getItem('authToken') ||
|
|
||||||
''
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadReservations = useCallback(async () => {
|
const loadReservations = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const token = getAuthToken();
|
const json = await getMyTransaction();
|
||||||
|
const items = Array.isArray(json) ? json : [];
|
||||||
const res = await fetch('http://45.93.137.91/api/Customer/GetMyTransaction', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error('فشل تحميل المدفوعات');
|
|
||||||
}
|
|
||||||
|
|
||||||
const json = await res.json();
|
|
||||||
const items = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [];
|
|
||||||
|
|
||||||
const mapped = items.map((item) => {
|
const mapped = items.map((item) => {
|
||||||
const deposit = item?.diposit || item?.deposit || {};
|
const deposit = item?.diposit || item?.deposit || {};
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
Mail,
|
Mail,
|
||||||
@ -17,11 +17,11 @@ import {
|
|||||||
Calendar,
|
Calendar,
|
||||||
MapPin,
|
MapPin,
|
||||||
Loader2,
|
Loader2,
|
||||||
Pencil
|
Pencil,
|
||||||
} from 'lucide-react';
|
} from "lucide-react";
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
import AuthService from '../services/AuthService';
|
import AuthService from "../services/AuthService";
|
||||||
import { getCustomerByUserId, getOwnerByUserId } from '../utils/api';
|
import { getCustomerByUserId, getOwnerByUserId } from "../utils/api";
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -29,52 +29,65 @@ export default function ProfilePage() {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const [editingBio, setEditingBio] = useState(false);
|
const [editingBio, setEditingBio] = useState(false);
|
||||||
const [bioDraft, setBioDraft] = useState('');
|
const [bioDraft, setBioDraft] = useState("");
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: "",
|
||||||
email: '',
|
email: "",
|
||||||
phone: '',
|
phone: "",
|
||||||
whatsapp: '',
|
whatsapp: "",
|
||||||
bio: '',
|
bio: "",
|
||||||
location: '',
|
location: "",
|
||||||
joinedDate: ''
|
joinedDate: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [avatarPreview, setAvatarPreview] = useState('');
|
const [avatarPreview, setAvatarPreview] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const authUser = AuthService.getUser();
|
const authUser = AuthService.getUser();
|
||||||
if (authUser) {
|
if (authUser) {
|
||||||
const userData = {
|
const userData = {
|
||||||
id: authUser.id,
|
id: authUser.id,
|
||||||
name: authUser.name || '',
|
name: authUser.name || "",
|
||||||
email: authUser.email || '',
|
email: authUser.email || "",
|
||||||
phone: authUser.phone || '',
|
phone: authUser.phone || "",
|
||||||
role: AuthService.isOwner() ? 'owner' : 'customer',
|
role: AuthService.isOwner() ? "owner" : "customer",
|
||||||
};
|
};
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
|
|
||||||
async function fetchProfile() {
|
async function fetchProfile() {
|
||||||
try {
|
try {
|
||||||
const fetchFn = userData.role === 'owner' ? getOwnerByUserId : getCustomerByUserId;
|
const fetchFn =
|
||||||
|
userData.role === "owner" ? getOwnerByUserId : getCustomerByUserId;
|
||||||
const profile = await fetchFn(userData.id);
|
const profile = await fetchFn(userData.id);
|
||||||
|
|
||||||
if (profile) {
|
if (profile) {
|
||||||
const profileData = {
|
const profileData = {
|
||||||
name: profile.fullName || profile.name || `${profile.firstName || ''} ${profile.lastName || ''}`.trim() || userData.name || '',
|
name:
|
||||||
email: profile.email || userData.email || '',
|
profile.fullName ||
|
||||||
phone: profile.phone || profile.phoneNumber || userData.phone || '',
|
profile.name ||
|
||||||
whatsapp: profile.whatsAppNumber || profile.whatsapp || '',
|
`${profile.firstName || ""} ${profile.lastName || ""}`.trim() ||
|
||||||
bio: profile.bio || '',
|
userData.name ||
|
||||||
location: profile.address || profile.location || '',
|
"",
|
||||||
|
email: profile.email || userData.email || "",
|
||||||
|
phone:
|
||||||
|
profile.phone || profile.phoneNumber || userData.phone || "",
|
||||||
|
whatsapp: profile.whatsAppNumber || profile.whatsapp || "",
|
||||||
|
bio: profile.bio || "",
|
||||||
|
location: profile.address || profile.location || "",
|
||||||
joinedDate: profile.createdAt
|
joinedDate: profile.createdAt
|
||||||
? new Date(profile.createdAt).toLocaleDateString('ar-SA', { month: 'long', year: 'numeric' })
|
? new Date(profile.createdAt).toLocaleDateString("ar-SA", {
|
||||||
: new Date().toLocaleDateString('ar-SA', { month: 'long', year: 'numeric' }),
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})
|
||||||
|
: new Date().toLocaleDateString("ar-SA", {
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
setFormData(profileData);
|
setFormData(profileData);
|
||||||
setBioDraft(profileData.bio);
|
setBioDraft(profileData.bio);
|
||||||
localStorage.setItem('userProfile', JSON.stringify(profileData));
|
localStorage.setItem("userProfile", JSON.stringify(profileData));
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -82,59 +95,62 @@ export default function ProfilePage() {
|
|||||||
// Ignore API errors and fall back to local data.
|
// Ignore API errors and fall back to local data.
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedProfile = localStorage.getItem('userProfile');
|
const savedProfile = localStorage.getItem("userProfile");
|
||||||
let profileData;
|
let profileData;
|
||||||
if (savedProfile) {
|
if (savedProfile) {
|
||||||
profileData = JSON.parse(savedProfile);
|
profileData = JSON.parse(savedProfile);
|
||||||
} else {
|
} else {
|
||||||
profileData = {
|
profileData = {
|
||||||
name: userData.name || '',
|
name: userData.name || "",
|
||||||
email: userData.email || '',
|
email: userData.email || "",
|
||||||
phone: '',
|
phone: "",
|
||||||
whatsapp: '',
|
whatsapp: "",
|
||||||
bio: '',
|
bio: "",
|
||||||
location: '',
|
location: "",
|
||||||
joinedDate: new Date().toLocaleDateString('ar-SA', { month: 'long', year: 'numeric' })
|
joinedDate: new Date().toLocaleDateString("ar-SA", {
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
setFormData(profileData);
|
setFormData(profileData);
|
||||||
setBioDraft(profileData.bio || '');
|
setBioDraft(profileData.bio || "");
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedAvatar = localStorage.getItem('userAvatar');
|
const savedAvatar = localStorage.getItem("userAvatar");
|
||||||
if (savedAvatar) {
|
if (savedAvatar) {
|
||||||
setAvatarPreview(savedAvatar);
|
setAvatarPreview(savedAvatar);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
} else {
|
} else {
|
||||||
router.push('/login');
|
router.push("/login");
|
||||||
}
|
}
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
const startBioEditing = () => {
|
const startBioEditing = () => {
|
||||||
setBioDraft(formData.bio || '');
|
setBioDraft(formData.bio || "");
|
||||||
setEditingBio(true);
|
setEditingBio(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelBioEditing = () => {
|
const cancelBioEditing = () => {
|
||||||
setBioDraft(formData.bio || '');
|
setBioDraft(formData.bio || "");
|
||||||
setEditingBio(false);
|
setEditingBio(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveBio = () => {
|
const saveBio = () => {
|
||||||
const updatedData = { ...formData, bio: bioDraft };
|
const updatedData = { ...formData, bio: bioDraft };
|
||||||
setFormData(updatedData);
|
setFormData(updatedData);
|
||||||
localStorage.setItem('userProfile', JSON.stringify(updatedData));
|
localStorage.setItem("userProfile", JSON.stringify(updatedData));
|
||||||
setEditingBio(false);
|
setEditingBio(false);
|
||||||
toast.success('تم تحديث نبذة عني بنجاح');
|
toast.success("تم تحديث نبذة عني بنجاح");
|
||||||
};
|
};
|
||||||
|
|
||||||
const fadeInUp = {
|
const fadeInUp = {
|
||||||
initial: { opacity: 0, y: 20 },
|
initial: { opacity: 0, y: 20 },
|
||||||
animate: { opacity: 1, y: 0 },
|
animate: { opacity: 1, y: 0 },
|
||||||
transition: { duration: 0.5 }
|
transition: { duration: 0.5 },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@ -177,12 +193,12 @@ export default function ProfilePage() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
className="absolute inset-0 bg-white/10"
|
className="absolute inset-0 bg-white/10"
|
||||||
animate={{
|
animate={{
|
||||||
x: ['-100%', '100%'],
|
x: ["-100%", "100%"],
|
||||||
}}
|
}}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 3,
|
duration: 3,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
ease: 'linear',
|
ease: "linear",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -198,7 +214,7 @@ export default function ProfilePage() {
|
|||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
formData.name?.charAt(0).toUpperCase() || 'U'
|
formData.name?.charAt(0).toUpperCase() || "U"
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -206,12 +222,14 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-6">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<h1 className="text-3xl font-bold text-gray-900">{formData.name}</h1>
|
<h1 className="text-3xl font-bold text-gray-900">
|
||||||
|
{formData.name}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-2 text-gray-500 mt-2">
|
<div className="flex items-center justify-center gap-2 text-gray-500 mt-2">
|
||||||
<MapPin className="w-4 h-4" />
|
<MapPin className="w-4 h-4" />
|
||||||
<span>{formData.location || 'الموقع غير محدد'}</span>
|
<span>{formData.location || "الموقع غير محدد"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-2 text-gray-500 mt-1">
|
<div className="flex items-center justify-center gap-2 text-gray-500 mt-1">
|
||||||
@ -241,7 +259,7 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-gray-900">
|
<div className="flex items-center gap-2 text-gray-900">
|
||||||
<Phone className="w-5 h-5 text-gray-400" />
|
<Phone className="w-5 h-5 text-gray-400" />
|
||||||
<span>{formData.phone || 'غير محدد'}</span>
|
<span>{formData.phone || "غير محدد"}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -253,7 +271,7 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-gray-900">
|
<div className="flex items-center gap-2 text-gray-900">
|
||||||
<MessageCircle className="w-5 h-5 text-gray-400" />
|
<MessageCircle className="w-5 h-5 text-gray-400" />
|
||||||
<span>{formData.whatsapp || 'غير محدد'}</span>
|
<span>{formData.whatsapp || "غير محدد"}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -262,7 +280,7 @@ export default function ProfilePage() {
|
|||||||
نوع الحساب
|
نوع الحساب
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{user?.role === 'owner' ? (
|
{user?.role === "owner" ? (
|
||||||
<>
|
<>
|
||||||
<Building className="w-5 h-5 text-amber-500" />
|
<Building className="w-5 h-5 text-amber-500" />
|
||||||
<span className="text-gray-900">مالك عقار</span>
|
<span className="text-gray-900">مالك عقار</span>
|
||||||
@ -276,8 +294,8 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* noo need for the descption of me on the profile*/}
|
||||||
<div className="mt-6 bg-gray-50 p-4 rounded-xl group">
|
{/* <div className="mt-6 bg-gray-50 p-4 rounded-xl group">
|
||||||
<div className="flex justify-between items-start mb-2">
|
<div className="flex justify-between items-start mb-2">
|
||||||
<label className="text-sm font-medium text-gray-600">
|
<label className="text-sm font-medium text-gray-600">
|
||||||
نبذة عني
|
نبذة عني
|
||||||
@ -324,9 +342,9 @@ export default function ProfilePage() {
|
|||||||
{formData.bio || 'لا توجد نبذة تعريفية بعد'}
|
{formData.bio || 'لا توجد نبذة تعريفية بعد'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{user?.role === 'owner' && (
|
{user?.role === "owner" && (
|
||||||
<div className="grid grid-cols-3 gap-4 mt-6">
|
<div className="grid grid-cols-3 gap-4 mt-6">
|
||||||
<div className="bg-amber-50 p-4 rounded-xl text-center">
|
<div className="bg-amber-50 p-4 rounded-xl text-center">
|
||||||
<div className="text-2xl font-bold text-amber-600">12</div>
|
<div className="text-2xl font-bold text-amber-600">12</div>
|
||||||
|
|||||||
@ -126,12 +126,20 @@ const AuthService = Object.freeze({
|
|||||||
return this.getRoles().includes('Owner');
|
return this.getRoles().includes('Owner');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has RealEstateAgent role
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isAgent() {
|
||||||
|
return this.getRoles().includes('RealEstateAgent');
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticated user without Owner role (i.e. customer)
|
* Authenticated user without Owner role (i.e. customer)
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isCustomer() {
|
isCustomer() {
|
||||||
return this.isAuthenticated() && !this.isOwner();
|
return this.isAuthenticated() && !this.isOwner() && !this.isAgent();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -276,7 +276,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
import AuthService from '../services/AuthService';
|
import AuthService from '../services/AuthService';
|
||||||
import { changePassword, deleteMyAccount } from '../utils/api';
|
import { changePassword, deleteMyAccount, submitReport } from '../utils/api';
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -285,10 +285,10 @@ export default function SettingsPage() {
|
|||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
|
||||||
const [showReportDialog, setShowReportDialog] = useState(false);
|
const [showContactDialog, setShowContactDialog] = useState(false);
|
||||||
const [reportSubject, setReportSubject] = useState('');
|
const [contactSubject, setContactSubject] = useState('');
|
||||||
const [reportBody, setReportBody] = useState('');
|
const [contactBody, setContactBody] = useState('');
|
||||||
const [isSendingReport, setIsSendingReport] = useState(false);
|
const [isSendingContact, setIsSendingContact] = useState(false);
|
||||||
|
|
||||||
const handleSignOut = () => {
|
const handleSignOut = () => {
|
||||||
AuthService.deleteToken();
|
AuthService.deleteToken();
|
||||||
@ -317,66 +317,23 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendGeneralReport = async () => {
|
const handleSendContact = async () => {
|
||||||
if (!reportSubject.trim() || !reportBody.trim()) {
|
if (!contactSubject.trim() || !contactBody.trim()) {
|
||||||
toast.error('الرجاء تعبئة عنوان البلاغ ونصه');
|
toast.error('الرجاء تعبئة العنوان والرسالة');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reportSubject.trim().length > 300) {
|
setIsSendingContact(true);
|
||||||
toast.error('عنوان البلاغ يجب ألا يتجاوز 300 حرف');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token =
|
|
||||||
AuthService.getToken?.() ||
|
|
||||||
(typeof window !== 'undefined'
|
|
||||||
? localStorage.getItem('token') ||
|
|
||||||
localStorage.getItem('accessToken') ||
|
|
||||||
localStorage.getItem('authToken')
|
|
||||||
: null);
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
console.error('No token found. Checked AuthService.getToken and localStorage keys: token, accessToken, authToken');
|
|
||||||
toast.error('لم يتم العثور على التوكن');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSendingReport(true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('http://45.93.137.91/api/Reports/SendGeneralReport', {
|
await submitReport(contactSubject.trim(), contactBody.trim());
|
||||||
method: 'POST',
|
toast.success('تم إرسال رسالتك بنجاح');
|
||||||
headers: {
|
setShowContactDialog(false);
|
||||||
'Content-Type': 'application/json',
|
setContactSubject('');
|
||||||
Authorization: `Bearer ${token}`,
|
setContactBody('');
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
subject: reportSubject.trim(),
|
|
||||||
body: reportBody.trim(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseText = await res.text();
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
console.error('Send report failed:', {
|
|
||||||
status: res.status,
|
|
||||||
statusText: res.statusText,
|
|
||||||
responseText,
|
|
||||||
});
|
|
||||||
throw new Error(responseText || `فشل إرسال البلاغ (HTTP ${res.status})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success(responseText || 'تم إرسال البلاغ بنجاح');
|
|
||||||
setShowReportDialog(false);
|
|
||||||
setReportSubject('');
|
|
||||||
setReportBody('');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Send report error:', err);
|
toast.error(err.message || 'حدث خطأ أثناء الإرسال');
|
||||||
toast.error(err.message || 'حدث خطأ أثناء إرسال البلاغ');
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsSendingReport(false);
|
setIsSendingContact(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -399,10 +356,9 @@ export default function SettingsPage() {
|
|||||||
title: 'الدعم',
|
title: 'الدعم',
|
||||||
items: [
|
items: [
|
||||||
{ icon: HelpCircle, label: 'الأسئلة الشائعة', href: '/faq', desc: 'إجابات للأسئلة المتكررة' },
|
{ icon: HelpCircle, label: 'الأسئلة الشائعة', href: '/faq', desc: 'إجابات للأسئلة المتكررة' },
|
||||||
{ icon: MessageCircle, label: 'تواصل معنا', href: '/support', desc: 'الحصول على المساعدة والدعم' },
|
{ icon: MessageCircle, label: 'تواصل معنا', desc: 'الحصول على المساعدة والدعم', action: () => setShowContactDialog(true) },
|
||||||
{ icon: FileText, label: 'الشروط والأحكام', href: '/terms', desc: 'سياسة الاستخدام والخصوصية' },
|
{ icon: FileText, label: 'الشروط والأحكام', href: '/terms', desc: 'سياسة الاستخدام والخصوصية' },
|
||||||
{ icon: Eye, label: 'سياسة الخصوصية', href: '/privacy', desc: 'كيف نحمي بياناتك' },
|
{ icon: Eye, label: 'سياسة الخصوصية', href: '/privacy', desc: 'كيف نحمي بياناتك' },
|
||||||
{ icon: AlertTriangle, label: 'إرسال بلاغ عام', desc: 'إرسال مشكلة أو ملاحظة إلى الإدارة', action: () => setShowReportDialog(true) },
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -421,7 +377,7 @@ export default function SettingsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
<div className="min-h-screen bg-gray-50 py-8 pb-28" dir="rtl">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
|
||||||
<div className="container mx-auto px-4 max-w-2xl">
|
<div className="container mx-auto px-4 max-w-2xl">
|
||||||
@ -586,7 +542,7 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showReportDialog && (
|
{showContactDialog && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
@ -598,14 +554,14 @@ export default function SettingsPage() {
|
|||||||
<MessageCircle className="w-5 h-5 text-amber-600" />
|
<MessageCircle className="w-5 h-5 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-gray-900">إرسال بلاغ عام</h3>
|
<h3 className="text-lg font-semibold text-gray-900">تواصل معنا</h3>
|
||||||
<p className="text-sm text-gray-500">سيتم إرسال البلاغ إلى الإدارة</p>
|
<p className="text-sm text-gray-500">سنقوم بالرد عليك في أقرب وقت</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowReportDialog(false);
|
setShowContactDialog(false);
|
||||||
setReportSubject('');
|
setContactSubject('');
|
||||||
setReportBody('');
|
setContactBody('');
|
||||||
}}
|
}}
|
||||||
className="mr-auto p-1 hover:bg-gray-100 rounded-lg transition-colors"
|
className="mr-auto p-1 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
@ -615,17 +571,17 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={reportSubject}
|
value={contactSubject}
|
||||||
onChange={(e) => setReportSubject(e.target.value)}
|
onChange={(e) => setContactSubject(e.target.value)}
|
||||||
placeholder="عنوان البلاغ"
|
placeholder="الموضوع"
|
||||||
maxLength={300}
|
maxLength={300}
|
||||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none"
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
value={reportBody}
|
value={contactBody}
|
||||||
onChange={(e) => setReportBody(e.target.value)}
|
onChange={(e) => setContactBody(e.target.value)}
|
||||||
placeholder="اكتب تفاصيل البلاغ هنا..."
|
placeholder="اكتب رسالتك هنا..."
|
||||||
rows={5}
|
rows={5}
|
||||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none resize-none"
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none resize-none"
|
||||||
/>
|
/>
|
||||||
@ -633,21 +589,21 @@ export default function SettingsPage() {
|
|||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowReportDialog(false);
|
setShowContactDialog(false);
|
||||||
setReportSubject('');
|
setContactSubject('');
|
||||||
setReportBody('');
|
setContactBody('');
|
||||||
}}
|
}}
|
||||||
className="flex-1 px-4 py-3 rounded-xl border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors text-sm font-medium"
|
className="flex-1 px-4 py-3 rounded-xl border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors text-sm font-medium"
|
||||||
>
|
>
|
||||||
إلغاء
|
إلغاء
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleSendGeneralReport}
|
onClick={handleSendContact}
|
||||||
disabled={isSendingReport}
|
disabled={isSendingContact}
|
||||||
className="flex-1 px-4 py-3 rounded-xl bg-amber-600 text-white hover:bg-amber-700 transition-colors text-sm font-medium disabled:opacity-50 flex items-center justify-center gap-2"
|
className="flex-1 px-4 py-3 rounded-xl bg-amber-600 text-white hover:bg-amber-700 transition-colors text-sm font-medium disabled:opacity-50 flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
{isSendingReport ? <Loader2 className="w-4 h-4 animate-spin" /> : <MessageCircle className="w-4 h-4" />}
|
{isSendingContact ? <Loader2 className="w-4 h-4 animate-spin" /> : <MessageCircle className="w-4 h-4" />}
|
||||||
{isSendingReport ? 'جاري الإرسال...' : 'إرسال البلاغ'}
|
{isSendingContact ? 'جاري الإرسال...' : 'إرسال'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -1,44 +1,54 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import { MessageCircle, Mail, Phone, MapPin, Send, Loader2 } from 'lucide-react';
|
import {
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
MessageCircle,
|
||||||
import { submitReport } from '../utils/api';
|
Mail,
|
||||||
import AuthService from '../services/AuthService';
|
Phone,
|
||||||
|
MapPin,
|
||||||
|
Send,
|
||||||
|
Loader2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
|
import { submitReport } from "../utils/api";
|
||||||
|
import AuthService from "../services/AuthService";
|
||||||
|
|
||||||
export default function SupportPage() {
|
export default function SupportPage() {
|
||||||
const [subject, setSubject] = useState('');
|
const [subject, setSubject] = useState("");
|
||||||
const [body, setBody] = useState('');
|
const [body, setBody] = useState("");
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!subject.trim() || !body.trim()) {
|
if (!subject.trim() || !body.trim()) {
|
||||||
toast.error('يرجى تعبئة جميع الحقول');
|
toast.error("يرجى تعبئة جميع الحقول");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!AuthService.isAuthenticated()) {
|
if (!AuthService.isAuthenticated()) {
|
||||||
toast.error('يرجى تسجيل الدخول أولاً لإرسال طلب دعم');
|
toast.error("يرجى تسجيل الدخول أولاً لإرسال طلب دعم");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await submitReport(subject, body);
|
await submitReport(subject, body);
|
||||||
toast.success('تم إرسال طلب الدعم بنجاح');
|
toast.success("تم إرسال طلب الدعم بنجاح");
|
||||||
setSubject('');
|
setSubject("");
|
||||||
setBody('');
|
setBody("");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('حدث خطأ أثناء إرسال الطلب. حاول مرة أخرى');
|
toast.error("حدث خطأ أثناء إرسال الطلب. حاول مرة أخرى");
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl">
|
<div
|
||||||
|
className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12"
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
<div className="container mx-auto px-4 max-w-5xl">
|
<div className="container mx-auto px-4 max-w-5xl">
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -49,9 +59,12 @@ export default function SupportPage() {
|
|||||||
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
|
||||||
<MessageCircle className="w-10 h-10 text-amber-600" />
|
<MessageCircle className="w-10 h-10 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">خدمة العملاء</h1>
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||||
|
خدمة العملاء
|
||||||
|
</h1>
|
||||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||||
نحن هنا لمساعدتك. تواصل معنا عبر النموذج أدناه أو من خلال معلومات الاتصال المباشرة
|
نحن هنا لمساعدتك. تواصل معنا عبر النموذج أدناه أو من خلال معلومات
|
||||||
|
الاتصال المباشرة
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@ -63,7 +76,9 @@ export default function SupportPage() {
|
|||||||
className="md:col-span-2"
|
className="md:col-span-2"
|
||||||
>
|
>
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">أرسل لنا رسالة</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
||||||
|
أرسل لنا رسالة
|
||||||
|
</h2>
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
@ -99,7 +114,7 @@ export default function SupportPage() {
|
|||||||
) : (
|
) : (
|
||||||
<Send className="w-5 h-5" />
|
<Send className="w-5 h-5" />
|
||||||
)}
|
)}
|
||||||
{isSubmitting ? 'جاري الإرسال...' : 'إرسال'}
|
{isSubmitting ? "جاري الإرسال..." : "إرسال"}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -112,7 +127,9 @@ export default function SupportPage() {
|
|||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
>
|
>
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
||||||
<h3 className="text-lg font-bold text-gray-900 mb-4">معلومات الاتصال</h3>
|
<h3 className="text-lg font-bold text-gray-900 mb-4">
|
||||||
|
معلومات الاتصال
|
||||||
|
</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 bg-blue-100 rounded-xl flex items-center justify-center shrink-0">
|
<div className="w-10 h-10 bg-blue-100 rounded-xl flex items-center justify-center shrink-0">
|
||||||
@ -120,7 +137,9 @@ export default function SupportPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">البريد الإلكتروني</p>
|
<p className="text-sm text-gray-500">البريد الإلكتروني</p>
|
||||||
<p className="font-medium text-gray-900">support@sweethome.com</p>
|
<p className="font-medium text-gray-900">
|
||||||
|
support@sweethome.com
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@ -129,7 +148,9 @@ export default function SupportPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">رقم الهاتف</p>
|
<p className="text-sm text-gray-500">رقم الهاتف</p>
|
||||||
<p className="font-medium text-gray-900" dir="ltr">+963 11 234 5678</p>
|
<p className="font-medium text-gray-900" dir="ltr">
|
||||||
|
+963 11 234 5678
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@ -145,7 +166,9 @@ export default function SupportPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-amber-50 rounded-2xl border border-amber-200 p-6">
|
<div className="bg-amber-50 rounded-2xl border border-amber-200 p-6">
|
||||||
<h3 className="text-lg font-bold text-amber-800 mb-2">ساعات العمل</h3>
|
<h3 className="text-lg font-bold text-amber-800 mb-2">
|
||||||
|
ساعات العمل
|
||||||
|
</h3>
|
||||||
<div className="space-y-2 text-amber-700">
|
<div className="space-y-2 text-amber-700">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>السبت - الخميس</span>
|
<span>السبت - الخميس</span>
|
||||||
|
|||||||
@ -374,10 +374,10 @@
|
|||||||
// // ─── Booking/Reservation Management ───
|
// // ─── Booking/Reservation Management ───
|
||||||
|
|
||||||
import AuthService from "../services/AuthService";
|
import AuthService from "../services/AuthService";
|
||||||
const API_BASE =
|
const API_BASE = // const API_BASE =
|
||||||
process.env.NEXT_PUBLIC_API_URL || "https://45.93.137.91.nip.io/api";
|
process.env.NEXT_PUBLIC_API_URL || "https://45.93.137.91.nip.io/api"; // process.env.NEXT_PUBLIC_API_URL || "https://45.93.137.91.nip.io/api";
|
||||||
const REPORT_API_BASE =
|
const REPORT_API_BASE =
|
||||||
process.env.NEXT_PUBLIC_REPORT_API_URL || "http://45.93.137.91/api";
|
process.env.NEXT_PUBLIC_API_URL || "http://45.93.137.91/api";
|
||||||
|
|
||||||
function isFormData(value) {
|
function isFormData(value) {
|
||||||
return typeof FormData !== "undefined" && value instanceof FormData;
|
return typeof FormData !== "undefined" && value instanceof FormData;
|
||||||
@ -1032,6 +1032,10 @@ export async function payDeposit(data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMyTransaction() {
|
||||||
|
return apiFetch("/Customer/GetMyTransaction");
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Owner Contact & Stats ───
|
// ─── Owner Contact & Stats ───
|
||||||
|
|
||||||
export async function getOwnerContactInformation(propertyInformationId) {
|
export async function getOwnerContactInformation(propertyInformationId) {
|
||||||
|
|||||||
442
full-project-analysis.md
Normal file
442
full-project-analysis.md
Normal file
@ -0,0 +1,442 @@
|
|||||||
|
# SweetHome — Full Project Analysis
|
||||||
|
|
||||||
|
**Project:** SweetHome Next.js Real Estate Application
|
||||||
|
**Date:** June 16, 2026
|
||||||
|
**Framework:** Next.js 16.1.6 (App Router) + React 18.3.1
|
||||||
|
**Language:** JavaScript (no TypeScript)
|
||||||
|
**Styling:** Tailwind CSS v4 + DaisyUI (unused) + Flowbite (unused)
|
||||||
|
**API:** Custom `.NET` backend at `https://45.93.137.91.nip.io/api`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Project Structure
|
||||||
|
|
||||||
|
### Directory Tree (Top 3 Levels)
|
||||||
|
|
||||||
|
```
|
||||||
|
SweetHome/
|
||||||
|
├── .gitea/workflows/ # CI/CD deployer
|
||||||
|
├── app/ # ★ MAIN APP (112 source files)
|
||||||
|
│ ├── account-verification/ # OTP email/phone verification
|
||||||
|
│ ├── auth/choose-role/ # Owner/Tenant/Agent role picker
|
||||||
|
│ ├── blocked/ # Blocked account page
|
||||||
|
│ ├── booked-properties/ # User's booked properties
|
||||||
|
│ ├── change-password/ # Change password form
|
||||||
|
│ ├── components/ # ★ 14 reusable components
|
||||||
|
│ │ ├── home/ # HeroSearch, PropertyMap
|
||||||
|
│ │ ├── property/ # BookingCalendar, PropertyMap
|
||||||
|
│ │ ├── ratings/ # StarRating, PropertyRatingForm, etc.
|
||||||
|
│ │ ├── BottomNav.js # Mobile bottom nav
|
||||||
|
│ │ ├── FloatingSidebar.js # Desktop floating sidebar
|
||||||
|
│ │ ├── NavLinks.js # Nav link atoms
|
||||||
|
│ │ ├── NotificationHandler.js # Firebase FCM handler
|
||||||
|
│ │ └── PropertyMapWithMarkers.js # Standalone Leaflet map
|
||||||
|
│ ├── contexts/ # ★ 3 context providers
|
||||||
|
│ │ ├── FavoritesContext.js
|
||||||
|
│ │ ├── NotificationsContext.js
|
||||||
|
│ │ └── PropertyContext.js # ORPHANED (never mounted)
|
||||||
|
│ ├── enums/ # 20 enum constant files
|
||||||
|
│ ├── faq/ # FAQ accordion page
|
||||||
|
│ ├── favorites/ # Favorites list page
|
||||||
|
│ ├── forgot-password/ # 3-step forgot password
|
||||||
|
│ ├── i18n/ # i18next config (Arabic/English)
|
||||||
|
│ ├── login/ # 2-step login (credential + OTP)
|
||||||
|
│ ├── my-rates/ # Ratings received from owners
|
||||||
|
│ ├── notifications/ # Notifications list page
|
||||||
|
│ ├── onboarding/ # 3-step intro wizard
|
||||||
|
│ ├── owner/ # ★ Owner dashboard (6 sub-routes)
|
||||||
|
│ │ ├── account-book/ # Financial dashboard
|
||||||
|
│ │ ├── bookings/ # Booking management
|
||||||
|
│ │ ├── calendar/ # Monthly calendar view
|
||||||
|
│ │ ├── profits/ # Revenue/profit stats
|
||||||
|
│ │ ├── properties/ # CRUD property management
|
||||||
|
│ │ └── reservations/ # Reservation requests
|
||||||
|
│ ├── payments/ # Deposit payment page
|
||||||
|
│ ├── privacy/ # Privacy policy
|
||||||
|
│ ├── profile/ # User profile CRUD
|
||||||
|
│ ├── properties/ # Property listings grid
|
||||||
|
│ ├── property/[id]/ # Property detail (dynamic route)
|
||||||
|
│ ├── register/ # Registration (tenant/owner/agent)
|
||||||
|
│ ├── reports/ # 3-tab report submission
|
||||||
|
│ ├── reservations/ # Customer reservations
|
||||||
|
│ ├── services/ # AuthService.js
|
||||||
|
│ ├── settings/ # Account settings
|
||||||
|
│ ├── support/ # Support contact form
|
||||||
|
│ ├── terms/ # Terms of service
|
||||||
|
│ ├── utils/ # ★ api.js, calculations, firebase, etc.
|
||||||
|
│ ├── ClientLayout.js # Client layout shell (804 lines)
|
||||||
|
│ ├── layout.js # Root server layout
|
||||||
|
│ ├── globals.css # Tailwind + custom fonts
|
||||||
|
│ ├── page.js # Home page
|
||||||
|
│ ├── error.js # Error boundary
|
||||||
|
│ ├── loading.js # Loading state
|
||||||
|
│ └── not-found.js # 404 page
|
||||||
|
├── public/ # Static files (fonts, images, APK)
|
||||||
|
│ ├── files/SweetHome.apk # Android APK
|
||||||
|
│ ├── fonts/ # Madani Arabic (woff2 + ttf)
|
||||||
|
│ └── firebase-messaging-sw.js # FCM service worker
|
||||||
|
├── package.json # 18 prod + 7 dev dependencies
|
||||||
|
├── next.config.mjs # Next.js config
|
||||||
|
├── postcss.config.mjs # PostCSS + Tailwind
|
||||||
|
├── jsconfig.json # Path aliases (@/* → ./)
|
||||||
|
└── security-audit-report.md # Previous security audit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Route Summary (33 pages)
|
||||||
|
|
||||||
|
| Type | Routes | Count |
|
||||||
|
|------|--------|-------|
|
||||||
|
| Public | `/`, `/login`, `/register/*`, `/forgot-password`, `/auth/choose-role`, `/properties`, `/property/[id]`, `/terms`, `/privacy`, `/faq`, `/support`, `/onboarding` | 14 |
|
||||||
|
| Protected (auth only) | `/profile`, `/settings`, `/change-password`, `/account-verification`, `/favorites`, `/booked-properties`, `/reservations`, `/notifications`, `/payments`, `/my-rates`, `/reports`, `/blocked` | 12 |
|
||||||
|
| Owner only | `/owner/properties`, `/owner/properties/add`, `/owner/reservations`, `/owner/bookings`, `/owner/calendar`, `/owner/profits`, `/owner/account-book` | 7 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Architecture Overview
|
||||||
|
|
||||||
|
### Layout Hierarchy
|
||||||
|
|
||||||
|
```
|
||||||
|
app/layout.js (Server Component — root HTML, fonts, metadata)
|
||||||
|
└── app/ClientLayout.js (Client Component — 804 lines)
|
||||||
|
├── <NotificationsProvider> (context)
|
||||||
|
│ └── <FavoritesProvider> (context)
|
||||||
|
│ └── <main>{children}</main>
|
||||||
|
├── <BottomNav /> (mobile, authenticated only)
|
||||||
|
└── <NotificationHandler /> (FCM push notifications)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **No nested layouts** — flat route structure
|
||||||
|
- **No `middleware.js`** — all route protection is client-side only
|
||||||
|
- Nav bar hidden on auth pages (`/login`, `/register/*`, `/blocked`, `/forgot-password`, `/auth/choose-role`)
|
||||||
|
|
||||||
|
### Data Flow Pattern
|
||||||
|
|
||||||
|
```
|
||||||
|
Page (useEffect on mount)
|
||||||
|
→ API call (apiFetch from app/utils/api.js)
|
||||||
|
→ AuthService.getToken() → Authorization: Bearer <JWT>
|
||||||
|
→ fetch() to https://45.93.137.91.nip.io/api/{endpoint}
|
||||||
|
→ Response: auto-unwrap { data: ... } envelope
|
||||||
|
→ Return parsed data or throw on non-ok
|
||||||
|
→ Component state (useState)
|
||||||
|
→ Render UI with Tailwind classes
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. API Layer
|
||||||
|
|
||||||
|
**File:** `app/utils/api.js` (1198 lines — 370 commented legacy + 828 active)
|
||||||
|
**Ratings API:** `app/utils/ratings.js` (separate file with own `apiFetch`)
|
||||||
|
|
||||||
|
### Fetch Helpers
|
||||||
|
|
||||||
|
| Function | Lines | Method | Auth | Error Handling | Return Type |
|
||||||
|
|----------|-------|--------|------|----------------|-------------|
|
||||||
|
| `apiFetch(endpoint, options)` | 421-478 | Any | Auto Bearer | Throws on non-ok (except 206) | `data` (auto-unwrapped) |
|
||||||
|
| `authFetch(endpoint, body, token)` | 483-524 | POST | Manual param | Returns `{status,data,ok,message}` | Object |
|
||||||
|
| `reportFetch(endpoint, body)` | 526-558 | POST | None | Returns `{status,data,ok,message}` | Object |
|
||||||
|
| `multipartAuthFetch(endpoint, formData)` | 772-803 | POST | Auto Bearer | Returns `{status,data,ok,message}` | Object |
|
||||||
|
|
||||||
|
### Exported API Functions (47 total)
|
||||||
|
|
||||||
|
| Category | Functions | Count |
|
||||||
|
|----------|-----------|-------|
|
||||||
|
| Rent Properties | `getRentProperties`, `getRentProperty`, `getRentPropertyLocations`, `filterRentProperties` | 4 |
|
||||||
|
| Sale Properties | `getSaleProperties`, `getSaleProperty`, `getSalePropertyById` | 3 |
|
||||||
|
| Generic Property | `getProperty` | 1 |
|
||||||
|
| Recommendations | `getRecommendations`, `getTopRecommendations` | 2 |
|
||||||
|
| Reservations | `getAvailableDateRanges`, `getReservations`, `getReservation`, `checkAvailability`, `bookReservation`, `getUserReservations`, `getOwnerReservationRequests`, `getOwnerReservationsByStatuses`, `ownerConfirmReservation`, `confirmDepositPayment`, `adminConfirmDeposit`, `updateBookingStatus`, `payDeposit` | 13 |
|
||||||
|
| Owner Management | `getMyRentListings`, `getMySaleListings`, `addRentProperty`, `addSaleProperty`, `editRentProperty`, `editSaleProperty`, `updateRentPropertyStatus`, `updateSalePropertyStatus`, `getOwnerContactInformation`, `getOwnerStatistics` | 10 |
|
||||||
|
| Auth | `loginWithEmail`, `loginWithPhone`, `sendEmailOTP`, `sendPhoneOTP`, `verifyEmail`, `verifyPhone`, `changePassword`, `requestForgetPasswordOtp`, `verifyForgetPasswordOtp`, `resetPassword`, `deleteMyAccount` | 11 |
|
||||||
|
| Registration | `addOwner`, `addCustomer`, `registerRealEstateAgent` | 3 |
|
||||||
|
| Favorites | `getUserFavoriteProperties`, `addFavoriteProperty`, `removeFavoriteProperty` | 3 |
|
||||||
|
| Notifications | `getUserNotifications`, `setFCMToken` | 2 |
|
||||||
|
| Terms | `getARTerms`, `getENTerms`, `addOrUpdateTerms` | 3 |
|
||||||
|
| Currencies | `getCurrencies` | 1 |
|
||||||
|
| Files | `uploadPicture` | 1 |
|
||||||
|
| Reports | `sendGeneralReport`, `submitReport`, `submitReservationReport`, `updateReservationReport`, `submitSaleReport`, `updateSaleReport` | 6 |
|
||||||
|
| Profile | `getCustomerByUserId`, `getOwnerByUserId` | 2 |
|
||||||
|
|
||||||
|
### Ratings API (5 functions)
|
||||||
|
|
||||||
|
`addPropertyRating`, `addCustomerRating`, `getPropertyRatings`, `getCustomerRatings`, `getPropertyAverageRating`
|
||||||
|
|
||||||
|
### API Endpoint Coverage
|
||||||
|
|
||||||
|
| HTTP Method | Count | Examples |
|
||||||
|
|-------------|-------|----------|
|
||||||
|
| GET | 26 | `/RentProperties/GetRentProperties`, `/Reservations/GetAvailableDates/available/${id}` |
|
||||||
|
| POST | 17 | `/Auth/LogInWithEmail`, `/Reservations/PayDeposit/pay-deposit`, `/Rating/AddPropertyRating` |
|
||||||
|
| PUT | 8 | `/RentProperties/EditRentProperty/${id}`, `/Reservations/AdminConfirmDeposit/admin-confirm-deposit` |
|
||||||
|
| DELETE | 2 | `/FavoriteProperty/Remove`, `/User/DeleteMyAccount` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. State Management
|
||||||
|
|
||||||
|
### Context Providers
|
||||||
|
|
||||||
|
| Context | File | State | Consumers | Status |
|
||||||
|
|---------|------|-------|-----------|--------|
|
||||||
|
| **FavoritesProvider** | `app/contexts/FavoritesContext.js` | `favorites[]`, `isLoading` | `FloatingSidebar`, `/favorites`, `/properties`, `/property/[id]` | ✅ Active |
|
||||||
|
| **NotificationsProvider** | `app/contexts/NotificationsContext.js` | `notifications[]`, `unreadCount`, `isLoading` | `BottomNav`, `FloatingSidebar` | ✅ Active |
|
||||||
|
| **PropertyContext** | `app/contexts/PropertyContext.js` | `properties[]`, `loading`, `error` | None | ❌ Orphaned (never mounted) |
|
||||||
|
| **PropertyContext** | `app/utils/PropertyContext.js` | Same as above | None | ❌ Orphaned duplicate |
|
||||||
|
|
||||||
|
### localStorage Keys (10 actively written, ~5 legacy read-only)
|
||||||
|
|
||||||
|
| Key | Data | Written By | Read By |
|
||||||
|
|-----|------|------------|---------|
|
||||||
|
| `auth_token` | JWT string | `AuthService.addToken()` | `AuthService.getToken()`, `firebase.js` |
|
||||||
|
| `cached_user` | User profile JSON | `AuthService.cacheUser()` | `AuthService.getCachedUser()` |
|
||||||
|
| `language` | "en" or "ar" | `ClientLayout.js` | `ClientLayout.js` |
|
||||||
|
| `sweethome_onboarding_completed` | "true" | `/onboarding/page.js` | `/onboarding/page.js` |
|
||||||
|
| `ownerProperties` | Properties array JSON | `/owner/properties/page.js` | `/owner/calendar/page.js` |
|
||||||
|
| `ownerBookings` | Bookings array JSON | `/owner/bookings/page.js` | `/owner/bookings/page.js` |
|
||||||
|
| `ownerProfitsTable` | Profit data JSON | `/owner/profits/page.js` | `/owner/profits/page.js` |
|
||||||
|
| `userProfile` | Profile form data JSON | `/profile/page.js` | `/profile/page.js` |
|
||||||
|
| `userAvatar` | Base64 data URL | `/profile/page.js` | `/profile/page.js` |
|
||||||
|
| `token`, `accessToken`, `authToken` | (legacy reads only) | — | `/settings`, `/payments`, `/reservations` |
|
||||||
|
|
||||||
|
### Auth Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Login (app/login/page.js)
|
||||||
|
→ loginWithEmail() / loginWithPhone()
|
||||||
|
→ authFetch(POST /Auth/LogInWithEmail, { credential, password, device: 0 })
|
||||||
|
→ 200: AuthService.addToken(token) → localStorage.setItem('auth_token', token)
|
||||||
|
→ 206: OTP required → sendEmailOTP() → verifyEmail(code) → get final token
|
||||||
|
→ AuthService.cacheUser(user) → localStorage.setItem('cached_user', ...)
|
||||||
|
→ router.push('/')
|
||||||
|
|
||||||
|
ClientLayout.js (every page navigation)
|
||||||
|
→ AuthService.getUser() → decode JWT payload (base64 atob)
|
||||||
|
→ setUser({ name, email, phone, role: isOwner() ? 'owner' : 'customer' })
|
||||||
|
→ UI branches on user.role (show owner nav / customer nav / guest nav)
|
||||||
|
|
||||||
|
Page-level checks
|
||||||
|
→ Each protected page independently calls:
|
||||||
|
AuthService.isAuthenticated() → else router.push('/login')
|
||||||
|
AuthService.isOwner() → else router.push('/')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Component Architecture
|
||||||
|
|
||||||
|
### Component Inventory (14 reusable components)
|
||||||
|
|
||||||
|
| Component | File | Lines | Purpose | State |
|
||||||
|
|-----------|------|-------|---------|-------|
|
||||||
|
| HeroSearch | `components/home/HeroSearch.js` | 321 | Hero search with filters | `filters`, login dialog |
|
||||||
|
| PropertyMap (home) | `components/home/PropertyMap.js` | 287 | Map with markers + popup | `selectedProperty`, map ref |
|
||||||
|
| PropertyMap (property) | `components/property/PropertyMap.js` | 166 | Map at property detail | Tooltip state |
|
||||||
|
| BookingCalendar | `components/property/BookingCalendar.js` | 124 | Monthly booking grid | `currentMonth`, `selectedStart` |
|
||||||
|
| PropertyMapWithMarkers | `components/PropertyMapWithMarkers.js` | 100 | Generic Leaflet map | Map ref, markers ref |
|
||||||
|
| StarRating | `components/ratings/StarRating.js` | 134 | 5-star input | `hoverRating` |
|
||||||
|
| PropertyRatingList | `components/ratings/PropertyRatingList.js` | 286 | Paginated reviews | `ratings`, `page`, `loading` |
|
||||||
|
| PropertyRatingForm | `components/ratings/PropertyRatingForm.js` | 317 | 4-field rating form | Clean/services/owner/experience |
|
||||||
|
| CustomerRatingForm | `components/ratings/CustomerRatingForm.js` | 92 | 3-field rating form | Furn/terms/behavior |
|
||||||
|
| BottomNav | `components/BottomNav.js` | 58 | Mobile bottom bar | Badge from context |
|
||||||
|
| FloatingSidebar | `components/FloatingSidebar.js` | 158 | Quick-access buttons | Framer-motion variants |
|
||||||
|
| NavLinks | `components/NavLinks.js` | 42 | Nav link atoms | `usePathname` active |
|
||||||
|
| NotificationHandler | `components/NotificationHandler.js` | 151 | FCM push handler | Permission state |
|
||||||
|
|
||||||
|
### Largest Page Files
|
||||||
|
|
||||||
|
| Rank | File | Lines | Complexity |
|
||||||
|
|------|------|-------|------------|
|
||||||
|
| 1 | `api.js` | 1198 | 60+ functions, 4 fetch helpers, 370 lines commented |
|
||||||
|
| 2 | `PropertyDetail.js` | 1743 | 56 imports, 8+ state vars, booking calendar, image gallery, owner check, ratings, map |
|
||||||
|
| 3 | `owner/properties/page.js` | 2057 | CRUD + modals + localStorage caching + API calls |
|
||||||
|
| 4 | `ClientLayout.js` | 804 | Nav (desktop+mobile), user menu, language switcher, context providers |
|
||||||
|
| 5 | `owner/properties/add/page.js` | 700+ | Multi-step form with map, image upload, pricing |
|
||||||
|
| 6 | `app/page.js` (Home) | 623 | Property fetching, multi-filter, hero, map, feature cards |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Key Patterns
|
||||||
|
|
||||||
|
### Data Fetching (4 Patterns)
|
||||||
|
|
||||||
|
| Pattern | Usage | Found In |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| `useEffect` + `useState` | ~90% of pages | Most pages |
|
||||||
|
| Context-level fetch | 2 providers | `FavoritesContext`, `NotificationsContext` |
|
||||||
|
| localStorage cache | Owner dashboards | `owner/bookings`, `owner/calendar`, `owner/profits` |
|
||||||
|
| Mock data fallback | 3 owner pages | `owner/bookings`, `owner/calendar`, `owner/profits` |
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
|
||||||
|
- 100% Tailwind CSS v4 (utility classes, no CSS modules)
|
||||||
|
- Custom `globals.css`: Madani Arabic fonts (9 weights), Leaflet overrides, keyframe animations
|
||||||
|
- Color palette: `amber-500/600` (primary), `#ede6e6` (bg), `#156874` (accent teal)
|
||||||
|
- RTL-first with dynamic LTR switching via `currentLanguage` state
|
||||||
|
|
||||||
|
### Animations (Framer Motion v12.29.2)
|
||||||
|
|
||||||
|
- `initial/animate` opacity + y/x translations (section reveals)
|
||||||
|
- `whileHover`/`whileTap` scale (buttons, interactive elements)
|
||||||
|
- `AnimatePresence` (property popups, user menus)
|
||||||
|
- `whileInView` + `viewport` (scroll-triggered animations on home page)
|
||||||
|
- `staggerChildren` (hero text)
|
||||||
|
- Spring transitions `{ type: "spring", damping: 25, stiffness: 300 }`
|
||||||
|
|
||||||
|
### Form Handling
|
||||||
|
|
||||||
|
- **All forms**: Controlled `useState` — no React Hook Form, Formik, or `useReducer`
|
||||||
|
- **Validation**: Inline `value === 0 && <p className="text-red-500">مطلوب</p>`
|
||||||
|
- **No form libraries** — all hand-rolled
|
||||||
|
|
||||||
|
### Responsive Breakpoints
|
||||||
|
|
||||||
|
| Breakpoint | Layout |
|
||||||
|
|------------|--------|
|
||||||
|
| Default (mobile) | Single column, BottomNav, hamburger menu |
|
||||||
|
| `md:` (768px) | Desktop nav, 2-3 column grids |
|
||||||
|
| `lg:` (1024px) | PropertyDetail sidebar, 3-column grids |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Owner Pages Analysis (Dashboard Section)
|
||||||
|
|
||||||
|
| Route | File | Lines | Status | Data Source |
|
||||||
|
|-------|------|-------|--------|-------------|
|
||||||
|
| `/owner/properties` | `owner/properties/page.js` | 2057 | ✅ Active | API (`getMyRentListings`, `getMySaleListings`) + localStorage cache |
|
||||||
|
| `/owner/properties/add` | `owner/properties/add/page.js` | 700+ | ✅ Active | API (`addRentProperty`, `addSaleProperty`, `uploadPicture`) |
|
||||||
|
| `/owner/reservations` | `owner/reservations/page.js` | 1400+ | ❌ Commented out | Mock data (commented) |
|
||||||
|
| `/owner/bookings` | `owner/bookings/page.js` | 560+ | ⚠️ Demo | localStorage mock data only |
|
||||||
|
| `/owner/calendar` | `owner/calendar/page.js` | 580+ | ⚠️ Demo | localStorage mock data only |
|
||||||
|
| `/owner/profits` | `owner/profits/page.js` | 600+ | ⚠️ Demo | localStorage mock data only |
|
||||||
|
| `/owner/account-book` | `owner/account-book/page.js` | 500+ | ✅ Active | API (`getOwnerStatistics`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Admin Functionality
|
||||||
|
|
||||||
|
| Component | Status | Data Source |
|
||||||
|
|-----------|--------|-------------|
|
||||||
|
| BookingRequests | ✅ Active | Real API (`getReservations`, `adminConfirmDeposit`) |
|
||||||
|
| Users | ❌ Not implemented | — |
|
||||||
|
| Properties | ❌ Not implemented | — |
|
||||||
|
| LedgerBook | ⚠️ Mock only | 3 hardcoded mock transactions |
|
||||||
|
| Dashboard | ❌ Not implemented | — |
|
||||||
|
|
||||||
|
Designed for internal admin use at `/admin` — tabs for Bookings/Users/Properties/Ledger/Dashboard.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Known Technical Debt
|
||||||
|
|
||||||
|
### Dead Code
|
||||||
|
|
||||||
|
| File | Dead Lines | Issue |
|
||||||
|
|------|-----------|-------|
|
||||||
|
| `api.js` | Lines 1-370 (370 lines) | Entire legacy version commented out |
|
||||||
|
| `StarRating.js` | Lines 1-93 (93 lines) | Old framer-motion version commented out |
|
||||||
|
| `PropertyRatingList.js` | ~150 lines | Old version commented out |
|
||||||
|
| `PropertyRatingForm.js` | ~220 lines | Old version commented out |
|
||||||
|
| `contexts/PropertyContext.js` | Entire file | Never mounted |
|
||||||
|
| `utils/PropertyContext.js` | Entire file | Duplicate orphan |
|
||||||
|
|
||||||
|
### Code Duplication
|
||||||
|
|
||||||
|
| Pattern | Files | Description |
|
||||||
|
|---------|-------|-------------|
|
||||||
|
| Leaflet maps | 3 separate files | `PropertyMapWithMarkers.js`, `home/PropertyMap.js`, `property/PropertyMap.js` — all setup Leaflet identically |
|
||||||
|
| Rating forms | `PropertyRatingForm.js` (317) + `CustomerRatingForm.js` (92) | ~75% code overlap; both define identical `RatingField` inline |
|
||||||
|
| formatCurrency | 4+ locations | Scattered across files with slightly different logic |
|
||||||
|
| Image URL building | 4+ locations | Different implementations for same task |
|
||||||
|
| Notifications page | `/notifications/page.js` vs `NotificationsContext` | Page duplicates all context state/logic |
|
||||||
|
|
||||||
|
### Architecture Issues
|
||||||
|
|
||||||
|
| Issue | Impact |
|
||||||
|
|-------|--------|
|
||||||
|
| No middleware.js | All route protection is client-side only |
|
||||||
|
| JWT in localStorage | XSS-vulnerable token storage |
|
||||||
|
| Client-side role checks | Users can modify JWT to elevate privileges |
|
||||||
|
| Owner pages use mock data | `/owner/bookings`, `/owner/calendar`, `/owner/profits` are demo-only |
|
||||||
|
| 3 unused UI libraries | DaisyUI, Flowbite, Yandex Maps imported but never used |
|
||||||
|
| No TypeScript | 112 JS files, no type safety |
|
||||||
|
| No ESLint | No linting configured |
|
||||||
|
| No testing framework | Zero tests |
|
||||||
|
| Flat route structure | No nested layouts; all pages share root layout |
|
||||||
|
| Active console.log | ~30+ files with console.log statements (security risk) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Dependencies
|
||||||
|
|
||||||
|
### Production (18)
|
||||||
|
|
||||||
|
| Package | Version | Purpose |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| next | 16.1.6 | Framework |
|
||||||
|
| react / react-dom | ^18.3.1 | UI |
|
||||||
|
| firebase | ^12.11.0 | FCM push notifications |
|
||||||
|
| leaflet / react-leaflet | 1.9.4 / 4.2.1 | Maps |
|
||||||
|
| @pbe/react-yandex-maps | ^1.2.5 | Yandex Maps (unused) |
|
||||||
|
| framer-motion | ^12.29.2 | Animations |
|
||||||
|
| lucide-react | ^0.563.0 | Icons |
|
||||||
|
| flowbite / flowbite-react | 4.0.1 / 0.12.16 | UI library (unused) |
|
||||||
|
| react-hot-toast | ^2.6.0 | Toasts |
|
||||||
|
| i18next / react-i18next / i18next-browser-languagedetector | 25.8 / 16.5 / 8.2 | i18n |
|
||||||
|
| jspdf / html2canvas | 4.2.1 / 1.4.1 | PDF generation |
|
||||||
|
| xlsx | ^0.18.5 | Excel export |
|
||||||
|
| react-intersection-observer | ^10.0.3 | Scroll detection |
|
||||||
|
|
||||||
|
### Dev (7)
|
||||||
|
|
||||||
|
| Package | Version | Purpose |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| tailwindcss | ^4.1.18 | CSS framework |
|
||||||
|
| @tailwindcss/postcss | ^4 | PostCSS plugin |
|
||||||
|
| postcss / autoprefixer | ^8.5.6 / ^10.4.23 | CSS processing |
|
||||||
|
| daisyui | ^5.5.14 | UI components (unused) |
|
||||||
|
| babel-plugin-react-compiler | 1.0.0 | React compiler optimization |
|
||||||
|
|
||||||
|
### Missing (notable absences)
|
||||||
|
- No TypeScript
|
||||||
|
- No ESLint
|
||||||
|
- No testing (Jest, Playwright, Cypress)
|
||||||
|
- No state management library (Redux, Zustand)
|
||||||
|
- No data fetching library (React Query, SWR)
|
||||||
|
- No form library (React Hook Form, Formik)
|
||||||
|
- No security lib (helmet, cors, csurf)
|
||||||
|
- No HTTP client (axios, ky)
|
||||||
|
- No JWT lib (jsonwebtoken, jose)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Security Summary
|
||||||
|
|
||||||
|
*(See `security-audit-report.md` for full details)*
|
||||||
|
|
||||||
|
| # | Finding | Severity |
|
||||||
|
|---|---------|----------|
|
||||||
|
| 1 | JWT in localStorage (XSS-theft) | 🔴 Critical |
|
||||||
|
| 2 | Stored XSS via Leaflet popup HTML | 🔴 Critical |
|
||||||
|
| 3 | OTP code logged to console | 🔴 Critical |
|
||||||
|
| 4 | No server-side middleware | 🔴 Critical |
|
||||||
|
| 5 | Passwords in URL query params | 🔴 Critical |
|
||||||
|
| 6 | IDOR on property edit/status APIs | 🔴 Critical |
|
||||||
|
| 7 | Client-only role checks | 🟠 High |
|
||||||
|
| 8 | HTTP endpoints leak tokens | 🟠 High |
|
||||||
|
| 9 | No CSP/security headers | 🟠 High |
|
||||||
|
| 10 | Mass assignment via spread | 🟠 High |
|
||||||
|
| 11 | Error messages leak internals | 🟠 High |
|
||||||
|
| 12 | Hardcoded IPs (10+ files) | 🟡 Medium |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. File Size Heatmap
|
||||||
|
|
||||||
|
| Size Range | Files | Examples |
|
||||||
|
|------------|-------|----------|
|
||||||
|
| 1500-2100 lines | 2 | `PropertyDetail.js` (1743), `owner/properties/page.js` (2057) |
|
||||||
|
| 600-1200 lines | 6 | `api.js` (1198), `ClientLayout.js` (804), `login/page.js` (800+), `owner/properties/add/page.js` (700+), `app/page.js` (623), `owner/bookings/page.js` (560+) |
|
||||||
|
| 200-600 lines | 15 | Most page files |
|
||||||
|
| < 200 lines | ~70 | Enum files, smaller components, error/loading pages |
|
||||||
488
security-audit-report.md
Normal file
488
security-audit-report.md
Normal file
@ -0,0 +1,488 @@
|
|||||||
|
# SweetHome — Full Security Audit & Pentest Report
|
||||||
|
|
||||||
|
**Project:** SweetHome Next.js Real Estate Application
|
||||||
|
**Date:** June 16, 2026
|
||||||
|
**Scope:** Client-side source code analysis (full white-box)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
| Severity | Count | Key Issues |
|
||||||
|
|----------|-------|------------|
|
||||||
|
| 🔴 **CRITICAL** | 8 | JWT in localStorage, XSS via Leaflet, OTP in console, no middleware, passwords in URL params, IDOR on edit APIs |
|
||||||
|
| 🟠 **HIGH** | 12 | Client-only role checks, HTTP endpoints, credential logging, mass assignment, error leakage, no CSP |
|
||||||
|
| 🟡 **MEDIUM** | 15 | Hardcoded IPs, stale localStorage keys, weak validation, no security headers, FCM token exposure |
|
||||||
|
| 🟢 **LOW** | 6 | Missing maxLength, base64 avatar in storage, weak email regex |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 CRITICAL FINDINGS
|
||||||
|
|
||||||
|
### C1. JWT Stored in `localStorage` (No httpOnly Cookie)
|
||||||
|
|
||||||
|
**Files:** `app/services/AuthService.js:23-38`, `app/settings/page.js:335-339`, `app/payments/page.js:185-189`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const TOKEN_KEY = 'auth_token';
|
||||||
|
const USER_KEY = 'cached_user';
|
||||||
|
|
||||||
|
const AuthService = Object.freeze({
|
||||||
|
addToken(token) {
|
||||||
|
localStorage.setItem(TOKEN_KEY, token); // <-- XSS-accessible
|
||||||
|
},
|
||||||
|
getToken() {
|
||||||
|
return localStorage.getItem(TOKEN_KEY); // <-- XSS-accessible
|
||||||
|
},
|
||||||
|
deleteToken() {
|
||||||
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
|
localStorage.removeItem(USER_KEY);
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Any XSS vulnerability gives attackers permanent token theft. No httpOnly, Secure, or SameSite protection. Token persists across tabs with no client-side expiration enforcement.
|
||||||
|
|
||||||
|
**Multiple fallback keys** (fragmented storage):
|
||||||
|
- `auth_token` (primary)
|
||||||
|
- `token`, `accessToken`, `authToken` (fallbacks in settings, payments, reservations pages)
|
||||||
|
- `cached_user` (user profile data)
|
||||||
|
- `userProfile`, `userAvatar` (profile page - base64 images!)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C2. Stored XSS via Leaflet Popup HTML Construction
|
||||||
|
|
||||||
|
**File:** `app/components/PropertyMapWithMarkers.js:51-65`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const popupContent = `
|
||||||
|
<div dir="rtl" style="text-align: right; padding: 12px; max-width: 250px;">
|
||||||
|
<h3 style="font-weight: bold; font-size: 16px; margin-bottom: 8px; color: #111;">
|
||||||
|
${property.title || 'عقار'}
|
||||||
|
</h3>
|
||||||
|
<p style="font-size: 14px; color: #666; margin-bottom: 8px;">
|
||||||
|
${property.address || property.location?.address || ''}
|
||||||
|
</p>
|
||||||
|
${property.images && property.images.length > 0
|
||||||
|
? `<img src="${property.images[0]}" alt="${property.title}"
|
||||||
|
style="width:100%;height:120px;object-fit:cover;border-radius:8px;"
|
||||||
|
onerror="this.src='/property-placeholder.jpg'" />`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
marker.bindPopup(popupContent);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Property data (title, address, image URLs) from API is interpolated directly into HTML without sanitization. A malicious owner or compromised API can execute arbitrary JavaScript in every viewer's session. `onerror` handler on `<img>` provides an additional execution vector.
|
||||||
|
|
||||||
|
**Also affected:** `app/components/home/PropertyMap.js:227` — `property.priceUSD` interpolated into `L.divIcon({ html: ... })`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C3. OTP Verification Code Logged to Browser Console
|
||||||
|
|
||||||
|
**File:** `app/login/page.js:200-203`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
console.log("[OTP] Verifying code:", otpCode); // <-- OTP in plaintext
|
||||||
|
console.log("[OTP] Verify response status:", result.status);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** OTP codes are one-time passwords. Browser extensions, devtools monitors, or error-logging services can capture them. Full account takeover via credential + OTP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C4. No Server-Side Route Middleware
|
||||||
|
|
||||||
|
**No `middleware.js` file exists** in the project. Every protected route relies on **client-side only** checks:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// app/ClientLayout.js:74-86 — Role read from JWT, no server verification
|
||||||
|
useEffect(() => {
|
||||||
|
const authUser = AuthService.getUser();
|
||||||
|
if (authUser) {
|
||||||
|
setUser({ role: AuthService.isOwner() ? UserRole.OWNER : UserRole.CUSTOMER });
|
||||||
|
}
|
||||||
|
}, [pathname]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Routes like `/owner/*`, `/settings`, `/profile`, `/payments`, `/reservations` have zero server-side protection. If JS fails to load, pages render unauthenticated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C5. Passwords Sent as URL Query Parameters
|
||||||
|
|
||||||
|
**File:** `app/utils/api.js:1082-1088`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export async function changePassword(oldPassword, newPassword) {
|
||||||
|
return apiFetch(
|
||||||
|
`/User/ChangePassword?oldPassword=${encodeURIComponent(oldPassword)}&newPassword=${encodeURIComponent(newPassword)}`,
|
||||||
|
{ method: "PUT" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteMyAccount(password) {
|
||||||
|
return apiFetch(
|
||||||
|
`/User/DeleteMyAccount?password=${encodeURIComponent(password)}`,
|
||||||
|
{ method: "DELETE" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Passwords in URL query parameters are logged by web servers, proxies, browser history, and the `apiFetch` `console.log("API Body:", ...)` at `api.js:445`. Also leaked via `Referer` header.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C6. IDOR — No Ownership Verification on Property Edit/Status APIs
|
||||||
|
|
||||||
|
**File:** `app/utils/api.js:691-732`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
editRentProperty(id, data) // PUT /RentProperties/EditRentProperty/${id}
|
||||||
|
editSaleProperty(id, data) // PUT /SaleProperties/EditSaleProperty/${id}
|
||||||
|
updateRentPropertyStatus(id, status) // PUT /RentProperties/UpdateStatus/${id}
|
||||||
|
updateSalePropertyStatus(id, status) // PUT /SaleProperties/UpdateStatus/${id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Any authenticated user can edit or deactivate **any** property by guessing/iterating IDs. The `GetMyRentListings` fetch filters to "my" properties, but the edit API has no server-side ownership check.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C7. IDOR — Reservation Confirmation Without Ownership Check
|
||||||
|
|
||||||
|
**File:** `app/owner/reservations/page.js:1309-1332`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const handleConfirm = async (r) => {
|
||||||
|
setActionLoadingId(r.id);
|
||||||
|
const res = await API(
|
||||||
|
AuthService.getToken(),
|
||||||
|
'PUT',
|
||||||
|
`/Reservations/OwnerConfirmReservation/owner-confirm/${r.id}`
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Any owner can confirm/reject **any** reservation by ID, regardless of whether they own the property. Combined with C6, an attacker could spam-confirm reservations across the platform.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C8. IDOR — Profile APIs Expose Any User's Data
|
||||||
|
|
||||||
|
**File:** `app/utils/api.js:670-676`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
getCustomerByUserId(userId) // GET /Customer/GetByUserId/${userId}
|
||||||
|
getOwnerByUserId(userId) // GET /Owner/GetByUserId/${userId}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** If the backend doesn't verify the requesting user matches the target `userId`, this allows enumerating all user profiles (name, email, phone, WhatsApp, national number).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟠 HIGH FINDINGS
|
||||||
|
|
||||||
|
### H1. Client-Side Only Role Checks (JWT Decoded Without Signature Verification)
|
||||||
|
|
||||||
|
**File:** `app/services/AuthService.js:66-75`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
decodeToken() {
|
||||||
|
const token = this.getToken();
|
||||||
|
if (!token) return null;
|
||||||
|
try {
|
||||||
|
const payload = token.split('.')[1];
|
||||||
|
return JSON.parse(atob(payload)); // <-- No signature verification, just base64
|
||||||
|
} catch { return null; }
|
||||||
|
},
|
||||||
|
|
||||||
|
isOwner() {
|
||||||
|
return this.getRoles().includes('Owner');
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** A user can trivially craft/modify a JWT in localStorage to add the `Owner` role. While server-side validation should catch this, the client-side UI fully trusts the decoded token.
|
||||||
|
|
||||||
|
**All owner pages use this pattern:**
|
||||||
|
```javascript
|
||||||
|
if (!AuthService.isOwner()) { router.push('/'); return; }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### H2. HTTP (Unencrypted) API Endpoints Used in Production
|
||||||
|
|
||||||
|
**File:** Multiple locations
|
||||||
|
|
||||||
|
| File | URL |
|
||||||
|
|------|-----|
|
||||||
|
| `app/utils/api.js:380` | `http://45.93.137.91/api` (REPORT_API_BASE) |
|
||||||
|
| `app/settings/page.js:351` | `http://45.93.137.91/api/Reports/SendGeneralReport` |
|
||||||
|
| `app/privacy/page.js:114` | `http://45.93.137.91/api` |
|
||||||
|
| `app/payments/page.js:198` | `http://45.93.137.91/api/Customer/GetMyTransaction` |
|
||||||
|
| `app/property/[id]/page.js:71` | `http://45.93.137.91${p.image}` |
|
||||||
|
|
||||||
|
**Impact:** Credentials, tokens, and personal data sent over HTTP are visible in plaintext to anyone on the network (MITM). The main API uses HTTPS (`*.nip.io`), but the Report API and several image fetches use raw HTTP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### H3. User Credentials Logged to Console
|
||||||
|
|
||||||
|
**File:** `app/login/page.js:87-92`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
console.log("[Login] Attempting login via", loginMethod, ":", formData.credential);
|
||||||
|
```
|
||||||
|
|
||||||
|
**File:** `app/utils/api.js:443-445` — Full API request bodies logged:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
console.log("API Request:", url);
|
||||||
|
console.log("API Method:", options.method || "GET");
|
||||||
|
console.log("API Body:", hasBody ? options.body : null); // <-- may contain passwords
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Login credentials, password change payloads, and any API body is visible in browser console. Any extension or error-logging service can capture this.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### H4. No CSP or Security Headers
|
||||||
|
|
||||||
|
**File:** `next.config.mjs`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const nextConfig = {
|
||||||
|
reactCompiler: true,
|
||||||
|
images: { remotePatterns: [ ... ] },
|
||||||
|
};
|
||||||
|
export default nextConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Missing headers:**
|
||||||
|
- `Content-Security-Policy` — would prevent XSS (C2)
|
||||||
|
- `Strict-Transport-Security` (HSTS) — would enforce HTTPS
|
||||||
|
- `X-Content-Type-Options: nosniff`
|
||||||
|
- `X-Frame-Options: DENY` — clickjacking protection
|
||||||
|
- `Referrer-Policy`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### H5. Mass Assignment via Spread Operator
|
||||||
|
|
||||||
|
**File:** `app/owner/properties/page.js:1592`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const updatedProperty = { ...property, ...formData };
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** User-controlled `formData` is spread directly into the property object. Unexpected fields (`status`, `ownerId`, `role`) could overwrite protected properties if the backend doesn't sanitize.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### H6. Error Messages Leak Internal Details to Users
|
||||||
|
|
||||||
|
**12 affected files.** Pattern:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
toast.error(err.message || 'فشل تحميل البيانات');
|
||||||
|
```
|
||||||
|
|
||||||
|
**Files:** `account-verification/page.js`, `change-password/page.js`, `notifications/page.js`, `reports/page.js`, `payments/page.js`, `reservations/page.js`, `PropertyDetail.js`, `owner/*/page.js`
|
||||||
|
|
||||||
|
**Impact:** Raw `err.message` may contain API paths, stack traces, or database errors. Shows internal implementation details to end users.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### H7. Firebase & VAPID Keys Hardcoded Client-Side
|
||||||
|
|
||||||
|
**File:** `app/utils/firebase.js:5-11`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const firebaseConfig = {
|
||||||
|
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
|
||||||
|
authDomain: "sweet-home-b2766.firebaseapp.com",
|
||||||
|
projectId: "sweet-home-b2766",
|
||||||
|
// ...
|
||||||
|
vapidKey: "BGZ4Fo8rRhoTdStLGlCySDZOnAX4ekCA0e3HDWXL5uEi2kOnXynYjbaDbY15002phUrFqxBpPPFHgfH2VhrmFDU",
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Duplicated in:** `public/firebase-messaging-sw.js:8-14`, `app/components/NotificationHandler.js:70`
|
||||||
|
|
||||||
|
**Impact:** While Firebase API keys are semi-public, the `projectId` and `storageBucket` exposure enables abuse if Firebase Security Rules are misconfigured. Push notifications can be sent without server authorization.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 MEDIUM FINDINGS
|
||||||
|
|
||||||
|
### M1. Hardcoded Server IP in 10+ Files
|
||||||
|
|
||||||
|
The IP `45.93.137.91` is hardcoded across the codebase with no environment variable abstraction for many URLs.
|
||||||
|
|
||||||
|
### M2. No CSRF Protection
|
||||||
|
|
||||||
|
No CSRF tokens, no `SameSite` cookie attributes (no cookies used at all), no custom headers for state-changing operations.
|
||||||
|
|
||||||
|
### M3. No Rate Limiting or Brute-Force Protection
|
||||||
|
|
||||||
|
Login functions (`loginWithEmail`, `loginWithPhone`) pass credentials directly without client-side rate limiting or exponential backoff.
|
||||||
|
|
||||||
|
### M4. CI/CD Server URL Exposed in Source
|
||||||
|
|
||||||
|
**File:** `.gitea/workflows/deployer.yaml:14`
|
||||||
|
```yaml
|
||||||
|
github-server-url: http://45.93.137.91:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### M5. FCM Push Token Logged to Console
|
||||||
|
|
||||||
|
**File:** `app/utils/firebase.js:45`
|
||||||
|
```javascript
|
||||||
|
console.log("[FCM] Token:", token); // persistent device identifier
|
||||||
|
```
|
||||||
|
|
||||||
|
### M6. Owner Properties Cached in localStorage
|
||||||
|
|
||||||
|
**File:** `app/owner/properties/page.js:1450-1453`
|
||||||
|
```javascript
|
||||||
|
localStorage.setItem("ownerProperties", JSON.stringify(newProperties));
|
||||||
|
```
|
||||||
|
|
||||||
|
### M7. Weak Email Validation Regex
|
||||||
|
|
||||||
|
**Pattern:** `/^[^\s@]+@[^\s@]+\.[^\s@]+$/` — allows ````, `|`, and special characters in local part.
|
||||||
|
|
||||||
|
### M8. Report API Sends Token Over HTTP
|
||||||
|
|
||||||
|
**File:** `app/settings/page.js:351` — `fetch('http://45.93.137.91/api/Reports/SendGeneralReport', ...)` with Bearer token header.
|
||||||
|
|
||||||
|
### M9. No `.env` Files in Repository
|
||||||
|
|
||||||
|
All environment variables fall back to hardcoded values (IPs, Firebase config). No `.env` or `.env.local` exists.
|
||||||
|
|
||||||
|
### M10. User Credential Reflected in OTP UI
|
||||||
|
|
||||||
|
**File:** `app/login/page.js:612` — `{formData.credential}` rendered in DOM (JSX-escaped, but could aid social engineering).
|
||||||
|
|
||||||
|
### M11. Client-Side Only File Upload Validation
|
||||||
|
|
||||||
|
`file.type.startsWith('image/')` — bypassable. Size limits (2MB/5MB) enforced client-side only.
|
||||||
|
|
||||||
|
### M12. All Reservations Loaded Then Filtered Client-Side
|
||||||
|
|
||||||
|
**File:** `app/reservations/page.js:871-899` — Fetches ALL rent properties via `getRentProperties()` for enrichment; if backend doesn't scope properly, sensitive data leaks.
|
||||||
|
|
||||||
|
### M13. Reservation Report Allows Reporting Any Reservation
|
||||||
|
|
||||||
|
**File:** `app/reservations/page.js:498-543` — No verification the reporter owns the reservation before filing a report.
|
||||||
|
|
||||||
|
### M14. Multiple Inconsistent localStorage Keys
|
||||||
|
|
||||||
|
At least 5 different keys store user data: `auth_token`, `token`, `accessToken`, `authToken`, `user`, `currentUser`, `authUser`, `profile`, `cached_user`.
|
||||||
|
|
||||||
|
### M15. i18next Configured to Cache in localStorage
|
||||||
|
|
||||||
|
**File:** `app/i18n/config.js:373-374` — Language detection data cached in localStorage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 LOW FINDINGS
|
||||||
|
|
||||||
|
| # | Finding | File(s) |
|
||||||
|
|---|---------|---------|
|
||||||
|
| L1 | Missing `maxLength` on text inputs | `reports/page.js`, `profile/page.js`, `login/page.js` |
|
||||||
|
| L2 | Base64 avatar images stored in localStorage | `profile/page.js:163` |
|
||||||
|
| L3 | Weak WhatsApp number sanitization | `PropertyDetail.js:1682` — only `[^0-9]` stripped |
|
||||||
|
| L4 | Profuse console.log throughout codebase | ~30+ files with active console.log statements |
|
||||||
|
| L5 | Business data cached in localStorage | `owner/bookings/page.js`, `owner/profits/page.js`, `owner/calendar/page.js` |
|
||||||
|
| L6 | No JWT library in dependencies (manual `atob` decode) | `package.json` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies Analysis
|
||||||
|
|
||||||
|
**File:** `package.json`
|
||||||
|
|
||||||
|
| Dependency | Risk |
|
||||||
|
|------------|------|
|
||||||
|
| `next@16.1.6` | Very bleeding-edge; verify official release |
|
||||||
|
| `firebase@^12.11.0` | FCM only; correctly scoped |
|
||||||
|
| `xlsx@^0.18.5` | Used for admin export; no user data flow |
|
||||||
|
| `jspdf@^4.2.1` | Client-side PDF generation; safe |
|
||||||
|
| `html2canvas@^1.4.1` | Screenshot capture; safe |
|
||||||
|
| No `jsonwebtoken`/`jose` | Manual JWT decode with `atob()` — no signature verification |
|
||||||
|
| No `helmet`/`cors`/`csurf` | No security middleware |
|
||||||
|
| No `axios`/`ky` | Raw `fetch` used throughout |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Attack Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: Full Account Takeover via XSS + OTP Logging
|
||||||
|
1. Attacker stores a malicious property with `<script>` in the title
|
||||||
|
2. Any user viewing the map (`PropertyMapWithMarkers.js`) executes the script
|
||||||
|
3. Script steals JWT from `localStorage` → full account access
|
||||||
|
4. If the victim is logging in, OTP is visible in console → complete takeover
|
||||||
|
|
||||||
|
### Scenario 2: Mass Property Manipulation via IDOR
|
||||||
|
1. Attacker enumerates property IDs via `GetRentProperties` (public)
|
||||||
|
2. Calls `editRentProperty(id, { price: 1 })` or `updateRentPropertyStatus(id, 5)` on any property
|
||||||
|
3. No server-side ownership check — all properties editable
|
||||||
|
|
||||||
|
### Scenario 3: Credential Harvesting via MITM
|
||||||
|
1. Attacker on same network intercepts HTTP traffic to `http://45.93.137.91/api`
|
||||||
|
2. Captures Bearer tokens, passwords in URL query params, and personal data
|
||||||
|
3. Credentials from `changePassword?oldPassword=...&newPassword=...` visible in plaintext
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Remediation Priority Matrix
|
||||||
|
|
||||||
|
| Priority | Fix | Effort | Impact |
|
||||||
|
|----------|-----|--------|--------|
|
||||||
|
| P0 | Move JWT to httpOnly Secure SameSite cookie | High | Eliminates XSS token theft |
|
||||||
|
| P0 | Sanitize Leaflet popup HTML with DOMPurify | Low | Fixes stored XSS |
|
||||||
|
| P0 | Remove all console.log statements (especially OTP) | Medium | Stops data leakage |
|
||||||
|
| P0 | Add `middleware.js` for server-side route protection | Medium | Real auth enforcement |
|
||||||
|
| P0 | Move passwords from URL params to POST body | Low | Stops credential leakage |
|
||||||
|
| P1 | Add CSP and security headers in `next.config.mjs` | Low | Mitigates XSS, MITM, clickjacking |
|
||||||
|
| P1 | Add ID validation on all API calls with path IDs | Medium | Defense-in-depth vs IDOR |
|
||||||
|
| P1 | Replace HTTP endpoints with HTTPS | Medium | Stops MITM credential theft |
|
||||||
|
| P1 | Add server-side ownership verification on edit APIs | High (backend) | Primary IDOR fix |
|
||||||
|
| P1 | Remove `err.message` from toast.error calls | Low | Prevents info leakage |
|
||||||
|
| P2 | Move all hardcoded IPs/secrets to env variables | Medium | Configurable, secure defaults |
|
||||||
|
| P2 | Enforce single localStorage key for token | Low | Consistent, auditable |
|
||||||
|
| P2 | Add rate limiting on login APIs | High (backend) | Brute-force protection |
|
||||||
|
| P2 | Add input maxLength and server-side validation | Low | Defense-in-depth |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Reviewed
|
||||||
|
|
||||||
|
| File | Lines | Key Security Findings |
|
||||||
|
|------|-------|----------------------|
|
||||||
|
| `app/utils/api.js` | 1088 | Token logging, passwords in URL, IDOR on edit APIs, no CSRF |
|
||||||
|
| `app/services/AuthService.js` | 151 | localStorage JWT, no signature verification, role from decoded JWT |
|
||||||
|
| `app/login/page.js` | 800+ | OTP in console, credential logging, credential in UI |
|
||||||
|
| `app/property/[id]/PropertyDetail.js` | 1700+ | Error messages leak, WhatsApp link construction |
|
||||||
|
| `app/components/PropertyMapWithMarkers.js` | 100+ | **Stored XSS** via Leaflet popup HTML |
|
||||||
|
| `app/components/home/PropertyMap.js` | 250+ | HTML injection in DivIcon |
|
||||||
|
| `app/owner/properties/page.js` | 1650+ | Mass assignment, localStorage caching, console.log |
|
||||||
|
| `app/owner/reservations/page.js` | 1350+ | IDOR on confirm/reject, localStorage fallback keys |
|
||||||
|
| `app/reservations/page.js` | 1000+ | Load all rent properties, report any reservation, error leakage |
|
||||||
|
| `app/settings/page.js` | 500+ | Token over HTTP, multiple localStorage fallback keys |
|
||||||
|
| `app/profile/page.js` | 250+ | localStorage caching, base64 avatar, console.log |
|
||||||
|
| `next.config.mjs` | 12 | No security headers, no CSP |
|
||||||
|
| `package.json` | — | No security libraries |
|
||||||
|
| `app/utils/firebase.js` | 60 | Hardcoded Firebase config, FCM token logging |
|
||||||
|
| `.gitea/workflows/deployer.yaml` | 30 | CI/CD internal URL exposed |
|
||||||
|
| `app/payments/page.js` | 300+ | Token from multiple localStorage keys |
|
||||||
|
| `app/owner/properties/add/page.js` | 700+ | Full payload logged to console |
|
||||||
|
| `app/register/*/page.js` | 2000+ | Client-only file upload validation |
|
||||||
|
| `app/change-password/page.js` | 60+ | Error leakage |
|
||||||
|
| `app/account-verification/page.js` | 110+ | Error leakage |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Report generated June 16, 2026. All findings based on static analysis of client-side source code.*
|
||||||
Reference in New Issue
Block a user