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
|
||||
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