Skip to content

SamiulIslam007/EcoSpark-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

48 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🌿 EcoSpark Hub β€” Backend API

RESTful API powering the EcoSpark Hub sustainability ideas platform.

Node.js TypeScript Express Prisma PostgreSQL Stripe Vercel

🌐 Live API Β· πŸ–₯ Frontend Repo Β· πŸ“‹ Report Bug


πŸ“– Table of Contents


🌱 About

EcoSpark API is the Express 5 backend for EcoSpark Hub. It provides:

  • Full CRUD for sustainability ideas with a multi-stage review workflow
  • Session-based authentication via Better Auth with PostgreSQL session storage
  • Stripe-powered payments for monetised ideas (checkout + webhook)
  • Cloudinary image uploads streamed from server memory via Multer
  • Threaded comments with nested replies (up to 3 levels)
  • Role-based access control β€” MEMBER and ADMIN roles
  • Admin moderation β€” approve/reject ideas, manage users and categories

πŸ›  Tech Stack

Category Technology
Runtime Node.js 20+
Language TypeScript 5 (ESM)
Framework Express 5
ORM Prisma 7 with @prisma/adapter-pg
Database PostgreSQL 15+
Authentication Better Auth 1.5.6 (email/password + sessions)
Payments Stripe 21.0 (Checkout Sessions + Webhooks)
File Upload Multer 1.4 (memory storage) β†’ Cloudinary 2.6
Security Helmet 8.1, CORS 2.8
Logging Morgan 1.10
Deployment Vercel (serverless via @vercel/node)

πŸ“ Project Structure

ecospart-api/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ server.ts                    # Entry point (local dev)
β”‚   β”œβ”€β”€ app.ts                       # Express app bootstrap (CORS, middleware, routes)
β”‚   └── app/
β”‚       β”œβ”€β”€ config/
β”‚       β”‚   β”œβ”€β”€ index.ts             # Better Auth configuration
β”‚       β”‚   └── validateEnv.ts       # Startup env validation (throws if missing)
β”‚       β”œβ”€β”€ lib/
β”‚       β”‚   β”œβ”€β”€ prisma.ts            # Prisma client singleton
β”‚       β”‚   β”œβ”€β”€ cloudinary.ts        # Cloudinary upload / delete utilities
β”‚       β”‚   └── catchAsync.ts        # Async error wrapper (eliminates try/catch boilerplate)
β”‚       β”œβ”€β”€ middleware/
β”‚       β”‚   β”œβ”€β”€ auth.middleware.ts   # `protect` β€” validates session, attaches req.user
β”‚       β”‚   β”œβ”€β”€ adminOnly.middleware.ts  # `adminOnly` β€” rejects non-ADMIN
β”‚       β”‚   β”œβ”€β”€ upload.middleware.ts # Multer config (5 MB, images only, memory storage)
β”‚       β”‚   β”œβ”€β”€ globalError.middleware.ts  # Global error handler
β”‚       β”‚   └── notFound.middleware.ts     # 404 handler
β”‚       β”œβ”€β”€ module/                  # Feature modules (controller / service / route / interface)
β”‚       β”‚   β”œβ”€β”€ admin/               # Admin stats, user management, newsletter
β”‚       β”‚   β”œβ”€β”€ category/            # Category CRUD
β”‚       β”‚   β”œβ”€β”€ comment/             # Threaded comments
β”‚       β”‚   β”œβ”€β”€ idea/                # Idea CRUD + status workflow
β”‚       β”‚   β”œβ”€β”€ payment/             # Stripe checkout + webhook
β”‚       β”‚   β”œβ”€β”€ upload/              # Image upload to Cloudinary
β”‚       β”‚   └── vote/                # Upvote / downvote
β”‚       β”œβ”€β”€ routes/
β”‚       β”‚   └── index.ts             # Central route aggregator
β”‚       β”œβ”€β”€ utils/
β”‚       β”‚   β”œβ”€β”€ sendResponse.ts      # Standardised JSON response helper
β”‚       β”‚   └── trustedOrigins.ts    # CORS origin builder from env
β”‚       β”œβ”€β”€ interfaces/
β”‚       β”‚   └── common.interface.ts
β”‚       └── errorHelpers/
β”‚           └── AppError.ts          # Custom error class
β”‚
β”œβ”€β”€ prisma/
β”‚   β”œβ”€β”€ schema.prisma                # Database schema (all models)
β”‚   └── seed.ts                      # Demo data seeder
β”‚
β”œβ”€β”€ dist/                            # Compiled JavaScript (git-ignored)
β”œβ”€β”€ vercel.json                      # Vercel deployment config
β”œβ”€β”€ .env.example                     # Environment variable template
β”œβ”€β”€ tsconfig.json
└── package.json

πŸ—„ Database Schema

Models Overview

User ──────┬──── ideas    ──── Idea ──┬──── votes    ──── Vote
           β”œβ”€β”€β”€β”€ votes                β”œβ”€β”€β”€β”€ comments ──── Comment (nested)
           β”œβ”€β”€β”€β”€ purchases            β”œβ”€β”€β”€β”€ purchases──── Purchase
           β”œβ”€β”€β”€β”€ comments             └──── category ──── Category
           β”œβ”€β”€β”€β”€ sessions  (Better Auth)
           └──── accounts  (Better Auth OAuth)

NewsletterSubscriber (standalone)
Verification (Better Auth)

Key Model Details

User

id          String   @id
name        String
email       String   @unique
role        Role     @default(MEMBER)   // MEMBER | ADMIN
isActive    Boolean  @default(true)

Idea

id                 String      @id @default(cuid())
title              String
problemStatement   String
proposedSolution   String
description        String
images             String[]    // Cloudinary URLs
isPaid             Boolean     @default(false)
price              Float?
status             IdeaStatus  @default(DRAFT)
                               // DRAFT | UNDER_REVIEW | APPROVED | REJECTED
rejectionFeedback  String?
authorId           String      // β†’ User
categoryId         String      // β†’ Category

Vote β€” unique per [userId, ideaId]

type    VoteType  // UPVOTE | DOWNVOTE

Comment β€” supports self-referential nesting

content   String
parentId  String?  // null = top-level comment
replies   Comment[]

Purchase β€” unique per [userId, ideaId]

stripePaymentId  String?

πŸ“‘ API Reference

Base URL: https://ecospark-api.vercel.app/api/v1 Local: http://localhost:5000/api/v1

All responses follow this envelope:

{
  "success": true,
  "statusCode": 200,
  "message": "Ideas retrieved successfully",
  "data": { ... }
}

πŸ” Authentication

Handled by Better Auth. Session cookie is set on /api/v1/auth/*.

Method Endpoint Description
POST /auth/sign-up/email Register with name, email, password
POST /auth/sign-in/email Login with email, password
POST /auth/sign-out Destroy session
GET /auth/get-session Get current session + user

🏷 Categories

Method Endpoint Auth Description
GET /categories β€” List all categories
POST /categories Admin Create a category { name }
DELETE /categories/:id Admin Delete a category

πŸ’‘ Ideas

Method Endpoint Auth Description
GET /ideas β€” List approved ideas
GET /ideas/:id Optional Get one idea (handles paid gate)
GET /ideas/my Member Get current user's ideas
GET /ideas/admin/all Admin Get all ideas (any status)
POST /ideas Member Create idea (multipart/form-data)
PATCH /ideas/:id Member Update idea (multipart/form-data)
DELETE /ideas/:id Member Delete idea (DRAFT only)
PATCH /ideas/:id/submit Member Submit DRAFT β†’ UNDER_REVIEW
PATCH /ideas/:id/approve Admin Approve idea β†’ APPROVED
PATCH /ideas/:id/reject Admin Reject with feedback { feedback }

GET /ideas β€” Query Parameters

Param Type Description
page number Page number (default: 1)
limit number Items per page (default: 10)
category string Filter by category ID
search string Search title / description
sort string newest | popular | price-asc | price-desc
isPaid boolean Filter free / paid ideas

GET /ideas/:id β€” Response Shapes

// Unauthenticated + paid idea
{ "requiresAuth": true }

// Authenticated, not yet purchased
{ "requiresPurchase": true, "price": 9.99, "teaser": { "title": "...", ... } }

// Authenticated + purchased (or free idea, or admin)
{ "id": "...", "title": "...", /* full idea */ }

Create / Update β€” multipart/form-data Fields

Field Type Required
title string βœ…
problemStatement string βœ…
proposedSolution string βœ…
description string βœ…
categoryId string βœ…
isPaid boolean βœ…
price number Only if isPaid: true
images File[] Up to 4 files, 5 MB each

πŸ‘ Votes

Method Endpoint Auth Description
GET /votes/:ideaId β€” Get all votes for an idea
POST /votes/:ideaId Member Cast / change / remove vote

POST /votes/:ideaId body: { "type": "UPVOTE" | "DOWNVOTE" }

  • Voting the same type again removes the vote (toggle)
  • Voting a different type updates the existing vote
  • Returns 201 Created for new vote, 200 OK for removal

πŸ’¬ Comments

Method Endpoint Auth Description
GET /comments/:ideaId β€” Get nested comment tree
POST /comments/:ideaId Member Post a comment or reply
DELETE /comments/:id Member/Admin Delete a comment

POST /comments/:ideaId body:

{ "content": "Great idea!", "parentId": "optional-parent-id" }

πŸ’³ Payments

Method Endpoint Auth Description
POST /payments/checkout Member Create Stripe Checkout Session
GET /payments/status/:ideaId Member Check purchase status
POST /payments/webhook β€” (Stripe) Handle checkout.session.completed

POST /payments/checkout body: { "ideaId": "..." } Returns: { "url": "https://checkout.stripe.com/..." }

After successful payment, Stripe redirects to: {CLIENT_URL}/ideas/{ideaId}?purchase=success


πŸ“€ Upload

Method Endpoint Auth Description
POST /upload/image Member Upload single image to Cloudinary

Request: multipart/form-data with field name file Response: { "url": "https://res.cloudinary.com/..." }

Constraints: images only, max 5 MB per file.


πŸ‘‘ Admin

Method Endpoint Auth Description
GET /admin/stats Admin Platform statistics
GET /admin/users Admin List all users
PATCH /admin/users/:id/toggle-active Admin Enable / disable user account
PATCH /admin/users/:id/role Admin Change user role
DELETE /admin/ideas/:id Admin Force-delete any idea
POST /admin/newsletter/subscribe β€” Subscribe email to newsletter

GET /admin/stats response:

{
  "totalUsers": 157,
  "totalIdeas": 342,
  "pendingIdeas": 12,
  "approvedIdeas": 289
}

GET /admin/users query params: page, limit, search


πŸš€ Getting Started

Prerequisites

  • Node.js 20 or later
  • PostgreSQL database (local or hosted β€” e.g. Supabase, Neon)
  • Stripe account (for payments)
  • Cloudinary account (for image uploads)

1. Clone the repository

git clone https://github.com/your-username/ecospart-api.git
cd ecospart-api

2. Install dependencies

npm install

3. Configure environment variables

cp .env.example .env

Fill in all values β€” see Environment Variables below.

4. Set up the database

# Push schema to your database (dev β€” no migration history)
npm run db:push

# Or use migrations (recommended for production)
npm run db:migrate

# Optional: seed with demo data
npm run db:seed

5. Start the development server

npm run dev

API available at http://localhost:5000.


πŸ”‘ Environment Variables

Create a .env file in the project root:

# ─── Database ───────────────────────────────────────────────────────────────
DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE?sslmode=require"

# ─── Better Auth ────────────────────────────────────────────────────────────
# Generate with: openssl rand -base64 32
BETTER_AUTH_SECRET="your_random_secret_min_32_chars"

# No trailing slash β€” must match the deployed API URL
BETTER_AUTH_URL="http://localhost:5000"

# Your frontend URL β€” used for CORS and Stripe redirect URLs
CLIENT_URL="http://localhost:3000"

# Optional: extra trusted origins (comma or space separated)
# TRUSTED_ORIGINS="https://preview.vercel.app https://staging.vercel.app"

# ─── Stripe ─────────────────────────────────────────────────────────────────
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."

# ─── Cloudinary ─────────────────────────────────────────────────────────────
CLOUDINARY_CLOUD_NAME="your_cloud_name"
CLOUDINARY_API_KEY="your_api_key"
CLOUDINARY_API_SECRET="your_api_secret"

# ─── Server ─────────────────────────────────────────────────────────────────
PORT=5000
NODE_ENV="development"

Startup validation: validateEnv.ts runs at boot and throws if any required variable is missing, preventing silent failures.


πŸ“œ Available Scripts

npm run dev          # Start dev server with hot-reload (tsx watch)
npm run build        # Compile TypeScript β†’ dist/
npm run start        # Run compiled production server
npm run db:push      # Push Prisma schema (no migrations)
npm run db:migrate   # Run Prisma migrations (dev)
npm run db:seed      # Seed database with demo data
npm run db:studio    # Open Prisma Studio in browser

πŸ” Authentication

This API uses Better Auth with PostgreSQL as the session store.

How it Works

  1. Sign-up / Sign-in β†’ Better Auth creates a session row in PostgreSQL and sets an HttpOnly session cookie
  2. Protected routes β†’ protect middleware calls auth.api.getSession() to validate the cookie
  3. Role check β†’ adminOnly middleware verifies req.user.role === "ADMIN"

Middleware

// Protect any route
router.get("/my-route", protect, adminOnly, handler);

// req.user is available in all protected handlers:
interface RequestUser {
  id: string;
  role: "MEMBER" | "ADMIN";
}

Cookie Names

The session cookie is named:

  • better-auth.session_token (HTTP)
  • __Secure-better-auth.session_token (HTTPS / production)

πŸ’³ Payment Flow

Member clicks "Unlock" on a paid idea
    β”‚
    β–Ό
POST /payments/checkout { ideaId }
    β”‚
    β–Ό
Server creates Stripe Checkout Session
    β”‚
    β–Ό
Response: { url: "https://checkout.stripe.com/..." }
    β”‚
    β–Ό
Client redirects user to Stripe-hosted checkout page
    β”‚
    β–Ό
User completes payment on Stripe
    β”‚
    β–Ό
Stripe calls POST /payments/webhook (checkout.session.completed)
    β”‚
    β–Ό
Server verifies webhook signature + creates Purchase record
    β”‚
    β–Ό
Stripe redirects user to {CLIENT_URL}/ideas/{ideaId}?purchase=success
    β”‚
    β–Ό
Frontend shows success toast + unlocks full idea content

Setting Up Stripe Webhooks (local dev)

# Install Stripe CLI
stripe listen --forward-to localhost:5000/api/v1/payments/webhook

# Copy the webhook signing secret it outputs into your .env:
# STRIPE_WEBHOOK_SECRET=whsec_...

πŸ–Ό Image Uploads

Images are uploaded in two steps:

  1. Multer receives the file into memory (max 5 MB, image/* only)
  2. Cloudinary receives the buffer via a server-side stream upload
// POST /upload/image
// multipart/form-data, field: "file"
// Response: { "url": "https://res.cloudinary.com/..." }

The returned Cloudinary URL is stored in the images: String[] array on the Idea model.


🌍 Deployment

The API is deployed on Vercel as a serverless Node.js function.

Deploy with Vercel CLI

npm run build        # Compile TypeScript first
vercel --prod

vercel.json

{
  "version": 2,
  "builds": [{ "src": "dist/src/server.js", "use": "@vercel/node" }],
  "routes": [{ "src": "/(.*)", "dest": "dist/src/server.js" }]
}

Production Checklist

  • All environment variables set in Vercel dashboard
  • DATABASE_URL points to a production PostgreSQL instance (e.g. Neon, Supabase)
  • CLIENT_URL set to the production frontend URL
  • BETTER_AUTH_URL set to the production API URL (no trailing slash)
  • Stripe webhook registered for the production URL: https://your-api.vercel.app/api/v1/payments/webhook
  • NODE_ENV=production set

πŸ’‘ Idea Status Flow

          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚  DRAFT   β”‚  ◄── Member creates idea
          β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
               β”‚  Member submits for review
               β–Ό
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚  UNDER_REVIEW β”‚  ◄── Awaiting admin decision
       β””β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜
           β”‚       β”‚
  Admin    β”‚       β”‚  Admin
  approves β”‚       β”‚  rejects (with feedback)
           β–Ό       β–Ό
      β”Œβ”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
      β”‚APPROVβ”‚  β”‚ REJECTED β”‚  ◄── Member can edit + resubmit
      β”‚  ED  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
      β””β”€β”€β”€β”€β”€β”€β”˜
         β”‚
  Visible on
  public ideas page

Made with πŸ’š for a greener world Β· EcoSpark Hub API

Releases

Packages

Contributors

Languages