matiru
Blockchain-Based Supply Chain Transparency for Agricultural Producegit clone https://github.com/maydayv7/matiru
backend/server.js (14021B)
1"use strict";
2require("dotenv").config();
3const express = require("express");
4const cors = require("cors");
5const morgan = require("morgan");
6const fs = require("fs");
7const path = require("path");
8const jwt = require("jsonwebtoken");
9const bcrypt = require("bcryptjs");
10const multer = require("multer");
11const { Gateway, Wallets } = require("fabric-network");
12
13const app = express();
14app.use(cors());
15app.use(morgan("dev"));
16
17// Notifications
18const NOTIFICATIONS_PATH = path.join(__dirname, "notifications.json");
19if (!fs.existsSync(NOTIFICATIONS_PATH))
20 fs.writeFileSync(NOTIFICATIONS_PATH, JSON.stringify([], null, 2));
21
22function readNotifications() {
23 return JSON.parse(fs.readFileSync(NOTIFICATIONS_PATH, "utf8"));
24}
25
26function writeNotifications(notifications) {
27 fs.writeFileSync(NOTIFICATIONS_PATH, JSON.stringify(notifications, null, 2));
28}
29
30async function addNotification(notification) {
31 const notifications = readNotifications();
32 const newNotification = {
33 id: `notif-${Date.now()}-${crypto.randomUUID()}`,
34 date: new Date().toISOString(),
35 read: false,
36 ...notification,
37 };
38 notifications.unshift(newNotification); // Add to top
39 writeNotifications(notifications);
40}
41
42// Image Storage
43app.use("/uploads", express.static("uploads"));
44const UPLOAD_DIR = path.join(__dirname, "uploads");
45if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR);
46
47const storage = multer.diskStorage({
48 destination: function (req, file, cb) {
49 cb(null, UPLOAD_DIR);
50 },
51 filename: function (req, file, cb) {
52 const uniqueName = Date.now() + "-" + file.originalname;
53 cb(null, uniqueName);
54 },
55});
56const upload = multer({ storage });
57
58// Fabric Network
59const { CHANNEL, CHAINCODE, AS_LOCALHOST, JWT_SECRET, WALLET_PATH } =
60 process.env;
61
62const orgConfig = {
63 Org1: {
64 ccpPath: process.env.CCP_PATH_ORG1,
65 identity: process.env.IDENTITY_ORG1,
66 },
67 Org2: {
68 ccpPath: process.env.CCP_PATH_ORG2,
69 identity: process.env.IDENTITY_ORG2,
70 },
71 Org3: {
72 ccpPath: process.env.CCP_PATH_ORG3,
73 identity: process.env.IDENTITY_ORG3,
74 },
75 Org4: {
76 ccpPath: process.env.CCP_PATH_ORG4,
77 identity: process.env.IDENTITY_ORG4,
78 },
79};
80
81const gateways = {};
82async function initializeGateways() {
83 console.log("Initializing Fabric Gateways...");
84 const wallet = await Wallets.newFileSystemWallet(path.resolve(WALLET_PATH));
85
86 for (const org of Object.keys(orgConfig)) {
87 const config = orgConfig[org];
88 if (!config || !config.ccpPath || !config.identity) {
89 console.warn(`Skipping gateway for ${org}: configuration missing`);
90 continue;
91 }
92
93 const ccp = JSON.parse(
94 fs.readFileSync(path.resolve(config.ccpPath), "utf8")
95 );
96 const gateway = new Gateway();
97
98 try {
99 await gateway.connect(ccp, {
100 wallet,
101 identity: config.identity,
102 discovery: { enabled: true, asLocalhost: AS_LOCALHOST === "true" },
103 });
104 gateways[org] = gateway;
105 console.log(`Gateway for ${org} initialized successfully`);
106 } catch (error) {
107 console.error(`Failed to initialize gateway for ${org}:`, error);
108 }
109 }
110}
111
112async function getContract(org) {
113 const gateway = gateways[org];
114 if (!gateway || !gateway.getNetwork)
115 throw new Error(`Gateway for ${org} is not available or not connected`);
116
117 const network = await gateway.getNetwork(CHANNEL);
118 return network.getContract(CHAINCODE);
119}
120
121// Middleware
122function authenticateMiddleware(req, res, next) {
123 if (req.path === "/auth/login") return next();
124
125 // Public Queries
126 if (
127 req.method === "GET" &&
128 (req.path.startsWith("/getProduce") || req.path.startsWith("/getUser"))
129 ) {
130 req.user = { org: "Org1" };
131 return next();
132 }
133
134 const header = req.headers["authorization"];
135 if (!header) return res.status(401).json({ error: "Missing auth header" });
136
137 const token = header.split(" ")[1];
138 if (!token) return res.status(401).json({ error: "Missing token" });
139
140 jwt.verify(token, JWT_SECRET, (err, payload) => {
141 if (err) return res.status(403).json({ error: "Invalid token" });
142 req.user = payload;
143 if (!req.user.org) {
144 return res
145 .status(403)
146 .json({ error: "Invalid token: missing Org identifier" });
147 }
148 next();
149 });
150}
151
152const asyncHandler = (fn) => (req, res, next) =>
153 Promise.resolve(fn(req, res, next)).catch(next);
154
155// User Authentication
156app.post(
157 "/api/auth/login",
158 express.json(),
159 asyncHandler(async (req, res) => {
160 const { username, password } = req.body || {};
161 if (!username || !password)
162 return res.status(400).json({ error: "Username and Password required" });
163
164 const usersPath = path.join(__dirname, "users.json");
165 if (!fs.existsSync(usersPath))
166 return res
167 .status(500)
168 .json({ error: "users.json missing -> run 'npm run init'" });
169
170 const users = JSON.parse(fs.readFileSync(usersPath, "utf8"));
171 const user = users.find((u) => u.username === username);
172 if (!user) return res.status(401).json({ error: "Invalid credentials" });
173
174 const ok = await bcrypt.compare(password, user.passwordHash);
175 if (!ok) return res.status(401).json({ error: "Invalid credentials" });
176
177 const token = jwt.sign(
178 { id: user.id, role: user.role, username: user.username, org: user.org },
179 JWT_SECRET,
180 { expiresIn: process.env.TOKEN_EXPIRES_IN || "1h" }
181 );
182 res.json({ token, ...user });
183 })
184);
185
186app.post(
187 "/api/uploadImage",
188 authenticateMiddleware,
189 upload.single("image"),
190 (req, res) => {
191 if (!req.file)
192 return res.status(400).json({ error: "No image file uploaded" });
193
194 const fileUrl = `${req.protocol}://${req.get("host")}/uploads/${
195 req.file.filename
196 }`;
197 res.json({ url: fileUrl });
198 }
199);
200
201// Functions
202const router = express.Router();
203
204router.get(
205 "/notifications",
206 asyncHandler(async (req, res) => {
207 const allNotifications = readNotifications();
208 const { id: userId, org: userOrg } = req.user;
209 const userNotifications = allNotifications.filter((n) => {
210 if (n.channel === "all") return true;
211 if (n.channel === "org" && n.targetId === userOrg) return true;
212 if (n.channel === "user" && n.targetId === userId) return true;
213 return false;
214 });
215 res.json({ notifications: userNotifications });
216 })
217);
218
219router.post(
220 "/registerUser",
221 asyncHandler(async (req, res) => {
222 const { role, details } = req.body;
223 const contract = await getContract(req.user.org);
224 const result = await contract.submitTransaction(
225 "registerUser",
226 role,
227 JSON.stringify(details)
228 );
229 return res.json({ success: true, user: JSON.parse(result.toString()) });
230 })
231);
232
233router.post(
234 "/registerProduce",
235 asyncHandler(async (req, res) => {
236 const { farmerId, details } = req.body;
237 const contract = await getContract(req.user.org);
238 const result = await contract.submitTransaction(
239 "registerProduce",
240 farmerId,
241 JSON.stringify(details)
242 );
243 const produce = JSON.parse(result.toString());
244 await addNotification({
245 title: "New Produce Registered",
246 message: `Registered a new batch of ${details.cropType} (ID: ${produce.id}).`,
247 channel: "user",
248 targetId: farmerId,
249 });
250 return res.json({ success: true, produce });
251 })
252);
253
254router.post(
255 "/updateLocation",
256 asyncHandler(async (req, res) => {
257 const { produceId, actorId, newLocation } = req.body;
258 const contract = await getContract(req.user.org);
259 const result = await contract.submitTransaction(
260 "updateLocation",
261 produceId,
262 actorId,
263 newLocation
264 );
265 const produce = JSON.parse(result.toString());
266 await addNotification({
267 title: "Location Updated",
268 message: `Location for ${produce.cropType} (ID: ${produceId}) has been updated to ${newLocation}.`,
269 channel: "user",
270 targetId: actorId,
271 });
272 return res.json({ success: true, produce });
273 })
274);
275
276router.post(
277 "/inspectProduce",
278 asyncHandler(async (req, res) => {
279 const { produceId, inspectorId, qualityUpdate } = req.body;
280 const contract = await getContract(req.user.org);
281 const result = await contract.submitTransaction(
282 "inspectProduce",
283 produceId,
284 inspectorId,
285 JSON.stringify(qualityUpdate)
286 );
287 const produce = JSON.parse(result.toString());
288
289 await addNotification({
290 title: "Produce Inspected",
291 message: `Your produce batch ${produce.cropType} (ID: ${produceId}) was inspected. Status: ${
292 qualityUpdate.failed
293 ? `Failed. Reason: ${qualityUpdate.reason}.`
294 : "Passed"
295 }.`,
296 channel: "user",
297 targetId: produce.currentOwner,
298 });
299
300 return res.json({ success: true, produce });
301 })
302);
303
304router.post(
305 "/transferOwnership",
306 asyncHandler(async (req, res) => {
307 const { produceId, newOwnerId, qty, salePrice } = req.body;
308 const contract = await getContract(req.user.org);
309 const produceData = await contract.evaluateTransaction(
310 "getProduceById",
311 produceId
312 );
313 const produce = JSON.parse(produceData.toString());
314 const prevOwnerId = produce.currentOwner;
315
316 const result = await contract.submitTransaction(
317 "transferOwnership",
318 produceId,
319 newOwnerId,
320 "" + qty,
321 "" + salePrice
322 );
323
324 await addNotification({
325 title: "Produce Received",
326 message: `You have received ${qty} ${
327 produce.qtyUnit || ""
328 } of ${produce.cropType} from ${prevOwnerId}.`,
329 channel: "user",
330 targetId: newOwnerId,
331 });
332
333 await addNotification({
334 title: "Produce Transferred",
335 message: `You transferred ${qty} ${
336 produce.qtyUnit || ""
337 } of ${produce.cropType} to ${newOwnerId}.`,
338 channel: "user",
339 targetId: prevOwnerId,
340 });
341
342 return res.json({ success: true, result: JSON.parse(result.toString()) });
343 })
344);
345
346router.post(
347 "/updateDetails",
348 asyncHandler(async (req, res) => {
349 const { produceId, actorId, details } = req.body;
350 const contract = await getContract(req.user.org);
351 const result = await contract.submitTransaction(
352 "updateDetails",
353 produceId,
354 actorId,
355 JSON.stringify(details)
356 );
357 const produce = JSON.parse(result.toString());
358 await addNotification({
359 title: "Details Updated",
360 message: `Details for your produce batch ${produce.cropType} (ID: ${produceId}) have been updated.`,
361 channel: "user",
362 targetId: actorId,
363 });
364 return res.json({ success: true, produce });
365 })
366);
367
368router.post(
369 "/markAsUnavailable",
370 asyncHandler(async (req, res) => {
371 const { produceId, actorId, reason, newStatus } = req.body;
372 const contract = await getContract(req.user.org);
373 const result = await contract.submitTransaction(
374 "markAsUnavailable",
375 produceId,
376 actorId,
377 reason || "",
378 newStatus || "Removed"
379 );
380 const produce = JSON.parse(result.toString());
381 await addNotification({
382 title: "Produce Status Changed",
383 message: `Your produce batch ${produce.cropType} (ID: ${produceId}) was marked as ${newStatus}.`,
384 channel: "user",
385 targetId: actorId,
386 });
387 return res.json({ success: true, produce });
388 })
389);
390
391router.post(
392 "/splitProduce",
393 asyncHandler(async (req, res) => {
394 const { produceId, qty, ownerId } = req.body;
395 const contract = await getContract(req.user.org);
396 const result = await contract.submitTransaction(
397 "splitProduce",
398 produceId,
399 "" + qty,
400 ownerId
401 );
402 const splitResult = JSON.parse(result.toString());
403 await addNotification({
404 title: "Produce Split",
405 message: `Your produce batch (ID: ${produceId}) was split. A new batch of ${qty} was created.`,
406 channel: "user",
407 targetId: ownerId,
408 });
409 return res.json({ success: true, split: splitResult });
410 })
411);
412
413router.post(
414 "/recordPayment",
415 asyncHandler(async (req, res) => {
416 const { produceId, transactionId, paymentStatus } = req.body;
417 const contract = await getContract(req.user.org);
418 const result = await contract.submitTransaction(
419 "recordPayment",
420 produceId,
421 transactionId,
422 paymentStatus,
423 req.body.paymentMethod,
424 req.body.paymentRef || ""
425 );
426 return res.json({ success: true, produce: JSON.parse(result.toString()) });
427 })
428);
429
430// Queries
431router.get(
432 "/getProduce/:id",
433 asyncHandler(async (req, res) => {
434 const contract = await getContract(req.user.org);
435 const result = await contract.evaluateTransaction(
436 "getProduceById",
437 req.params.id
438 );
439 return res.json({ success: true, produce: JSON.parse(result.toString()) });
440 })
441);
442
443router.get(
444 "/getProduceByOwner/:ownerId",
445 asyncHandler(async (req, res) => {
446 const contract = await getContract(req.user.org);
447 const result = await contract.evaluateTransaction(
448 "getProduceByOwner",
449 req.params.ownerId
450 );
451 return res.json({ success: true, produces: JSON.parse(result.toString()) });
452 })
453);
454
455router.get(
456 "/getUser/:userKey",
457 asyncHandler(async (req, res) => {
458 const contract = await getContract(req.user.org);
459 const result = await contract.evaluateTransaction(
460 "getUserDetails",
461 req.params.userKey
462 );
463 return res.json({ success: true, user: JSON.parse(result.toString()) });
464 })
465);
466
467app.use("/api", authenticateMiddleware, express.json(), router);
468
469function errorHandler(err, req, res, next) {
470 console.error(err);
471
472 const userMessage =
473 err.message.includes("DiscoveryService") ||
474 err.message.includes("CommitError")
475 ? "Blockchain transaction failed. Please try again."
476 : err.message;
477
478 res.status(500).json({ error: userMessage });
479}
480app.use(errorHandler);
481
482const PORT = process.env.PORT || 4000;
483initializeGateways()
484 .then(() => {
485 app.listen(PORT, "0.0.0.0", () =>
486 console.log(`Server listening on port ${PORT}`)
487 );
488 })
489 .catch((e) => {
490 console.error("Server failed to start:", e);
491 process.exit(1);
492 });