18 KiB
SweetHome — Full Project Analysis (Updated Aug 2026)
Project: SweetHome Next.js Real Estate Application
Framework: Next.js 16.1.6 (App Router) + React 18.3.1 + React Compiler enabled
Language: JavaScript (no TypeScript)
Styling: Tailwind CSS v4, Framer Motion, lucide-react
Backend: .NET REST API at https://45.93.137.91.nip.io/api (+ report API at http://45.93.137.91/api)
1. Project Structure
SweetHome/
├── middleware.js # Root-level route protection (54 lines)
├── next.config.mjs # reactCompiler + images remotePatterns
├── full-project-analysis.md # This document
├── edit-property-flow.md # Flutter edit-property investigation (backend bug doc)
├── security-audit-report.md # Security findings
├── app/
│ ├── layout.js # Root layout: fonts, metadata, providers
│ ├── ClientLayout.js # Nav (desktop+mobile), language, user menu
│ ├── page.js # Home page (hero, map, features)
│ ├── components/ # 20+ reusable components
│ ├── contexts/ # Favorites, Notifications, Theme (+orphan Property)
│ ├── enums/ # 24 enum files (index.js barrel)
│ ├── utils/
│ │ ├── api.js # 70+ API functions + 4 fetch helpers
│ │ ├── ratings.js # Rating API (own private apiFetch)
│ │ └── constants.js
│ ├── services/AuthService.js # JWT cookie storage, role detection
│ ├── i18n/config.js # AR/EN translations (3173 lines)
│ ├── validations/ # OwnerValidatoin.js (email/phone/step validation)
│ ├── HelperFunction/ # ApiProperty.js mapper, UploadImage.js
│ ├── hooks/ # useApplyFilters, useAuth, UseFilterOfHeroSearch
│ ├── auth/ blocked/ faq/ favorites/ login/ onboarding/ ... # pages
│ └── owner/ # Dashboard (7 sub-routes)
├── public/ # APK, fonts, firebase-messaging-sw.js
└── package.json
Routes (~33 pages): Public (/, /login, /register/*, /properties, /property/[id], /terms, /privacy, /faq, /support, /onboarding) | Auth-protected (/profile, /reservations, /payments, /favorites, /booked-properties, /my-rates, /notifications, /reports, /settings, /change-password) | Owner-only (/owner/properties, /owner/properties/add, /owner/reservations, /owner/bookings, /owner/calendar, /owner/profits, /owner/account-book)
2. Architecture
Layout hierarchy
app/layout.js (server) — fonts (Geist + Madani Arabic), metadata, <html lang="ar" dir="rtl">
└── <ThemeProvider>
└── <NotificationsProvider>
└── <FavoritesProvider>
└── <ClientLayout> # nav, user menu, language switcher
└── <main>{children}</main>
├── <BottomNav /> # mobile (authenticated)
└── <NotificationHandler /> # Firebase FCM
Route protection (root middleware.js)
// middleware.js — reads cookies: auth_token + cached_user
if (!token && !isPublicRoute && !isAuthRoute) return NextResponse.redirect(new URL("/login", request.url));
if (token && isAuthRoute) return NextResponse.redirect(new URL("/", request.url));
if (IsOwner && !isOwner) return NextResponse.redirect(new URL("/", request.url)); // owner routes
if (IsPrivateRoute && !token) return NextResponse.redirect(new URL("/login", request.url));
- Owner detection relies on
cached_usercookie'srolesarray (not the JWT). - Static assets excluded via matcher regex.
Data flow
Page (useEffect) → apiFetch(endpoint, options) → AuthService.getToken() → Bearer JWT
→ fetch → unwrap { data } envelope → useState → render Tailwind UI
3. API Layer (app/utils/api.js, 783 lines)
Fetch helpers
| Helper | Lines | Behavior |
|---|---|---|
apiFetch |
38-87 | Auto Bearer, auto JSON-stringify (skips FormData), unwraps {data}, 206 tolerated, 451 → /blocked redirect, Arabic network error |
authFetch |
89-134 | POST only, returns {status,data,ok,message} (no throw) |
reportFetch |
136-172 | POST to REPORT_API_BASE (no auth) |
multipartAuthFetch |
418-448 | POST + FormData + Bearer |
async function apiFetch(endpoint, options = {}) {
const token = AuthService.getToken();
const headers = { ...(token && { Authorization: `Bearer ${token}` }), ...(options.headers || {}) };
// ... FormData passthrough, JSON.stringify for objects
// unwraps: if (json && "data" in json) return json.data;
}
Key API functions (endpoints)
| Function | Endpoint | Method |
|---|---|---|
getRentProperties |
/RentProperties/GetRentProperties |
GET |
getRentProperty |
/RentProperties/GetRentPropertyById/{id} |
GET |
getMyRentListings / getMySaleListings |
/RentProperties/GetMyRentListings, /SaleProperties/GetMySaleListings |
GET |
addRentProperty |
/RentProperties/AddRentProperty (uses buildRentPropertyPayload) |
POST |
editRentProperty |
/RentProperties/EditRentProperty/{id} — strict camelCase schema |
PUT |
editSaleProperty |
/SaleProperties/EditSaleProperty/{id} |
PUT |
updateRentPropertyStatus |
/RentProperties/UpdateStatus/{id} {status} |
PUT |
uploadPicture |
/Files/UploadPicture (FormData image) → {value: "/Pictures/xxx.jpg"} |
POST |
bookReservation |
/Reservations/BookReservation/book |
POST |
getUserReservations |
/Reservations/GetUserResevations |
GET |
getOwnerReservationRequests |
/Reservations/GetOwnerResevationRequests |
GET |
ownerConfirmReservation |
/Reservations/OwnerConfirmReservation/owner-confirm/{id} |
PUT |
payDeposit |
/Reservations/PayDeposit/pay-deposit (FormData) |
POST |
getPaymentTypes |
/PaymentType/GetAll |
GET |
getMyTransaction |
/Customer/GetMyTransaction |
GET |
getOwnerStatistics |
/Statistics/GetOwnerStatistics |
GET |
getOwnerContactInformation |
/Owner/GetOwnerContactInformation?propertyInformationId= |
GET |
loginWithEmail |
/Auth/LogInWithEmail {credential,password,device:0} |
POST |
loginWithPhone |
/Auth/LogInWithPhoneNumber |
POST |
getCurrencies |
/Currency/GetAll |
GET |
filterRentProperties |
/RentProperties/FilterRentProperties?{params} |
GET |
getUserNotifications |
/Notifications/GetUserNotifications |
GET |
setFCMToken |
/User/SetFCMToken {token, deviceType:2} |
POST |
submitReservationReport |
/ReservationReports |
POST |
buildRentPropertyPayload — city/governorate/documentType normalizer
export function buildRentPropertyPayload(data = {}) {
const rawCityValue = data?.city ?? data?.governorate ?? propertyInformation?.city ?? 1;
const cityInt = parseInt(rawCityValue, 10) || 1;
return { ...data, city: cityInt, governorate: cityInt, documentType: data?.documentType ?? 1,
propertyInformation: { ...propertyInformation, city: cityInt, governorate: cityInt, documentType: ... } };
}
editRentProperty — strict swagger-contract payload (current, works)
export async function editRentProperty(id, data) {
const pi = data?.propertyInformation || {};
const images = Array.isArray(pi.images) ? pi.images : [];
const body = {
propertyInformation: {
activityStatus: 1, images: images.length > 0 ? images : [""],
cordsX: pi.cordsX ?? null, cordsY: pi.cordsY ?? null,
address: pi.address ?? "", description: pi.description ?? null,
numberOfBathRooms: pi.numberOfBathRooms ?? 0, numberOfRooms: pi.numberOfRooms ?? 0,
numberOfBedRooms: pi.numberOfBedRooms ?? 0, space: pi.space ?? 0,
detailsJSON: pi.detailsJSON ?? "", buildingType: pi.buildingType ?? 0,
status: pi.status ?? 0, propertyType: pi.propertyType ?? 0, city: data.city ?? pi.city ?? 1,
},
deposit: data.deposit ?? 0, acceptedCertificate: data.acceptedCertificate ?? 0,
monthlyRent: data.monthlyRent ?? 0, dailyRent: data.dailyRent ?? 0,
rating: data.rating ?? 1, currencyId: data.currencyId ?? 1, rentType: data.rentType ?? 0,
isSmokeAllow: data.isSmokeAllow ?? false, specializedFor: data.specializedFor ?? false,
isVisitorAllow: data.isVisitorAllow ?? false, allowedPaymentPeriod: data.allowedPaymentPeriod ?? "",
type: data.type ?? 0,
};
return apiFetch(`/RentProperties/EditRentProperty/${id}`, { method: "PUT", body });
}
4. Auth (app/services/AuthService.js, 219 lines)
- Storage:
js-cookie—auth_token(7d, secure, sameSite=lax),cached_user. - Role detection: JWT claims via
atob(payload); roles fromhttp://schemas.microsoft.com/ws/2008/06/identity/claims/role(string or array). getUser()merges cookie profile (name/email/phone) + JWT (idfromnameidentifier/sub).login()flow:loginWithEmail/Phone→ 200 = SUCCESS +cacheCurrentUser(); 206 = OTP_REQUIRED.
getRoles() {
const payload = this.decodeToken();
const roles = payload["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"];
return Array.isArray(roles) ? roles : typeof roles === "string" ? [roles] : [];
}
isOwner() { return this.getRoles().includes("Owner"); }
isAgent() { return this.getRoles().includes("RealEstateAgent"); }
⚠ Bug:
login()callssendEmailOTP()/sendPhoneOTP()(lines 203-204) but they are not imported → ReferenceError on OTP-required login.
5. Owner Dashboard
| Page | Lines | Data source | Endpoints |
|---|---|---|---|
/owner/reservations |
1537 (active from 646) | Real API | GET GetOwnerResevationRequests + batch GetRentProperties enrichment; PUT OwnerConfirmReservation/owner-confirm/{id}; POST ChangeReservationStatus?id=&newStatus=5; POST ReservationReports/ReportReservation |
/owner/properties |
2270 | Real API + localStorage | GET GetMyRentListings/GetMySaleListings; PUT EditRentProperty/{id}; PUT EditSaleProperty/{id}; PUT UpdateStatus/{id}; POST UploadPicture. Delete is local-only |
/owner/properties/add |
1712 | Real API | POST AddRentProperty/AddSaleProperty; POST UploadPicture; GET Currency/GetAll; Nominatim geocoding |
/owner/bookings |
600 | Real API | GET GetOwnerResevationRequests |
/owner/calendar |
743 | Mock/localStorage | None (seeds fake properties; calls undefined loadCalendar()) |
/owner/profits |
594 | Mock/localStorage | None (hard-coded sampleData, 5% commission, XLSX export) |
/owner/account-book |
142 | Real API | GET Statistics/GetOwnerStatistics |
Owner reservations page — key code
// loadReservations (1259-1314): parallel fetch + batch enrich
const [resResult, rentProps] = await Promise.all([
fetch(`${API_BASE}/Reservations/GetOwnerResevationRequests`, { headers: { Authorization: `Bearer ${token}` } })
.then(async (res) => { ... return list; }),
getRentProperties().catch(() => []),
]);
const propMap = {};
propsList.forEach(rp => {
const info = rp?.propertyInformation ?? {};
propMap[rp.propertyInformationId] = info;
if (rp?.propertyInformation?.id) propMap[rp.propertyInformation.id] = info;
});
const enriched = resResult.map(r => { if (r.propertyId && propMap[r.propertyId]) r._prop = propMap[r.propertyId]; return r; });
Owner properties — edit modal
- formData: propertyType, furnished, description, bedrooms/bathrooms/floor/salons/balconies/livingRooms/area, services
{}, serviceDetails{}, terms{}, customTerms[], 6 nearby distances, purpose, currencyId, daily/monthlyPrice, deposit, rentType, allowedPaymentPeriod, salePrice. - Services: 13 enum-based (
PropertyService) checkboxes with detail inputs. - Terms: 3 enum-based (
PropertyTerm) + custom terms chips. - Images:
existingImages(API) +newImages(upload viauploadPicture, preview viaURL.createObjectURL, max 10, 5MB each) with delete/reorder. - Validation: description, bedrooms≥1, bathrooms≥1, area>0, price required.
- Save: builds
detailsJSON→propInfo→editRentProperty(property.id, payload).
6. Customer Pages
| Page | Lines | Data source |
|---|---|---|
/reservations |
1826 (active from 391) | getUserReservations + getRentProperties enrich + getPaymentTypes |
/payments |
600 | getMyTransaction + getPaymentTypes |
/property/[id] |
1231 | getRentProperty → getSalePropertyById → getSaleProperty cascade |
/properties |
851 | FilterRentProperties + getSaleProperties hybrid |
/ home |
HomeClient 71 + hooks | getRentProperties + getSaleProperties client-side filter |
/booked-properties |
220 | getUserReservations |
/favorites |
128 | FavoritesContext |
/my-rates |
148 | getCustomerRatings |
/notifications |
159 | getUserNotifications (mark-read client-only) |
Customer reservations — PaymentDialog (Haram receipt flow)
const selectedMethod = paymentMethods?.find((m) => String(m?.id ?? m?.name) === String(selectedPayment));
const isHaram = selectedMethod?.name?.toLowerCase?.() === "haram";
const canPay = payingId !== reservation.id && hasSelectedMethod && (!isHaram || !!receiptImage);
// handleConfirmPay: payDeposit({ reservationId, paymentTypeId: selectedPayment, comment, paymentImage: receiptImage })
// payDeposit → FormData: ReservationId, PaymentTypeId, Comment, paymentImage (multipart)
- CountdownTimer: deadline =
ownerApprovalDate + allowedPaymentPeriod(parseTimeSpanhandlesd.hh:mm:ss). - Inline rating form (4 categories) for
depositPaid/completed.
Property detail
- Image gallery (chevron prev/next, thumbnails, counter), Leaflet map, availability calendar (
availableDatesSetfromGetAvailableDates/available/{id}), booking flow (daily/monthly toggle, calendar month grid, summary,bookReservation). - Unused: owner-contact feature (
getOwnerContactInformationdefined but no UI),handleBookNow/bookingDatesdead code.
7. i18n (app/i18n/config.js, 3173 lines)
- 2 languages:
en(1-1662),ar(1663-3155); flat dot-namespaced keys;fallbackLng: "en". - Init:
i18n.use(LanguageDetector).use(initReactI18next).init({ resources, fallbackLng: "en", detection: { order: ["localStorage", "navigator"], caches: ["localStorage"] } }). - ClientLayout overrides:
localStorage("language")default"ar", flipsdocument.documentElement.dir(rtl/ltr). - Active language toggle: mobile header + authenticated sticky bar (desktop navbar one commented out).
8. State Management
| Context | Status |
|---|---|
FavoritesProvider (app/contexts/FavoritesContext.js) |
✅ Mounted in layout.js; favorites list, add/remove (optimistic w/ rollback), refetch |
NotificationsProvider (app/contexts/NotificationsContext.js) |
✅ Mounted; unreadCount = full array length; markAsRead is client-only |
ThemeProvider |
✅ Mounted |
PropertyContext (contexts/ + utils/ duplicate) |
❌ Orphaned — never mounted/imported; pure in-memory mock |
9. Enums (24 files in app/enums/)
Key ones: BuildingType (0-9 numeric), PropertyService (13 string), PropertyTerm (3 string), PropertyStatus (0/1/2), BookingStatus (string, ⚠ mixed casing PENDING vs ownerConfirmed), RentType (MONTHLY 0, DAILY 1), Currency (SYP 1, USD 2, EUR 3, TRY 4), Governorate (13), DocumentType (Passport/IDCard/Both), UserRole, City (Arabic values + CityEnum 0-12), RentPropertyCondition, RentPropertyType, TransactionType, CancellationReason.
10. Known Bugs & Issues
| # | Issue | Location |
|---|---|---|
| 1 | sendEmailOTP/sendPhoneOTP used but not imported → ReferenceError on 206 |
AuthService.js 203-204 |
| 2 | Edit endpoint ignores images + detailsJSON (backend bug) |
Backend PUT handler |
| 3 | Delete property is local-only (no API call) | owner/properties/page.js 1657-1666 |
| 4 | /owner/calendar calls undefined loadCalendar() (line 499) |
owner/calendar/page.js |
| 5 | /owner/calendar + /owner/profits are mock/localStorage only |
— |
| 6 | useApplyFilters.js:62 price bucket "2000-3000" compares > 300 (typo, should be 3000) |
hooks |
| 7 | HeroSearch propertyType onChange writes to filters.city (line 69) |
components/home/HeroSearch.js |
| 8 | addOwner appends both back-ID + license images under same key RearIdCarImagePath |
api.js 470-471 |
| 9 | getSaleProperty fetches whole list and filters client-side |
api.js 194 |
| 10 | Notifications markAsRead client-only (no persistence) | contexts + page |
| 11 | BookingCalendar.js date click is a no-op |
components/property |
| 12 | BookingStatus enum mixed casing | enums/BookingStatus.js |
| 13 | Ratings API has own private apiFetch without 451 handling | utils/ratings.js |
| 14 | home/PropertyMap.js:162 quick-book uses alert() stub |
components/home |
| 15 | Hardcoded phone +963567823411 in payments + reservations |
multiple |
| 16 | Firebase config hard-coded in NotificationHandler.js | components |
| 17 | Owner-contact feature dead on property detail | PropertyDetail.js 269-361 |
11. Dead Code
app/contexts/PropertyContext.js+app/utils/PropertyContext.js(both orphaned)reservations/page.jslines 1-390,profits/page.jslines 1-291,StarRating.js1-94,PropertyRatingList.js1-151,PropertyRatingForm.js1-219 (commented legacy)handleBookNow/bookingDatesin PropertyDetail.js (383-403)- Desktop language switcher (ClientLayout 259-268, commented)
12. Dependencies
Prod (20): next 16.1.6, react 18.3.1, firebase, leaflet/react-leaflet, framer-motion, lucide-react, react-hot-toast, i18next/react-i18next/browser-languagedetector, jspdf, html2canvas, xlsx, react-intersection-observer, flowbite/flowbite-react (unused), @pbe/react-yandex-maps (unused), js-cookie.
Dev (7): tailwindcss v4, @tailwindcss/postcss, postcss, autoprefixer, daisyui (unused), babel-plugin-react-compiler.
Missing: TypeScript, ESLint, tests, state library, data-fetching library, form library, HTTP client, JWT lib.
13. Security (summary — see security-audit-report.md)
| # | Finding | Severity |
|---|---|---|
| 1 | JWT in cookie (secure flag OK) but roles in client-decodable cookie | High |
| 2 | Client-side role gating (cached_user cookie editable) | High |
| 3 | Passwords passed via URL query params (changePassword, resetPassword) |
Critical |
| 4 | Stored XSS via Leaflet HTML popups | Critical |
| 5 | OTP codes logged to console | Critical |
| 6 | IDOR on property edit/status APIs (no server ownership check) | Critical |
| 7 | Firebase config public | Medium |
| 8 | Hardcoded IPs/credentials across files | Medium |