Essential backend functionality, including logins.

This commit is contained in:
Rikke Solbjørg 2019-08-21 12:52:12 +02:00
parent d2168ab4a0
commit 99f2c5e7d8
14 changed files with 3965 additions and 1 deletions

View File

@ -1,2 +1,17 @@
# Biblio # Biblio
PVV sitt biblI/Otek RESTful API for PVV's library system (BiblI/Otek).
It's about time I put this up somewhere.
Currently assumes a running local MongoDB instance called "ils", with a password-protected admin user "ils_operator".
To run, type:
```
npm install
npm start [password]
```
ExpressJS is used for routing, bcrypt is used for passwords, mongoose is used to interface with MongoDB.
HATEOAS isn't implemented yet.

View File

@ -0,0 +1,49 @@
'use strict';
const mongoose = require('mongoose'),
Book = mongoose.model('Book');
exports.list_all = function(req, res) {
Book.find({}, function(error, books) {
if (error) res.status(400).send(error);
else res.json(books);
});
};
exports.create = function(req, res) {
// TODO authenticate
const book = new Book(req.body);
book.save(function(error, book) {
if (error) res.status(400).send(error);
else res.json(book);
});
};
exports.get = function(req, res) {
Book.findById(req.params.bookId, function(error, book) {
if (error) res.status(400).send(error);
else res.json(book);
});
};
exports.update = function(req, res) {
Book.findOneAndUpdate({
_id: req.params.bookId
}, req.body, {
new: true
}, function(error, book) {
if (error) res.status(400).send(error);
else res.json(book);
});
};
exports.delete = function(req, res) {
Book.remove({
_id: req.params.bookId
}, function(error, book) {
if (error) res.status(400).send(error);
else res.json({
message: 'Book ' + book.title + ' deleted'
});
});
};

View File

@ -0,0 +1,75 @@
'use strict';
const mongoose = require('mongoose');
const User = mongoose.model('User'),
Book = mongoose.model('Book');
exports.list_all = function(req, res) {
res.json(req.user.borrowing);
};
exports.update = function(req, res) {
Book.findOne({
_id: req.body.bookId
}).then((book) => {
if (!book) {
res.status(400).send("Book not found.");
} else {
// TODO check that the book isn't already borrowed
req.user.borrowing.push({
bookId: book._id,
date: Date.now()
});
User.findOneAndUpdate({
_id: req.user._id
}, req.user, {
useFindAndModify: false
}).exec();
book.amount_loaned += 1;
Book.findOneAndUpdate({
_id: book._id
}, book, {
useFindAndModify: false
}).exec();
res.json(req.user.borrowing);
}
});
};
// TODO
exports.delete = function(req, res) {
Book.findOne({
_id: req.body.bookId
}).then((book) => {
if (!book) {
res.status(400).send("Book not found.");
} else {
let isBookLoaned = false;
for (let i = 0; i < req.user.borrowing.length; i++) {
const b = req.user.borrowing[i];
console.log(`${typeof b.bookId}, ${typeof book.id}`);
if (b.bookId.toString() === book.id) {
console.log(`${b.bookId}, ${book._id}`);
req.user.borrowing.splice(i, 1);
isBookLoaned = true;
}
}
if (isBookLoaned) {
User.findOneAndUpdate({
_id: req.user._id
}, req.user, {
useFindAndModify: false
}).exec();
book.amount_loaned -= 1;
Book.findOneAndUpdate({
_id: book._id
}, book, {
useFindAndModify: false
}).exec();
res.json(req.user.borrowing);
} else {
res.status(400).send("You haven't loaned this book.")
}
}
});
};

View File

@ -0,0 +1,86 @@
'use strict';
const mongoose = require('mongoose'),
User = mongoose.model('User'),
bcrypt = require('bcrypt');
const saltRounds = 10; // TODO make this configurable.
exports.list_all = function(req, res) {
User.find({}, function(err, users) {
if (err) res.status(400).send(err);
else res.json(users);
});
};
exports.create = function(req, res) {
User.findOne({
"username": req.body.username
}, function(err, user) {
if (err) res.status(400).send(err);
else if (user) res.status(400).send("User with that username already exists.");
else { // user doesn't exist, allow creation of new one
const user = new User(req.body);
bcrypt.hash(user.password, saltRounds, function(err, hash) {
if (err) res.status(500).send(err);
else {
user.password = hash;
user.save(function(err, user) {
if (err) res.status(400).send(err);
else res.json(user);
});
}
});
}
});
};
exports.get = function(req, res) {
User.findById(req.params.userId, function(error, user) {
if (error) res.status(400).send(error);
else res.json(user);
});
};
exports.update = function(req, res) {
function updateUser(newUser) {
User.findOneAndUpdate({
_id: req.params.userId
}, req.body, {
new: true,
useFindAndModify: false
}, function(error, user) {
if (error) res.status(400).send(error);
res.json(user);
});
}
if (req.body.password) {
req.body.password = bcrypt.hash(req.body.password, saltRounds).then(hash => {
req.body.password = hash;
updateUser(req.body);
});
} else {
updateUser(req.body);
}
};
exports.delete = function(req, res) {
User.findById(req.params.userId, function(error, user) {
if (error) {
res.status(400).send(error);
} else if (user.loaning.length > 0) {
res.status(403).json({
message: 'User ' + user.username + ' must return books before deletion.'
})
} else {
User.deleteOne({
_id: req.params.userId
}, function(error) {
if (error) res.status(400).send(error);
else res.json({
message: 'User deleted.'
});
});
}
});
};

28
api/models/bookModel.js Normal file
View File

@ -0,0 +1,28 @@
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const BookSchema = new Schema({
title: {
type: String,
required: 'Book title missing'
},
publication_date: {
type: Date,
default: Date(0)
},
quantity: {
type: Number,
default: 1
},
amount_loaned: {
type: Number,
default: 0
},
isbn13: {
type: String,
required: 'Book ISBN-13 missing'
}
});
module.exports = mongoose.model('Book', BookSchema);

70
api/models/userModel.js Normal file
View File

@ -0,0 +1,70 @@
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const permission_set = {
add_books: {
type: Boolean,
default: true
},
update_books: {
type: Boolean,
default: false
},
borrow_books: {
type: Boolean,
default: true
},
add_users: {
type: Boolean,
default: false
},
delete_users: {
type: Boolean,
default: false
},
delete_books: {
type: Boolean,
default: false
},
update_users: {
type: Boolean,
default: false
},
change_permissions: {
type: Boolean,
default: false
}
};
const UserSchema = new Schema({
username: {
type: String,
required: 'Username missing'
},
password: {
type: String,
required: 'Password missing'
},
email: String,
student_card: String,
borrowing: [{
bookId: Schema.Types.ObjectId,
date: Date
}],
permissions: permission_set
});
UserSchema.set('toJSON', {
transform: function(doc, ret, options) {
const json = {
username: ret.username,
borrowing: ret.borrowing,
permissions: ret.permissions,
_id: ret._id
};
return json;
}
});
module.exports = mongoose.model('User', UserSchema);

21
api/routes/baseRouter.js Normal file
View File

@ -0,0 +1,21 @@
const express = require('express');
const router = express.Router();
const userRouter = require('./userRouter'),
bookRouter = require('./bookRouter'),
borrowRouter = require('./borrowRouter');
// logger
router.use(function(req, res, next) {
if (req.user) {
console.log("User %s: %s %s", req.user.username, req.method, req.url);
} else {
console.log('%s %s', req.method, req.url);
}
next();
});
router.use('/users', userRouter);
router.use('/books', bookRouter);
router.use('/borrow', borrowRouter);
module.exports = router

17
api/routes/bookRouter.js Normal file
View File

@ -0,0 +1,17 @@
'use strict';
const express = require('express');
const router = express.Router();
const book = require('../controllers/bookController');
const requirePermission = require('../../utils/requirePermission');
router.route('/')
.get(book.list_all)
.post(requirePermission("borrow_books"), book.create);
router.route('/:bookId')
.get(book.get)
.put(book.update)
.delete(book.delete);
module.exports = router;

View File

@ -0,0 +1,14 @@
'use strict';
const express = require('express')
const router = express.Router()
const borrow = require('../controllers/borrowController');
const requirePermission = require('../../utils/requirePermission');
router.all(requirePermission("borrow_books"));
router.route('/')
.get(borrow.list_all)
.put(borrow.update)
.delete(borrow.delete);
module.exports = router;

17
api/routes/userRouter.js Normal file
View File

@ -0,0 +1,17 @@
'use strict';
const express = require('express');
const router = express.Router();
const user = require('../controllers/userController');
const requirePermission = require('../../utils/requirePermission');
router.route('/')
.get(user.list_all)
.post(requirePermission("add_users"), user.create);
router.route('/:userId')
.get(user.get)
.put(requirePermission("update_users"), user.update)
.delete(requirePermission("delete_users"), user.delete);
module.exports = router;

3475
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "BiblIO",
"version": "1.0.0",
"description": "The backend of PVV's integrated library system.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon server.js"
},
"author": "",
"license": "GPL-3.0",
"devDependencies": {
"nodemon": "^1.19.0"
},
"dependencies": {
"bcrypt": "^3.0.6",
"body-parser": "^1.19.0",
"express": "^4.16.4",
"mongoose": "^5.5.9"
}
}

63
server.js Normal file
View File

@ -0,0 +1,63 @@
const express = require('express'),
router = express.Router(),
app = express(),
port = process.env.PORT || 3000,
Book = require('./api/models/bookModel'),
User = require('./api/models/userModel'),
baseRouter = require('./api/routes/baseRouter'),
mongoose = require('mongoose'),
bodyParser = require('body-parser'),
bcrypt = require('bcrypt');
mongoose.connect('mongodb://127.0.0.1:27017/ils', {
useNewUrlParser: true,
user: "ils_operator",
pass: process.argv[2],
});
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(bodyParser.json());
// Authentication. Currently uses basic access, which sucks, but it should still be secure.
router.use(function(req, res, next) {
const header = req.header('Authorization');
if (header) {
const token = header.split(" ")[1],
parts = new Buffer.from(token, 'base64').toString().split(':'),
username = parts[0],
password = parts[1];
User.findOne({
'username': username
}).then((user) => {
if (user) {
bcrypt.compare(password, user.password).then((success) => {
if (!success) { // incorrect password
res.status(403).send("Incorrect password.");
} else {
// authenticated
req.user = user;
next();
}
});
} else {
res.status(403).send("User " + username + " not found.");
}
});
} else { // No authentication provided, proceed with no permissions.
next();
}
});
router.use('/', baseRouter);
app.use('/', router);
app.listen(port);
console.log('RESTful API server started on: ' + port);

View File

@ -0,0 +1,13 @@
'use strict';
// Adapted from https://stackoverflow.com/questions/9609325/node-js-express-js-user-permission-security-model
module.exports = function requirePermission(permission) {
return function(req, res, next) {
console.log(req.user);
if (req.user && req.user.permissions[permission]) {
next();
} else {
res.status(403).send(`Permission ${permission} not met.`);
}
}
}