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: <
</h3></div></main></body></html>

Nessun commento:
Posta un commento