Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
997a688
feat(models): add MVP mongoose schemas for auth, orders, payments, pr…
akrs-code Jun 12, 2026
73c658f
feat(routes): add MVP route stubs for all modules (admin, auth, order…
akrs-code Jun 12, 2026
310fcc9
feat(controllers): add MVP controller stubs for all modules (admin, a…
akrs-code Jun 12, 2026
1e0b6ed
feat(middleware): add auth middleware stubs (verifyToken, requireRole…
akrs-code Jun 12, 2026
a8d382a
feat: added newattributes
akrs-code Jun 13, 2026
045bed1
feat: integrated add Product API
Wahabyas Jun 13, 2026
318e46e
feat: integrated register user returning the whole user payload, stat…
Wahabyas Jun 13, 2026
c6b6ce4
feat: integrated a login that returns token and object
Wahabyas Jun 13, 2026
5958728
feat: added a Third party Storage API (Cloudinary)
Wahabyas Jun 14, 2026
537b0cf
chore: Changed the way how uploader in mutler and cloudinary is being…
Wahabyas Jun 14, 2026
5708af9
chore: updated/ improved the register user allowing to send an image
Wahabyas Jun 14, 2026
f73a570
feat: Session restore task getMe
Wahabyas Jun 14, 2026
15ac26e
feat: get user profile
Wahabyas Jun 17, 2026
237f11b
feat: user profile update
Wahabyas Jun 17, 2026
06ad4c0
feat: made a getnearby shop and shop by id
Wahabyas Jun 17, 2026
6b09296
feat:seller: create shop with coords → pin appears on map
Wahabyas Jun 17, 2026
adbb61c
chore: add an upload cover and logo to the create shop
Wahabyas Jun 17, 2026
cbc6673
feat: getreview
Wahabyas Jun 17, 2026
15eea3f
feat: added a Add_review to shop
Wahabyas Jun 17, 2026
8c35ef0
feat: added a edit product
Wahabyas Jun 17, 2026
8de73a9
feat: Remove product
Wahabyas Jun 17, 2026
183919d
feat: established the payment gateway paymong
Wahabyas Jun 17, 2026
a9542b9
feat: place order
Wahabyas Jun 17, 2026
73bc88c
feat: added a getMyOrders
Wahabyas Jun 18, 2026
08cbc90
feat : incoming orders for their shop
Wahabyas Jun 18, 2026
d8e1c76
feat: seller: pending → confirmed
Wahabyas Jun 18, 2026
6eb220b
feat: confirm to shipped
Wahabyas Jun 18, 2026
885b4e2
feat: shipped to delivered
Wahabyas Jun 18, 2026
69614d2
feat: cancel order
Wahabyas Jun 18, 2026
9c0124a
chore: updated the create checkout and paymongo sevices
Wahabyas Jun 18, 2026
cb38316
Chore: final update for paymongo configuration
Wahabyas Jun 18, 2026
9617105
feat: dashboard: total users, shops, orders, revenue
Wahabyas Jun 18, 2026
d9583c8
feat: list all users under admin
Wahabyas Jun 18, 2026
e40b0ee
feat:toggle user isActive (suspend / unsuspend)
Wahabyas Jun 18, 2026
d18426a
feat: list all shops under admin
Wahabyas Jun 18, 2026
657a0c8
feat: toggle shop isActive under Admin
Wahabyas Jun 18, 2026
fdab615
feat: read-only ledger of all orders under admin
Wahabyas Jun 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 0 additions & 53 deletions .env.example

This file was deleted.

9 changes: 9 additions & 0 deletions config/cloudinary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const cloudinary = require('cloudinary').v2;

cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});

module.exports = cloudinary;
4 changes: 2 additions & 2 deletions config/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const mongoose = require('mongoose');

const connectDB = async () => {
try {
const mongoURI = process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/artisan_hub';
const mongoURI = process.env.MONGO_URI || 'mongodb://localhost:27017/artisan_hub';
await mongoose.connect(mongoURI);
console.log('✅ MongoDB Connected');
} catch (err) {
Expand All @@ -11,4 +11,4 @@ const connectDB = async () => {
}
};

module.exports = connectDB;
module.exports = connectDB;
Empty file removed middleware/.gitkeep
Empty file.
231 changes: 231 additions & 0 deletions middleware/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// TODO: Implement auth middleware

const jwt = require('jsonwebtoken');
const Shop = require('../modules/shops/shop.model'); // adjust path if needed
const Order = require('../modules/orders/order.model'); // adjust if existsProduct is not defined
const Product = require('../modules/products/product.model');
/**
* verifyToken
* Checks Authorization: Bearer <token>, decodes it,
* and attaches { id, role } to req.user.
* Usage: router.get('/protected', verifyToken, handler)
*/
const verifyToken = (req, res, next) => {
try {
const authHeader = req.headers.authorization;

if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
status: 'error',
message: 'No token provided'
});
}

const token = authHeader.split(' ')[1];

const decoded = jwt.verify(token, process.env.JWT_SECRET);

req.user = {
id: decoded.id,
role: decoded.role
};

next();

} catch (error) {
return res.status(401).json({
status: 'error',
message: 'Invalid or expired token'
});
}
};

/**
* requireRole(...roles)
* Factory — checks req.user.role against the allowed list.
* Usage: router.get('/admin', verifyToken, requireRole('admin'), handler)
*/
const requireRole = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
status: `error ${req.user}`,
message: 'Unauthorized'
});
}

if (!roles.includes(req.user.role)) {
return res.status(403).json({
status: 'error',
message: 'Forbidden: insufficient permissions'
});
}

next();
};
};

/**
* isShopOwner
* Confirms shop.owner === req.user.id before allowing mutations.
* Expects :shopId or :id param that resolves to a Shop document.
*/
const isShopOwner = async (req, res, next) => {
try {
const shopId = req.params.shopId || req.params.id;

if (!shopId) {
return res.status(400).json({
status: 'error',
message: 'Shop ID is required'
});
}

const shop = await Shop.findById(shopId);

if (!shop) {
return res.status(404).json({
status: 'error',
message: 'Shop not found'
});
}

if (shop.owner.toString() !== req.user.id) {
return res.status(403).json({
status: 'error',
message: 'You are not the owner of this shop'
});
}

req.shop = shop;
next();

} catch (error) {
next(error);
}
};

const isProductOwner = async (req, res, next) => {
try {
const { id } = req.params;

const product = await Product.findById(id);

if (!product) {
return res.status(404).json({
status: 'error',
message: 'Product not found'
});
}

const shop = await Shop.findById(product.shop);

if (!shop) {
return res.status(404).json({
status: 'error',
message: 'Shop not found'
});
}

if (shop.owner.toString() !== req.user.id) {
return res.status(403).json({
status: 'error',
message: 'You are not the owner of this product'
});
}

req.product = product;
req.shop = shop;

next();

} catch (error) {
next(error);
}
};

/**
* isOrderOwner
* Confirms order.buyer === req.user.id (buyer access)
* OR order.shop.owner === req.user.id (seller access).
*/
const isOrderOwner = async (req, res, next) => {
try {
const orderId = req.params.id;

const order = await Order.findById(orderId).populate('shop');

if (!order) {
return res.status(404).json({
status: 'error',
message: 'Order not found'
});
}

const isBuyer = order.buyer.toString() === req.user.id;
const isSeller = order.shop.owner.toString() === req.user.id;

if (!isBuyer && !isSeller) {
return res.status(403).json({
status: 'error',
message: 'Access denied'
});
}

req.order = order;
next();

} catch (error) {
next(error);
}
};


/**
* verifyWebhookSignature
* Validates the PayMongo webhook signature header
* using PAYMONGO_WEBHOOK_SECRET from env.
*/
const verifyWebhookSignature = (req, res, next) => {
try {
const signature = req.headers['paymongo-signature'];

if (!signature) {
return res.status(401).json({
status: 'error',
message: 'Missing webhook signature'
});
}

const expected = process.env.PAYMONGO_WEBHOOK_SECRET;

if (!expected) {
return res.status(500).json({
status: 'error',
message: 'Webhook secret not configured'
});
}

// Simple validation (depends on PayMongo format)
if (signature !== expected) {
return res.status(401).json({
status: 'error',
message: 'Invalid webhook signature'
});
}

next();

} catch (error) {
next(error);
}
};

module.exports = {
verifyToken,
requireRole,
isShopOwner,
isOrderOwner,
verifyWebhookSignature,
isProductOwner,
};
14 changes: 14 additions & 0 deletions middleware/multer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const multer = require('multer');

const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + file.originalname);
}
});

const upload = multer({ storage });

module.exports = upload;
Loading
Loading