Your marketplace has THREE user roles with complete dashboards:
- Admin (YOU) - Control everything, approve listings & hosts, view all bookings
- Hosts - List RVs, manage bookings, receive payments
- Renters - Book RVs, manage their bookings
Renter pays $1,870
β
Platform receives $1,870
β
Platform keeps $170 (service fee)
Platform pays host $1,700 (manually or via Stripe Connect)
Benefits:
- β Full control over payments
- β Can hold funds if needed
- β Easy accounting
- β Simpler integration (what we built)
See STRIPE_CONNECT_GUIDE.md for implementation when you're ready to scale.
URL: /admin/dashboard
API Endpoints:
// Get dashboard statistics
GET /api/admin/stats
Response: {
totalBookings, pendingBookings, totalRevenue,
totalPlatformFees, totalVehicles, pendingVehicles
}
// Get all bookings (with pagination & filters)
GET /api/admin/bookings?page=1&limit=20&status=pending
Response: {
bookings: [...],
pagination: { total, page, pages }
}
// Get specific booking details
GET /api/admin/bookings/:bookingId
Response: { booking: {...} }What You Can Do:
- View all bookings across all hosts
- Filter by status (pending, approved, declined, cancelled, completed)
- See payment status and amounts
- View renter and host details
- Generate revenue reports
- Export booking data
URL: /host/dashboard
API Endpoints:
// Get host statistics
GET /api/host/stats
Response: {
totalBookings, pendingBookings, totalEarnings,
totalVehicles, approvedVehicles
}
// Get host's bookings
GET /api/host/bookings
Response: { bookings: [...] }
// Approve a booking
POST /api/host/bookings/:bookingId/approve
Response: { message, booking }
// Decline a booking
POST /api/host/bookings/:bookingId/decline
Body: { reason: "Not available" }
Response: { message, booking }What Hosts Can Do:
- View bookings for THEIR vehicles only
- Approve/decline booking requests
- See their earnings
- Manage their vehicle listings
URL: /my-bookings
API Endpoints:
// Get renter's bookings
GET /api/renter/my-bookings
Response: { bookings: [...] }
// Get specific booking details
GET /api/renter/bookings/:bookingId
Response: { booking: {...} }
// Cancel a booking
POST /api/renter/bookings/:bookingId/cancel
Body: { reason: "Plans changed" }
Response: { message, booking }What Renters Can Do:
- View all their bookings (past & upcoming)
- See booking status (pending, approved, declined)
- View vehicle and host details
- Cancel bookings if needed
- Track payment status
POST /api/host/request-access
Headers: { Authorization: "Bearer <firebase-token>" }
Body: {
bio: "I own 3 RVs and love sharing them with travelers",
phone: "+971 50 123 4567",
address: {
street: "123 Main St",
city: "Dubai",
state: "Dubai",
zipCode: "00000",
country: "UAE"
}
}
Response: { message: "Host access requested. Awaiting admin approval." }What Happens:
- User's role changes to "both" (can rent AND host)
- Host profile status set to "pending"
- Admin receives notification to approve
// Admin dashboard - View pending hosts
GET /api/admin/hosts/pending
Response: { hosts: [...] }
// Admin approves host
POST /api/admin/hosts/:userId/approve
Response: { message: "Host approved successfully" }
// Or rejects
POST /api/admin/hosts/:userId/reject
Body: { reason: "Incomplete documentation" }
Response: { message: "Host rejected" }POST /api/host/vehicles
Headers: { Authorization: "Bearer <firebase-token>" }
Body: {
title: "Luxury Class A Motorhome",
description: "Beautiful 35ft motorhome with full amenities",
type: "Class A",
year: 2023,
make: "Thor",
model: "Challenger",
length: 35,
sleeps: 6,
price: 1200, // AED per day
location: {
address: "Marina Walk",
city: "Dubai",
state: "Dubai",
zipCode: "00000",
coordinates: { lat: 25.08, lng: 55.14 }
},
images: [
"https://cloudinary.com/image1.jpg",
"https://cloudinary.com/image2.jpg"
],
amenities: [
"Full Kitchen",
"Bathroom with Shower",
"Air Conditioning",
"Solar Panels",
"WiFi",
"TV",
"Generator"
],
rules: "No smoking, no pets, clean water tank before return"
}
Response: {
message: "Vehicle submitted for approval",
vehicle: {...}
}Listing Status Flow:
Host submits listing
β
Status: "pending"
Vehicle is invisible to renters
β
Admin reviews listing
β
Admin approves β Status: "approved" β Visible to renters β
OR
Admin rejects β Status: "rejected" β Host can edit & resubmit
// View pending vehicle listings
GET /api/admin/vehicles/pending
Response: { vehicles: [...] }
// Approve a vehicle
POST /api/admin/vehicles/:vehicleId/approve
Response: {
message: "Vehicle approved",
vehicle: { ... }
}
// Reject a vehicle
POST /api/admin/vehicles/:vehicleId/reject
Body: { reason: "Images are unclear, please upload better photos" }
Response: { message: "Vehicle rejected" }After Approval:
- Vehicle becomes visible in search results
- Renters can book it
- Host receives bookings and manages them
Hosts provide image URLs (from Cloudinary, AWS S3, etc.)
npm install cloudinary multer-storage-cloudinary multer// server/routes/upload.js
const cloudinary = require('cloudinary').v2;
const { CloudinaryStorage } = require('multer-storage-cloudinary');
const multer = require('multer');
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
const storage = new CloudinaryStorage({
cloudinary: cloudinary,
params: {
folder: 'mirhal-vehicles',
allowed_formats: ['jpg', 'jpeg', 'png', 'webp'],
transformation: [
{ width: 1920, height: 1080, crop: 'limit' },
{ quality: 'auto:good' }
],
},
});
const upload = multer({ storage });
// Upload multiple images
router.post('/images',
verifyFirebaseToken,
upload.array('images', 10), // Max 10 images
(req, res) => {
const imageUrls = req.files.map(file => file.path);
res.json({ images: imageUrls });
}
);// components/ImageUploader.tsx
const ImageUploader = ({ onUpload }) => {
const [uploading, setUploading] = useState(false);
const handleUpload = async (files: File[]) => {
setUploading(true);
const formData = new FormData();
files.forEach(file => formData.append('images', file));
const response = await fetch('/api/upload/images', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: formData,
});
const { images } = await response.json();
onUpload(images);
setUploading(false);
};
return (
<div>
<input
type="file"
multiple
accept="image/*"
onChange={(e) => handleUpload(Array.from(e.target.files))}
/>
{uploading && <p>Uploading images...</p>}
</div>
);
};Free Tier: 25 GB storage, 25 GB bandwidth/month
// pages/AdminDashboard.tsx
const AdminDashboard = () => {
const [stats, setStats] = useState(null);
const [bookings, setBookings] = useState([]);
const [pendingVehicles, setPendingVehicles] = useState([]);
useEffect(() => {
fetchAdminStats();
fetchAllBookings();
fetchPendingVehicles();
}, []);
return (
<div className="admin-dashboard">
<h1>Admin Dashboard</h1>
{/* Stats Overview */}
<div className="stats-grid">
<StatCard title="Total Revenue" value={`AED ${stats?.totalRevenue}`} />
<StatCard title="Platform Fees" value={`AED ${stats?.totalPlatformFees}`} />
<StatCard title="Pending Bookings" value={stats?.pendingBookings} />
<StatCard title="Total Vehicles" value={stats?.totalVehicles} />
</div>
{/* Pending Approvals */}
<section>
<h2>Pending Vehicle Approvals</h2>
<VehicleApprovalList vehicles={pendingVehicles} />
</section>
{/* All Bookings */}
<section>
<h2>All Bookings</h2>
<BookingsTable bookings={bookings} />
</section>
</div>
);
};// pages/MyBookings.tsx
const MyBookings = () => {
const [bookings, setBookings] = useState([]);
useEffect(() => {
fetch('/api/renter/my-bookings', {
headers: { Authorization: `Bearer ${token}` }
})
.then(res => res.json())
.then(data => setBookings(data.bookings));
}, []);
return (
<div className="my-bookings-page">
<h1>My Bookings</h1>
<div className="bookings-list">
{bookings.map(booking => (
<BookingCard
key={booking._id}
booking={booking}
vehicle={booking.vehicleId}
host={booking.hostId}
/>
))}
</div>
</div>
);
};
const BookingCard = ({ booking, vehicle, host }) => (
<div className="booking-card">
<img src={vehicle.images[0]} alt={vehicle.title} />
<div className="booking-details">
<h3>{vehicle.title}</h3>
<p>Check-in: {new Date(booking.startDate).toLocaleDateString()}</p>
<p>Check-out: {new Date(booking.endDate).toLocaleDateString()}</p>
<p>Status: <StatusBadge status={booking.status} /></p>
<p>Total: AED {booking.finalTotal}</p>
<p>Host: {host.name}</p>
{booking.status === 'pending' && (
<button onClick={() => cancelBooking(booking._id)}>
Cancel Booking
</button>
)}
</div>
</div>
);// pages/HostDashboard.tsx
const HostDashboard = () => {
const [stats, setStats] = useState(null);
const [bookings, setBookings] = useState([]);
const [vehicles, setVehicles] = useState([]);
return (
<div className="host-dashboard">
<h1>Host Dashboard</h1>
{/* Earnings Overview */}
<div className="stats-grid">
<StatCard title="Total Earnings" value={`AED ${stats?.totalEarnings}`} />
<StatCard title="Pending Bookings" value={stats?.pendingBookings} />
<StatCard title="My Vehicles" value={stats?.totalVehicles} />
</div>
{/* My Vehicles */}
<section>
<h2>My Vehicles</h2>
<button onClick={() => navigate('/host/add-vehicle')}>
+ Add New Vehicle
</button>
<VehiclesList vehicles={vehicles} />
</section>
{/* Bookings */}
<section>
<h2>Booking Requests</h2>
<HostBookingsList bookings={bookings} />
</section>
</div>
);
};// pages/AddVehicle.tsx
const AddVehicle = () => {
const [formData, setFormData] = useState({
title: '',
description: '',
type: 'Class A',
year: 2024,
make: '',
model: '',
length: 0,
sleeps: 2,
price: 0,
location: {},
images: [],
amenities: [],
});
const handleSubmit = async () => {
const response = await fetch('/api/host/vehicles', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(formData),
});
const data = await response.json();
alert(data.message); // "Vehicle submitted for approval"
navigate('/host/dashboard');
};
return (
<div className="add-vehicle-page">
<h1>List Your RV</h1>
<form onSubmit={handleSubmit}>
<input
placeholder="Title"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
/>
{/* ... more fields ... */}
<ImageUploader onUpload={(images) => setFormData({ ...formData, images })} />
<button type="submit">Submit for Approval</button>
</form>
</div>
);
};// hooks/useAuth.ts
export const useAuth = () => {
const [user, setUser] = useState(null);
const [mongoUser, setMongoUser] = useState(null);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async (firebaseUser) => {
if (firebaseUser) {
setUser(firebaseUser);
// Fetch MongoDB user profile (includes role)
const token = await firebaseUser.getIdToken();
const response = await fetch('/api/users/me', {
headers: { Authorization: `Bearer ${token}` }
});
const mongoUserData = await response.json();
setMongoUser(mongoUserData);
}
});
return unsubscribe;
}, []);
return {
user,
mongoUser,
isAdmin: mongoUser?.isAdmin,
isHost: mongoUser?.role === 'host' || mongoUser?.role === 'both',
isRenter: mongoUser?.role === 'renter' || mongoUser?.role === 'both',
};
};
// Usage
const { isAdmin, isHost } = useAuth();
if (isAdmin) {
// Show admin dashboard
}
if (isHost) {
// Show host features
}// components/ProtectedRoute.tsx
const ProtectedRoute = ({ children, requiredRole }) => {
const { mongoUser } = useAuth();
if (!mongoUser) {
return <Navigate to="/login" />;
}
if (requiredRole === 'admin' && !mongoUser.isAdmin) {
return <Navigate to="/" />;
}
if (requiredRole === 'host' && !['host', 'both'].includes(mongoUser.role)) {
return <Navigate to="/" />;
}
return children;
};
// In routes
<Route path="/admin/dashboard" element={
<ProtectedRoute requiredRole="admin">
<AdminDashboard />
</ProtectedRoute>
} />
<Route path="/host/dashboard" element={
<ProtectedRoute requiredRole="host">
<HostDashboard />
</ProtectedRoute>
} />GET http://localhost:5001/api/renter/my-bookings
Authorization: Bearer <your-firebase-token>GET http://localhost:5001/api/admin/bookings?page=1&limit=20
Authorization: Bearer <admin-firebase-token>POST http://localhost:5001/api/admin/vehicles/6903341f9e1713f546fb98c1/approve
Authorization: Bearer <admin-firebase-token>POST http://localhost:5001/api/host/vehicles
Authorization: Bearer <host-firebase-token>
Content-Type: application/json
{
"title": "Luxury Motorhome",
"description": "Amazing RV",
"type": "Class A",
"year": 2023,
"make": "Thor",
"model": "Challenger",
"length": 35,
"sleeps": 6,
"price": 1200,
"location": {
"address": "Dubai Marina",
"city": "Dubai",
"state": "Dubai",
"zipCode": "00000",
"coordinates": { "lat": 25.08, "lng": 55.14 }
},
"images": [
"https://picsum.photos/seed/rv1/1024/768"
],
"amenities": ["Kitchen", "Bathroom", "AC"]
}-
John wants to become a host
POST /api/host/request-access β Status: "pending" β Admin receives notification -
Admin approves John
POST /api/admin/hosts/:johnId/approve β John is now an approved host β John receives email confirmation -
John lists his RV
POST /api/host/vehicles β Vehicle status: "pending" β Admin receives notification -
Admin approves the RV listing
POST /api/admin/vehicles/:rvId/approve β RV becomes visible in search β John receives confirmation -
Sarah (renter) books the RV
POST /api/payments/create-payment-intent POST /api/payments/confirm-payment β Booking created with status: "pending" β Payment: AED 1,870 charged β John receives booking notification -
John approves the booking
POST /api/host/bookings/:bookingId/approve β Booking status: "approved" β Sarah receives confirmation -
Everyone can see the booking:
- Sarah:
GET /api/renter/my-bookings - John:
GET /api/host/bookings - Admin:
GET /api/admin/bookings
- Sarah:
-
Admin monitors revenue
GET /api/admin/stats β Total Revenue: AED 1,870 β Platform Fee: AED 170 β Host Payout: AED 1,700
-
Create an admin user in your database:
// In MongoDB or via script db.users.updateOne( { email: 'your-admin-email@example.com' }, { $set: { isAdmin: true } } );
-
Build the frontend pages:
- Admin Dashboard
- Host Dashboard
- Renter My Bookings
- Add Vehicle Form
-
Test the complete flow:
- Create a test host
- Approve them as admin
- Host lists a vehicle
- Approve the vehicle
- Make a test booking
- View it in all 3 dashboards
-
Add image upload (Cloudinary integration)
-
Set up email notifications (SendGrid already configured)
-
Deploy to production when ready!
| Endpoint | Method | Description | Auth |
|---|---|---|---|
/api/admin/stats |
GET | Dashboard statistics | Admin |
/api/admin/bookings |
GET | All bookings | Admin |
/api/admin/vehicles/pending |
GET | Pending vehicles | Admin |
/api/admin/vehicles/:id/approve |
POST | Approve vehicle | Admin |
/api/admin/hosts/pending |
GET | Pending hosts | Admin |
/api/admin/hosts/:id/approve |
POST | Approve host | Admin |
/api/host/request-access |
POST | Request to be a host | User |
/api/host/my-vehicles |
GET | Host's vehicles | Host |
/api/host/vehicles |
POST | Create listing | Host |
/api/host/bookings |
GET | Host's bookings | Host |
/api/host/bookings/:id/approve |
POST | Approve booking | Host |
/api/renter/my-bookings |
GET | Renter's bookings | User |
/api/renter/bookings/:id/cancel |
POST | Cancel booking | User |
The system is fully functional! π
You can now: β View all bookings as admin β Hosts can list vehicles (with approval) β Renters can see their bookings β Payment flows through you (the platform) β Complete approval workflow for hosts & vehicles