sabato 11 ottobre 2025

Corso di JavaScript & Programmazione Web: 11 – Codice del sito di e-commerce

11 – Codice del sito di e-commerce

merchant-backend/
├─ package.json
├─ .env.example
└─ src/
   ├─ index.js
   ├─ config/
   │  └─ db.js
   ├─ models/
   │  ├─ User.js
   │  └─ Product.js
   ├─ controllers/
   │  ├─ authController.js
   │  └─ productController.js
   └─ routes/
      ├─ auth.js
      └─ products.js

{
  "name": "merchant-backend",
  "version": "1.0.0",
  "description": "Backend per sito commerciante con e-commerce minimale (Node/Express/MongoDB/JWT)",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js",
    "lint": "eslint . --ext .js"
  },
  "author": "Tuonome",
  "license": "MIT",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "cors": "^2.8.5",
    "dotenv": "^16.1.4",
    "express": "^4.18.2",
    "express-async-errors": "^3.1.1",
    "jsonwebtoken": "^9.0.0",
    "mongoose": "^7.5.0",
    "morgan": "^1.10.0",
    "multer": "^1.4.5-lts.1"
  },
  "devDependencies": {
    "nodemon": "^2.0.22",
    "eslint": "^8.50.0"
  }
}

# Rename to .env and fill values
PORT=4000
MONGO_URI=mongodb://localhost:27017/merchantdb
JWT_SECRET=changeme_super_secret_jwt_key
JWT_EXPIRES_IN=7d
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=ChangeMe123!
UPLOADS_DIR=uploads


/**
 * Entry point for the backend server
 * - express app
 * - basic middlewares (cors, json, morgan)
 * - routes: /api/auth, /api/products
 * - global error handler
 */

require('dotenv').config();
require('express-async-errors'); // to allow throwing errors in async routes
const express = require('express');
const morgan = require('morgan');
const cors = require('cors');
const path = require('path');

const connectDB = require('./config/db');
const authRoutes = require('./routes/auth');
const productRoutes = require('./routes/products');

const app = express();

// Connect DB
connectDB();

// Middlewares
app.use(cors());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(morgan('dev'));

// Serve uploaded images (if using local storage)
const uploadsDir = process.env.UPLOADS_DIR || 'uploads';
app.use(`/static/${uploadsDir}`, express.static(path.join(__dirname, '..', uploadsDir)));

// API Routes
app.use('/api/auth', authRoutes);
app.use('/api/products', productRoutes);

// Health check
app.get('/api/health', (req, res) => res.json({ status: 'ok', time: new Date().toISOString() }));

// Global error handler
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
  console.error(err);
  const status = err.status || 500;
  const message = err.message || 'Internal Server Error';
  res.status(status).json({ error: message, stack: process.env.NODE_ENV === 'production' ? undefined : err.stack });
});

// Start server
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
  // create admin user on startup? optional, can be implemented
  console.log(`Server running on port ${PORT}`);
});


/**
 * MongoDB connection via mongoose
 */

const mongoose = require('mongoose');

const connectDB = async () => {
  const uri = process.env.MONGO_URI;
  if (!uri) {
    console.error('MONGO_URI not set in environment');
    process.exit(1);
  }
  try {
    await mongoose.connect(uri, {
      // options (mongoose 7+ has sensible defaults)
    });
    console.log('MongoDB connected');
  } catch (err) {
    console.error('MongoDB connection error:', err);
    process.exit(1);
  }
};

module.exports = connectDB;

/**
 * User model (basic for authentication)
 * Roles: user, admin
 */

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const UserSchema = new mongoose.Schema({
  name: { type: String, required: true, trim: true, maxlength: 100 },
  email: { type: String, required: true, unique: true, lowercase: true, trim: true },
  password: { type: String, required: true },
  role: { type: String, enum: ['user', 'admin'], default: 'user' },
  createdAt: { type: Date, default: Date.now }
});

// Hash password before save
UserSchema.pre('save', async function (next) {
  if (!this.isModified('password')) return next();
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

// Instance method to compare password
UserSchema.methods.comparePassword = async function (candidate) {
  return bcrypt.compare(candidate, this.password);
};

module.exports = mongoose.model('User', UserSchema);



/**
 * Product model for e-commerce items
 * Images: store relative path or URL
 */

const mongoose = require('mongoose');

const ProductSchema = new mongoose.Schema({
  title: { type: String, required: true, trim: true, maxlength: 200 },
  slug: { type: String, required: true, unique: true }, // url-friendly
  description: { type: String, default: '' },
  price: { type: Number, required: true, min: 0 },
  currency: { type: String, default: 'EUR' },
  stock: { type: Number, default: 0 },
  category: { type: String, default: 'general' },
  images: [{ type: String }], // store URLs or local paths (/static/uploads/...)
  featured: { type: Boolean, default: false },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

ProductSchema.pre('save', function (next) {
  this.updatedAt = Date.now();
  next();
});

module.exports = mongoose.model('Product', ProductSchema);


/**
 * Auth controller: register, login, profile
 */

const jwt = require('jsonwebtoken');
const User = require('../models/User');

const JWT_SECRET = process.env.JWT_SECRET || 'changeme';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';

const signToken = (user) => {
  return jwt.sign({ id: user._id, role: user.role, email: user.email }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
};

exports.register = async (req, res) => {
  const { name, email, password } = req.body;
  if (!name || !email || !password) return res.status(400).json({ error: 'name, email, password required' });

  const existing = await User.findOne({ email });
  if (existing) return res.status(409).json({ error: 'Email already registered' });

  const user = new User({ name, email, password });
  await user.save();

  const token = signToken(user);
  res.status(201).json({ token, user: { id: user._id, name: user.name, email: user.email, role: user.role } });
};

exports.login = async (req, res) => {
  const { email, password } = req.body;
  if (!email || !password) return res.status(400).json({ error: 'email and password required' });

  const user = await User.findOne({ email });
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  const match = await user.comparePassword(password);
  if (!match) return res.status(401).json({ error: 'Invalid credentials' });

  const token = signToken(user);
  res.json({ token, user: { id: user._id, name: user.name, email: user.email, role: user.role } });
};

exports.me = async (req, res) => {
  // expects req.user set by auth middleware
  const user = await User.findById(req.user.id).select('-password');
  res.json({ user });
};

/**
 * Product controller: CRUD operations
 * - list with pagination / filters
 * - get by slug / id
 * - create (admin)
 * - update (admin)
 * - delete (admin)
 *
 * Note: For image upload we will accept a multipart/form-data route in the route file using multer.
 */

const Product = require('../models/Product');
const slugify = (s) => s.toLowerCase().trim().replace(/\s+/g, '-').replace(/[^\w\-]+/g, '');

exports.list = async (req, res) => {
  const page = Math.max(1, parseInt(req.query.page) || 1);
  const limit = Math.min(100, parseInt(req.query.limit) || 12);
  const skip = (page - 1) * limit;

  const filter = {};
  if (req.query.category) filter.category = req.query.category;
  if (req.query.q) filter.$or = [
    { title: { $regex: req.query.q, $options: 'i' } },
    { description: { $regex: req.query.q, $options: 'i' } }
  ];

  const [items, total] = await Promise.all([
    Product.find(filter).sort({ featured: -1, createdAt: -1 }).skip(skip).limit(limit),
    Product.countDocuments(filter)
  ]);

  res.json({ items, meta: { page, limit, total, pages: Math.ceil(total / limit) } });
};

exports.getBySlug = async (req, res) => {
  const product = await Product.findOne({ slug: req.params.slug });
  if (!product) return res.status(404).json({ error: 'Product not found' });
  res.json({ product });
};

exports.create = async (req, res) => {
  const { title, description, price, currency = 'EUR', stock = 0, category = 'general', images = [] } = req.body;
  if (!title || price == null) return res.status(400).json({ error: 'title and price required' });

  const slug = slugify(title);
  const existing = await Product.findOne({ slug });
  let finalSlug = slug;
  if (existing) finalSlug = `${slug}-${Date.now().toString().slice(-4)}`;

  const p = new Product({
    title, slug: finalSlug, description, price: parseFloat(price), currency, stock: parseInt(stock, 10), category, images
  });
  await p.save();
  res.status(201).json({ product: p });
};

exports.update = async (req, res) => {
  const updates = req.body;
  if (updates.title) updates.slug = slugify(updates.title);
  const product = await Product.findByIdAndUpdate(req.params.id, updates, { new: true });
  if (!product) return res.status(404).json({ error: 'Not found' });
  res.json({ product });
};

exports.remove = async (req, res) => {
  const product = await Product.findByIdAndDelete(req.params.id);
  if (!product) return res.status(404).json({ error: 'Not found' });
  res.json({ success: true });
};


/**
 * Auth routes
 */

const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const { authMiddleware } = require('../utils/middleware'); // file middleware che aggiungeremo dopo

// Public
router.post('/register', authController.register);
router.post('/login', authController.login);

// Protected
router.get('/me', authMiddleware, authController.me);

module.exports = router;

/**
 * Product routes
 * - GET /      -> list, with query params
 * - GET /:slug -> get by slug
 * - POST /     -> create (admin)
 * - PUT /:id   -> update (admin)
 * - DELETE /:id -> delete (admin)
 *
 * Image uploads: optionally POST /:id/images (handled separately)
 */

const express = require('express');
const router = express.Router();
const productController = require('../controllers/productController');
const { authMiddleware, adminOnly } = require('../utils/middleware');

// Public
router.get('/', productController.list);
router.get('/slug/:slug', productController.getBySlug);

// Protected admin
router.post('/', authMiddleware, adminOnly, productController.create);
router.put('/:id', authMiddleware, adminOnly, productController.update);
router.delete('/:id', authMiddleware, adminOnly, productController.remove);

module.exports = router;

/**
 * Utility middleware: authMiddleware (JWT), adminOnly
 */

const jwt = require('jsonwebtoken');
const User = require('../models/User');

const JWT_SECRET = process.env.JWT_SECRET || 'changeme';

exports.authMiddleware = async (req, res, next) => {
  const auth = req.headers.authorization;
  if (!auth || !auth.startsWith('Bearer ')) return res.status(401).json({ error: 'Missing token' });
  const token = auth.split(' ')[1];
  try {
    const payload = jwt.verify(token, JWT_SECRET);
    // attach user minimal info
    req.user = { id: payload.id, role: payload.role, email: payload.email };
    // optionally fetch full user if needed
    // req.userFull = await User.findById(payload.id).select('-password');
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid token' });
  }
};

exports.adminOnly = (req, res, next) => {
  if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
  if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' });
  next();
};


"dependencies": {
  "...": "...",
  "stripe": "^11.0.0",
  "express-validator": "^7.0.1",
  "nodemailer": "^6.9.4"
}


(esegui npm install stripe express-validator nodemailer)

.env.example — aggiornamento

Aggiungi questi parametri:

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

# Email (opzionale)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=your@email
SMTP_PASS=secret

# Admin seed
ADMIN_NAME=Admin
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=ChangeMe123!

1) Upload immagini — src/utils/upload.js

Crea src/utils/upload.js per gestire multer e storage locale (cartella uploads/).

const path = require('path');
const multer = require('multer');
const fs = require('fs');

const uploadsDir = process.env.UPLOADS_DIR || path.join(__dirname, '..', '..', 'uploads');

// assicurati che la cartella esista
if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });

const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, uploadsDir);
  },
  filename: function (req, file, cb) {
    const ext = path.extname(file.originalname);
    const name = path.basename(file.originalname, ext).replace(/\s+/g, '-').toLowerCase();
    const unique = Date.now() + '-' + Math.round(Math.random() * 1e9);
    cb(null, `${name}-${unique}${ext}`);
  }
});

const fileFilter = (req, file, cb) => {
  // accetta solo immagini
  if (/image\/(jpeg|png|gif|webp)/.test(file.mimetype)) cb(null, true);
  else cb(new Error('Only image files are allowed!'), false);
};

const upload = multer({ storage, fileFilter, limits: { fileSize: 5 * 1024 * 1024 } }); // 5MB

module.exports = { upload, uploadsDir };

2) Route per upload immagini — src/routes/uploads.js
const express = require('express');
const router = express.Router();
const { upload } = require('../utils/upload');
const { authMiddleware, adminOnly } = require('../utils/middleware');
const path = require('path');

router.post('/single', authMiddleware, adminOnly, upload.single('image'), (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
  // restituisci percorso pubblico relativo
  const publicPath = `/static/${process.env.UPLOADS_DIR || 'uploads'}/${req.file.filename}`;
  res.status(201).json({ url: publicPath, filename: req.file.filename });
});

router.post('/multiple', authMiddleware, adminOnly, upload.array('images', 8), (req, res) => {
  if (!req.files || req.files.length === 0) return res.status(400).json({ error: 'No files uploaded' });
  const urls = req.files.map(f => `/static/${process.env.UPLOADS_DIR || 'uploads'}/${f.filename}`);
  res.status(201).json({ urls });
});

module.exports = router;


Aggiungi la route in src/index.js:

const uploadRoutes = require('./routes/uploads');
app.use('/api/uploads', uploadRoutes);

3) Modello Cart e Order — src/models/Cart.js e src/models/Order.js
src/models/Cart.js

(Semplice rappresentazione lato backend — cart può essere memorizzata in DB oppure solo client-side; qui implementiamo una versione server-side associata a user)

const mongoose = require('mongoose');

const CartItemSchema = new mongoose.Schema({
  product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true },
  title: { type: String },
  price: { type: Number, required: true },
  qty: { type: Number, default: 1 },
  image: { type: String }
}, { _id: false });

const CartSchema = new mongoose.Schema({
  user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, unique: true },
  items: [CartItemSchema],
  updatedAt: { type: Date, default: Date.now }
});

CartSchema.pre('save', function (next) {
  this.updatedAt = Date.now();
  next();
});

module.exports = mongoose.model('Cart', CartSchema);

src/models/Order.js
const mongoose = require('mongoose');

const OrderItemSchema = new mongoose.Schema({
  product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' },
  title: String,
  price: Number,
  qty: Number,
  image: String
}, { _id: false });

const OrderSchema = new mongoose.Schema({
  user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  items: [OrderItemSchema],
  shipping: {
    name: String,
    address: String,
    city: String,
    postalCode: String,
    country: String
  },
  totalAmount: { type: Number, required: true },
  currency: { type: String, default: 'EUR' },
  payment: {
    provider: { type: String, default: 'stripe' },
    status: { type: String, enum: ['pending', 'paid', 'failed', 'refunded'], default: 'pending' },
    stripePaymentIntent: String
  },
  status: { type: String, enum: ['created','processing','shipped','completed','cancelled'], default: 'created' },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

OrderSchema.pre('save', function (next) {
  this.updatedAt = Date.now();
  next();
});

module.exports = mongoose.model('Order', OrderSchema);

4) Controller Carrello / Ordini — src/controllers/cartController.js e src/controllers/orderController.js
src/controllers/cartController.js
const Cart = require('../models/Cart');
const Product = require('../models/Product');

/**
 * API:
 * GET /api/cart        -> get cart for user
 * POST /api/cart/add   -> add product { productId, qty }
 * POST /api/cart/update -> update item qty { productId, qty }
 * POST /api/cart/remove -> remove item { productId }
 * POST /api/cart/clear -> clear cart
 */

exports.getCart = async (req, res) => {
  let cart = await Cart.findOne({ user: req.user.id }).populate('items.product');
  if (!cart) {
    cart = new Cart({ user: req.user.id, items: [] });
    await cart.save();
  }
  res.json({ cart });
};

exports.addToCart = async (req, res) => {
  const { productId, qty = 1 } = req.body;
  if (!productId) return res.status(400).json({ error: 'productId required' });
  const product = await Product.findById(productId);
  if (!product) return res.status(404).json({ error: 'Product not found' });

  let cart = await Cart.findOne({ user: req.user.id });
  if (!cart) cart = new Cart({ user: req.user.id, items: [] });

  const idx = cart.items.findIndex(i => i.product.toString() === productId);
  if (idx >= 0) {
    cart.items[idx].qty = Math.max(1, cart.items[idx].qty + Number(qty));
  } else {
    cart.items.push({
      product: product._id,
      title: product.title,
      price: product.price,
      qty: Number(qty),
      image: product.images && product.images[0] ? product.images[0] : ''
    });
  }
  await cart.save();
  res.json({ cart });
};

exports.updateItem = async (req, res) => {
  const { productId, qty } = req.body;
  if (!productId) return res.status(400).json({ error: 'productId required' });
  let cart = await Cart.findOne({ user: req.user.id });
  if (!cart) return res.status(404).json({ error: 'Cart not found' });

  const idx = cart.items.findIndex(i => i.product.toString() === productId);
  if (idx === -1) return res.status(404).json({ error: 'Item not in cart' });
  cart.items[idx].qty = Math.max(0, Number(qty));
  if (cart.items[idx].qty === 0) cart.items.splice(idx, 1);
  await cart.save();
  res.json({ cart });
};

exports.removeItem = async (req, res) => {
  const { productId } = req.body;
  if (!productId) return res.status(400).json({ error: 'productId required' });
  let cart = await Cart.findOne({ user: req.user.id });
  if (!cart) return res.status(404).json({ error: 'Cart not found' });

  cart.items = cart.items.filter(i => i.product.toString() !== productId);
  await cart.save();
  res.json({ cart });
};

exports.clearCart = async (req, res) => {
  await Cart.findOneAndUpdate({ user: req.user.id }, { $set: { items: [] } }, { upsert: true });
  res.json({ success: true });
};

src/controllers/orderController.js
const Order = require('../models/Order');
const Cart = require('../models/Cart');
const Product = require('../models/Product');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

/**
 * API:
 * POST /api/orders/create -> crea ordine a partire dal carrello (può anche avviare sessione Stripe)
 * GET  /api/orders/:id    -> dettagli ordine (user o admin)
 * GET  /api/orders        -> lista ordini (admin: tutti, user: propri)
 *
 * POST /api/orders/checkout-session -> crea sessione Stripe dalla cartella/ordine
 */

exports.createOrderFromCart = async (req, res) => {
  const cart = await Cart.findOne({ user: req.user.id }).populate('items.product');
  if (!cart || cart.items.length === 0) return res.status(400).json({ error: 'Cart empty' });

  // calcola totale
  const total = cart.items.reduce((s, it) => s + (it.price * it.qty), 0);

  const order = new Order({
    user: req.user.id,
    items: cart.items.map(i => ({
      product: i.product._id,
      title: i.title,
      price: i.price,
      qty: i.qty,
      image: i.image
    })),
    totalAmount: total,
    currency: 'EUR',
    payment: { provider: 'stripe', status: 'pending' },
    status: 'created'
  });

  await order.save();
  // opzionale: svuota carrello (o lascia al checkout)
  // await Cart.findOneAndUpdate({ user: req.user.id }, { items: [] });

  res.status(201).json({ order });
};

exports.getOrder = async (req, res) => {
  const order = await Order.findById(req.params.id).populate('user', 'name email');
  if (!order) return res.status(404).json({ error: 'Order not found' });
  // autorizzazione: user può vedere solo i suoi ordini
  if (req.user.role !== 'admin' && order.user._id.toString() !== req.user.id) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  res.json({ order });
};

exports.listOrders = async (req, res) => {
  const filter = {};
  if (req.user.role !== 'admin') filter.user = req.user.id;
  const items = await Order.find(filter).sort({ createdAt: -1 }).limit(200);
  res.json({ items });
};

/**
 * Crea sessione di checkout Stripe con items dal carrello o da ordine
 * POST /api/orders/checkout-session
 * body: { orderId? , successUrl, cancelUrl }
 */
exports.createCheckoutSession = async (req, res) => {
  const { orderId, successUrl, cancelUrl } = req.body;
  let order;
  if (orderId) {
    order = await Order.findById(orderId);
    if (!order) return res.status(404).json({ error: 'Order not found' });
  } else {
    // crea ordine provvisorio dal carrello
    const cart = await Cart.findOne({ user: req.user.id }).populate('items.product');
    if (!cart || cart.items.length === 0) return res.status(400).json({ error: 'Cart empty' });
    const total = cart.items.reduce((s, it) => s + (it.price * it.qty), 0);
    order = new Order({
      user: req.user.id,
      items: cart.items.map(i => ({
        product: i.product._id,
        title: i.title,
        price: i.price,
        qty: i.qty,
        image: i.image
      })),
      totalAmount: total,
      currency: 'EUR',
      payment: { provider: 'stripe', status: 'pending' },
      status: 'created'
    });
    await order.save();
  }

  // trasforma items in line_items per stripe
  const line_items = order.items.map(it => ({
    price_data: {
      currency: order.currency || 'EUR',
      product_data: { name: it.title, images: it.image ? [ `${req.protocol}://${req.get('host')}${it.image}` ] : [] },
      unit_amount: Math.round(it.price * 100)
    },
    quantity: it.qty
  }));

  // crea sessione
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    mode: 'payment',
    line_items,
    success_url: successUrl || `${req.protocol}://${req.get('host')}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: cancelUrl || `${req.protocol}://${req.get('host')}/checkout/cancel`,
    metadata: { orderId: order._id.toString(), userId: req.user.id }
  });

  // salva intent id / session id sull'ordine (opzionale)
  order.payment.stripePaymentIntent = session.payment_intent || session.id;
  await order.save();

  res.json({ sessionId: session.id, url: session.url });
};

5) Route ordini e carrello — src/routes/cart.js e src/routes/orders.js
src/routes/cart.js
const express = require('express');
const router = express.Router();
const cartController = require('../controllers/cartController');
const { authMiddleware } = require('../utils/middleware');

router.get('/', authMiddleware, cartController.getCart);
router.post('/add', authMiddleware, cartController.addToCart);
router.post('/update', authMiddleware, cartController.updateItem);
router.post('/remove', authMiddleware, cartController.removeItem);
router.post('/clear', authMiddleware, cartController.clearCart);

module.exports = router;


Aggiungi in src/index.js:

const cartRoutes = require('./routes/cart');
app.use('/api/cart', cartRoutes);

src/routes/orders.js
const express = require('express');
const router = express.Router();
const orderController = require('../controllers/orderController');
const { authMiddleware, adminOnly } = require('../utils/middleware');

router.post('/create', authMiddleware, orderController.createOrderFromCart);
router.post('/checkout-session', authMiddleware, orderController.createCheckoutSession);
router.get('/:id', authMiddleware, orderController.getOrder);
router.get('/', authMiddleware, orderController.listOrders);

module.exports = router;


Aggiungi in src/index.js:

const orderRoutes = require('./routes/orders');
app.use('/api/orders', orderRoutes);

6) Stripe Webhook — src/routes/webhooks.js e handler

Per ricevere eventi Stripe (pagamento riuscito ecc.) è meglio esporre endpoint separato e verificare firma.

src/routes/webhooks.js
const express = require('express');
const router = express.Router();
const Order = require('../models/Order');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;

router.post('/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    console.error('Webhook signature verification failed.', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // handle the event
  switch (event.type) {
    case 'checkout.session.completed':
      {
        const session = event.data.object;
        // metadata contains orderId if we set it
        const orderId = session.metadata && session.metadata.orderId;
        if (orderId) {
          await Order.findByIdAndUpdate(orderId, {
            'payment.status': 'paid',
            'payment.stripePaymentIntent': session.payment_intent || session.id,
            status: 'processing'
          });
        }
      }
      break;
    case 'payment_intent.payment_failed':
      {
        const intent = event.data.object;
        // qui si può aggiornare l'ordine attraverso metadata se presente
        console.warn('Payment failed', intent);
      }
      break;
    default:
      // console.log(`Unhandled event type ${event.type}`);
  }

  res.json({ received: true });
});

module.exports = router;


Aggiungi in src/index.js prima di app.use(express.json()) o gestisci solo per la route webhook come fatto:

const webhookRoutes = require('./routes/webhooks');
app.use('/api/webhooks', webhookRoutes);


Importante: express.raw() è necessario per la verifica della firma Stripe. Quando testi in locale con stripe-cli imposta il webhook secret.

7) Seed admin — src/utils/seedAdmin.js

Script che crea admin all'avvio se non esiste (opzionale).

const User = require('../models/User');

module.exports = async function seedAdmin() {
  try {
    const adminEmail = process.env.ADMIN_EMAIL;
    const adminPassword = process.env.ADMIN_PASSWORD;
    const adminName = process.env.ADMIN_NAME || 'Admin';

    if (!adminEmail || !adminPassword) {
      console.warn('ADMIN_EMAIL or ADMIN_PASSWORD not set; skipping admin seed');
      return;
    }

    const existing = await User.findOne({ email: adminEmail });
    if (existing) {
      console.log('Admin already exists');
      return;
    }

    const user = new User({
      name: adminName,
      email: adminEmail,
      password: adminPassword,
      role: 'admin'
    });
    await user.save();
    console.log('Admin user created:', adminEmail);
  } catch (err) {
    console.error('Error seeding admin:', err);
  }
};


Richiamalo in src/index.js subito dopo connectDB():

const seedAdmin = require('./utils/seedAdmin');
...
connectDB().then(() => seedAdmin());


(se hai connectDB sincrono, adegua la chiamata)


<html lang="it">
<head>
  <meta charset="UTF-8"></meta>
  <meta content="width=device-width, initial-scale=1.0" name="viewport"></meta>
  <title>Carrello - Negozio Online</title>
  <link href="style.css" rel="stylesheet"></link>
</head>
<body>
  <header>
    <div class="logo">🛍️ Il Mio Negozio</div>
    <nav>
      <ul>
        <li><a href="index.html">Home</a></li>
        <li><a href="shop.html">Prodotti</a></li>
        <li><a class="active" href="cart.html">Carrello 🛒</a></li>
        <li><a href="checkout.html">Checkout</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <h1>🛒 Il tuo Carrello</h1>
    <table id="cart-table">
      <thead>
        <tr>
          <th>Prodotto</th>
          <th>Prezzo</th>
          <th>Quantità</th>
          <th>Totale</th>
          <th>Azioni</th>
        </tr>
      </thead>
      <tbody id="cart-items">
        <!-- Qui vengono inseriti i prodotti dal JS -->
      </tbody>
    </table>

    <div class="cart-summary">
      <h3>Totale Carrello: &lt;

</h3></div></main></body></html>

Nessun commento:

Posta un commento

Corso Fondamenti di Informatica e Reti: 6 Reti di computer e Internet

Reti di computer e Internet Introduzione Prova a pensare alla vita quotidiana senza reti informatiche: niente messaggi WhatsApp, niente m...