1st commit
This commit is contained in:
24
server/.gitignore
vendored
Normal file
24
server/.gitignore
vendored
Normal 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
9
server/README.md
Normal 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
138
server/app.js
Normal 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
55
server/lib/HelperXhr.js
Normal 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
36
server/lib/Libsecurity.js
Normal 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
47
server/lib/logger.js
Normal 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
167
server/openapi.yaml
Normal 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
2493
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
server/package.json
Normal file
29
server/package.json
Normal 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
95
server/routes.js
Normal 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;
|
Reference in New Issue
Block a user