Skip to content

Commit

Permalink
feature: export model endpoint (#213)
Browse files Browse the repository at this point in the history
* Adds export model endpoint

This commit adds the necessary structure to export model

Resolves #75

* Adds import model endpoint

This commit adds import model endpoint for tests

Resolves #76

* Adds crypto unit test

Refs #75 #76

* Adds export model unit test

Resolves #75
  • Loading branch information
feekosta authored Nov 10, 2021
1 parent 103a038 commit aff6a66
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 6 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
###############################################################################
.idea
.DS_Store
.env

###############################################################################
# node-waf configuration
Expand All @@ -24,4 +25,5 @@ package-lock.json
###############################################################################
app/css
app/assets
data/
data/
coverage/
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"bootstrap": "^3.4.1",
"cssnano": "^4.1.10",
"del": "^5.1.0",
"dotenv": "^10.0.0",
"ejs": "^3.1.3",
"errorhandler": "^1.5.1",
"express": "^4.17.1",
Expand All @@ -41,6 +42,7 @@
"mongodb": "~3.5.8",
"mongoose": "^5.9.18",
"morgan": "^1.10.0",
"raw-body": "^2.4.1",
"response-time": "^2.3.2",
"sweet-feedback": "^1.0.1",
"textangular": "^1.5.16"
Expand Down
6 changes: 6 additions & 0 deletions server_app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const session = require("express-session");
const bodyParser = require("body-parser");
const ejs = require("ejs");
const path = require("path");
const { uploadMiddleware } = require("./middleware/middleware");

require("dotenv").config();

let app = express();

Expand All @@ -17,6 +20,8 @@ app.engine("html", ejs.renderFile);
app.use(morgan("dev"));
app.use(bodyParser.json()); // support json encoded bodies
app.use(bodyParser.urlencoded({ extended: true })); // support encoded bodies

app.use(uploadMiddleware);
const appPath = path.join(__dirname, "../app");
app.use(express.static(appPath));
app.use(express.static(`${appPath}/assets`));
Expand All @@ -35,6 +40,7 @@ app.use(errorhandler());
const userHandler = require("./user/handler");
const modelHandler = require("./model/handler");


app.use("/users", userHandler);
app.use("/models", modelHandler);

Expand Down
34 changes: 34 additions & 0 deletions server_app/helpers/crypto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const crypto = require("crypto");

const ALGORITHM = "aes-256-ctr";
const ENCRYPTION_KEY = process.env.SECRET_TOKEN;
const IV_LENGTH = 16;

const encrypt = (data) => {
const key = crypto
.createHash("sha256")
.update(String(ENCRYPTION_KEY))
.digest("base64")
.substr(0, 32);
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(ALGORITHM, Buffer.from(key), iv);
return Buffer.concat([iv, cipher.update(data), cipher.final()]);
};

const decrypt = (hash) => {
const key = crypto
.createHash("sha256")
.update(String(ENCRYPTION_KEY))
.digest("base64")
.substr(0, 32);
const iv = hash.slice(0, IV_LENGTH);
const decipher = crypto.createDecipheriv(ALGORITHM, Buffer.from(key), iv);
const data = hash.slice(IV_LENGTH);
const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
return decrypted.toString();
};

module.exports = {
encrypt,
decrypt,
};
35 changes: 35 additions & 0 deletions server_app/helpers/crypto.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const { encrypt, decrypt } = require("./crypto");

describe("crypto test", () => {
const OLD_ENV = process.env;

beforeEach(() => {
jest.resetModules();
process.env = { ...OLD_ENV };
});

afterAll(() => {
process.env = OLD_ENV;
});

test("Encrypt data", async () => {
process.env.SECRET_TOKEN = "some_token";
const data = { name: "some_name" };
const json = JSON.stringify(data);

const encrypted = encrypt(json);

expect(encrypted).not.toBeNull();
});

test("Decrypt data", async () => {
process.env.SECRET_TOKEN = "some_token";
const data = { name: "some_name" };
const json = JSON.stringify(data);

const decrypted = decrypt(encrypt(json));

expect(decrypted).not.toBeNull();
expect(JSON.parse(decrypted)).toEqual(data);
});
});
22 changes: 22 additions & 0 deletions server_app/middleware/middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const getRawBody = require("raw-body");

const uploadMiddleware = (req, res, next) => {
if (req.headers["content-type"] === "application/octet-stream") {
const options = {
length: req.headers["content-length"],
encoding: req.charset,
};
const callback = (err, string) => {
if (err) return next(err);
req.body = string;
return next();
};
getRawBody(req, options, callback);
} else {
next();
}
};

module.exports = {
uploadMiddleware,
};
38 changes: 35 additions & 3 deletions server_app/model/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const express = require("express");
const bodyParser = require("body-parser");
const modelService = require("./service");
const modelValidator = require("./validator");
const { decrypt } = require("../helpers/crypto");

const router = express.Router();
router.use(bodyParser.json());
Expand Down Expand Up @@ -34,9 +35,9 @@ const save = async (req, res) => {
try {
const name = req.body.name;
const type = req.body.type;
const userId = req.body.user;
const userId = req.body.user;
const model = req.body.model;

const validation = modelValidator.validateSaveParams({
name,
type,
Expand Down Expand Up @@ -100,10 +101,41 @@ const rename = async (req, res) => {
}
};

const exportModel = async (req, res) => {
try {
const { modelId } = req.params;
const { name, data } = await modelService.exportModel(modelId);
res.writeHead(200, {
"Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename=${name}.brm`,
"Content-Length": data.length,
});
return res.end(data);
} catch (error) {
console.error(error);
return res
.status(500)
.send("There's an error while treating export your model request");
}
};

const importModel = async (req, res) => {
try {
return res.status(200).json(JSON.parse(decrypt(req.body)));
} catch (error) {
console.error(error);
return res
.status(500)
.send("There's an error while treating import your model request");
}
};

module.exports = router
.get("/", listAll)
.post("/", save)
.get("/:modelId", getById)
.put("/:modelId", edit)
.delete("/:modelId", remove)
.put("/:modelId/rename", rename);
.put("/:modelId/rename", rename)
.get("/:modelId/export", exportModel)
.post("/import", importModel);
65 changes: 63 additions & 2 deletions server_app/model/handler.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const request = require("supertest");
const app = require("../app");

jest.mock("./service");
const mockModelService = require("./service");
const { encrypt } = require("../helpers/crypto");

afterEach(() => {
jest.restoreAllMocks();
Expand Down Expand Up @@ -41,9 +43,68 @@ describe("Test save /models", () => {

describe("Test list all /models", () => {
test("It should send 200 user exists", async () => {
const response = await request(app).get("/").send([]);
expect(response.statusCode).toBe(200);
});
});

describe("Test export /models", () => {
const OLD_ENV = process.env;

beforeEach(() => {
jest.resetModules();
process.env = { ...OLD_ENV };
});

afterAll(() => {
process.env = OLD_ENV;
});

const model = {
_id: "6179eacfc9cac3976aef0fec",
who: "6179eac1c9cac3976aef0fe8",
type: "conceptual",
model:
'{"cells":[{"type":"erd.Entity","supertype":"Entity","isExtended":false,"autorelationship":false,"size":{"width":80,"height":40},"position":{"x":280,"y":120},"angle":0,"id":"9a6cc538-d1bd-4e39-8c38-a85078814dc9","z":1,"attrs":{".outer":{"fill":"#FFFFFF","stroke":"black"},"text":{"text":"Entidade"}}},{"type":"erd.Relationship","supertype":"Relationship","autorelationship":false,"size":{"width":85,"height":45},"position":{"x":560,"y":120},"angle":0,"id":"f5a1e4c3-a6a2-49f1-9e92-7b7210695d5a","z":2,"attrs":{".outer":{"fill":"#FFFFFF","stroke":"black"},"text":{"text":"Rel"}}},{"type":"erd.BlockAssociative","supertype":"Entity","size":{"width":100,"height":50},"position":{"x":780,"y":120},"angle":0,"id":"18f3b6fc-83de-4ad0-9dc4-e07569598374","z":3,"embeds":["1d11c65e-89d4-47f2-b991-dd0068833d77"],"attrs":{".outer":{"fill":"transparent","stroke":"black"}}},{"type":"erd.Relationship","supertype":"Relationship","autorelationship":false,"size":{"width":85,"height":45},"position":{"x":786,"y":122},"angle":0,"id":"1d11c65e-89d4-47f2-b991-dd0068833d77","z":4,"parent":"18f3b6fc-83de-4ad0-9dc4-e07569598374","attrs":{".outer":{"fill":"#FFFFFF","stroke":"black"},"text":{"text":"Rel"}}},{"type":"link","source":{"id":"f5a1e4c3-a6a2-49f1-9e92-7b7210695d5a"},"target":{"id":"18f3b6fc-83de-4ad0-9dc4-e07569598374"},"id":"b99e4aef-2826-4df3-b4fd-17bcb81f5fa5","z":5,"labels":[{"position":0.7,"attrs":{"text":{"text":"(0, n)"}}}],"attrs":{}},{"type":"link","source":{"id":"f5a1e4c3-a6a2-49f1-9e92-7b7210695d5a"},"target":{"id":"9a6cc538-d1bd-4e39-8c38-a85078814dc9"},"id":"7c54152c-b6f1-4423-9543-3c57c6087036","z":6,"labels":[{"position":0.7,"attrs":{"text":{"text":"(0, n)"}}}],"attrs":{}}]}',
name: "modeloteste",
created: "2021-10-28T00:11:59.121Z",
__v: 0,
};

test("It should send 200 when model is exported", async () => {
process.env.SECRET_TOKEN = "some_token";
mockModelService.exportModel.mockReturnValue({
name: "modeloteste",
data: encrypt(JSON.stringify(model)),
});

const response = await request(app)
.get("/")
.send([]);
.get("/models/6179eacfc9cac3976aef0fec/export")
.send(encrypt(JSON.stringify(model)));

expect(response.header).toHaveProperty("content-type");
expect(response.header).toHaveProperty("content-disposition");
expect(response.header).toHaveProperty("content-length");
expect(response.header["content-type"]).toEqual("application/octet-stream");
expect(response.header["content-disposition"]).toEqual(
"attachment; filename=modeloteste.brm"
);
expect(response.header["content-length"]).toEqual("2137");
expect(response.statusCode).toBe(200);
expect(mockModelService.exportModel).toHaveBeenCalled();
});

test("It should send 500 when model is not exported", async () => {
process.env.SECRET_TOKEN = "some_token";
mockModelService.exportModel.mockImplementation(() => {
throw new Error();
});

const response = await request(app)
.get("/models/6179eacfc9cac3976aef0fec/export")
.send(encrypt(JSON.stringify(model)));

expect(response.statusCode).toBe(500);
expect(mockModelService.exportModel).toHaveBeenCalled();
});
});
17 changes: 17 additions & 0 deletions server_app/model/service.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const modelRepository = require("./model");
const { encrypt } = require("../helpers/crypto");

const listAll = async (userId) => {
return new Promise(async (resolve, reject) => {
Expand Down Expand Up @@ -95,13 +96,29 @@ const remove = async (modelId) => {
});
};

const exportModel = async (modelId) => {
return new Promise(async (resolve, reject) => {
try {
const model = await getById(modelId);
return resolve({
name: model.name.replace(/[^a-zA-Z0-9]/g, ""),
data: encrypt(JSON.stringify(model)),
});
} catch (error) {
console.error(error);
return reject(error);
}
});
};

const modelService = {
listAll,
getById,
save,
edit,
remove,
rename,
exportModel,
};

module.exports = modelService;

0 comments on commit aff6a66

Please sign in to comment.