matiru

Blockchain-Based Supply Chain Transparency for Agricultural Produce

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  });