initial commit

This commit is contained in:
Brian Emilius
2020-05-17 22:00:53 +02:00
commit a60a37f441
57 changed files with 902 additions and 0 deletions

2
.env Normal file
View File

@@ -0,0 +1,2 @@
NODE_ENV="development"
JWT_SECRET=kj3h45bl2k34jt23498570q9n8098354t7029358tyowie5uthw8475tyq98347ty834irhfoqi34uyt18

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules
package-lock.json
.vscode

16
README.md Normal file
View File

@@ -0,0 +1,16 @@
# trainer-api
RESTful web API for the Trainer App.
## Install
```
npm install
```
## Run
```
npm start
```
## Documentation
http://localhost:4000

15
app.js Normal file
View File

@@ -0,0 +1,15 @@
require("dotenv").config();
var express = require("express");
var app = express();
var router = require("./router");
var { testConnection } = require("./config/database");
var formidable = require("express-formidable");
var cors = require("cors");
testConnection();
app.use(cors());
app.use(formidable());
app.use(router);
module.exports = app;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
assets/1589743622345pia.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

46
bin/www Normal file
View File

@@ -0,0 +1,46 @@
#!/usr/bin/env node
require("dotenv").config();
var app = require("../app");
var http = require("http");
// Define a port for the server to listen on
var port = process.env.PORT || 4000;
app.set("port", port);
// Create a server instance
var server = http.createServer(app);
// Make the server listen on a port
server.listen(port);
// Handle errors and success
server.on("error", onError);
server.on("listening", onListening);
function pipeOrPort(address) {
return typeof address == "string" ? `pipe ${address}` : `port ${address.port}`;
}
function onError(error) {
if (error.syscall != "listen") {
throw error;
}
let bind = pipeOrPort(server.address());
switch (error.code) {
case "EACCES":
console.error(`${bind} requires elevated privileges.`);
process.exit(1);
case "EADDRINUSE":
console.error(`${bind} is already in use.`);
process.exit(1);
default:
throw error;
}
}
function onListening() {
let bind = pipeOrPort(server.address());
console.log(`Listening on ${bind}`);
}

20
config/database.js Normal file
View File

@@ -0,0 +1,20 @@
var { Sequelize } = require("sequelize");
var sequelize = new Sequelize({
dialect: "sqlite",
storage: "./storage/database.sqlite3",
logging: false
});
async function testConnection() {
try {
await sequelize.authenticate();
console.log("Connection established");
} catch (error) {
console.error("Unable to connect", error);
}
}
module.exports = {
testConnection,
sequelize
};

8
config/sqlite.js Normal file
View File

@@ -0,0 +1,8 @@
module.exports = {
development: {
force: true,
},
production: {
force: false,
},
};

16
config/winston.js Normal file
View File

@@ -0,0 +1,16 @@
var winston = require("winston");
var expressWinston = require("express-winston");
var requestLogger = expressWinston.logger({
transports: [
new winston.transports.Console()
],
format: winston.format.simple(),
expressFormat: true,
meta: false,
colorize: true,
});
module.exports = {
requestLogger
};

View File

@@ -0,0 +1,74 @@
var { About } = require("../models/models");
async function getSingleAbout(req, res, next) {
try {
let about = await About.findByPk(parseInt(req.params.id));
res.json(about);
} catch (error) {
console.log(error);
res.status(500).end();
}
}
async function getAllAbouts(req, res, next) {
try {
let about = await About.findAll();
res.json(about);
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function createSingleAbout(req, res, next) {
try {
let about = await About.create({
title: req.fields.title,
content: req.fields.content
});
res.json(about);
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function updateSingleAbout(req, res, next) {
try {
let about = await About.findByPk(parseInt(req.params.id));
if(about) {
about.title = req.fields.title;
about.content = req.fields.content;
about.save();
res.json(about);
} else {
res.status(404).end();
}
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function deleteSingleAbout(req, res, next) {
try {
await About.destroy({
where: {
id: parseInt(req.params.id)
}
})
res.end();
} catch (error) {
console.error(error);
res.status(500).end();
}
}
module.exports = {
createSingleAbout,
getSingleAbout,
getAllAbouts,
updateSingleAbout,
deleteSingleAbout
};

View File

@@ -0,0 +1,76 @@
var { Adoptsection, Asset } = require("../models/models");
async function getSingleAdoptSection(req, res, next) {
try {
let adoptsection = await Adoptsection.findByPk(parseInt(req.params.id), { include: [ Asset ] });
res.json(adoptsection);
} catch (error) {
console.log(error);
res.status(500).end();
}
}
async function getAllAdoptSections(req, res, next) {
try {
let adoptsections = await Adoptsection.findAll({ include: [ Asset ] });
res.json(adoptsections);
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function createSingleAdoptSection(req, res, next) {
try {
let adoptsection = await Adoptsection.create({
title: req.fields.title,
content: req.fields.content,
assetId: parseInt(req.fields.assetId)
});
res.json(adoptsection);
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function updateSingleAdoptSection(req, res, next) {
try {
let adoptsection = await Adoptsection.findByPk(parseInt(req.params.id), { include: [ Asset ] });
if (adoptsection) {
adoptsection.title = req.fields.title;
adoptsection.content = req.fields.content;
adoptsection.assetId = parseInt(req.fields.assetId);
adoptsection.save();
res.json(adoptsection);
} else {
res.status(404).end();
}
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function deleteSingleAdoptSection(req, res, next) {
try {
await Adoptsection.destroy({
where: {
id: parseInt(req.params.id)
}
});
res.end();
} catch (error) {
console.error(error);
res.status(500).end();
}
}
module.exports = {
createSingleAdoptSection,
getSingleAdoptSection,
getAllAdoptSections,
updateSingleAdoptSection,
deleteSingleAdoptSection
};

View File

@@ -0,0 +1,77 @@
var { Animal, Asset } = require("../models/models");
async function getSingleAnimal(req, res, next) {
try {
let animal = await Animal.findByPk(parseInt(req.params.id), { include: [ Asset ] });
res.json(animal);
} catch (error) {
console.log(error);
res.status(500).end();
}
}
async function getAllAnimals(req, res, next) {
try {
let animals = await Animal.findAll({ include: [ Asset ] });
res.json(animals);
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function createSingleAnimal(req, res, next) {
try {
let animal = await Animal.create({
name: req.fields.name,
description: req.fields.description,
age: req.fields.age,
assetId: parseInt(req.fields.assetId)
});
res.json(animal);
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function updateSingleAnimal(req, res, next) {
try {
let animal = await Animal.findByPk(parseInt(req.params.id), { include: [ Asset ] });
if (animal) {
animal.name = req.fields.name;
animal.description = req.fields.description;
animal.assetId = parseInt(req.fields.assetId);
animal.save();
res.json(animal);
} else {
res.status(404).end();
}
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function deleteSingleAnimal(req, res, next) {
try {
await Animal.destroy({
where: {
id: parseInt(req.params.id)
}
});
res.end();
} catch (error) {
console.error(error);
res.status(500).end();
}
}
module.exports = {
createSingleAnimal,
getSingleAnimal,
getAllAnimals,
updateSingleAnimal,
deleteSingleAnimal
};

View File

@@ -0,0 +1,41 @@
var { Asset } = require("../models/models");
var saveFile = require("../services/asset");
async function createSingleAsset(req, res, next) {
try {
let file = saveFile(req.files.file);
let asset = await Asset.create({
url: "http://localhost:4000/file-bucket/" + file
});
res.json(asset);
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function getAllAssets(req, res, next) {
try {
let assets = await Asset.findAll();
res.json(assets);
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function getSingleAsset(req, res, next) {
try {
let asset = await Asset.findByPk(req.params.id);
res.json(asset);
} catch (error) {
console.error(error);
res.status(500).end();
}
}
module.exports = {
createSingleAsset,
getAllAssets,
getSingleAsset
};

View File

@@ -0,0 +1,44 @@
var { Subscriber } = require("../models/models");
async function getAllSubscribers(req, res, next) {
try {
let subscribers = await Subscriber.findAll();
res.json(subscribers);
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function createSingleSubscriber(req, res, next) {
try {
let subscriber = await Subscriber.create({
name: req.fields.name,
email: req.fields.email
});
res.json(subscriber);
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function deleteSingleSubscriber(req, res, next) {
try {
await Subscriber.destroy({
where: {
email: req.params.email
}
});
res.end();
} catch (error) {
console.error(error);
res.status(500).end();
}
}
module.exports = {
createSingleSubscriber,
getAllSubscribers,
deleteSingleSubscriber
};

View File

@@ -0,0 +1,31 @@
var { User } = require("../models/models");
var { compareSync } = require("bcryptjs");
var { sign } = require("jsonwebtoken");
async function createToken(req, res, next) {
try {
let user = await User.findOne({ where: { username: req.fields.username } });
if (!user) return res.status(401).end();
if (!compareSync(req.fields.password, user.password))
return res.status(401).end();
let token = sign({
data: user
}, process.env.JWT_SECRET, { expiresIn: "1h" });
res.json({
userId: user.id,
token,
validUntil: Date.now() + (60*60*1000)
});
} catch (error) {
console.error(error);
res.status(500).end();
}
}
module.exports = {
createToken
};

View File

@@ -0,0 +1,34 @@
var { User, Class } = require("../models/models");
var { hashSync } = require("bcryptjs");
async function getSingleUser(req, res, next) {
try {
let user = await User.findByPk(parseInt(req.params.id));
if (user) {
res.json(user);
} else {
res.status(404).end();
}
} catch (error) {
console.log(error);
res.status(500).end();
}
}
async function createSingleUser(req, res, next) {
try {
let user = await User.create({
username: req.fields.username,
password: hashSync(req.fields.password, 15)
});
res.json(user);
} catch (error) {
console.error(error);
res.status(500).end();
}
}
module.exports = {
createSingleUser,
getSingleUser
};

View File

@@ -0,0 +1,78 @@
var { Volunteer, Asset } = require("../models/models");
async function getSingleVolunteer(req, res, next) {
try {
let volunteer = await Volunteer.findByPk(parseInt(req.params.id), { include: [ Asset ] });
res.json(volunteer);
} catch (error) {
console.log(error);
res.status(500).end();
}
}
async function getAllVolunteers(req, res, next) {
try {
let volunteers = await Volunteer.findAll({ include: [ Asset ] });
res.json(volunteers);
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function createSingleVolunteer(req, res, next) {
try {
let volunteer = await Volunteer.create({
title: req.fields.title,
content: req.fields.content,
extra: req.fields.extra,
assetId: parseInt(req.fields.assetId)
});
res.json(volunteer);
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function updateSingleVolunteer(req, res, next) {
try {
let volunteer = await Volunteer.findByPk(parseInt(req.params.id), { include: [ Asset ] });
if (volunteer) {
volunteer.title = req.fields.title;
volunteer.content = req.fields.content;
volunteer.extra = req.fields.extra;
volunteer.assetId = parseInt(req.fields.assetId);
volunteer.save();
res.json(volunteer);
} else {
res.status(404).end();
}
} catch (error) {
console.error(error);
res.status(500).end();
}
}
async function deleteSingleVolunteer(req, res, next) {
try {
await Volunteer.destroy({
where: {
id: parseInt(req.params.id)
}
});
res.end();
} catch (error) {
console.error(error);
res.status(500).end();
}
}
module.exports = {
createSingleVolunteer,
getSingleVolunteer,
getAllVolunteers,
updateSingleVolunteer,
deleteSingleVolunteer
};

15
docs/bundle.css Normal file

File diff suppressed because one or more lines are too long

34
docs/bundle.css.map Normal file

File diff suppressed because one or more lines are too long

9
docs/bundle.js Normal file

File diff suppressed because one or more lines are too long

1
docs/bundle.js.map Normal file

File diff suppressed because one or more lines are too long

BIN
docs/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

16
docs/index.html Normal file
View File

@@ -0,0 +1,16 @@
<!doctype html>
<html>
<head>
<meta charset="utf8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<link rel="shortcut icon" href="favicon.ico">
<link rel="stylesheet" href="bundle.css">
<script defer src="bundle.js"></script>
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"
integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
</head>
<body>
<noscript>In order to view this documentation page, you have to enable JavaScript in your web browser.</noscript>
<div id="app"></div>
</body>
</html>

1
docs/insomnia.json Normal file

File diff suppressed because one or more lines are too long

BIN
docs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

1
docs/rest-api.json Normal file

File diff suppressed because one or more lines are too long

31
middleware/auth.js Normal file
View File

@@ -0,0 +1,31 @@
var { verify } = require("jsonwebtoken");
function isAuthorized(req, res, next) {
if (!req.headers.authorization)
return res.status(401).end();
if (req.headers.authorization.split(" ")[0] !== "Bearer")
return res.status(403).end();
if (!verify(req.headers.authorization.split(" ")[1], process.env.JWT_SECRET))
return res.status(403).end();
next();
}
function isRelevantUser(req, res, next) {
let reqId = req.params.id;
let token = req.headers.authorization.split(" ")[1];
let decodedToken = verify(token, process.env.JWT_SECRET);
if (decodedToken.data.id !== parseInt(reqId)) return res.status(403).end();
if (decodedToken.data.exp < Date.now()) return res.status(403).end();
next();
}
module.exports = {
isAuthorized,
isRelevantUser
};

73
models/models.js Normal file
View File

@@ -0,0 +1,73 @@
var { DataTypes, Model } = require("sequelize");
var { sequelize } = require("../config/database");
class About extends Model {};
class Volunteer extends Model {};
class Animal extends Model {};
class Asset extends Model {};
class Subscriber extends Model {};
class Adoptsection extends Model {};
class User extends Model {};
User.init({
username: DataTypes.TEXT,
password: DataTypes.TEXT
}, { sequelize, modelName: "user" });
About.init({
title: DataTypes.TEXT,
content: DataTypes.TEXT
}, { sequelize, modelName: "about" });
Volunteer.init({
title: DataTypes.TEXT,
content: DataTypes.TEXT,
extra: DataTypes.TEXT
}, { sequelize, modelName: "volunteer" });
Animal.init({
name: DataTypes.TEXT,
description: DataTypes.TEXT,
age: DataTypes.INTEGER
}, { sequelize, modelName: "animal" });
Asset.init({
url: DataTypes.TEXT
}, { sequelize, modelName: "asset" });
Subscriber.init({
name: DataTypes.TEXT,
email: { type: DataTypes.TEXT, unique: true }
}, { sequelize, modelName: "subscriber" });
Adoptsection.init({
title: DataTypes.TEXT,
content: DataTypes.TEXT
}, { sequelize, modelName: "adoptsection" });
Animal.belongsTo(Asset, { foreignKey: "assetId" });
Asset.hasOne(Animal, { foreignKey: "assetId" });
Volunteer.belongsTo(Asset, { foreignKey: "assetId" });
Asset.hasOne(Volunteer, { foreignKey: "assetId" });
Adoptsection.belongsTo(Asset, { foreignKey: "assetId" });
Asset.hasOne(Adoptsection, { foreignKey: "assetId" });
sequelize.sync({ force: false })
.then(function() {
console.log("Tabels created");
})
.catch(function(error) {
console.error(error);
});
module.exports = {
User,
About,
Volunteer,
Animal,
Asset,
Subscriber,
Adoptsection
};

29
package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "general-purpose-rest-api",
"version": "0.0.0",
"description": "A general purpose RESTful web-API written with Express and MySQL",
"main": "index.js",
"scripts": {
"start": "node bin/www",
"dev": "nodemon bin/www",
"test": "echo \"Error: no test specified\" && exit 1",
"docs": "cd ./docs && npx insomnia-documenter --config ./rest-api.json"
},
"author": "Brian Emilius <be@rts.dk>",
"license": "MIT",
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-formidable": "^1.2.0",
"express-winston": "^4.0.2",
"jsonwebtoken": "^8.5.1",
"sequelize": "^5.21.3",
"sqlite3": "^4.1.1",
"winston": "^3.2.1"
},
"devDependencies": {
"nodemon": "^2.0.2"
}
}

22
router.js Normal file
View File

@@ -0,0 +1,22 @@
var router = require("express").Router();
var { readdir } = require("fs");
var { join } = require("path");
var { requestLogger } = require("./config/winston");
router.use(requestLogger);
readdir(join(__dirname, "routes"), routesIterator);
function routesIterator(err, files) {
if (err) {
throw err;
}
files.forEach(file => requireRoute(file));
}
function requireRoute(file) {
require(join(__dirname, "routes", file))(router);
}
module.exports = router;

10
routes/about.route.js Normal file
View File

@@ -0,0 +1,10 @@
var { createSingleAbout, getSingleAbout, getAllAbouts, updateSingleAbout, deleteSingleAbout } = require("../controllers/about.controller");
var { isAuthorized } = require("../middleware/auth");
module.exports = function(router) {
router.post("/api/v1/abouts", isAuthorized, createSingleAbout);
router.get("/api/v1/abouts/:id", getSingleAbout);
router.get("/api/v1/abouts", getAllAbouts);
router.put("/api/v1/abouts/:id", isAuthorized, updateSingleAbout);
router.delete("/api/v1/abouts/:id", isAuthorized, deleteSingleAbout);
};

View File

@@ -0,0 +1,10 @@
var { createSingleAdoptSection, getSingleAdoptSection, getAllAdoptSections, updateSingleAdoptSection, deleteSingleAdoptSection } = require("../controllers/adoptsection.controller");
var { isAuthorized } = require("../middleware/auth");
module.exports = function(router) {
router.post("/api/v1/adoptsections", isAuthorized, createSingleAdoptSection);
router.get("/api/v1/adoptsections/:id", getSingleAdoptSection);
router.get("/api/v1/adoptsections", getAllAdoptSections);
router.put("/api/v1/adoptsections/:id", isAuthorized, updateSingleAdoptSection);
router.delete("/api/v1/adoptsections/:id", isAuthorized, deleteSingleAdoptSection);
};

10
routes/animal.route.js Normal file
View File

@@ -0,0 +1,10 @@
var { createSingleAnimal, getSingleAnimal, getAllAnimals, updateSingleAnimal, deleteSingleAnimal } = require("../controllers/animal.controller");
var { isAuthorized } = require("../middleware/auth");
module.exports = function(router) {
router.post("/api/v1/animals", isAuthorized, createSingleAnimal);
router.get("/api/v1/animals/:id", getSingleAnimal);
router.get("/api/v1/animals", getAllAnimals);
router.put("/api/v1/animals/:id", isAuthorized, updateSingleAnimal);
router.delete("/api/v1/animals/:id", isAuthorized, deleteSingleAnimal);
};

8
routes/asset.route.js Normal file
View File

@@ -0,0 +1,8 @@
var { createSingleAsset, getAllAssets, getSingleAsset } = require("../controllers/asset.controller");
var { isAuthorized } = require("../middleware/auth");
module.exports = function(router) {
router.post("/api/v1/assets", isAuthorized, createSingleAsset);
router.get("/api/v1/assets", getAllAssets);
router.get("/api/v1/assets/:id", getSingleAsset);
};

View File

@@ -0,0 +1,5 @@
var { static } = require("express");
module.exports = function(router) {
router.use("/file-bucket", static("assets"));
};

5
routes/home.route.js Normal file
View File

@@ -0,0 +1,5 @@
var express = require("express");
module.exports = function(router) {
router.use(express.static("docs"));
}

View File

@@ -0,0 +1,8 @@
var { createSingleSubscriber, getAllSubscribers, deleteSingleSubscriber } = require("../controllers/subscriber.controller");
var { isAuthorized } = require("../middleware/auth");
module.exports = function(router) {
router.post("/api/v1/subscribers", createSingleSubscriber);
router.get("/api/v1/subscribers", isAuthorized, getAllSubscribers);
router.delete("/api/v1/subscribers/:email", deleteSingleSubscriber);
};

5
routes/token.route.js Normal file
View File

@@ -0,0 +1,5 @@
var { createToken } = require("../controllers/token.controller");
module.exports = function(router) {
router.post("/auth/token", createToken);
};

6
routes/user.route.js Normal file
View File

@@ -0,0 +1,6 @@
var { createSingleUser, getSingleUser } = require("../controllers/user.controller");
module.exports = function(router) {
router.get("/api/v1/users/:id", getSingleUser);
router.post("/api/v1/users", createSingleUser);
};

10
routes/volunteer.route.js Normal file
View File

@@ -0,0 +1,10 @@
var { createSingleVolunteer, getSingleVolunteer, getAllVolunteers, updateSingleVolunteer, deleteSingleVolunteer } = require("../controllers/volunteer.controller");
var { isAuthorized } = require("../middleware/auth");
module.exports = function(router) {
router.post("/api/v1/volunteers", isAuthorized, createSingleVolunteer);
router.get("/api/v1/volunteers/:id", getSingleVolunteer);
router.get("/api/v1/volunteers", getAllVolunteers);
router.put("/api/v1/volunteers/:id", isAuthorized, updateSingleVolunteer);
router.delete("/api/v1/volunteers/:id", isAuthorized, deleteSingleVolunteer);
};

12
services/asset.js Normal file
View File

@@ -0,0 +1,12 @@
var { readFileSync, writeFileSync } = require("fs");
var { join } = require("path");
function saveFile(file) {
let tmpFile = readFileSync(file.path);
let newFileName = Date.now() + file.name;
let newFile = join(__dirname, "..", "assets", newFileName);
writeFileSync(newFile, tmpFile);
return newFileName;
}
module.exports = saveFile;

Binary file not shown.

BIN
storage/database.sqlite3 Normal file

Binary file not shown.