1st commit

This commit is contained in:
2022-06-04 20:04:25 +02:00
commit 159683d7ba
17 changed files with 3540 additions and 0 deletions

24
server/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
/data/*
/logs/*
# testing
/coverage
/logs/*
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

9
server/README.md Normal file
View File

@ -0,0 +1,9 @@
# minimalist Nodejs server for mydraft application (https:/mydraft.cc)
## Swagger
## Installation (Docker)
## Execute
pay **ATTENTION**

138
server/app.js Normal file
View File

@ -0,0 +1,138 @@
require("include-path")(["./lib"]);
const express = require("express");
//to parser body as json fetch must have parameter
//headers: {
// 'Content-Type': 'application/json',
//},
const bodyParser = require("body-parser");
const path = require("path");
const cookieParser = require("cookie-parser");
const http = require("http");
//Helpers
const HelperXhr = require("HelperXhr");
//Logs nodejs
//--> http: morgan
const morgan = require("morgan");
const logger = require("logger");
//cors
const cors = require("cors");
//framework express
const app = express();
//Body parser
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
//API routes
const routes = require("./routes.js");
//Swagger UI
const swaggerUi = require("swagger-ui-express");
const YAML = require("yamljs");
const swaggerDocumentCore = YAML.load("./openapi.yaml");
const swaggeroptions = {
explorer: true,
};
//cors options - no options
app.use(cors({}));
/**
* explainations : https://dev.to/p0oker/why-is-my-browser-sending-an-options-http-request-instead-of-post-5621
*/
app.use(function (req, res, next) {
res.header("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS");
res.header(
"Access-Control-Allow-Headers",
"Content-Type, InstantKey, Content-Length, X-Requested-With"
);
//OPTIONS methods interception
if ("OPTIONS" === req.method) {
res.sendStatus(200);
} else {
next();
}
});
// listening port server
app.set("PORT", process.env.PORT || "4000");
// listening IP address server
app.set("IPADDRESS", process.env.IPADDRESS || "0.0.0.0");
// Max size allowed to upload Json files (Default is 2Mo)
app.set("JSONMAXSIZE", process.env.JSONMAXSIZE || 2 * 1024 * 1024);
//logger become global to controller via req.app.get('logger')
app.set("logger", logger);
//Application logs - format see wiki about morgan logs
let loghttpformat = "combined";
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
//set log http
app.use(morgan(loghttpformat));
// Routes
try {
// route / load public content
app.use("/", express.static(path.join(__dirname, "public")));
// route /api-docs load Swagger UI
app.use(
"/api-docs",
swaggerUi.serve,
swaggerUi.setup(swaggerDocumentCore, swaggeroptions)
);
// route /xxx-xxx-xxxx where xxx-xxx-xxx is a diagram Id
app.use(
"/:diagramId",
express.static(path.join(__dirname, "public", "index.html"))
);
// API Routes
app.use(routes);
} catch (err) {
// logger.error(err.toString(), err);
logger.error(err.toString(), err);
}
// resource not found - 404 and forward to error handler
app.use(function (req, res, next) {
console.log("Resource is missing");
req.statusCode = 404;
next(new Error(), req, res);
});
// Global error handler
app.use(function (err, req, res, next) {
let statuscode = req.statusCode; //404 received ???
if (!statuscode) statuscode = 500;
if (statuscode !== 404) {
logger.error(err.toString(), err);
message = "Internal Error - see logs";
} else {
message = "404-Resource Not Found";
}
if (HelperXhr.isXhrRequest(req)) {
res.status(statuscode).json({ error: message });
} else {
res.status(statuscode).send("<html><h1>" + message + "</h1></html>");
}
});
const httpServer = http.createServer(app);
//catch httpServer Errors type uncaughtException :EADDRNOTAVAIL EADDRINUSE
process.on("uncaughtException", function (err) {
logger.error(err.toString(), err);
});
//start server
httpServer.listen(app.get("PORT"), app.get("IPADDRESS"), function () {
logger.info({
listeningonipaddress: app.get("IPADDRESS"),
listeningonport: app.get("PORT"),
});
});
module.exports = app;

55
server/lib/HelperXhr.js Normal file
View File

@ -0,0 +1,55 @@
"use strict";
const Libsecurity = require("Libsecurity");
class HelperXhr {
/**
* Return req.xhr state to determine if request is xhr type
* @param {object} req - req Express var
* @return {boolean}
*/
static isXhrRequest(req) {
if (req && req.headers && req.headers.accept) {
return req.headers.accept.indexOf("json") > -1;
}
return false;
}
/**
* Push received Data on object
* @return object
*/
static setSettings(req) {
let obj = {};
console.log(req);
try {
if (req.method.match(/POST|PUT/i)) {
obj = Object.keys(req.body).length > 0 ? req.body : {};
} else if (req.method.match(/GET/i)) {
//no body with GET method !! use query.data
obj = req.query && req.query.data ? JSON.parse(req.query.data) : {};
} else {
throw new Error(
"HelperXhr::setSettings-req.method not supported-" + req.method
);
}
// // Parsing req.body API receive only json so string is JSON
// if (typeof obj === "string") {
// obj = JSON.parse(obj);
// }
//check size
if (typeof obj === "object") {
Libsecurity.jsonSizeIsAcceptable(obj, req.app.get("JSONMAXSIZE"));
}
//url paramèters are also put in object
if (req.params && typeof req.params === "object") {
for (let param in req.params) {
obj[param] = req.params[param];
}
}
return obj;
} catch (error) {
//if called from controller to call express error handler
throw error;
}
}
}
module.exports = HelperXhr;

36
server/lib/Libsecurity.js Normal file
View File

@ -0,0 +1,36 @@
"use strict";
class Libsecurity {
/**
*
* @param {object} obj
* @param {number} maxsize
* @returns
*/
static jsonSizeIsAcceptable(obj, maxsize) {
const size = JSON.stringify(obj).length;
if (typeof obj === "object" && size > maxsize)
throw new Error(
"Warning Date received exceed defined max size - Libsecurity",
"size received:",
obj.length,
"acceptable",
size
);
return true;
}
/**
*
* @param {string} str - filename to sanitize
* @returns
*/
static sanitizeFileName(str) {
return str
.replace(/(.*\/)|(\/.*)/g, "")
.replace(/\.\./g, "")
.replace(/;/g, "");
}
}
module.exports = Libsecurity;

47
server/lib/logger.js Normal file
View File

@ -0,0 +1,47 @@
const { createLogger, format, transports, config } = require("winston");
//exception log filename
const exceptionlogfile = __dirname + "/../logs/exceptions.log";
const rejectionslogfile = __dirname + "/../logs/rejections.log";
const errorslogfile = __dirname + "/../logs/errors.log";
const debugslogfile = __dirname + "/../logs/debug.log";
/**
* Levels winston
* {
error: 0,
warn: 1,
info: 2,
http: 3,
verbose: 4,
debug: 5,
silly: 6
}
*/
const logger = createLogger({
transports: [
new transports.Console({
level: "info",
format: format.combine(
// format.colorize(),
format.timestamp(),
format.json()
),
}),
new transports.File({
level: "error",
format: format.combine(format.timestamp(), format.json()),
filename: errorslogfile,
}),
new transports.File({
level: "debug",
format: format.combine(format.timestamp(), format.json()),
filename: debugslogfile,
}),
],
exceptionHandlers: [new transports.File({ filename: exceptionlogfile })],
rejectionHandlers: [new transports.File({ filename: rejectionslogfile })],
//see winston documentation https://www.npmjs.com/package/winston#logging-levels
exitOnError: false,
});
module.exports = logger;

167
server/openapi.yaml Normal file
View File

@ -0,0 +1,167 @@
openapi: 3.0.0
info:
version: 3.0.0
description: "Nodejs API server for https://mydraft.cc - UI"
title: "Mydraft Nodejs server"
contact:
website: www.mytinydc.com
license:
name: "MIT Licence"
url: "https://mit-license.org/"
tags:
- name: "manage"
description: "Load/Save draft"
servers:
- url: 'http://localhost:4000'
components:
schemas:
serverresponsestore:
type: object
properties:
writeToken:
type: string
readToken:
type: string
internalerror:
type: object
properties:
error:
type: string
diagrammestructure:
type: array
items:
type: object
paths:
/:
post:
tags:
- "manage"
summary: "Store new draft document"
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/diagrammestructure'
examples:
JsonOK:
value: [{"type":"diagram/add","payload":{"diagramId":"f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a","timestamp":1654256184083}},{"type":"items/addVisual","payload":{"diagramId":"f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a","timestamp":1654256186400,"shapeId":"82170ec5-2cc5-c0f2-1414-38e8e477dd01","renderer":"Button","position":{"x":199.5,"y":119.30000305175781}}}]
badFormat:
value: 'This is not JSON format'
responses:
"200":
description: "successful operation"
content:
application/json:
schema:
$ref: '#/components/schemas/serverresponsestore'
"500":
description: "Server Internal ERROR"
content:
application/json:
schema:
$ref: '#/components/schemas/internalerror'
security: [] # no authentication
/{tokenToRead}/{tokenToWrite}:
put:
tags:
- "manage"
summary: "Update draft document"
parameters:
- name: "tokenToRead"
in: "path"
required: true
schema:
type: "string"
examples:
GoodId:
value: 'f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a'
WronId:
value: 'not the good id'
- name: "tokenToWrite"
in: "path"
required: true
schema:
type: "string"
examples:
GoodId:
value: 'f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a'
WronId:
value: 'not the good id'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/diagrammestructure'
examples:
JsonOK:
value: [{"type":"diagram/add","payload":{"diagramId":"f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a","timestamp":1654256184083}},{"type":"items/addVisual","payload":{"diagramId":"f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a","timestamp":1654256186400,"shapeId":"82170ec5-2cc5-c0f2-1414-38e8e477dd01","renderer":"Button","position":{"x":199.5,"y":119.30000305175781}}}]
badFormat:
value: 'This is not JSON format'
responses:
"200":
description: "successful operation"
content:
application/json:
schema:
$ref: '#/components/schemas/serverresponsestore'
"400":
description: "probleme with body content JSON structure"
content:
application/json:
schema:
$ref: '#/components/schemas/internalerror'
"500":
description: "Server Internal ERROR"
content:
application/json:
schema:
$ref: '#/components/schemas/internalerror'
security: [] # no authentication
/get/{diagramID}:
get:
tags:
- "manage"
summary: "return JSON Object"
parameters:
- name: "diagramID"
in: "path"
description: "diagramme ID"
required: true
schema:
type: "string"
examples:
GoodId:
value: 'f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a'
WronId:
value: 'not the good id'
RunDir:
value: '../f9e48fc9-da3d-aa39-1ab5-0641b0a0f59a'
responses:
"200":
description: "successful operation"
content:
application/json:
schema:
$ref: '#/components/schemas/diagrammestructure'
"500":
description: "Server Internal ERROR"
content:
application/json:
schema:
$ref: '#/components/schemas/internalerror'
security: [] # no authentication

2493
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
server/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "mydraftcc-nodejs-server",
"version": "1.0.0",
"description": "built by https://wwww.mytinydc.com - Mydraftcc - NodesJs server",
"author": "Damien HENRY - https://www.mytinydc.com",
"license": "MIT License",
"main": "app.js",
"directories": {
"lib": "lib"
},
"scripts": {
"debug-startserver": "IPADDRESS=0.0.0.0 PORT=4000 JSONMAXSIZE=2097152 nodemon -w ./ app.js"
},
"dependencies": {
"axios": "^0.21.4",
"body-parser": "^1.20.0",
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"ejs": "^3.1.7",
"express": "~4.17.1",
"express-session": "^1.17.1",
"http-errors": "~1.6.3",
"include-path": "^0.4.7",
"morgan": "~1.9.1",
"swagger-ui-express": "^4.4.0",
"winston": "^3.3.3",
"yamljs": "^0.3.0"
}
}

95
server/routes.js Normal file
View File

@ -0,0 +1,95 @@
"use strict";
/**
* Not use logger here use try catch and next(Error) in catch bloc to call the global error handler
*/
const express = require("express");
const router = express.Router();
const HelperXhr = require("HelperXhr");
const Libsecurity = require("Libsecurity");
const fs = require("fs");
const path = require("path");
//Dir storage
const datadir = __dirname + "/data";
let response = "";
let codestatus = 500;
/**
* see swagger model
* Store json file
*/
router.post("/", async function (req, res, next) {
try {
//check data received
let xhr = HelperXhr.setSettings(req);
console.log(xhr);
if (Array.isArray(xhr) && xhr[0].payload.diagramId) {
const id = xhr[0].payload.diagramId;
if (id) {
if (fs.existsSync(datadir)) {
fs.writeFileSync(datadir + "/" + id, JSON.stringify(xhr));
codestatus = 200;
response = { writeToken: id, readToken: id };
}
}
} else {
throw new Error("Json structure is wrong");
}
res.status(codestatus).json(response);
} catch (error) {
next(error);
}
});
/**
* see swagger model
*/
router.put("/:tokenWrite/:tokenRead", async function (req, res, next) {
try {
const tokenWrite = req.params.tokenWrite;
const tokenRead = req.params.tokenRead;
//check data received
let xhr = HelperXhr.setSettings(req);
if (
Array.isArray(xhr) &&
xhr[0] &&
xhr[0].payload &&
xhr[0].payload.diagramId &&
tokenWrite &&
tokenWrite === tokenRead &&
tokenWrite === xhr[0].payload.diagramId
) {
const id = xhr[0].payload.diagramId;
if (id) {
fs.writeFileSync(datadir + "/" + id, JSON.stringify(xhr));
codestatus = 200;
response = { writeToken: id, readToken: id };
}
} else {
throw new Error("Json structure is wrong");
}
res.status(codestatus).json(response);
} catch (error) {
next(error);
}
});
/**
* 2 cases
* - isXhr request return json
* - else public/index.html
*/
router.get("/get/:diagramId", function (req, res, next) {
try {
const id = Libsecurity.sanitizeFileName(req.params.diagramId);
response = JSON.parse(fs.readFileSync(datadir + "/" + id, "utf8"));
codestatus = 200;
res.status(codestatus).json(response);
} catch (error) {
next(error);
}
});
module.exports = router;