Added missing files

This commit is contained in:
Tom Butcher 2025-05-09 22:19:35 +01:00
parent d3f91d302a
commit 29d17bb0f9
20 changed files with 1435 additions and 0 deletions

127
src/keycloak.js Normal file
View File

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

View File

@ -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;

View File

@ -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;

55
src/routes/parts/index.js Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

45
src/routes/vendors/index.js vendored Normal file
View File

@ -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;

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
);

View File

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

View File

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

View File

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

View File

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

252
src/services/parts/index.js Normal file
View File

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

View File

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

View File

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

132
src/services/vendors/index.js vendored Normal file
View File

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