From 29d17bb0f9ec73a4f9af77c4530690d606970894 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Fri, 9 May 2025 22:19:35 +0100 Subject: [PATCH] Added missing files --- src/keycloak.js | 127 +++++++++++++++ src/routes/filaments/index.js | 45 ++++++ src/routes/materials/index.js | 45 ++++++ src/routes/parts/index.js | 55 +++++++ src/routes/products/index.js | 45 ++++++ src/routes/spotlight/index.js | 11 ++ src/routes/vendors/index.js | 45 ++++++ src/schemas/filament.schema.js | 25 +++ src/schemas/host.schema.js | 17 ++ src/schemas/material.schema.js | 15 ++ src/schemas/part.schema.js | 22 +++ src/schemas/printsubjob.schema.js | 51 ++++++ src/schemas/product.schema.js | 24 +++ src/schemas/vendor.schema.js | 19 +++ src/services/filaments/index.js | 121 ++++++++++++++ src/services/materials/index.js | 136 ++++++++++++++++ src/services/parts/index.js | 252 ++++++++++++++++++++++++++++++ src/services/products/index.js | 135 ++++++++++++++++ src/services/spotlight/index.js | 113 ++++++++++++++ src/services/vendors/index.js | 132 ++++++++++++++++ 20 files changed, 1435 insertions(+) create mode 100644 src/keycloak.js create mode 100644 src/routes/filaments/index.js create mode 100644 src/routes/materials/index.js create mode 100644 src/routes/parts/index.js create mode 100644 src/routes/products/index.js create mode 100644 src/routes/spotlight/index.js create mode 100644 src/routes/vendors/index.js create mode 100644 src/schemas/filament.schema.js create mode 100644 src/schemas/host.schema.js create mode 100644 src/schemas/material.schema.js create mode 100644 src/schemas/part.schema.js create mode 100644 src/schemas/printsubjob.schema.js create mode 100644 src/schemas/product.schema.js create mode 100644 src/schemas/vendor.schema.js create mode 100644 src/services/filaments/index.js create mode 100644 src/services/materials/index.js create mode 100644 src/services/parts/index.js create mode 100644 src/services/products/index.js create mode 100644 src/services/spotlight/index.js create mode 100644 src/services/vendors/index.js diff --git a/src/keycloak.js b/src/keycloak.js new file mode 100644 index 0000000..02a6811 --- /dev/null +++ b/src/keycloak.js @@ -0,0 +1,127 @@ +import Keycloak from "keycloak-connect"; +import session from "express-session"; +import dotenv from "dotenv"; +import axios from "axios"; +import jwt from "jsonwebtoken"; +import log4js from "log4js"; + +dotenv.config(); + +const logger = log4js.getLogger("Keycloak"); +logger.level = process.env.LOG_LEVEL || "info"; + +// Initialize Keycloak +const keycloakConfig = { + realm: process.env.KEYCLOAK_REALM || "farm-control", + "auth-server-url": process.env.KEYCLOAK_URL || "http://localhost:8080/auth", + "ssl-required": process.env.NODE_ENV === "production" ? "external" : "none", + resource: process.env.KEYCLOAK_CLIENT_ID || "farmcontrol-client", + "confidential-port": 0, + "bearer-only": true, + "public-client": false, + "use-resource-role-mappings": true, + "verify-token-audience": true, + credentials: { + secret: process.env.KEYCLOAK_CLIENT_SECRET, + }, +}; + +const memoryStore = new session.MemoryStore(); + +var expressSession = session({ + secret: process.env.SESSION_SECRET || "n00Dl3s23!", + resave: false, + saveUninitialized: true, // Set this to true to ensure session is initialized + store: memoryStore, + cookie: { + maxAge: 1800000, // 30 minutes + }, +}); + +var keycloak = new Keycloak({ store: memoryStore }, keycloakConfig); + +// Custom middleware to check if the user is authenticated +const isAuthenticated = async (req, res, next) => { + let token = null; + + // Try to get token from Authorization header + const authHeader = req.headers.authorization; + if (authHeader && authHeader.startsWith('Bearer ')) { + token = authHeader.substring(7); + + try { + // Verify token with Keycloak introspection endpoint + const response = await axios.post( + `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token/introspect`, + new URLSearchParams({ + token: token, + client_id: process.env.KEYCLOAK_CLIENT_ID, + client_secret: process.env.KEYCLOAK_CLIENT_SECRET, + }), + { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + } + ); + + const introspection = response.data; + if (!introspection.active) { + logger.info("Token is not active"); + return res.status(401).json({ error: "Not authenticated" }); + } + + // Parse token to extract user info + const decodedToken = jwt.decode(token); + req.user = { + id: decodedToken.sub, + username: decodedToken.preferred_username, + email: decodedToken.email, + name: decodedToken.name, + roles: extractRoles(decodedToken), + }; + + return next(); + } catch (error) { + logger.error("Token verification error:", error.message); + return res.status(401).json({ error: "Not authenticated" }); + } + } + + // Fallback to session-based authentication + if (req.session && req.session["keycloak-token"]) { + const sessionToken = req.session["keycloak-token"]; + if (sessionToken.expires_at > new Date().getTime()) { + return next(); + } + } + + return res.status(401).json({ error: "Not authenticated" }); +}; + +// Helper function to extract roles from token +function extractRoles(token) { + const roles = []; + + // Extract realm roles + if (token.realm_access && token.realm_access.roles) { + roles.push(...token.realm_access.roles); + } + + // Extract client roles + if (token.resource_access) { + for (const client in token.resource_access) { + if (token.resource_access[client].roles) { + roles.push( + ...token.resource_access[client].roles.map( + (role) => `${client}:${role}` + ) + ); + } + } + } + + return roles; +} + +export { keycloak, expressSession, isAuthenticated }; diff --git a/src/routes/filaments/index.js b/src/routes/filaments/index.js new file mode 100644 index 0000000..1898902 --- /dev/null +++ b/src/routes/filaments/index.js @@ -0,0 +1,45 @@ +import express from "express"; +import { isAuthenticated } from "../../keycloak.js"; +import { parseStringIfNumber } from "../../util/index.js"; + +const router = express.Router(); +import { + listFilamentsRouteHandler, + getFilamentRouteHandler, + editFilamentRouteHandler, + newFilamentRouteHandler, +} from "../../services/filaments/index.js"; + +// list of filaments +router.get("/", isAuthenticated, (req, res) => { + const { page, limit, property } = req.query; + + const allowedFilters = ["type", "brand", "diameter", "color"]; + + const filter = {}; + + for (const [key, value] of Object.entries(req.query)) { + for (var i = 0; i < allowedFilters.length; i++) { + if (key == allowedFilters[i]) { + filter[key] = parseStringIfNumber(value); + } + } + } + + listFilamentsRouteHandler(req, res, page, limit, property, filter); +}); + +router.post("/", isAuthenticated, (req, res) => { + newFilamentRouteHandler(req, res); +}); + +router.get("/:id", isAuthenticated, (req, res) => { + getFilamentRouteHandler(req, res); +}); + +// update printer info +router.put("/:id", isAuthenticated, async (req, res) => { + editFilamentRouteHandler(req, res); +}); + +export default router; diff --git a/src/routes/materials/index.js b/src/routes/materials/index.js new file mode 100644 index 0000000..4c474cc --- /dev/null +++ b/src/routes/materials/index.js @@ -0,0 +1,45 @@ +import express from "express"; +import { isAuthenticated } from "../../keycloak.js"; +import { parseStringIfNumber } from "../../util/index.js"; + +const router = express.Router(); +import { + listMaterialsRouteHandler, + getMaterialRouteHandler, + editMaterialRouteHandler, + newMaterialRouteHandler, +} from "../../services/materials/index.js"; + +// list of materials +router.get("/", isAuthenticated, (req, res) => { + const { page, limit, property } = req.query; + + const allowedFilters = ["type", "brand", "diameter", "color"]; + + const filter = {}; + + for (const [key, value] of Object.entries(req.query)) { + for (var i = 0; i < allowedFilters.length; i++) { + if (key == allowedFilters[i]) { + filter[key] = parseStringIfNumber(value); + } + } + } + + listMaterialsRouteHandler(req, res, page, limit, property, filter); +}); + +router.post("/", isAuthenticated, (req, res) => { + newMaterialRouteHandler(req, res); +}); + +router.get("/:id", isAuthenticated, (req, res) => { + getMaterialRouteHandler(req, res); +}); + +// update printer info +router.put("/:id", isAuthenticated, async (req, res) => { + editMaterialRouteHandler(req, res); +}); + +export default router; diff --git a/src/routes/parts/index.js b/src/routes/parts/index.js new file mode 100644 index 0000000..b692689 --- /dev/null +++ b/src/routes/parts/index.js @@ -0,0 +1,55 @@ +import express from "express"; +import { isAuthenticated } from "../../keycloak.js"; +import { parseStringIfNumber } from "../../util/index.js"; + +const router = express.Router(); +import { + listPartsRouteHandler, + getPartRouteHandler, + editPartRouteHandler, + newPartRouteHandler, + uploadPartFileContentRouteHandler, + getPartFileContentRouteHandler, +} from "../../services/parts/index.js"; + +// list of parts +router.get("/", isAuthenticated, (req, res) => { + const { page, limit, property } = req.query; + + const allowedFilters = ["type", "brand", "diameter", "color"]; + + const filter = {}; + + for (const [key, value] of Object.entries(req.query)) { + for (var i = 0; i < allowedFilters.length; i++) { + if (key == allowedFilters[i]) { + filter[key] = parseStringIfNumber(value); + } + } + } + + listPartsRouteHandler(req, res, page, limit, property, filter); +}); + +router.post("/", isAuthenticated, (req, res) => { + newPartRouteHandler(req, res); +}); + +router.post("/:id/content", isAuthenticated, (req, res) => { + uploadPartFileContentRouteHandler(req, res); +}); + +router.get("/:id/content", isAuthenticated, (req, res) => { + getPartFileContentRouteHandler(req, res); +}); + +router.get("/:id", isAuthenticated, (req, res) => { + getPartRouteHandler(req, res); +}); + +// update printer info +router.put("/:id", isAuthenticated, async (req, res) => { + editPartRouteHandler(req, res); +}); + +export default router; diff --git a/src/routes/products/index.js b/src/routes/products/index.js new file mode 100644 index 0000000..1d37a4c --- /dev/null +++ b/src/routes/products/index.js @@ -0,0 +1,45 @@ +import express from "express"; +import { isAuthenticated } from "../../keycloak.js"; +import { parseStringIfNumber } from "../../util/index.js"; + +const router = express.Router(); +import { + listProductsRouteHandler, + getProductRouteHandler, + editProductRouteHandler, + newProductRouteHandler, +} from "../../services/products/index.js"; + +// list of products +router.get("/", isAuthenticated, (req, res) => { + const { page, limit, property } = req.query; + + const allowedFilters = ["type", "brand", "diameter", "color"]; + + const filter = {}; + + for (const [key, value] of Object.entries(req.query)) { + for (var i = 0; i < allowedFilters.length; i++) { + if (key == allowedFilters[i]) { + filter[key] = parseStringIfNumber(value); + } + } + } + + listProductsRouteHandler(req, res, page, limit, property, filter); +}); + +router.post("/", isAuthenticated, (req, res) => { + newProductRouteHandler(req, res); +}); + +router.get("/:id", isAuthenticated, (req, res) => { + getProductRouteHandler(req, res); +}); + +// update printer info +router.put("/:id", isAuthenticated, async (req, res) => { + editProductRouteHandler(req, res); +}); + +export default router; diff --git a/src/routes/spotlight/index.js b/src/routes/spotlight/index.js new file mode 100644 index 0000000..ba0653f --- /dev/null +++ b/src/routes/spotlight/index.js @@ -0,0 +1,11 @@ +import express from "express"; +import { isAuthenticated } from "../../keycloak.js"; + +const router = express.Router(); +import { getSpotlightRouteHandler } from "../../services/spotlight/index.js"; + +router.get("/:query", isAuthenticated, (req, res) => { + getSpotlightRouteHandler(req, res); +}); + +export default router; diff --git a/src/routes/vendors/index.js b/src/routes/vendors/index.js new file mode 100644 index 0000000..f6ce66f --- /dev/null +++ b/src/routes/vendors/index.js @@ -0,0 +1,45 @@ +import express from "express"; +import { isAuthenticated } from "../../keycloak.js"; +import { parseStringIfNumber } from "../../util/index.js"; + +const router = express.Router(); +import { + listVendorsRouteHandler, + getVendorRouteHandler, + editVendorRouteHandler, + newVendorRouteHandler, +} from "../../services/vendors/index.js"; + +// list of vendors +router.get("/", isAuthenticated, (req, res) => { + const { page, limit, property } = req.query; + + const allowedFilters = ["type", "brand", "diameter", "color"]; + + const filter = {}; + + for (const [key, value] of Object.entries(req.query)) { + for (var i = 0; i < allowedFilters.length; i++) { + if (key == allowedFilters[i]) { + filter[key] = parseStringIfNumber(value); + } + } + } + + listVendorsRouteHandler(req, res, page, limit, property, filter); +}); + +router.post("/", isAuthenticated, (req, res) => { + newVendorRouteHandler(req, res); +}); + +router.get("/:id", isAuthenticated, (req, res) => { + getVendorRouteHandler(req, res); +}); + +// update printer info +router.put("/:id", isAuthenticated, async (req, res) => { + editVendorRouteHandler(req, res); +}); + +export default router; diff --git a/src/schemas/filament.schema.js b/src/schemas/filament.schema.js new file mode 100644 index 0000000..7da3cd4 --- /dev/null +++ b/src/schemas/filament.schema.js @@ -0,0 +1,25 @@ +import mongoose from "mongoose"; + +const filamentSchema = new mongoose.Schema({ + name: { required: true, type: String }, + barcode: { required: false, type: String }, + url: { required: false, type: String }, + image: { required: false, type: Buffer }, + color: { required: true, type: String }, + brand: { required: true, type: String }, + type: { required: true, type: String }, + price: { required: true, type: Number }, + diameter: { required: true, type: Number }, + density: { required: true, type: Number }, + createdAt: { required: true, type: Date }, + updatedAt: { required: true, type: Date }, + emptySpoolWeight: { required: true, type: Number }, +}); + +filamentSchema.virtual("id").get(function () { + return this._id.toHexString(); +}); + +filamentSchema.set("toJSON", { virtuals: true }); + +export const filamentModel = mongoose.model("Filament", filamentSchema); diff --git a/src/schemas/host.schema.js b/src/schemas/host.schema.js new file mode 100644 index 0000000..490d22e --- /dev/null +++ b/src/schemas/host.schema.js @@ -0,0 +1,17 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +const hostSchema = new mongoose.Schema({ + online: { required: true, type: Boolean }, + hostId: { required: true, type: String }, + connectedAt: { required: true, type: Date }, + status: { type: { required: true, type: String } }, +}); + +hostSchema.virtual("id").get(function () { + return this._id.toHexString(); +}); + +hostSchema.set("toJSON", { virtuals: true }); + +export const hostModel = mongoose.model("Host", hostSchema); diff --git a/src/schemas/material.schema.js b/src/schemas/material.schema.js new file mode 100644 index 0000000..3e8b49b --- /dev/null +++ b/src/schemas/material.schema.js @@ -0,0 +1,15 @@ +import mongoose from "mongoose"; + +const materialSchema = new mongoose.Schema({ + name: { required: true, type: String }, + url: { required: false, type: String }, + image: { required: false, type: Buffer }, +}); + +materialSchema.virtual("id").get(function () { + return this._id.toHexString(); +}); + +materialSchema.set("toJSON", { virtuals: true }); + +export const materialModel = mongoose.model("Material", materialSchema); diff --git a/src/schemas/part.schema.js b/src/schemas/part.schema.js new file mode 100644 index 0000000..606b6be --- /dev/null +++ b/src/schemas/part.schema.js @@ -0,0 +1,22 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +// Define the main part schema +const partSchema = new Schema( + { + name: { type: String, required: true }, + products: [{ type: mongoose.Schema.Types.ObjectId, ref: "Product" }], + }, + { timestamps: true }, +); + +// Add virtual id getter +partSchema.virtual("id").get(function () { + return this._id.toHexString(); +}); + +// Configure JSON serialization to include virtuals +partSchema.set("toJSON", { virtuals: true }); + +// Create and export the model +export const partModel = mongoose.model("Part", partSchema); diff --git a/src/schemas/printsubjob.schema.js b/src/schemas/printsubjob.schema.js new file mode 100644 index 0000000..ba78999 --- /dev/null +++ b/src/schemas/printsubjob.schema.js @@ -0,0 +1,51 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +const printSubJobSchema = new mongoose.Schema({ + printer: { + type: Schema.Types.ObjectId, + ref: "Printer", + required: true, + }, + printJob: { + type: Schema.Types.ObjectId, + ref: "PrintJob", + required: true, + }, + subJobId: { + type: String, + required: true, + }, + gcodeFile: { + type: Schema.Types.ObjectId, + ref: "GCodeFile", + required: true, + }, + state: { + type: { required: true, type: String }, + percent: { required: false, type: Number }, + }, + number: { + type: Number, + required: true, + }, + createdAt: { + type: Date, + default: Date.now, + }, + updatedAt: { + type: Date, + default: Date.now, + }, +}); + +printSubJobSchema.virtual("id").get(function () { + return this._id.toHexString(); +}); + +printSubJobSchema.set("toJSON", { virtuals: true }); + +export const printSubJobModel = mongoose.model( + "PrintSubJob", + printSubJobSchema, +); diff --git a/src/schemas/product.schema.js b/src/schemas/product.schema.js new file mode 100644 index 0000000..3e189c9 --- /dev/null +++ b/src/schemas/product.schema.js @@ -0,0 +1,24 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +// Define the main product schema +const productSchema = new Schema( + { + name: { type: String, required: true }, + tags: [{ type: String }], + version: { type: String }, + parts: [{ type: mongoose.Schema.Types.ObjectId, ref: "Part" }], + }, + { timestamps: true }, +); + +// Add virtual id getter +productSchema.virtual("id").get(function () { + return this._id.toHexString(); +}); + +// Configure JSON serialization to include virtuals +productSchema.set("toJSON", { virtuals: true }); + +// Create and export the model +export const productModel = mongoose.model("product", productSchema); diff --git a/src/schemas/vendor.schema.js b/src/schemas/vendor.schema.js new file mode 100644 index 0000000..9e9d03a --- /dev/null +++ b/src/schemas/vendor.schema.js @@ -0,0 +1,19 @@ +import mongoose from "mongoose"; + +const vendorSchema = new mongoose.Schema( + { + name: { required: true, type: String }, + website: { required: false, type: String }, + contact: { required: false, type: String }, + image: { required: false, type: Buffer }, + }, + { timestamps: true }, +); + +vendorSchema.virtual("id").get(function () { + return this._id.toHexString(); +}); + +vendorSchema.set("toJSON", { virtuals: true }); + +export const vendorModel = mongoose.model("Vendor", vendorSchema); diff --git a/src/services/filaments/index.js b/src/services/filaments/index.js new file mode 100644 index 0000000..1346139 --- /dev/null +++ b/src/services/filaments/index.js @@ -0,0 +1,121 @@ +import dotenv from "dotenv"; +import { filamentModel } from "../../schemas/filament.schema.js" +import jwt from "jsonwebtoken"; +import log4js from "log4js"; +import mongoose from "mongoose"; + +dotenv.config(); + +const logger = log4js.getLogger("Filaments"); +logger.level = process.env.LOG_LEVEL; + +export const listFilamentsRouteHandler = async (req, res, page = 1, limit = 25, property = "", filter = {}) => { + try { + // Calculate the skip value based on the page number and limit + const skip = (page - 1) * limit; + + + let filament; + let aggregateCommand = []; + + if (filter != {}) { // use filtering if present + aggregateCommand.push({ $match: filter }); + } + + if (property != "") { + aggregateCommand.push({ $group: { _id: `$${property}` } }) // group all same properties + aggregateCommand.push({ $project: { _id: 0, [property]: "$_id" }}); // rename _id to the property name + } else { + aggregateCommand.push({ $project: { image: 0, url: 0 }}); + } + + aggregateCommand.push({ $skip: skip }); + aggregateCommand.push({ $limit: Number(limit) }); + + console.log(aggregateCommand) + + filament = await filamentModel.aggregate(aggregateCommand) + + logger.trace(`List of filaments (Page ${page}, Limit ${limit}, Property ${property}):`, filament); + res.send(filament); + } catch (error) { + logger.error("Error listing filaments:", error); + res.status(500).send({ error: error }); + } +}; + +export const getFilamentRouteHandler = async (req, res) => { + try { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + // Fetch the filament with the given remote address + const filament = await filamentModel.findOne({ + _id: id + }); + + if (!filament) { + logger.warn(`Filament not found with supplied id.`); + return res.status(404).send({ error: "Print job not found." }); + } + + logger.trace(`Filament with ID: ${id}:`, filament); + res.send(filament); + } catch (error) { + logger.error("Error fetching Filament:", error); + res.status(500).send({ error: error.message }); + } +}; + +export const editFilamentRouteHandler = async (req, res) => { + try { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + // Fetch the filament with the given remote address + const filament = await filamentModel.findOne({ _id: id }); + + if (!filament) { // Error handling + logger.warn(`Filament not found with supplied id.`); + return res.status(404).send({ error: "Print job not found." }); + } + + logger.trace(`Filament with ID: ${id}:`, filament); + + try { + const { createdAt, updatedAt, started_at, status, ...updateData } = req.body; + + const result = await filamentModel.updateOne( + { _id: id }, + { $set: updateData } + ); + if (result.nModified === 0) { + logger.error("No Filament updated."); + res.status(500).send({ error: "No filaments updated." }); + } + } catch (updateError) { + logger.error("Error updating filament:", updateError); + res.status(500).send({ error: updateError.message }); + } + res.send("OK"); + } catch (fetchError) { + logger.error("Error fetching filament:", fetchError); + res.status(500).send({ error: fetchError.message }); + } +}; + +export const newFilamentRouteHandler = async (req, res) => { + + try { + let { ...newFilament } = req.body; + newFilament = { ...newFilament, createdAt: new Date(), updatedAt: new Date() } + + const result = await filamentModel.create(newFilament); + if (result.nCreated === 0) { + logger.error("No filament created."); + res.status(500).send({ error: "No filament created." }); + } + res.status(200).send({ status: "ok" }); + } catch (updateError) { + logger.error("Error updating filament:", updateError); + res.status(500).send({ error: updateError.message }); + } +}; \ No newline at end of file diff --git a/src/services/materials/index.js b/src/services/materials/index.js new file mode 100644 index 0000000..69e7bc7 --- /dev/null +++ b/src/services/materials/index.js @@ -0,0 +1,136 @@ +import dotenv from "dotenv"; +import { materialModel } from "../../schemas/material.schema.js"; +import jwt from "jsonwebtoken"; +import log4js from "log4js"; +import mongoose from "mongoose"; + +dotenv.config(); + +const logger = log4js.getLogger("Materials"); +logger.level = process.env.LOG_LEVEL; + +export const listMaterialsRouteHandler = async ( + req, + res, + page = 1, + limit = 25, + property = "", + filter = {}, +) => { + try { + // Calculate the skip value based on the page number and limit + const skip = (page - 1) * limit; + + let material; + let aggregateCommand = []; + + if (filter != {}) { + // use filtering if present + aggregateCommand.push({ $match: filter }); + } + + if (property != "") { + aggregateCommand.push({ $group: { _id: `$${property}` } }); // group all same properties + aggregateCommand.push({ $project: { _id: 0, [property]: "$_id" } }); // rename _id to the property name + } else { + aggregateCommand.push({ $project: { image: 0, url: 0 } }); + } + + aggregateCommand.push({ $skip: skip }); + aggregateCommand.push({ $limit: Number(limit) }); + + console.log(aggregateCommand); + + material = await materialModel.aggregate(aggregateCommand); + + logger.trace( + `List of materials (Page ${page}, Limit ${limit}, Property ${property}):`, + material, + ); + res.send(material); + } catch (error) { + logger.error("Error listing materials:", error); + res.status(500).send({ error: error }); + } +}; + +export const getMaterialRouteHandler = async (req, res) => { + try { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + // Fetch the material with the given remote address + const material = await materialModel.findOne({ + _id: id, + }); + + if (!material) { + logger.warn(`Material not found with supplied id.`); + return res.status(404).send({ error: "Print job not found." }); + } + + logger.trace(`Material with ID: ${id}:`, material); + res.send(material); + } catch (error) { + logger.error("Error fetching Material:", error); + res.status(500).send({ error: error.message }); + } +}; + +export const editMaterialRouteHandler = async (req, res) => { + try { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + // Fetch the material with the given remote address + const material = await materialModel.findOne({ _id: id }); + + if (!material) { + // Error handling + logger.warn(`Material not found with supplied id.`); + return res.status(404).send({ error: "Print job not found." }); + } + + logger.trace(`Material with ID: ${id}:`, material); + + try { + const { createdAt, updatedAt, started_at, status, ...updateData } = + req.body; + + const result = await materialModel.updateOne( + { _id: id }, + { $set: updateData }, + ); + if (result.nModified === 0) { + logger.error("No Material updated."); + res.status(500).send({ error: "No materials updated." }); + } + } catch (updateError) { + logger.error("Error updating material:", updateError); + res.status(500).send({ error: updateError.message }); + } + res.send("OK"); + } catch (fetchError) { + logger.error("Error fetching material:", fetchError); + res.status(500).send({ error: fetchError.message }); + } +}; + +export const newMaterialRouteHandler = async (req, res) => { + try { + let { ...newMaterial } = req.body; + newMaterial = { + ...newMaterial, + createdAt: new Date(), + updatedAt: new Date(), + }; + + const result = await materialModel.create(newMaterial); + if (result.nCreated === 0) { + logger.error("No material created."); + res.status(500).send({ error: "No material created." }); + } + res.status(200).send({ status: "ok" }); + } catch (updateError) { + logger.error("Error updating material:", updateError); + res.status(500).send({ error: updateError.message }); + } +}; diff --git a/src/services/parts/index.js b/src/services/parts/index.js new file mode 100644 index 0000000..4e0eb64 --- /dev/null +++ b/src/services/parts/index.js @@ -0,0 +1,252 @@ +import dotenv from "dotenv"; +import { partModel } from "../../schemas/part.schema.js"; +import log4js from "log4js"; +import mongoose from "mongoose"; +import multer from "multer"; +import fs from "fs"; +import path from "path"; + +dotenv.config(); + +const logger = log4js.getLogger("Parts"); +logger.level = process.env.LOG_LEVEL; + +// Set storage engine +const partsStorage = multer.diskStorage({ + destination: process.env.PART_STORAGE, + filename: async function (req, file, cb) { + // Retrieve custom file name from request body + const customFileName = req.params.id || "default"; // Default to 'default' if not provided + // Create the final filename ensuring it ends with .g + const finalFilename = `${customFileName}.stl`; + + // Call callback with the final filename + cb(null, finalFilename); + }, +}); + +// Initialise upload +const partUpload = multer({ + storage: partsStorage, + limits: { fileSize: 500000000 }, // 50MB limit + fileFilter: function (req, file, cb) { + checkFileType(file, cb); + }, +}).single("partFile"); // The name attribute of the file input in the HTML form + +// Check file type +function checkFileType(file, cb) { + // Allowed ext + const filetypes = /stl|stl|stl/; + // Check ext + const extname = filetypes.test(path.extname(file.originalname).toLowerCase()); + + if (extname) { + console.log(file); + return cb(null, true); + } else { + cb("Error: .stl files only!"); + } +} + +export const listPartsRouteHandler = async ( + req, + res, + page = 1, + limit = 25, + property = "", + filter = {}, +) => { + try { + // Calculate the skip value based on the page number and limit + const skip = (page - 1) * limit; + + let part; + let aggregateCommand = []; + + if (filter != {}) { + // use filtering if present + aggregateCommand.push({ $match: filter }); + } + + if (property != "") { + aggregateCommand.push({ $group: { _id: `$${property}` } }); // group all same properties + aggregateCommand.push({ $project: { _id: 0, [property]: "$_id" } }); // rename _id to the property name + } else { + aggregateCommand.push({ $project: { image: 0, url: 0 } }); + } + + aggregateCommand.push({ $skip: skip }); + aggregateCommand.push({ $limit: Number(limit) }); + + console.log(aggregateCommand); + + part = await partModel.aggregate(aggregateCommand); + + logger.trace( + `List of parts (Page ${page}, Limit ${limit}, Property ${property}):`, + part, + ); + res.send(part); + } catch (error) { + logger.error("Error listing parts:", error); + res.status(500).send({ error: error }); + } +}; + +export const getPartRouteHandler = async (req, res) => { + try { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + // Fetch the part with the given remote address + const part = await partModel.findOne({ + _id: id, + }); + + if (!part) { + logger.warn(`Part not found with supplied id.`); + return res.status(404).send({ error: "Print job not found." }); + } + + logger.trace(`Part with ID: ${id}:`, part); + res.send(part); + } catch (error) { + logger.error("Error fetching Part:", error); + res.status(500).send({ error: error.message }); + } +}; + +export const editPartRouteHandler = async (req, res) => { + try { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + // Fetch the part with the given remote address + const part = await partModel.findOne({ _id: id }); + + if (!part) { + // Error handling + logger.warn(`Part not found with supplied id.`); + return res.status(404).send({ error: "Print job not found." }); + } + + logger.trace(`Part with ID: ${id}:`, part); + + try { + const { createdAt, updatedAt, started_at, status, ...updateData } = + req.body; + + const result = await partModel.updateOne( + { _id: id }, + { $set: updateData }, + ); + if (result.nModified === 0) { + logger.error("No Part updated."); + res.status(500).send({ error: "No parts updated." }); + } + } catch (updateError) { + logger.error("Error updating part:", updateError); + res.status(500).send({ error: updateError.message }); + } + res.send("OK"); + } catch (fetchError) { + logger.error("Error fetching part:", fetchError); + res.status(500).send({ error: fetchError.message }); + } +}; + +export const newPartRouteHandler = async (req, res) => { + try { + let { ...newPart } = req.body; + newPart = { ...newPart, createdAt: new Date(), updatedAt: new Date() }; + + const result = await partModel.create(newPart); + if (result.nCreated === 0) { + logger.error("No part created."); + res.status(500).send({ error: "No part created." }); + } + res.status(200).send(result); + } catch (updateError) { + logger.error("Error updating part:", updateError); + res.status(500).send({ error: updateError.message }); + } +}; + +export const uploadPartFileContentRouteHandler = async (req, res) => { + try { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + // Fetch the part with the given id + const part = await partModel.findOne({ _id: id }); + if (!part) { + // Error handling + logger.warn(`Part not found with supplied id.`); + return res.status(404).send({ error: "Print job not found." }); + } + logger.trace(`Part with ID: ${id}`); + try { + partUpload(req, res, async (err) => { + if (err) { + res.status(500).send({ + error: err, + }); + } else { + if (req.file == undefined) { + res.send({ + message: "No file selected!", + }); + } else { + res.send({ + status: "OK", + file: `${req.file.filename}`, + }); + } + } + }); + } catch (updateError) { + logger.error("Error updating part:", updateError); + res.status(500).send({ error: updateError.message }); + } + } catch (fetchError) { + logger.error("Error fetching part:", fetchError); + res.status(500).send({ error: fetchError.message }); + } +}; + +export const getPartFileContentRouteHandler = async (req, res) => { + try { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + // Fetch the part with the given remote address + const part = await partModel.findOne({ + _id: id, + }); + + if (!part) { + logger.warn(`Part not found with supplied id.`); + return res.status(404).send({ error: "Part not found." }); + } + + logger.trace(`Returning part file contents with ID: ${id}:`); + + const filePath = path.join(process.env.PART_STORAGE, id + ".stl"); + + // Read the file + fs.readFile(filePath, "utf8", (err, data) => { + if (err) { + if (err.code === "ENOENT") { + // File not found + return res.status(404).send({ error: "File not found!" }); + } else { + // Other errors + return res.status(500).send({ error: "Error reading file." }); + } + } + + // Send the file contents in the response + res.send(data); + }); + } catch (error) { + logger.error("Error fetching Part:", error); + res.status(500).send({ error: error.message }); + } +}; diff --git a/src/services/products/index.js b/src/services/products/index.js new file mode 100644 index 0000000..fd45cde --- /dev/null +++ b/src/services/products/index.js @@ -0,0 +1,135 @@ +import dotenv from "dotenv"; +import { productModel } from "../../schemas/product.schema.js"; +import log4js from "log4js"; +import mongoose from "mongoose"; + +dotenv.config(); + +const logger = log4js.getLogger("Products"); +logger.level = process.env.LOG_LEVEL; + +export const listProductsRouteHandler = async ( + req, + res, + page = 1, + limit = 25, + property = "", + filter = {}, +) => { + try { + // Calculate the skip value based on the page number and limit + const skip = (page - 1) * limit; + + let product; + let aggregateCommand = []; + + if (filter != {}) { + // use filtering if present + aggregateCommand.push({ $match: filter }); + } + + if (property != "") { + aggregateCommand.push({ $group: { _id: `$${property}` } }); // group all same properties + aggregateCommand.push({ $project: { _id: 0, [property]: "$_id" } }); // rename _id to the property name + } else { + aggregateCommand.push({ $project: { image: 0, url: 0 } }); + } + + aggregateCommand.push({ $skip: skip }); + aggregateCommand.push({ $limit: Number(limit) }); + + console.log(aggregateCommand); + + product = await productModel.aggregate(aggregateCommand); + + logger.trace( + `List of products (Page ${page}, Limit ${limit}, Property ${property}):`, + product, + ); + res.send(product); + } catch (error) { + logger.error("Error listing products:", error); + res.status(500).send({ error: error }); + } +}; + +export const getProductRouteHandler = async (req, res) => { + try { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + // Fetch the product with the given remote address + const product = await productModel.findOne({ + _id: id, + }); + + if (!product) { + logger.warn(`Product not found with supplied id.`); + return res.status(404).send({ error: "Print job not found." }); + } + + logger.trace(`Product with ID: ${id}:`, product); + res.send(product); + } catch (error) { + logger.error("Error fetching Product:", error); + res.status(500).send({ error: error.message }); + } +}; + +export const editProductRouteHandler = async (req, res) => { + try { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + // Fetch the product with the given remote address + const product = await productModel.findOne({ _id: id }); + + if (!product) { + // Error handling + logger.warn(`Product not found with supplied id.`); + return res.status(404).send({ error: "Print job not found." }); + } + + logger.trace(`Product with ID: ${id}:`, product); + + try { + const { createdAt, updatedAt, started_at, status, ...updateData } = + req.body; + + const result = await productModel.updateOne( + { _id: id }, + { $set: updateData }, + ); + if (result.nModified === 0) { + logger.error("No Product updated."); + res.status(500).send({ error: "No products updated." }); + } + } catch (updateError) { + logger.error("Error updating product:", updateError); + res.status(500).send({ error: updateError.message }); + } + res.send("OK"); + } catch (fetchError) { + logger.error("Error fetching product:", fetchError); + res.status(500).send({ error: fetchError.message }); + } +}; + +export const newProductRouteHandler = async (req, res) => { + try { + let { ...newProduct } = req.body; + newProduct = { + ...newProduct, + createdAt: new Date(), + updatedAt: new Date(), + }; + + const result = await productModel.create(newProduct); + if (result.nCreated === 0) { + logger.error("No product created."); + res.status(500).send({ error: "No product created." }); + } + res.status(200).send(result); + } catch (updateError) { + logger.error("Error updating product:", updateError); + res.status(500).send({ error: updateError.message }); + } +}; diff --git a/src/services/spotlight/index.js b/src/services/spotlight/index.js new file mode 100644 index 0000000..0f3e920 --- /dev/null +++ b/src/services/spotlight/index.js @@ -0,0 +1,113 @@ +import dotenv from "dotenv"; +import { printJobModel } from "../../schemas/printjob.schema.js"; +import { printSubJobModel } from "../../schemas/printsubjob.schema.js"; +import log4js from "log4js"; +import { printerModel } from "../../schemas/printer.schema.js"; +import { filamentModel } from "../../schemas/filament.schema.js"; +import { gcodeFileModel } from "../../schemas/gcodefile.schema.js"; + +dotenv.config(); + +const logger = log4js.getLogger("PrintJobs"); +logger.level = process.env.LOG_LEVEL; + +const formatPrintersResponse = (printers) => { + return printers.map((printer) => ({ + id: printer.id, + name: printer.printerName, + link: `/production/printers/info?printerId=${printer.id}`, + printer: printer, + })); +}; + +const formatJobsResponse = (jobs) => { + return jobs.map((job) => ({ + id: job.id, + name: job.gcodeFile.name, + link: `/production/printjobs/info?printJobId=${job.id}`, + job: job, + })); +}; + +const formatFilamentsResponse = (filaments) => { + return filaments.map((filament) => ({ + id: filament.id, + name: filament.name, + link: `/management/filaments/info?filamentId=${filament.id}`, + filament: filament, + })); +}; + +const formatGCodeFilesResponse = (gcodeFiles) => { + return gcodeFiles.map((gcodeFile) => ({ + id: gcodeFile.id, + name: gcodeFile.name, + link: `/management/gcodefiles/info?gcodeFileId=${gcodeFile.id}`, + gcodeFile: gcodeFile, + })); +}; + +export const getSpotlightRouteHandler = async (req, res) => { + try { + const query = req.params.query; + if (query.length <= 4) { + res.status(200).send([]); + return; + } + const prefix = query.substring(0, 3); + const delimiter = query.substring(3, 4); + const suffix = query.substring(4); + + if (delimiter == ":") { + switch (prefix) { + case "PRN": + const printer = await printerModel.findOne({ id: suffix }); + if (!printer) { + res.status(404).send({ error: "Job not found" }); + } else { + res.status(200).send(formatPrintersResponse([printer])); + } + break; + case "JOB": + const job = await printJobModel + .findOne({ _id: suffix }) + .populate("gcodeFile", "name"); + if (!job) { + res.status(404).send({ error: "Job not found" }); + } else { + res.status(200).send(formatJobsResponse([job])); + } + break; + case "FIL": + const filament = await filamentModel.findOne({ _id: suffix }); + if (!filament) { + res.status(404).send({ error: "Filament not found" }); + } else { + res.status(200).send(formatFilamentsResponse([filament])); + } + break; + case "GCF": + const gcodeFile = await gcodeFileModel.findOne({ _id: suffix }); + if (!gcodeFile) { + res.status(404).send({ error: "Filament not found" }); + } else { + res.status(200).send(formatGCodeFilesResponse([gcodeFile])); + } + break; + case "SBJ": + const subJob = await printSubJobModel.findOne({ id: suffix }); + if (!subJob) { + res.status(404).send({ error: "SubJob not found" }); + } else { + res.status(200).send([subJob]); + } + break; + default: + res.status(400).send({ error: "Invalid prefix" }); + } + } + } catch (error) { + logger.error("Error listing print jobs:", error); + res.status(500).send({ error: error }); + } +}; diff --git a/src/services/vendors/index.js b/src/services/vendors/index.js new file mode 100644 index 0000000..f9bb36a --- /dev/null +++ b/src/services/vendors/index.js @@ -0,0 +1,132 @@ +import dotenv from "dotenv"; +import { vendorModel } from "../../schemas/vendor.schema.js"; +import jwt from "jsonwebtoken"; +import log4js from "log4js"; +import mongoose from "mongoose"; + +dotenv.config(); + +const logger = log4js.getLogger("Vendors"); +logger.level = process.env.LOG_LEVEL; + +export const listVendorsRouteHandler = async ( + req, + res, + page = 1, + limit = 25, + property = "", + filter = {}, +) => { + try { + // Calculate the skip value based on the page number and limit + const skip = (page - 1) * limit; + + let vendor; + let aggregateCommand = []; + + if (filter != {}) { + // use filtering if present + aggregateCommand.push({ $match: filter }); + } + + if (property != "") { + aggregateCommand.push({ $group: { _id: `$${property}` } }); // group all same properties + aggregateCommand.push({ $project: { _id: 0, [property]: "$_id" } }); // rename _id to the property name + } else { + aggregateCommand.push({ $project: { image: 0, url: 0 } }); + } + + aggregateCommand.push({ $skip: skip }); + aggregateCommand.push({ $limit: Number(limit) }); + + console.log(aggregateCommand); + + vendor = await vendorModel.aggregate(aggregateCommand); + + logger.trace( + `List of vendors (Page ${page}, Limit ${limit}, Property ${property}):`, + vendor, + ); + res.send(vendor); + } catch (error) { + logger.error("Error listing vendors:", error); + res.status(500).send({ error: error }); + } +}; + +export const getVendorRouteHandler = async (req, res) => { + try { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + // Fetch the vendor with the given remote address + const vendor = await vendorModel.findOne({ + _id: id, + }); + + if (!vendor) { + logger.warn(`Vendor not found with supplied id.`); + return res.status(404).send({ error: "Print job not found." }); + } + + logger.trace(`Vendor with ID: ${id}:`, vendor); + res.send(vendor); + } catch (error) { + logger.error("Error fetching Vendor:", error); + res.status(500).send({ error: error.message }); + } +}; + +export const editVendorRouteHandler = async (req, res) => { + try { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + // Fetch the vendor with the given remote address + const vendor = await vendorModel.findOne({ _id: id }); + + if (!vendor) { + // Error handling + logger.warn(`Vendor not found with supplied id.`); + return res.status(404).send({ error: "Print job not found." }); + } + + logger.trace(`Vendor with ID: ${id}:`, vendor); + + try { + const { createdAt, updatedAt, started_at, status, ...updateData } = + req.body; + + const result = await vendorModel.updateOne( + { _id: id }, + { $set: updateData }, + ); + if (result.nModified === 0) { + logger.error("No Vendor updated."); + res.status(500).send({ error: "No vendors updated." }); + } + } catch (updateError) { + logger.error("Error updating vendor:", updateError); + res.status(500).send({ error: updateError.message }); + } + res.send("OK"); + } catch (fetchError) { + logger.error("Error fetching vendor:", fetchError); + res.status(500).send({ error: fetchError.message }); + } +}; + +export const newVendorRouteHandler = async (req, res) => { + try { + let { ...newVendor } = req.body; + newVendor = { ...newVendor, createdAt: new Date(), updatedAt: new Date() }; + + const result = await vendorModel.create(newVendor); + if (result.nCreated === 0) { + logger.error("No vendor created."); + res.status(500).send({ error: "No vendor created." }); + } + res.status(200).send({ status: "ok" }); + } catch (updateError) { + logger.error("Error updating vendor:", updateError); + res.status(500).send({ error: updateError.message }); + } +};