Initial Commit

This commit is contained in:
Tom Butcher 2024-07-28 18:04:18 +01:00
commit 204964a44c
36 changed files with 13741 additions and 0 deletions

9
.env.example Normal file
View File

@ -0,0 +1,9 @@
DB_LINK="mongo-link-to-connect"
JWT_SECRET="token"
APP_URL_CLIENT=https://material-dashboard-react-node.creative-tim.com
APP_URL_API=https://node-json-api-free.creative-tim.com/login
MAILTRAP_USER=
MAILTRAP_PASSWORD=

134
.gitignore vendored Normal file
View File

@ -0,0 +1,134 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
.DS_STORE
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.nova

BIN
images/admin.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
images/creator.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

BIN
images/member.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

86
index.js Normal file
View File

@ -0,0 +1,86 @@
import bcrypt from "bcrypt";
import dotenv from "dotenv";
import { userModel } from "../../schemas/user.schema.js";
import { printerModel } from "../../schemas/printer.schema.js";
import jwt from "jsonwebtoken";
import log4js from "log4js";
dotenv.config();
const logger = log4js.getLogger("Printers");
logger.level = process.env.LOG_LEVEL;
export const listPrintersRouteHandler = async (
req,
res,
page = 1,
limit = 25
) => {
try {
// Calculate the skip value based on the page number and limit
const skip = (page - 1) * limit;
// Fetch users with pagination
const printers = await printerModel.find().skip(skip).limit(limit);
logger.trace(`List of printers (Page ${page}, Limit ${limit}):`);
res.send(printers);
} catch (error) {
logger.error("Error listing users:", error);
res.status(500).send({ error: error });
}
};
export const getPrinterRouteHandler = async (req, res) => {
const remoteAddress = req.params.remoteAddress;
try {
// Fetch the printer with the given remote address
const printer = await printerModel.findOne({ remoteAddress });
if (!printer) {
logger.warn(`Printer with remote address ${remoteAddress} not found.`);
return res.status(404).send({ error: "Printer not found" });
}
logger.trace(`Printer with remote address ${remoteAddress}:`, printer);
res.send(printer);
} catch (error) {
logger.error("Error fetching printer:", error);
res.status(500).send({ error: error.message });
}
};
export const editPrinterRouteHandler = async (req, res) => {
const remoteAddress = req.params.remoteAddress;
const { friendlyName } = req.body;
try {
// Fetch the printer with the given remote address
const printer = await printerModel.findOne({ remoteAddress });
if (!printer) {
logger.warn(`Printer with remote address ${remoteAddress} not found.`);
return res.status(404).send({ error: "Printer not found" });
}
logger.trace(`Editing printer with remote address ${remoteAddress}:`, printer);
try {
const result = await printerModel.updateOne(
{ remoteAddress: remoteAddress },
{ $set: req.body }
);
if (result.nModified === 0) {
logger.error("No printers updated.");
res.status(500).send({ error: "No printers updated." });
}
} catch (updateError) {
logger.error("Error updating printer:", updateError);
res.status(500).send({ error: updateError.message });
}
res.send("OK");
} catch (fetchError) {
logger.error("Error fetching printer:", fetchError);
res.status(500).send({ error: fetchError.message });
}
};

11960
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
package.json Normal file
View File

@ -0,0 +1,53 @@
{
"name": "node-json-api-free",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"@simplewebauthn/server": "^10.0.0",
"@tremor/react": "^3.17.2",
"antd": "*",
"bcrypt": "*",
"body-parser": "*",
"cors": "^2.8.5",
"dotenv": "*",
"express": "*",
"express-session": "^1.18.0",
"jsonwebtoken": "*",
"log4js": "^6.9.1",
"mongodb": "*",
"mongoose": "*",
"mongoose-sequence": "^6.0.1",
"mysql": "^2.18.1",
"mysql2": "^2.3.3",
"node-cron": "^3.0.2",
"nodemailer": "*",
"nodemon": "^2.0.16",
"passport": "*",
"passport-jwt": "*",
"passport-local": "*",
"pg": "^8.7.3",
"random-token": "*",
"sequelize": "^6.20.1"
},
"type": "module",
"devDependencies": {
"@babel/cli": "^7.17.10",
"@babel/core": "^7.18.5",
"@babel/node": "^7.18.5",
"@babel/plugin-proposal-class-properties": "^7.17.12",
"@babel/plugin-proposal-object-rest-spread": "^7.18.0",
"@babel/preset-env": "^7.18.2",
"@babel/register": "^7.17.7",
"sequelize-cli": "^6.4.1",
"standard": "^17.1.0"
},
"scripts": {
"start:dev": "nodemon --exec babel-node --experimental-specifier-resolution=node src/index.js",
"test": "echo \"Error: no test specified\" && exit 1",
"seed": "node src/mongo/seedData.js",
"clear": "node src/mongo/clearDbs.js"
},
"author": "",
"license": "ISC"
}

60
src/index.js Normal file
View File

@ -0,0 +1,60 @@
import express from "express";
import bodyParser from "body-parser";
import cors from "cors";
import dotenv from "dotenv";
import "./passport.js";
import { dbConnect } from "./mongo/index.js";
import { apiRoutes, authRoutes, printerRoutes, printJobRoutes, gcodeFileRoutes, fillamentRoutes } from "./routes/index.js";
import path from "path";
import * as fs from "fs";
import cron from "node-cron";
import ReseedAction from "./mongo/ReseedAction.js";
import log4js from "log4js";
dotenv.config();
const PORT = process.env.PORT || 8080;
const app = express();
const logger = log4js.getLogger("App");
logger.level = process.env.LOG_LEVEL;
app.use(log4js.connectLogger(logger, { level: "trace" }));
const whitelist = [process.env.APP_URL_CLIENT];
const corsOptions = {
origin: function (origin, callback) {
if (!origin || whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
credentials: true,
};
dbConnect();
app.use(cors(corsOptions));
app.use(bodyParser.json({ type: "application/json", strict: false, limit: '50mb' }));
app.use(express.json());
app.get("/", function (req, res) {
const __dirname = fs.realpathSync(".");
res.sendFile(path.join(__dirname, "/src/landing/index.html"));
});
app.use("/auth", authRoutes);
app.use("/overview", apiRoutes);
app.use("/printers", printerRoutes);
app.use("/printjobs", printJobRoutes);
app.use("/gcodefiles", gcodeFileRoutes);
app.use("/fillaments", fillamentRoutes);
if (process.env.SCHEDULE_HOUR) {
cron.schedule(`0 */${process.env.SCHEDULE_HOUR} * * *'`, () => {
ReseedAction();
});
}
app.listen(PORT, () => logger.info(`Server listening to port ${PORT}`));

77
src/landing/index.html Normal file
View File

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Node.js API FREE by Creative Tim & UPDIVISION</title>
<link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet"
/>
<style>
html,
body {
background-color: #fff;
color: #636b6f;
font-family: "Nunito", sans-serif;
font-weight: 200;
height: 100vh;
margin: 0;
}
.full-height {
height: 100vh;
}
.flex-center {
align-items: center;
display: flex;
justify-content: center;
}
.position-ref {
position: relative;
}
.top-right {
position: absolute;
right: 10px;
top: 18px;
}
.content {
text-align: center;
}
.title {
font-size: 84px;
}
.links > a {
color: #636b6f;
padding: 0 25px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.1rem;
text-decoration: none;
text-transform: uppercase;
}
.m-b-md {
margin-bottom: 30px;
}
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
<div class="content">
<div class="title m-b-md">Headless CMS with ExpressJS API:FREE</div>
<div class="links">
<a href="https://expressjs.com/" target="_blank">Express.js</a>
<a href="https://www.mongodb.com/" target="_blank">MongoDB</a>
<a href="https://documenter.getpostman.com/view/8138626/Uze1virp" target="_blank">Documentation</a>
</div>
</div>
</div>
</body>
</html>

36
src/mongo/ReseedAction.js Normal file
View File

@ -0,0 +1,36 @@
import mongoose from "mongoose";
import bcrypt from "bcrypt";
import { userModel } from "../schemas/user.schema.js";
import { dbConnect } from "./index.js";
const ReseedAction = () => {
async function clear() {
dbConnect();
await userModel.deleteMany({});
console.log("DB cleared");
}
async function seedDB() {
await clear();
const salt = await bcrypt.genSalt(10);
const hashPassword = await bcrypt.hash("secret", salt);
const user = {
_id: mongoose.Types.ObjectId(1),
name: "Admin",
email: "admin@jsonapi.com",
password: hashPassword,
created_at: new Date(),
profile_image: "../../images/admin.jpg",
};
const admin = new userModel(user);
await admin.save();
console.log("DB seeded");
}
seedDB();
};
export default ReseedAction;

13
src/mongo/clearDbs.js Normal file
View File

@ -0,0 +1,13 @@
import mongoose from "mongoose";
import { userModel } from "../schemas/user.schema.js";
import { dbConnect } from "../mongo/index.js";
async function clear() {
dbConnect();
await userModel.deleteMany({});
console.log("DB cleared");
}
clear().then(() => {
mongoose.connection.close();
});

18
src/mongo/index.js Normal file
View File

@ -0,0 +1,18 @@
import mongoose from "mongoose";
import dotenv from "dotenv";
import log4js from "log4js";
const logger = log4js.getLogger("MongoDB");
logger.level = process.env.LOG_LEVEL;
dotenv.config();
function dbConnect() {
mongoose.connection.once("open", () => logger.info("Database connected."));
return mongoose.connect(
`mongodb://${process.env.DB_LINK}/farmcontrol?retryWrites=true&w=majority`,
{ }
);
}
export { dbConnect };

42
src/mongo/seedData.js Normal file
View File

@ -0,0 +1,42 @@
import bcrypt from "bcrypt";
import mongoose from "mongoose";
import { userModel } from "../schemas/user.schema.js";
import { printJobModel } from "../schemas/printjob.schema.js";
import { dbConnect } from "../mongo/index.js";
async function seedDB() {
dbConnect();
const salt = await bcrypt.genSalt(10);
const hashPassword = await bcrypt.hash("secret", salt);
const user = {
_id: new mongoose.Types.ObjectId(1),
name: "Admin",
email: "admin@jsonapi.com",
password: hashPassword,
created_at: new Date(),
profile_image: "../../images/admin.jpg",
};
const admin = new userModel(user);
await admin.save();
const printJob = {
_id: new mongoose.Types.ObjectId(1),
status : {
type: "Queued"
},
created_at: new Date(),
updated_at: new Date(),
started_at: new Date(),
};
const newPrintJob = new printJobModel(printJob);
await newPrintJob.save();
console.log("DB seeded");
}
seedDB().then(() => {
mongoose.connection.close();
});

27
src/passport.js Normal file
View File

@ -0,0 +1,27 @@
import { ExtractJwt } from "passport-jwt";
import passportJWT from "passport-jwt";
import dotenv from "dotenv";
import passport from "passport";
import { userModel } from "./schemas/user.schema.js";
const JWTStrategy = passportJWT.Strategy;
dotenv.config();
passport.use(
new JWTStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
},
function (jwtPayload, done) {
return userModel
.findOne({ _id: jwtPayload.id })
.then((user) => {
return done(null, user);
})
.catch((err) => {
return done(err);
});
}
)
);

23
src/routes/api/index.js Normal file
View File

@ -0,0 +1,23 @@
import express from "express";
import passport from "passport";
import jwt from 'jsonwebtoken';
const router = express.Router();
import { getProfileRouteHandler, patchProfileRouteHandler, getDashboardRouteHandler } from "../../services/api/index.js";
// get main dashboard info profile
router.get("/", passport.authenticate('jwt',{session: false}), (req, res) => {
getDashboardRouteHandler(req, res);
});
// get user's profile
router.get("/user", passport.authenticate('jwt',{session: false}), (req, res) => {
getProfileRouteHandler(req, res);
});
// update user's profile
router.patch("/", passport.authenticate('jwt',{session: false}), async (req, res) => {
patchProfileRouteHandler(req, res);
});
export default router;

59
src/routes/auth/index.js Normal file
View File

@ -0,0 +1,59 @@
import express from "express";
import passport from "passport";
import {
getAuthModesHandler,
forgotPasswordRouteHandler,
loginRouteHandler,
registerPasskeyRouteHandler,
loginPasskeyRouteHandler,
registerRouteHandler,
resetPasswordRouteHandler,
validateTokenRouteHandler,
} from "../../services/auth/index.js";
const router = express.Router();
router.post("/modes", async (req, res, next) => {
const { email } = req.body;
await getAuthModesHandler(req, res, email);
});
router.post("/login", async (req, res, next) => {
const { email, password } = req.body;
await loginRouteHandler(req, res, email, password);
});
router.post("/validate-token", async (req, res, next) => {
const { token } = req.body;
await validateTokenRouteHandler(req, res, token);
});
router.post("/logout", (req, res) => {
return res.sendStatus(204);
});
router.post("/register", async (req, res) => {
const { name, email, password } = req.body;
await registerRouteHandler(req, res, name, email, password);
});
router.post("/passkey/register", passport.authenticate('jwt',{session: false}), async (req, res) => {
await registerPasskeyRouteHandler(req, res);
});
router.post("/passkey/login", async (req, res) => {
const { email, attestationResponse } = req.body;
await loginPasskeyRouteHandler(req, res, email, attestationResponse);
});
router.post("/password-forgot", async (req, res) => {
const { email } = req.body;
await forgotPasswordRouteHandler(req, res, email);
});
router.post("/password-reset", async (req, res) => {
await resetPasswordRouteHandler(req, res);
});
export default router;

View File

@ -0,0 +1,47 @@
import express from "express";
import passport from "passport";
import jwt from 'jsonwebtoken';
import { parseStringIfNumber } from '../../util/index.js'
const router = express.Router();
import { listFillamentsRouteHandler, getFillamentRouteHandler, editFillamentRouteHandler, newFillamentRouteHandler } from "../../services/fillaments/index.js";
// list of fillaments
router.get("/", passport.authenticate('jwt',{session: false}), (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);
}
}
}
listFillamentsRouteHandler(req, res, page, limit, property, filter);
});
router.post("/", passport.authenticate('jwt',{session: false}), (req, res) => {
newFillamentRouteHandler(req, res);
});
router.get("/:id", passport.authenticate('jwt',{session: false}), (req, res) => {
getFillamentRouteHandler(req, res);
});
// update printer info
router.put("/:id", passport.authenticate('jwt',{session: false}), async (req, res) => {
editFillamentRouteHandler(req, res);
});
export default router;

View File

@ -0,0 +1,23 @@
import express from "express";
import passport from "passport";
import jwt from 'jsonwebtoken';
const router = express.Router();
import { listGCodeFilesRouteHandler, getGCodeFileRouteHandler, editGCodeFileRouteHandler } from "../../services/gcodefiles/index.js";
// list of printers
router.get("/", passport.authenticate('jwt',{session: false}), (req, res) => {
const { page, limit } = req.body;
listGCodeFilesRouteHandler(req, res, page, limit);
});
router.get("/:id", passport.authenticate('jwt',{session: false}), (req, res) => {
getGCodeFileRouteHandler(req, res);
});
// update printer info
router.put("/:id", passport.authenticate('jwt',{session: false}), async (req, res) => {
editGCodeFileRouteHandler(req, res);
});
export default router;

9
src/routes/index.js Normal file
View File

@ -0,0 +1,9 @@
import userRoutes from './users/index.js';
import apiRoutes from './api/index.js';
import authRoutes from './auth/index.js';
import printerRoutes from './printers/index.js';
import printJobRoutes from './printjobs/index.js';
import gcodeFileRoutes from './gcodefiles/index.js'
import fillamentRoutes from './fillaments/index.js'
export { userRoutes, apiRoutes, authRoutes, printerRoutes, printJobRoutes, gcodeFileRoutes, fillamentRoutes };

View File

@ -0,0 +1,25 @@
import express from "express";
import passport from "passport";
import jwt from 'jsonwebtoken';
const router = express.Router();
import { listPrintersRouteHandler, editPrinterRouteHandler, getPrinterRouteHandler } from "../../services/printers/index.js";
// list of printers
router.get("/", passport.authenticate('jwt',{session: false}), (req, res) => {
const { page, limit } = req.body;
listPrintersRouteHandler(req, res, page, limit);
});
router.get("/:remoteAddress", passport.authenticate('jwt',{session: false}), (req, res) => {
getPrinterRouteHandler(req, res);
});
// update printer info
router.put("/:remoteAddress", passport.authenticate('jwt',{session: false}), async (req, res) => {
editPrinterRouteHandler(req, res);
});
export default router;

View File

@ -0,0 +1,23 @@
import express from "express";
import passport from "passport";
import jwt from 'jsonwebtoken';
const router = express.Router();
import { listPrintJobsRouteHandler, getPrintJobRouteHandler, editPrintJobRouteHandler } from "../../services/printjobs/index.js";
// list of printers
router.get("/", passport.authenticate('jwt',{session: false}), (req, res) => {
const { page, limit } = req.body;
listPrintJobsRouteHandler(req, res, page, limit);
});
router.get("/:jobNumber", passport.authenticate('jwt',{session: false}), (req, res) => {
getPrintJobRouteHandler(req, res);
});
// update printer info
router.put("/:jobNumber", passport.authenticate('jwt',{session: false}), async (req, res) => {
editPrintJobRouteHandler(req, res);
});
export default router;

22
src/routes/users/index.js Normal file
View File

@ -0,0 +1,22 @@
import express from 'express';
const router = express.Router();
router.get('/', (req, res) => {
res.send({
data: [
{
id: 1,
firstName: 'John',
lastName: 'Smith',
},
{
id: 2,
firstName: 'Stacey',
lastName: 'Smith',
},
],
});
});
export default router;

View File

@ -0,0 +1,23 @@
import mongoose from "mongoose";
const fillamentSchema = 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 },
created_at: { required: true, type: Date },
updated_at: { required: true, type: Date },
});
fillamentSchema.virtual("id").get(function () {
return this._id.toHexString();
});
fillamentSchema.set("toJSON", { virtuals: true });
export const fillamentModel = mongoose.model("Fillament", fillamentSchema);

View File

@ -0,0 +1,22 @@
import mongoose from "mongoose";
const { Schema } = mongoose;
const gcodeFileSchema = new mongoose.Schema({
name: { required: true, type: String },
gcodeFileName: { required: true, type: String },
size: { type: Number, required: false },
lines: { type: Number, required: false },
fillament: { type: Schema.Types.ObjectId, ref: 'Fillament', required: true },
image: { type: Buffer, required: false },
printTimeMins: { type: Number, required: false },
created_at: { type: Date },
updated_at: { type: Date },
});
gcodeFileSchema.virtual("id").get(function () {
return this._id.toHexString();
});
gcodeFileSchema.set("toJSON", { virtuals: true });
export const gcodeFileModel = mongoose.model("GCodeFile", gcodeFileSchema);

View File

@ -0,0 +1,15 @@
import mongoose from "mongoose";
const passwordResetSchema = new mongoose.Schema({
email: { required: true, type: String },
token: { required: true, type: String },
created_at: { type: Date },
});
passwordResetSchema.virtual("id").get(function () {
return this._id.toHexString();
});
passwordResetSchema.set("toJSON", { virtuals: true });
export const passwordResetModel = mongoose.model("PasswordReset", passwordResetSchema);

View File

@ -0,0 +1,22 @@
import mongoose from "mongoose";
const printerSchema = new mongoose.Schema({
friendlyName: { required: true, type: String },
online: { required: true, type: Boolean },
status: {
type: { required: true, type: String },
percent: { required: false, type: Number },
},
remoteAddress: { required: true, type: String },
hostId: { required: true, type: String },
connectedAt: { required: true, type: Date },
loadedFillament: { required: true, type: Object }
});
printerSchema.virtual("id").get(function () {
return this._id.toHexString();
});
printerSchema.set("toJSON", { virtuals: true });
export const printerModel = mongoose.model("Printer", printerSchema);

View File

@ -0,0 +1,21 @@
import mongoose from "mongoose";
const { Schema } = mongoose;
const printJobSchema = new mongoose.Schema({
status: {
type: { required: true, type: String },
printer: { type: Schema.Types.ObjectId, ref: 'Printer', required: false },
},
created_at: { required: true, type: Date },
updated_at: { required: true, type: Date },
started_at: { required: true, type: Date },
gcode_file: { type: Schema.Types.ObjectId, ref: 'GCodeFile', required: false }
});
printJobSchema.virtual("id").get(function () {
return this._id.toHexString();
});
printJobSchema.set("toJSON", { virtuals: true });
export const printJobModel = mongoose.model("PrintJob", printJobSchema);

View File

@ -0,0 +1,28 @@
import { Binary } from "mongodb";
import mongoose from "mongoose";
const userSchema = new mongoose.Schema({
name: { required: true, type: String },
email: { required: true, type: String },
email_verified_at: { type: Date },
password: { required: true, type: String },
webAuthnCredentials: [{
id: String,
publicKey: Buffer,
counter: Number,
deviceType: String,
backedUp: Boolean,
transports: [String]
}],
profile_image: { type: String },
created_at: { type: Date },
updated_at: { type: Date },
});
userSchema.virtual("id").get(function () {
return this._id.toHexString();
});
userSchema.set("toJSON", { virtuals: true });
export const userModel = mongoose.model("User", userSchema);

92
src/services/api/index.js Normal file
View File

@ -0,0 +1,92 @@
import bcrypt from "bcrypt";
import dotenv from 'dotenv';
import { userModel } from "../../schemas/user.schema.js";
import jwt from 'jsonwebtoken';
dotenv.config();
export const getDashboardRouteHandler = (req, res) => {
const sentData = {
data: {}
}
res.send(sentData);
}
export const getProfileRouteHandler = (req, res) => {
const meUser = req.user;
const stringId = req.user.id;
const decId = stringId.substring(4, 8);
const intId = parseInt(decId, 16);
const sentData = {
data: {
type: 'users',
id: intId === 1 ? intId : meUser.id,
attributes: {
name: meUser.name,
email: meUser.email,
profile_image: null,
createdAt: meUser.createdAt,
updateAt: meUser.updateAt
},
links: {
self: `${process.env.APP_URL_API}/users/${meUser.id}`
}
}
}
res.send(sentData);
}
export const patchProfileRouteHandler = async (req, res) => {
const currentDataOfUser = req.user;
const { name, email, newPassword, confirmPassword } = req.body.data.attributes;
const foundUser = await userModel.findOne({ email: currentDataOfUser.email});
if (!foundUser) {
res.status(400).json({error: 'No user matches the credentials'});
} else {
// check password more than 8 characters, new password matched the password confirmation
if (newPassword && newPassword < 7 || newPassword != confirmPassword) {
res.status(400).json({errors: { password: ["The password should have at lest 8 characters and match the password confirmation."] }});
} else if (newPassword && newPassword > 7 && newPassword == confirmPassword) {
const salt = await bcrypt.genSalt(10);
const hashPassword = await bcrypt.hash(newPassword, salt);
try{
await userModel.updateOne( { email: foundUser.email }, { $set :{ "name": name, "email": email, "password": hashPassword } });
} catch(err) {
console.error(err);
}
const sentData = {
data: {
type: 'users',
id: foundUser.id,
attributes: {
name: name,
email: email,
profile_image: null,
}
}
}
res.send(sentData);
} else if (!newPassword) {
try {
await userModel.updateOne( { email: foundUser.email }, { $set :{ "name": name, "email": email } });
} catch(err) {
console.error(err);
}
const sentData = {
data: {
type: 'users',
id: foundUser.id,
attributes: {
name: name,
email: email,
profile_image: null,
}
}
}
res.send(sentData);
}
}
}

376
src/services/auth/index.js Normal file
View File

@ -0,0 +1,376 @@
import dotenv from "dotenv";
import nodemailer from "nodemailer";
import randomToken from "random-token";
import bcrypt from "bcrypt";
import url from "url";
import { userModel } from "../../schemas/user.schema.js";
import { passwordResetModel } from "../../schemas/passwordResets.schema.js";
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse,
} from "@simplewebauthn/server";
import { isoUint8Array } from "@simplewebauthn/server/helpers";
import jwt from "jsonwebtoken";
import log4js from "log4js";
const logger = log4js.getLogger("Auth");
logger.level = process.env.LOG_LEVEL;
dotenv.config();
let challenges = {};
const rpName = "Farm Control";
const rpID = url.parse(process.env.APP_URL_CLIENT).host;
const origin = `https://${rpID}`;
const transporter = nodemailer.createTransport({
host: "smtp.mailtrap.io",
port: 2525,
auth: {
user: process.env.MAILTRAP_USER,
pass: process.env.MAILTRAP_PASSWORD,
},
});
function generateToken() {
}
export const getAuthModesHandler = async (req, res, email) => {
let foundUser = await userModel.findOne({ email: email });
if (foundUser == null) {
return res.status(400).json({
error: "Invalid email address.",
});
}
if (foundUser.webAuthnCredentials.length > 0) {
return res.status(200).json({
authModes: ["password", "passkey"],
});
} else {
return res.status(200).json({
authModes: ["password"],
});
}
};
export const loginRouteHandler = async (req, res, email, password) => {
//Check If User Exists
let foundUser = await userModel.findOne({ email: email });
if (foundUser == null) {
return res.status(400).json({
error: "Invalid credentials.",
});
} else {
const validPassword = await bcrypt.compare(password, foundUser.password);
if (validPassword) {
// Generate JWT token
const token = jwt.sign(
{ id: foundUser.id, email: foundUser.email },
process.env.JWT_SECRET,
{
expiresIn: "24h",
}
);
return res.json({
user: {
id: foundUser.id,
name: foundUser.name,
email: foundUser.email,
},
access_token: token,
});
} else {
return res.status(400).json({
error: "Invalid credentials.",
});
}
}
};
export const validateTokenRouteHandler = async (req, res, token) => {
try {
jwt.verify(token, process.env.JWT_SECRET);
res.status(200).send({
status: "OK",
});
} catch (err) {
console.error("Token verification error:", err);
res.status(401).send("Invalid token");
}
};
export const registerPasskeyRouteHandler = async (req, res) => {
// check to see if the request has provided a user
const user = req.user;
if (!user) {
// if no user exists
return res.status(400).json({ error: "User not specified." });
}
if (req.body.token) {
const options = await generateRegistrationOptions({
rpName: rpName,
rpID: rpID,
userName: user.email,
userDisplayName: user.name,
excludeCredentials: user.webAuthnCredentials.map(
(webAuthnCredential) => ({
id: webAuthnCredential.id,
transports: webAuthnCredential.transports,
})
),
attestationType: "none",
authenticatorSelection: {
residentKey: "preferred",
userVerification: "preferred",
authenticatorAttachment: "platform",
},
});
challenges[user.id] = options.challenge;
return res.status(200).send(options);
}
const expectedChallenge = challenges[user.id];
const attestationResponse = req.body;
let verification;
try {
verification = await verifyRegistrationResponse({
response: attestationResponse,
expectedChallenge,
expectedOrigin: process.env.APP_URL_CLIENT,
expectedRPID: url.parse(process.env.APP_URL_CLIENT).host,
});
const { registrationInfo } = verification;
const {
credentialID,
credentialPublicKey,
counter,
credentialDeviceType,
credentialBackedUp,
} = registrationInfo;
const webAuthnCredential = {
id: credentialID,
publicKey: Buffer.from(new Uint8Array(credentialPublicKey)),
counter,
deviceType: credentialDeviceType,
backedUp: credentialBackedUp,
transports: attestationResponse.response.transports,
};
console.log(webAuthnCredential);
user.webAuthnCredentials.push(webAuthnCredential);
await user.save();
res.status(200).send({ status: "OK" });
} catch (error) {
console.log(error);
return res.status(400).json({ error: error.message });
}
if (verification.verified) {
} else {
res.status(400).send({ error: "Not verified." });
}
};
export const loginPasskeyRouteHandler = async (
req,
res,
email,
attestationResponse
) => {
if (!email) {
return;
}
let user = await userModel.findOne({ email: email });
if (user == null) {
return res.status(400).json({
error: "Invalid email address.",
});
}
if (attestationResponse) {
logger.info("Verfifying challenge...");
const expectedChallenge = challenges[user.id];
let verification;
try {
const webAuthnCredentialIndex = user.webAuthnCredentials.findIndex(
(cred) => cred.id === attestationResponse.id
);
const webAuthnCredential = user.webAuthnCredentials[webAuthnCredentialIndex];
verification = await verifyAuthenticationResponse({
response: attestationResponse,
expectedChallenge,
expectedOrigin: process.env.APP_URL_CLIENT,
expectedRPID: url.parse(process.env.APP_URL_CLIENT).host,
authenticator: {
credentialID: webAuthnCredential.id,
credentialPublicKey: new Uint8Array(webAuthnCredential.publicKey),
counter: webAuthnCredential.counter,
transports: webAuthnCredential.transports,
},
});
user.webAuthnCredentials[webAuthnCredentialIndex].counter = verification.authenticationInfo.newCounter; // Update connection counter
await user.save();
// Generate JWT token
const token = jwt.sign(
{ id: user.id, email: user.email },
process.env.JWT_SECRET,
{
expiresIn: "24h",
}
);
return res.json({
user: {
id: user.id,
name: user.name,
email: user.email,
},
access_token: token,
});
} catch (error) {
console.log(error);
res.status(400).send({ error });
}
} else {
// Get options
logger.info("Sending authentication options...");
const options = await generateAuthenticationOptions({
rpID: url.parse(process.env.APP_URL_CLIENT).host,
allowCredentials: user.webAuthnCredentials.map((cred) => ({
id: cred.id,
type: "public-key",
transports: cred.transports,
})),
});
challenges[user.id] = options.challenge;
res.status(200).send(options);
}
};
export const registerRouteHandler = async (req, res, name, email, password) => {
// check if user already exists
let foundUser = await userModel.findOne({ email: email });
if (foundUser) {
// does not get the error
return res.status(400).json({ message: "Email is already in use" });
}
// check password to exist and be at least 8 characters long
if (!password || password.length < 8) {
return res
.status(400)
.json({ message: "Password must be at least 8 characters long." });
}
// hash password to save in db
const salt = await bcrypt.genSalt(10);
const hashPassword = await bcrypt.hash(password, salt);
const newUser = new userModel({
name: name,
email: email,
password: hashPassword,
});
await newUser.save();
// Generate JWT token
const token = jwt.sign({ id: newUser.id, email: newUser.email }, "token", {
expiresIn: "24h",
});
return res.status(200).json({
token_type: "Bearer",
expires_in: "24h",
access_token: token,
refresh_token: token,
});
};
export const forgotPasswordRouteHandler = async (req, res, email) => {
let foundUser = await userModel.findOne({ email: email });
if (!foundUser) {
return res.status(400).json({
errors: { email: ["The email does not match any existing user."] },
});
} else {
let token = randomToken(20);
// send mail with defined transport object
let info = await transporter.sendMail({
from: "admin@jsonapi.com", // sender address
to: email, // list of receivers
subject: "Reset Password", // Subject line
html: `<p>You requested to change your password.If this request was not made by you please contact us. Access <a href='${process.env.APP_URL_CLIENT}/auth/reset-password?token=${token}&email=${email}'>this link</a> to reste your password </p>`, // html body
});
const dataSent = {
data: "password-forgot",
attributes: {
redirect_url: `${process.env.APP_URL_API}/password-reset`,
email: email,
},
};
// save token in db
await passwordResetModel.create({
email: foundUser.email,
token: token,
created_at: new Date(),
});
return res.status(204).json(dataSent);
}
};
export const resetPasswordRouteHandler = async (req, res) => {
const foundUser = await userModel.findOne({
email: req.body.data.attributes.email,
});
if (!foundUser || !foundToken) {
return res.status(400).json({
errors: {
email: ["The email or token does not match any existing user."],
},
});
} else {
const { password, password_confirmation } = req.body.data.attributes;
// validate password
if (password.length < 8) {
return res.status(400).json({
errors: {
password: ["The password should have at lest 8 characters."],
},
});
}
if (password != password_confirmation) {
return res.status(400).json({
errors: {
password: ["The password and password confirmation must match."],
},
});
}
const salt = await bcrypt.genSalt(10);
const hashPassword = await bcrypt.hash(password, salt);
await passwordResetModel.deleteOne({ email: foundUser.email });
await userModel.updateOne(
{ email: foundUser.email },
{ $set: { password: hashPassword } }
);
return res.sendStatus(204);
}
};

View File

@ -0,0 +1,121 @@
import dotenv from "dotenv";
import { fillamentModel } from "../../schemas/fillament.schema.js"
import jwt from "jsonwebtoken";
import log4js from "log4js";
import mongoose from "mongoose";
dotenv.config();
const logger = log4js.getLogger("Fillaments");
logger.level = process.env.LOG_LEVEL;
export const listFillamentsRouteHandler = 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 fillament;
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)
fillament = await fillamentModel.aggregate(aggregateCommand)
logger.trace(`List of filaments (Page ${page}, Limit ${limit}, Property ${property}):`, fillament);
res.send(fillament);
} catch (error) {
logger.error("Error listing filaments:", error);
res.status(500).send({ error: error });
}
};
export const getFillamentRouteHandler = async (req, res) => {
try {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
// Fetch the fillament with the given remote address
const fillament = await fillamentModel.findOne({
_id: id
});
if (!fillament) {
logger.warn(`Fillament not found with supplied id.`);
return res.status(404).send({ error: "Print job not found." });
}
logger.trace(`Fillament with ID: ${id}:`, fillament);
res.send(fillament);
} catch (error) {
logger.error("Error fetching Fillament:", error);
res.status(500).send({ error: error.message });
}
};
export const editFillamentRouteHandler = async (req, res) => {
try {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
// Fetch the fillament with the given remote address
const fillament = await fillamentModel.findOne({ _id: id });
if (!fillament) { // Error handling
logger.warn(`Fillament not found with supplied id.`);
return res.status(404).send({ error: "Print job not found." });
}
logger.trace(`Fillament with ID: ${id}:`, fillament);
try {
const { created_at, updated_at, started_at, status, ...updateData } = req.body;
const result = await fillamentModel.updateOne(
{ _id: id },
{ $set: updateData }
);
if (result.nModified === 0) {
logger.error("No Fillament updated.");
res.status(500).send({ error: "No fillaments updated." });
}
} catch (updateError) {
logger.error("Error updating fillament:", updateError);
res.status(500).send({ error: updateError.message });
}
res.send("OK");
} catch (fetchError) {
logger.error("Error fetching fillament:", fetchError);
res.status(500).send({ error: fetchError.message });
}
};
export const newFillamentRouteHandler = async (req, res) => {
try {
let { ...newFillament } = req.body;
newFillament = { ...newFillament, created_at: new Date(), updated_at: new Date() }
const result = await fillamentModel.create(newFillament);
if (result.nCreated === 0) {
logger.error("No fillament created.");
res.status(500).send({ error: "No fillament created." });
}
res.status(200).send({ status: "ok" });
} catch (updateError) {
logger.error("Error updating fillament:", updateError);
res.status(500).send({ error: updateError.message });
}
};

View File

@ -0,0 +1,93 @@
import dotenv from "dotenv";
import { gcodeFileModel } from "../../schemas/gcodefile.schema.js"
import jwt from "jsonwebtoken";
import log4js from "log4js";
dotenv.config();
const logger = log4js.getLogger("GCodeFiles");
logger.level = process.env.LOG_LEVEL;
export const listGCodeFilesRouteHandler = async (
req,
res,) => {
try {
// Fetch gcode files and group
const gcodeFiles = await gcodeFileModel.aggregate([
{
$group: {
_id: "$status",
totalQuantity: { $sum: "$quantity" },
totalPrice: { $sum: "$price" },
orders: { $push: "$$ROOT" }
}
}
]);
logger.trace(`List of print jobs (Page ${page}, Limit ${limit}):`);
res.send(gcodeFile);
} catch (error) {
logger.error("Error listing print jobs:", error);
res.status(500).send({ error: error });
}
};
export const getGCodeFileRouteHandler = async (req, res) => {
try {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
// Fetch the gcodeFile with the given remote address
const gcodeFile = await gcodeFileModel.findOne({
_id: id
});
if (!gcodeFile) {
logger.warn(`GCodeFile not found with supplied id.`);
return res.status(404).send({ error: "Print job not found." });
}
logger.trace(`GCodeFile with ID: ${id}:`, gcodeFile);
res.send(gcodeFile);
} catch (error) {
logger.error("Error fetching GCodeFile:", error);
res.status(500).send({ error: error.message });
}
};
export const editGCodeFileRouteHandler = async (req, res) => {
try {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
// Fetch the gcodeFile with the given remote address
const gcodeFile = await gcodeFileModel.findOne({ _id: id });
if (!gcodeFile) { // Error handling
logger.warn(`GCodeFile not found with supplied id.`);
return res.status(404).send({ error: "Print job not found." });
}
logger.trace(`GCodeFile with ID: ${id}:`, gcodeFile);
try {
const { created_at, updated_at, started_at, status, ...updateData } = req.body;
const result = await gcodeFileModel.updateOne(
{ _id: id },
{ $set: updateData }
);
if (result.nModified === 0) {
logger.error("No gcodeFile updated.");
res.status(500).send({ error: "No gcodeFiles updated." });
}
} catch (updateError) {
logger.error("Error updating gcodeFile:", updateError);
res.status(500).send({ error: updateError.message });
}
res.send("OK");
} catch (fetchError) {
logger.error("Error fetching gcodeFile:", fetchError);
res.status(500).send({ error: fetchError.message });
}
};

View File

@ -0,0 +1,86 @@
import bcrypt from "bcrypt";
import dotenv from "dotenv";
import { userModel } from "../../schemas/user.schema.js";
import { printerModel } from "../../schemas/printer.schema.js";
import jwt from "jsonwebtoken";
import log4js from "log4js";
dotenv.config();
const logger = log4js.getLogger("Printers");
logger.level = process.env.LOG_LEVEL;
export const listPrintersRouteHandler = async (
req,
res,
page = 1,
limit = 25
) => {
try {
// Calculate the skip value based on the page number and limit
const skip = (page - 1) * limit;
// Fetch users with pagination
const printers = await printerModel.find().skip(skip).limit(limit);
logger.trace(`List of printers (Page ${page}, Limit ${limit}):`);
res.send(printers);
} catch (error) {
logger.error("Error listing users:", error);
res.status(500).send({ error: error });
}
};
export const getPrinterRouteHandler = async (req, res) => {
const remoteAddress = req.params.remoteAddress;
try {
// Fetch the printer with the given remote address
const printer = await printerModel.findOne({ remoteAddress });
if (!printer) {
logger.warn(`Printer with remote address ${remoteAddress} not found.`);
return res.status(404).send({ error: "Printer not found" });
}
logger.trace(`Printer with remote address ${remoteAddress}:`, printer);
res.send(printer);
} catch (error) {
logger.error("Error fetching printer:", error);
res.status(500).send({ error: error.message });
}
};
export const editPrinterRouteHandler = async (req, res) => {
const remoteAddress = req.params.remoteAddress;
const { friendlyName } = req.body;
try {
// Fetch the printer with the given remote address
const printer = await printerModel.findOne({ remoteAddress });
if (!printer) {
logger.warn(`Printer with remote address ${remoteAddress} not found.`);
return res.status(404).send({ error: "Printer not found" });
}
logger.trace(`Editing printer with remote address ${remoteAddress}:`, printer);
try {
const result = await printerModel.updateOne(
{ remoteAddress: remoteAddress },
{ $set: req.body }
);
if (result.nModified === 0) {
logger.error("No printers updated.");
res.status(500).send({ error: "No printers updated." });
}
} catch (updateError) {
logger.error("Error updating printer:", updateError);
res.status(500).send({ error: updateError.message });
}
res.send("OK");
} catch (fetchError) {
logger.error("Error fetching printer:", fetchError);
res.status(500).send({ error: fetchError.message });
}
};

View File

@ -0,0 +1,88 @@
import dotenv from "dotenv";
import { printJobModel } from "../../schemas/printjob.schema.js"
import jwt from "jsonwebtoken";
import log4js from "log4js";
dotenv.config();
const logger = log4js.getLogger("PrintJobs");
logger.level = process.env.LOG_LEVEL;
export const listPrintJobsRouteHandler = async (
req,
res,
page = 1,
limit = 25
) => {
try {
// Calculate the skip value based on the page number and limit
const skip = (page - 1) * limit;
// Fetch users with pagination
const printJobs = await printJobModel.find().skip(skip).limit(limit);
logger.trace(`List of print jobs (Page ${page}, Limit ${limit}):`);
res.send(printJobs);
} catch (error) {
logger.error("Error listing print jobs:", error);
res.status(500).send({ error: error });
}
};
export const getPrintJobRouteHandler = async (req, res) => {
try {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
// Fetch the printJob with the given remote address
const printJob = await printJobModel.findOne({
_id: id
});
if (!printJob) {
logger.warn(`PrintJob not found with supplied id.`);
return res.status(404).send({ error: "Print job not found." });
}
logger.trace(`PrintJob with ID: ${id}:`, printJob);
res.send(printJob);
} catch (error) {
logger.error("Error fetching printJob:", error);
res.status(500).send({ error: error.message });
}
};
export const editPrintJobRouteHandler = async (req, res) => {
try {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
// Fetch the printJob with the given remote address
const printJob = await printJobModel.findOne({ _id: id });
if (!printJob) { // Error handling
logger.warn(`PrintJob not found with supplied id.`);
return res.status(404).send({ error: "Print job not found." });
}
logger.trace(`PrintJob with ID: ${id}:`, printJob);
try {
const { created_at, updated_at, started_at, status, ...updateData } = req.body;
const result = await printJobModel.updateOne(
{ _id: id },
{ $set: updateData }
);
if (result.nModified === 0) {
logger.error("No printJobs updated.");
res.status(500).send({ error: "No printJobs updated." });
}
} catch (updateError) {
logger.error("Error updating printJob:", updateError);
res.status(500).send({ error: updateError.message });
}
res.send("OK");
} catch (fetchError) {
logger.error("Error fetching printJob:", fetchError);
res.status(500).send({ error: fetchError.message });
}
};

8
src/util/index.js Normal file
View File

@ -0,0 +1,8 @@
function parseStringIfNumber(input) {
if (typeof input === 'string' && !isNaN(input) && !isNaN(parseFloat(input))) {
return parseFloat(input);
}
return input;
}
export {parseStringIfNumber};