Motorcycle Dealership
A showroom for buyers, a locked dashboard for the shop
The interesting part of this build is not the showroom — it is the boundary between the showroom and the back office. The shop needed to update stock and answer enquiries from a phone on the yard, without publishing anything that should be private. So the admin area is a real, guarded section of the same Express app rather than a static page that merely pretends to be behind a login.
The brief
Stock changed daily: bikes sold, prices moved, new arrivals landed. The old workflow was a spreadsheet nobody trusted by midweek. The requirement was a public storefront that is always current, a private dashboard to manage inventory and enquiries, and a way for customers to offer a bike for sale with a photo attached.
What I built
- A public showroom — home, buy a motorcycle, motorcycle details, sell your motorcycle and about pages, all reading GET /api/public/vehicles.
- A private dashboard at /admin/ with inventory, enquiries, reports, settings and admin-login screens.
- A sell-your-bike form posting to POST /api/sell-requests with a photo upload.
- Enquiry handling with statuses — New, In progress or Resolved — enforced server-side rather than trusted from the browser.
- An inventory sync endpoint so the dashboard can save a whole edited stock list in one transaction instead of dozens of requests.
How it works
Only bikes with status Available are ever sent to the storefront. The public endpoint filters on that column, so a Reserved or Sold machine can stay in the database without appearing for sale — no separate "published" flag to keep in sync.
Admin access uses a session cookie signed with HMAC-SHA256 and marked HttpOnly; SameSite=Strict, and Secure in production. The password and signing secret come from environment variables, never from source. A requireAdmin middleware then guards the whole admin app plus the inventory and enquiries APIs — so hitting a URL directly fails, it does not just hide a button.
Submissions are validated before they touch the database: required fields, an email pattern, a year that must fall between 1950 and next year, mileage and price clamped at zero, and every string length-capped. Uploads use Multer with a 5 MB limit, a jpeg/png/webp mime filter and a randomised filename. The bulk save endpoints wrap their work in a transaction with rollback on failure, so a half-written inventory can't happen.
What I would do next
- The sync endpoints replace the whole table before inserting. It is fast and atomic, but two people editing inventory at once would overwrite each other — I would move to per-row update and delete.
- Hash the admin password in the database rather than comparing against an environment value, so more than one staff account becomes possible.
- Resize photos on upload; a 5 MB phone photo should not be what the next visitor downloads.