diff --git a/app/ClientLayout.js b/app/ClientLayout.js index 1a50712..164ba89 100644 --- a/app/ClientLayout.js +++ b/app/ClientLayout.js @@ -78,7 +78,7 @@ export default function ClientLayout({ children }) { name: authUser.name || authUser.email, email: authUser.email, phone: authUser.phone, - role: AuthService.isOwner() ? UserRole.OWNER : UserRole.CUSTOMER, + role: AuthService.isOwner() ? UserRole.OWNER : AuthService.isAgent() ? UserRole.AGENT : UserRole.CUSTOMER, }); } else { setUser(null); @@ -135,6 +135,8 @@ export default function ClientLayout({ children }) { const isProfilePage = pathname === "/profile"; const isOwner = user?.role === UserRole.OWNER; + const isAgent = user?.role === UserRole.AGENT; + const isOwnerOrAgent = isOwner || isAgent; const isCustomer = user?.role === UserRole.CUSTOMER; const isAuthenticated = !!user; @@ -718,7 +720,7 @@ export default function ClientLayout({ children }) { {isAuthenticated && !isAuthPage && ( - + )} diff --git a/app/components/BottomNav.js b/app/components/BottomNav.js index 7729c25..c72e92a 100644 --- a/app/components/BottomNav.js +++ b/app/components/BottomNav.js @@ -1,11 +1,11 @@ "use client"; import Link from "next/link"; -import { Home, Building, Calendar, Heart, Bell, Settings, CreditCard } from "lucide-react"; +import { Home, Building, Calendar, Heart, Bell, Settings, CreditCard, Briefcase } from "lucide-react"; import React, { useEffect, useState } from "react"; import { useNotifications } from "@/app/contexts/NotificationsContext"; -export default function BottomNav({ isOwner }) { +export default function BottomNav({ isOwner, isOwnerOrAgent }) { const { unreadCount } = useNotifications(); const [isMounted, setIsMounted] = useState(false); const bookingsHref = isOwner ? "/owner/reservations" : "/reservations"; @@ -17,6 +17,7 @@ export default function BottomNav({ isOwner }) { const items = [ { href: "/", label: "الرئيسية", icon: Home }, { href: "/properties", label: "عقاراتنا", icon: Building }, + ...(isOwnerOrAgent ? [{ href: "/owner/properties", label: "عقاراتي", icon: Briefcase }] : []), { href: bookingsHref, label: "الحجوزات", icon: Calendar }, { href: "/favorites", label: "المفضلة", icon: Heart }, { href: "/payments", label: "المدفوعات", icon: CreditCard }, diff --git a/app/enums/UserRole.js b/app/enums/UserRole.js index 5c9ade1..213088b 100644 --- a/app/enums/UserRole.js +++ b/app/enums/UserRole.js @@ -7,18 +7,21 @@ const UserRole = Object.freeze({ GUEST: 'guest', CUSTOMER: 'customer', OWNER: 'owner', + AGENT: 'agent', }); const UserRoleLabels = Object.freeze({ [UserRole.GUEST]: 'زائر', [UserRole.CUSTOMER]: 'مستأجر', [UserRole.OWNER]: 'مالك عقار', + [UserRole.AGENT]: 'وسيط عقاري', }); const UserRoleColors = Object.freeze({ [UserRole.GUEST]: 'gray', [UserRole.CUSTOMER]: 'blue', [UserRole.OWNER]: 'amber', + [UserRole.AGENT]: 'purple', }); export { UserRole, UserRoleLabels, UserRoleColors }; diff --git a/app/payments/page.js b/app/payments/page.js index 8b6caba..209fe63 100644 --- a/app/payments/page.js +++ b/app/payments/page.js @@ -159,7 +159,7 @@ import { motion } from 'framer-motion'; import { CreditCard, Loader2, Home, Calendar, Check, X, Clock, LogIn, Lock } from 'lucide-react'; import toast, { Toaster } from 'react-hot-toast'; 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']; @@ -179,35 +179,10 @@ export default function PaymentsPage() { const [payingId, setPayingId] = useState(null); const [isGuest, setIsGuest] = useState(null); - 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 () => { try { - const token = getAuthToken(); - - 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 data = await getMyTransaction(); + const items = Array.isArray(data) ? data : []; const mapped = items.map((item) => { const deposit = item?.diposit || item?.deposit || {}; diff --git a/app/services/AuthService.js b/app/services/AuthService.js index 6a4ec2f..cd8380c 100644 --- a/app/services/AuthService.js +++ b/app/services/AuthService.js @@ -126,12 +126,20 @@ const AuthService = Object.freeze({ 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) * @returns {boolean} */ isCustomer() { - return this.isAuthenticated() && !this.isOwner(); + return this.isAuthenticated() && !this.isOwner() && !this.isAgent(); }, /** diff --git a/app/utils/api.js b/app/utils/api.js index fc91bda..2ba1ac8 100644 --- a/app/utils/api.js +++ b/app/utils/api.js @@ -1032,6 +1032,12 @@ export async function payDeposit(data) { }); } +export async function getMyTransaction() { + return apiFetch("/Customer/GetMyTransaction", { + method: "GET", + }); +} + // ─── Owner Contact & Stats ─── export async function getOwnerContactInformation(propertyInformationId) { diff --git a/full-project-analysis.md b/full-project-analysis.md new file mode 100644 index 0000000..ae6ec16 --- /dev/null +++ b/full-project-analysis.md @@ -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) + ├── (context) + │ └── (context) + │ └──
{children}
+ ├── (mobile, authenticated only) + └── (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 + → 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 &&

مطلوب

` +- **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 | diff --git a/security-audit-report.md b/security-audit-report.md new file mode 100644 index 0000000..e69f972 --- /dev/null +++ b/security-audit-report.md @@ -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 = ` +
+

+ ${property.title || 'عقار'} +

+

+ ${property.address || property.location?.address || ''} +

+ ${property.images && property.images.length > 0 + ? `${property.title}` + : '' + } +
+`; +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 `` 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 `