Skip to content

Latest commit

Β 

History

90 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Crest Walk API

A modular Express 5 + MongoDB/Mongoose REST API powering a sneaker/shoe e-commerce platform, with JWT auth, Cloudinary image uploads, and VNPAY payments.

This repository is the backend-only service of the Crest Walk platform. It exposes a public storefront API (products, cart, wishlist, checkout, order tracking, reviews) and an admin API (products, categories, brands, inventory, orders, users, vouchers, reviews, banners, revenue & bestseller stats), consumed by a separate React frontend over JWT-authenticated HTTP.

Node.js Express MongoDB Mongoose JWT Cloudinary VNPAY Jest Apache 2.0 License

πŸ“š Table of Contents


🎯 Overview

Key Features

  • Storefront API
    • Public product catalog with listing, search, filtering, and detail lookup.
    • Cart and wishlist tied to the authenticated user, plus a guest-or-authenticated order-tracking endpoint.
    • Checkout that snapshots cart items into an order, decrements size-level stock, and (optionally) kicks off a VNPAY payment.
    • Product reviews with a purchase-eligibility check before a user is allowed to review.
  • Authentication & Session
    • JWT access token (short-lived, returned in the response body) + JWT refresh token (long-lived, stored server-side and issued as an httpOnly cookie).
    • POST /auth/refresh-token mints a new access token from the refresh cookie; POST /auth/logout revokes it.
    • Password reset flow backed by a hashed, TTL-expiring PasswordResetToken and a pluggable email sender (noop / console / resend).
    • Role-based access control (user / admin) via authenticate + authorize([...roles]) middleware.
  • Admin API
    • Full CRUD for Products, Categories, Brands, Inventory, Orders, Users, Vouchers, Reviews (moderation), and Banners β€” all gated behind authenticate + authorize(['admin']).
    • Revenue and bestseller analytics endpoints with date-range/interval query params.
    • A server-side remote-image fetch proxy (SSRF-guarded) so the admin UI can pull external images without CORS issues.
  • Media & Payments
    • Multer (in-memory) + Cloudinary for product/banner image uploads, with 2MB/file limits and automatic overwrite of previously uploaded assets.
    • VNPAY integration for online payments: signed redirect URL at checkout, HMAC-SHA512-verified return/IPN callback.
  • Security & Ops
    • helmet, cors (credentialed, single allowed origin), express-rate-limit (100 req / 15 min / IP), compression, request body limits raised for base64 image payloads.
    • winston structured logging, graceful shutdown on SIGTERM/SIGINT with MongoDB disconnect.

πŸ› οΈ Tech & Architecture

Tech Stack & Key Libraries

  • Runtime & Framework: Node.js (>=18), Express 5, ES Modules ("type": "module")
  • Database & ODM: MongoDB via Mongoose 9
  • Auth: jsonwebtoken (access + refresh tokens), bcrypt (password hashing), cookie-parser (refresh-token cookie)
  • Validation: express-validator
  • Media: multer (memory storage), cloudinary
  • Payments: VNPAY (HMAC-SHA512 signed URLs, sandbox gateway)
  • Security & Perf: helmet, cors, express-rate-limit, compression
  • Logging: winston
  • Testing: Jest 30 (ESM via --experimental-vm-modules), supertest (installed, not yet wired to route tests)
  • Tooling: Prettier (npm run lint is check-only), nodemon for dev reload, GitHub Actions CI (Node 18.x/20.x/22.x matrix)

Project Structure

crest-walk-api-JS/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ server.js               # Entry point: middleware stack, DB connect, listen, graceful shutdown
β”‚   β”œβ”€β”€ config/
β”‚   β”‚   └── env.config.js       # Reads/validates environment variables
β”‚   β”œβ”€β”€ lib/                    # Cross-cutting infra: mongoose, jwt, cloudinary, rate_limit, winston, emailSender
β”‚   β”œβ”€β”€ middleware/              # authenticate, authorize, optionalAuthenticate, uploadImage, validationError
β”‚   β”œβ”€β”€ model/                   # Mongoose schemas (User, Product, Order, Cart, Category, Brand, Review, ...)
β”‚   β”œβ”€β”€ router/
β”‚   β”‚   β”œβ”€β”€ index.router.js      # Mounts every route under /api/v1
β”‚   β”‚   β”œβ”€β”€ auth.router.js       # /auth/*
β”‚   β”‚   β”œβ”€β”€ admin/                # /admin/* β€” products, categories, brands, orders, users, inventory,
β”‚   β”‚   β”‚                          # vouchers, reviews, banners, stats, fetch-remote-image
β”‚   β”‚   └── user/                  # /products, /cart, /orders, /wishlist, /payment, /reviews
β”‚   β”œβ”€β”€ controller/               # Request/response handling, mirrors router/ resource-by-resource
β”‚   β”œβ”€β”€ service/                   # Business logic layer, mirrors controller/ 1:1
β”‚   β”œβ”€β”€ utils/                     # Small helpers (slug generation, username generation)
β”‚   └── test/                       # Jest tests
β”œβ”€β”€ nodemon.json             # Dev-mode watch config
β”œβ”€β”€ package.json / package-lock.json
β”œβ”€β”€ LICENSE
└── README.md

Application Architecture

Architecture pattern: router β†’ controller β†’ service β†’ model, applied consistently across every resource.

Request Lifecycle

graph TD
    REQ["Incoming request"] --> CORS["cors<br/>(credentialed, fixed origin)"]
    CORS --> JSON["express.json / urlencoded<br/>(20mb limit for image payloads)"]
    JSON --> CP["cookie-parser"]
    CP --> COMP["compression"]
    COMP --> HEL["helmet"]
    HEL --> RL["express-rate-limit<br/>(100 req / 15 min / IP)"]
    RL --> ROUTER["/api/v1 router"]
    ROUTER --> CTRL["controller"]
    CTRL --> SVC["service"]
    SVC --> MODEL["Mongoose model"]
    MODEL --> DB[("MongoDB")]
Loading

Route Map

Base path: /api/v1.

Mount Router file Guard
/auth router/auth.router.js per-route (see Auth Endpoints)
/admin/* router/admin/index.router.js πŸ”’ authenticate + authorize(['admin'])
/products router/user/product.router.js public
/brands router/admin/brand.router.js public
/categories router/admin/category.router.js public
/banners router/admin/banner.router.js public
/cart router/user/cart.router.js πŸ”’ authenticate
/orders/track trackOrder.controller.js optionalAuthenticate
/orders router/user/order.router.js πŸ”’ authenticate
/wishlist router/user/wishlist.router.js πŸ”’ authenticate
/payment router/user/payment.router.js public
/reviews router/user/review.router.js per-route (see Storefront Endpoints)

⚠️ Known gap: /brands, /categories, and /banners are wired directly to the same router files used under /admin (full CRUD, including POST/PUT/DELETE), but are mounted without authenticate/authorize. Only GET is intended to be public β€” treat write access on these three paths as unauthenticated until this is locked down.

🧩 Click to expand key runtime flows (Auth, Refresh, Orders, Tracking)
1. Register / Login flow
sequenceDiagram
  autonumber
  participant C as Client
  participant R as auth.router.js
  participant Svc as auth service
  participant DB as MongoDB (User, Token)

  C->>R: POST /auth/login { email, password }
  R->>Svc: login(email, password)
  Svc->>DB: find User, compare bcrypt hash
  Svc->>Svc: generateAccessToken(userId)
  Svc->>Svc: generateRefreshToken(userId)
  Svc->>DB: persist Token { userId, token } (TTL 1w)
  Svc-->>R: { user, accessToken, refreshToken }
  R-->>C: 200 { user, accessToken } + Set-Cookie refreshToken (httpOnly, sameSite=strict)
Loading
2. Access-token refresh
sequenceDiagram
  autonumber
  participant C as Client
  participant R as auth.router.js
  participant DB as MongoDB (Token)

  C->>R: POST /auth/refresh-token (Cookie: refreshToken)
  R->>DB: find Token by cookie value
  R->>R: verifyRefreshToken(token)
  R->>R: generateAccessToken(userId)
  R-->>C: 200 { accessToken }
Loading
3. Checkout with online payment
sequenceDiagram
  autonumber
  participant C as Client
  participant R as order.router.js
  participant Svc as checkout.service.js
  participant DB as MongoDB (Cart, Order, Product)
  participant VNP as VNPAY

  C->>R: POST /orders/checkout (Bearer token)
  R->>Svc: checkout(userId, body)
  Svc->>DB: read Cart, snapshot items into Order
  Svc->>DB: decrement Product size stock
  alt payment_method = Online
    Svc->>Svc: build signed VNPAY URL (HMAC-SHA512)
    Svc-->>R: { order, paymentUrl }
    R-->>C: 201 { order, paymentUrl }
    C->>VNP: redirect to paymentUrl
    VNP-->>R: GET /payment/vnpay_return (signed query params)
    R->>R: verify checksum
    R->>DB: set order.payment_status = paid | order.status = cancelled
  else payment_method = COD
    Svc-->>R: { order }
    R-->>C: 201 { order }
  end
Loading
4. Order history & detail
sequenceDiagram
  autonumber
  participant C as Client
  participant R as order.router.js
  participant Svc as history.service.js / detail.service.js
  participant DB as MongoDB (Order)

  C->>R: GET /orders (Bearer token)
  R->>Svc: historyService(userId)
  Svc->>DB: Order.find({ user_id }).sort({ createdAt: -1 })
  DB-->>Svc: orders[]
  Svc-->>R: orders[]
  R-->>C: 200 { data: orders[] }

  C->>R: GET /orders/:id (Bearer token)
  R->>Svc: detailService(userId, orderId)
  Svc->>DB: Order.findOne({ _id: orderId, user_id })
  alt order found and owned by user
    DB-->>Svc: order
    Svc-->>R: order
    R-->>C: 200 { data: order }
  else not found / not owned
    Svc-->>R: 404 error
    R-->>C: 404 { message }
  end
Loading
5. Order tracking (guest or authenticated)
sequenceDiagram
  autonumber
  participant C as Client
  participant OA as optionalAuthenticate
  participant Ctrl as trackOrder.controller.js
  participant Svc as trackOrder.service.js
  participant DB as MongoDB (Order)

  C->>OA: GET /orders/track?orderId=...&phone=... (Bearer token optional)
  OA->>OA: decode Bearer token if present β†’ req.userId, else continue
  OA->>Ctrl: next()
  Ctrl->>Svc: trackOrderService(orderId, phone, userId)
  Svc->>DB: Order.findById(orderId)
  alt order not found
    Svc-->>Ctrl: 404 error
    Ctrl-->>C: 404 { message }
  else req.userId matches order.user_id
    Svc-->>Ctrl: order (owner match, phone not required)
    Ctrl-->>C: 200 { data: order }
  else phone matches order.phone (last 9 digits)
    Svc-->>Ctrl: order (guest match by phone)
    Ctrl-->>C: 200 { data: order }
  else phone missing or mismatched
    Svc-->>Ctrl: 400/403 error
    Ctrl-->>C: 400/403 { message }
  end
Loading

πŸš€ Getting Started

Requires Node.js >= 18.x, a MongoDB instance (local or Atlas), and a Cloudinary account for image uploads.

  1. Clone the repository

    git clone https://github.com/MT-KS-04/crest-walk-api.git
    cd crest-walk-api-JS
  2. Install dependencies

    npm install
  3. Configure environment variables

    Create a .env file in the project root:

    # Server
    PORT=3000
    NODE_ENV=development
    LOG_LEVELS=info
    
    # Database
    MONGOOSE_URL=mongodb://localhost:27017/crest-walk-api
    
    # JWT
    JWT_ACCESS_SECRET=change-me-access-secret
    JWT_REFRESH_SECRET=change-me-refresh-secret
    ACCESS_TOKEN_EXPIRY=15m
    REFRESH_TOKEN_EXPIRY=7d
    
    # Cloudinary
    CLOUDINARY_CLOUD_NAME=your-cloud-name
    CLOUDINARY_API_KEY=your-api-key
    CLOUDINARY_API_SECRET=your-api-secret
    
    # Password reset email (optional β€” defaults to a no-op sender)
    EMAIL_PROVIDER=noop        # noop | console | resend
    EMAIL_FROM=no-reply@example.com
    EMAIL_API_KEY=
    FRONTEND_URL=http://localhost:3001
    PASSWORD_RESET_TOKEN_EXPIRY=15m
    PASSWORD_RESET_EXPOSE_TOKEN=false

    src/server.js currently hardcodes the allowed CORS origin to http://localhost:3001 and VNPAY credentials are hardcoded placeholders in src/service/user/payment/vnpayUtils.js β€” update both directly in code if your frontend origin or VNPAY merchant details differ.

  4. Run the server in development

    npm run dev

    The API is available at http://localhost:3000/api/v1/ and auto-reloads on file changes (nodemon.json).

  5. Run the server in production

    npm start
  6. Lint

    npm run lint    # prettier --check .
  7. Run tests

    npm test        # Jest, ESM mode, coverage report in coverage/

πŸ“‘ API Reference

Health Check

The root endpoint returns a health check:

{
  "message": "API is live",
  "status": "ok",
  "serviceName": "crest-walk-api",
  "version": "1.0.0",
  "environment": "development",
  "uptime": 12.345,
  "server": "Express + Node.js",
  "docs": "https://docs.crest-walk-api.mk-ts-04.com",
  "timestamp": "2026-07-16T16:12:00.000Z"
}

Auth Endpoints

Base path: /auth.

Method Endpoint Access Notes
POST /auth/register Public { email, password, ... }
POST /auth/login Public Sets refreshToken httpOnly cookie
GET /auth/me πŸ”’ user or admin Current user profile
POST /auth/refresh-token Cookie (refreshToken) Issues a new access token
POST /auth/logout Cookie (refreshToken) Revokes the refresh token, clears the cookie
POST /auth/forgot-password Public Starts password-reset flow (emails a token)
POST /auth/reset-password Public Consumes the reset token, sets a new password

Storefront Endpoints

Public / authenticated-user routes.

Method Endpoint Access Notes
GET /products Public List products
GET /products/search Public Keyword search
GET /products/filter Public Filter by category/brand/price/size, etc.
GET /products/:id Public Product detail
GET /cart πŸ”’ auth Current user's cart
POST /cart/add πŸ”’ auth Add item
PUT /cart/update πŸ”’ auth Update quantity/size
DELETE /cart/remove πŸ”’ auth Remove item
POST /wishlist/add πŸ”’ auth Add product to wishlist
GET /wishlist πŸ”’ auth List wishlist
DELETE /wishlist/:productId πŸ”’ auth Remove from wishlist
POST /orders/checkout πŸ”’ auth Builds order from cart; VNPAY URL if Online
GET /orders πŸ”’ auth Order history
GET /orders/:id πŸ”’ auth Order detail
GET /orders/track Guest or πŸ”’ auth Order lookup (optionalAuthenticate)
POST /reviews πŸ”’ auth Submit a review (purchase-eligibility checked)
GET /reviews/product/:productId Public List a product's reviews
GET /reviews/can-review/:productId πŸ”’ auth Check purchase/review eligibility
GET /payment/vnpay_return Public VNPAY return/IPN callback

Admin Endpoints

Base path: /admin/*. Every route below requires Authorization: Bearer <accessToken> and role: admin.

Products

Method Endpoint Notes
GET /admin/products List products
POST /admin/products Create; multipart field images (up to 10)
GET /admin/products/:id Product detail
PUT /admin/products/:id Update; images optional on re-upload
DELETE /admin/products/:id Delete

Categories

Method Endpoint Notes
GET /admin/categories List
POST /admin/categories Create
GET /admin/categories/:id Detail
PUT /admin/categories/:id Update
DELETE /admin/categories/:id Delete

Brands

Method Endpoint Notes
GET /admin/brands List
POST /admin/brands Create
GET /admin/brands/:id Detail
PUT /admin/brands/:id Update
DELETE /admin/brands/:id Delete

Orders

Method Endpoint Notes
GET /admin/orders List all orders
GET /admin/orders/:id Order detail
PUT /admin/orders/:id/status Update status / payment_status

Users

Method Endpoint Notes
GET /admin/users List users
GET /admin/users/:id User detail
PUT /admin/users/:id/status Update status / role
PUT /admin/users/:id/reset-password Admin resets a user's password

Inventory

Method Endpoint Notes
GET /admin/inventory List stock; ?lowStock= filter
PATCH /admin/inventory/:productId/size/:size Set/increment stock; body.mode: set|inc

Vouchers

Method Endpoint Notes
GET /admin/vouchers List
POST /admin/vouchers Create
GET /admin/vouchers/:id Detail
PUT /admin/vouchers/:id Update
DELETE /admin/vouchers/:id Delete

Reviews (moderation)

Method Endpoint Notes
GET /admin/reviews List; filter by status/productId/userId
GET /admin/reviews/:id Detail
PUT /admin/reviews/:id/status Approve / reject
DELETE /admin/reviews/:id Delete

Banners

Method Endpoint Notes
GET /admin/banners List; filter by position/is_active
POST /admin/banners Create; multipart field image_url
GET /admin/banners/:id Detail
PUT /admin/banners/:id Update; image_url optional
DELETE /admin/banners/:id Delete

Stats

Method Endpoint Query params
GET /admin/stats/revenue interval=day|month|year, startDate, endDate
GET /admin/stats/bestsellers limit, startDate, endDate

Media

Method Endpoint Notes
POST /admin/fetch-remote-image SSRF-guarded proxy that fetches an external image by { url } for the admin UI

All authenticated requests send Authorization: Bearer <accessToken>; the refresh-token cookie is only used by /auth/refresh-token and /auth/logout.


πŸ—„οΈ Data Model

All 12 schemas live in src/model/. There is no separate "Shoe" model β€” footwear is represented by Product, with size/stock tracked per entry in its embedded sizes array.

Entity Relationships

Model Related Model Cardinality Linked via
User Cart 1 : 1 Cart.user_id
User Order 1 : N Order.user_id
User Wishlist 1 : N Wishlist.user_id
User Review 1 : N Review.user_id
User Token 1 : N Token.userId (refresh tokens)
User PasswordResetToken 1 : N PasswordResetToken.userId
Category Product 1 : N Product.category_id
Brand Product 1 : N Product.brand_id
Product Review 1 : N Review.product_id
Product Wishlist 1 : N Wishlist.product_id
Voucher Order 1 : N Order.voucher_id (optional)
Product Cart N : N Embedded in Cart.items[].product_id
Product Order N : N Snapshotted in Order.items[].product_id

Identity & Auth Models

User

Field Type Notes
username String, unique Max 20 chars
password String bcrypt-hashed in a pre('save') hook
full_name String Max 100 chars
email String, unique Lowercased, regex-validated
phone String Optional, 9-11 digits
role String enum admin | user (default user)
status String enum active | blocked (default active)

Token (refresh-token store)

Field Type Notes
token String Raw refresh-token value
userId ObjectId β†’ User Owner
createdAt Date TTL index, auto-deletes after 1w

PasswordResetToken

Field Type Notes
userId ObjectId β†’ User Indexed
tokenHash String SHA-256 hash of the raw reset token, indexed
expiresAt Date TTL index, Mongo deletes at this exact date
usedAt Date, nullable Set once the token is consumed

Catalog Models

Product

Field Type Notes
category_id ObjectId β†’ Category Required
brand_id ObjectId β†’ Brand Required
name String Max 150 chars
price Number β‰₯ 0
original_price Number, nullable Used to compute sale discount
images [String] At least 1 required
description String Defaults to ''
is_new Boolean Default false
is_sale Boolean Default false
rating Number 0-5, default 0
review_count Number Default 0
sizes [{ size: Number, quantity: Number }] Embedded, per-size stock

Category

Field Type Notes
name String, unique Max 50 chars
slug String, unique Lowercased

Brand

Field Type Notes
name String, unique -
slug String, unique Lowercased
logo String, nullable -
description String, nullable -

Review

Field Type Notes
user_id ObjectId β†’ User Required
product_id ObjectId β†’ Product Required
rating Number 1-5
comment String Max 1000 chars, defaults to ''
status String enum pending | approved | rejected

Commerce Models

Cart

Field Type Notes
user_id ObjectId β†’ User, unique One cart per user
items [{ product_id, size: Number, quantity: Number }] Embedded, default []

Order

Field Type Notes
user_id ObjectId β†’ User Required
voucher_id ObjectId β†’ Voucher, nullable Optional applied voucher
items [{ product_id, product_name, size, quantity, price }] Embedded snapshot at time of purchase, at least 1 item
total_price Number β‰₯ 0
status String enum pending | confirmed | shipping | delivered | cancelled
payment_method String enum COD | Online
payment_status String enum unpaid | paid (default unpaid)
address String Shipping address
phone String 9-11 digits

Wishlist

Field Type Notes
user_id ObjectId β†’ User Part of compound unique index
product_id ObjectId β†’ Product Part of compound unique index (user_id + product_id)

Voucher

Field Type Notes
code String, unique Uppercased
description String Defaults to ''
discount_type String enum percent | fixed (default fixed)
discount_amount Number β‰₯ 0
min_order Number Default 0
max_uses Number Default 0 (0 = unlimited)
used_count Number Default 0
start_date Date, nullable -
end_date Date, nullable -
is_active Boolean Default true

Content Models

Banner

Field Type Notes
title String Required
image_url String Required
link_url String Defaults to ''
position String enum hero | sidebar | popup (default hero)
order_index Number Display order, default 0
is_active Boolean Default true

πŸ“„ Project Info

Related Frontend

The companion React storefront + admin dashboard lives in a separate repository and talks to this API via VITE_API_URL (must include /api/v1):


Author & Contact

This project is conceptualized and implemented by K'To Mis & His Team. Feel free to reach out via the following channels πŸ‘‡


License

This project is distributed under the Apache License 2.0. See the LICENSE file for full terms, rights, and limitations.


Β© 2026 K'To Mis & His Team. All rights reserved.
Crest Walk API β€” an Express + MongoDB backend for a shoe e-commerce platform.

About

The backend service behind Crest Walk, a sneaker e-commerce platform. It handles everything from the shopping experience to online payments and gives store admins full control over their business.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages