Essential backend functionality, including logins.
This commit is contained in:
parent
d2168ab4a0
commit
99f2c5e7d8
17
README.md
17
README.md
|
@ -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.
|
||||||
|
|
|
@ -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'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
|
@ -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.'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
|
@ -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);
|
|
@ -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);
|
|
@ -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
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
@ -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.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue