├── .env.example ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .jest └── setEnvVars.js ├── README.md ├── app ├── app.js ├── app.test.js ├── authentication.js ├── booklendings.js ├── booklendings.test.js ├── books.js ├── books.test.js ├── models │ ├── book.js │ ├── booklending.js │ └── student.js ├── students.js ├── students.test.js └── tokenChecker.js ├── index.js ├── jest.config.js ├── jsconfig.json ├── oas3.yaml ├── package-lock.json ├── package.json ├── scripts ├── clearBooks.js └── setup.js └── static ├── index.html └── script.js /.env.example: -------------------------------------------------------------------------------- 1 | # .env.example 2 | 3 | # This is an example file for running the app locally. 4 | # This must be copied into .env file 5 | # To run 6 | # - use npm script: npm run start_local 7 | # - HerokuCLI: heroku local web 8 | 9 | # Passphrase used to generate jwt token 10 | SUPER_SECRET='your-secret-key' 11 | 12 | # Url to the MongoDb database 13 | # DB_URL='your-db-url-with-user-and-password' 14 | DB_URL='mongodb://127.0.0.1:27017' 15 | 16 | # Path to external frontend - If not provided, basic frontend in static/index.html is used 17 | FRONTEND='../EasyLibVue/dist' -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | test: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | # This is used to load Environment-level secrets, from the specified environment. 18 | # Instead, repository secrets are loaded by default. 19 | environment: production 20 | 21 | env: 22 | SUPER_SECRET: ${{ secrets.SUPER_SECRET }} # Must be set as a GitHub secret 23 | DB_URL: ${{ secrets.DB_URL }} # Must be set as a GitHub secret 24 | 25 | strategy: 26 | matrix: 27 | node-version: [18.x] 28 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 29 | 30 | steps: 31 | - uses: actions/checkout@v3 32 | - uses: actions/setup-node@v3 33 | name: Use Node.js ${{ matrix.node-version }} 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | cache: 'npm' 37 | - run: npm ci 38 | - run: npm run build --if-present 39 | - run: npm test 40 | 41 | deploy: 42 | name: Deploy to Render 43 | runs-on: ubuntu-latest 44 | needs: test 45 | steps: 46 | - name: Trigger deployment 47 | uses: sws2apps/render-deployment@main #consider using pin for dependabot auto update 48 | with: 49 | serviceId: ${{ secrets.RENDER_ID }} 50 | apiKey: ${{ secrets.RENDER_API_KEY }} 51 | multipleDeployment: false #optional, default true 52 | 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,windows,linux,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### Node ### 20 | # Logs 21 | logs 22 | *.log 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # Runtime data 28 | pids 29 | *.pid 30 | *.seed 31 | *.pid.lock 32 | 33 | # Directory for instrumented libs generated by jscoverage/JSCover 34 | lib-cov 35 | 36 | # Coverage directory used by tools like istanbul 37 | coverage 38 | 39 | # nyc test coverage 40 | .nyc_output 41 | 42 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | bower_components 47 | 48 | # node-waf configuration 49 | .lock-wscript 50 | 51 | # Compiled binary addons (https://nodejs.org/api/addons.html) 52 | build/Release 53 | 54 | # Dependency directories 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # TypeScript v1 declaration files 59 | typings/ 60 | 61 | # Optional npm cache directory 62 | .npm 63 | 64 | # Optional eslint cache 65 | .eslintcache 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # next.js build output 83 | .next 84 | 85 | # nuxt.js build output 86 | .nuxt 87 | 88 | # vuepress build output 89 | .vuepress/dist 90 | 91 | # Serverless directories 92 | .serverless 93 | 94 | # FuseBox cache 95 | .fusebox/ 96 | 97 | ### Windows ### 98 | # Windows thumbnail cache files 99 | Thumbs.db 100 | ehthumbs.db 101 | ehthumbs_vista.db 102 | 103 | # Dump file 104 | *.stackdump 105 | 106 | # Folder config file 107 | [Dd]esktop.ini 108 | 109 | # Recycle Bin used on file shares 110 | $RECYCLE.BIN/ 111 | 112 | # Windows Installer files 113 | *.cab 114 | *.msi 115 | *.msix 116 | *.msm 117 | *.msp 118 | 119 | # Windows shortcuts 120 | *.lnk 121 | 122 | ### VisualStudioCode ### 123 | .vscode/* 124 | !.vscode/settings.json 125 | !.vscode/tasks.json 126 | !.vscode/launch.json 127 | !.vscode/extensions.json 128 | 129 | # End of https://www.gitignore.io/api/node,windows,linux,visualstudiocode 130 | 131 | 132 | **/.DS_Store -------------------------------------------------------------------------------- /.jest/setEnvVars.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EasyLib 2 | 3 | [![Build Status](https://travis-ci.org/2020-unitn-SE2/EasyLib.svg?branch=master)](https://travis-ci.org/2020-unitn-SE2/EasyLib) 4 | 5 | Application deployed on Heroku: 6 | https://easy-lib.herokuapp.com/ 7 | 8 | To start locally, use the npm script start_local, which loads environment variables from the `.env` file. Ideally, passwords in the `.env` file should not be reveled publicly, therefore the file should not be put in the repository. However, people should be able to easily re-create it starting from the `.env.example` file. 9 | 10 | ```shell 11 | > npm run start_local 12 | ``` 13 | 14 | 15 | 16 | ## Authentication 17 | 18 | https://medium.com/@technospace/understanding-json-web-tokens-jwt-a9064621f2ca 19 | 20 | ![Simple procedure on JWT token generation and validation at the same server](https://miro.medium.com/max/700/1*7T41R0dSLEzssIXPHpvimQ.png) 21 | 22 | Use this to manually encode and decode a token: 23 | - https://jwt.io/ 24 | 25 | Implementation in EasyLib: 26 | 27 | ```javascript 28 | app.post('/api/v1/authentications', async function(req, res) { 29 | 30 | // find the user 31 | let user = await Student.findOne({ email: req.body.email }).exec(); 32 | 33 | // user not found 34 | if (!user) 35 | res.json({ success: false, message: 'Authentication failed. User not found.' }); 36 | 37 | // check if password matches 38 | if (user.password != req.body.password) 39 | res.json({ success: false, message: 'Authentication failed. Wrong password.' }); 40 | 41 | // if user is found and password is right create a token 42 | var token = jwt.sign({ email: user.email }, process.env.SUPER_SECRET, { expiresIn: 86400 }); 43 | 44 | res.json({ 45 | success: true, 46 | message: 'Enjoy your token!', 47 | token: token 48 | }); 49 | 50 | }); 51 | } 52 | ``` 53 | 54 | File tokenChecker.js: 55 | ```javascript 56 | const tokenChecker = function(req, res, next) { 57 | 58 | // check header or url parameters or post parameters for token 59 | var token = req.body.token || req.query.token || req.headers['x-access-token']; 60 | 61 | // if there is no token 62 | if (!token) { 63 | return res.status(401).send({ 64 | success: false, 65 | message: 'No token provided.' 66 | }); 67 | } 68 | 69 | // decode token, verifies secret and checks exp 70 | jwt.verify(token, process.env.SUPER_SECRET, function(err, decoded) { 71 | if (err) { 72 | return res.status(403).send({ 73 | success: false, 74 | message: 'Failed to authenticate token.' 75 | }); 76 | } else { 77 | // if everything is good, save to request for use in other routes 78 | req.loggedUser = decoded; 79 | next(); 80 | } 81 | }); 82 | 83 | }; 84 | 85 | module.exports = tokenChecker 86 | ``` 87 | 88 | To protect endpoints, we can put the tockenChecker.js middelware before all route handlers that we want to protect: 89 | ```javascript 90 | app.use('/api/v1/authentications', authentication); 91 | ... 92 | // requests handled until here are not authenticated 93 | app.use('', tokenChecker); 94 | // request with no valid token stop here 95 | // requests that goes through the tokenChecker have the field `req.loggedUser` set to the decoded token 96 | ``` 97 | Or we can protect specific endpoints, by associating the tockenChecker with specific url: 98 | ```javascript 99 | app.use('/api/v1/booklendings', tokenChecker); 100 | ... 101 | // only requests matching '/api/v1/booklendings' have been authenticated by the tokenChecker 102 | app.use('/api/v1/booklendings', booklendings); 103 | // Requests on '/api/v1/students' are not authenticated 104 | app.use('/api/v1/students', books); 105 | ``` 106 | 107 | https://livecodestream.dev/post/2020-08-11-a-practical-guide-to-jwt-authentication-with-nodejs/ 108 | 109 | 110 | 111 | ## MongoDb 112 | 113 | https://mongoosejs.com/docs/ 114 | 115 | 116 | 117 | # Environment Variables 118 | 119 | Environment variables are used to: 120 | - Remove password and private configurations from the code; 121 | - Manage different configurations for different execution environments. 122 | 123 | ## Local Environment Variables 124 | 125 | https://medium.com/the-node-js-collection/making-your-node-js-work-everywhere-with-environment-variables-2da8cdf6e786 126 | 127 | Locally, environment variables can be set at system level or passed in the command line: 128 | 129 | ```shell 130 | > PORT=8626 node server.js 131 | ``` 132 | 133 | Alternatively, environment variables can be managed in a `.env` file, which ideally should not be put under version control. However, it is useful to provide a `.env.example` file with the list of all variables. 134 | 135 | The .env file can be pre-loaded using the `dotenv` module. 136 | 137 | ```shell 138 | > npm install dotenv 139 | > node -r dotenv/config server.js 140 | ``` 141 | 142 | A script can be defined in the package.json: 143 | 144 | ```javascript 145 | scripts: { 146 | "test": "jest", 147 | "start": "node index.js", 148 | "start_local": "node -r dotenv/config server.js" 149 | } 150 | ``` 151 | 152 | It can be run by: 153 | 154 | ```shell 155 | > npm run start_local 156 | ``` 157 | 158 | ### Loading .env when using Jest 159 | 160 | https://stackoverflow.com/questions/48033841/test-process-env-with-jest 161 | 162 | When using Jest locally, a setup file can be used to load variablem from `.env` file with `dotenv`. 163 | 164 | Jest configuration file `setEnvVars.js`: 165 | ```javascript 166 | require("dotenv").config() 167 | ``` 168 | 169 | The `setupFiles` option can be specified in `jest.config.js` or directly in the `package.json`. 170 | 171 | https://jestjs.io/docs/en/configuration#setupfiles-array 172 | 173 | 174 | ## Cloud services Environment Variables 175 | 176 | On Heroku, TravisCI, or other cloud services, it is possible to manually set env variables. 177 | 178 | - Heroku https://devcenter.heroku.com/articles/config-vars 179 | - TravisCI https://docs.travis-ci.com/user/environment-variables/ 180 | 181 | When HerokuCLI is used to run the app locally, it automatically load env variables from .env file. 182 | 183 | https://devcenter.heroku.com/articles/heroku-local#copy-heroku-config-vars-to-your-local-env-file 184 | 185 | ```shell 186 | > heroku local web 187 | ``` 188 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | 3 | const express = require('express'); 4 | const app = express(); 5 | const cors = require('cors') 6 | 7 | const authentication = require('./authentication.js'); 8 | const tokenChecker = require('./tokenChecker.js'); 9 | 10 | const students = require('./students.js'); 11 | const books = require('./books.js'); 12 | const booklendings = require('./booklendings.js'); 13 | 14 | 15 | /** 16 | * Configure Express.js parsing middleware 17 | */ 18 | app.use(express.json()); 19 | app.use(express.urlencoded({ extended: true })); 20 | 21 | 22 | 23 | /** 24 | * CORS requests 25 | */ 26 | app.use(cors()) 27 | 28 | // // Add headers before the routes are defined 29 | // app.use(function (req, res, next) { 30 | 31 | // // Website you wish to allow to connect 32 | // res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000'); 33 | 34 | // // Request methods you wish to allow 35 | // res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); 36 | 37 | // // Request headers you wish to allow 38 | // res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); 39 | 40 | // // Set to true if you need the website to include cookies in the requests sent 41 | // // to the API (e.g. in case you use sessions) 42 | // res.setHeader('Access-Control-Allow-Credentials', true); 43 | 44 | // // Pass to next layer of middleware 45 | // next(); 46 | // }); 47 | 48 | 49 | 50 | /** 51 | * Serve front-end static files 52 | */ 53 | const FRONTEND = process.env.FRONTEND || Path.join( __dirname, '..', 'node_modules', 'easylibvue', 'dist' ); 54 | app.use('/EasyLibApp/', express.static( FRONTEND )); 55 | 56 | // If process.env.FRONTEND folder does not contain index.html then use the one from static 57 | app.use('/', express.static('static')); // expose also this folder 58 | 59 | 60 | 61 | app.use((req,res,next) => { 62 | console.log(req.method + ' ' + req.url) 63 | next() 64 | }) 65 | 66 | 67 | 68 | /** 69 | * Authentication routing and middleware 70 | */ 71 | app.use('/api/v1/authentications', authentication); 72 | 73 | // Protect booklendings endpoint 74 | // access is restricted only to authenticated users 75 | // a valid token must be provided in the request 76 | app.use('/api/v1/booklendings', tokenChecker); 77 | app.use('/api/v1/students/me', tokenChecker); 78 | 79 | 80 | 81 | /** 82 | * Resource routing 83 | */ 84 | 85 | app.use('/api/v1/books', books); 86 | app.use('/api/v1/students', students); 87 | app.use('/api/v1/booklendings', booklendings); 88 | 89 | 90 | 91 | /* Default 404 handler */ 92 | app.use((req, res) => { 93 | res.status(404); 94 | res.json({ error: 'Not found' }); 95 | }); 96 | 97 | 98 | 99 | /* Default error handler */ 100 | app.use((err, req, res, next) => { 101 | console.error(err.stack); 102 | res.status(500).json({ error: 'Something broke!' }); 103 | }); 104 | 105 | 106 | 107 | module.exports = app; 108 | -------------------------------------------------------------------------------- /app/app.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.npmjs.com/package/supertest 3 | */ 4 | const request = require('supertest'); 5 | const jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens 6 | const app = require('./app'); 7 | 8 | test('app module should be defined', () => { 9 | expect(app).toBeDefined(); 10 | }); 11 | 12 | test('GET / should return 200', () => { 13 | return request(app) 14 | .get('/') 15 | .expect(200); 16 | }); 17 | -------------------------------------------------------------------------------- /app/authentication.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const Student = require('./models/student'); // get our mongoose model 4 | const jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens 5 | const {OAuth2Client} = require('google-auth-library'); 6 | 7 | const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID; 8 | /** 9 | * https://developers.google.com/identity/gsi/web/guides/verify-google-id-token?hl=it#node.js 10 | */ 11 | const client = new OAuth2Client( GOOGLE_CLIENT_ID ); 12 | async function verify( token ) { 13 | const ticket = await client.verifyIdToken({ 14 | idToken: token, 15 | // audience: GOOGLE_CLIENT_ID, // Specify the CLIENT_ID of the app that accesses the backend 16 | // Or, if multiple clients access the backend: 17 | //[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3] 18 | }); 19 | const payload = ticket.getPayload(); 20 | const userid = payload['sub']; 21 | // If the request specified a Google Workspace domain: 22 | // const domain = payload['hd']; 23 | return payload; 24 | } 25 | 26 | // --------------------------------------------------------- 27 | // route to authenticate and get a new token 28 | // --------------------------------------------------------- 29 | router.post('', async function(req, res) { 30 | 31 | var user = {}; 32 | 33 | if ( req.body.googleToken ) { 34 | const payload = await verify( req.body.googleToken ).catch(console.error); 35 | console.log(payload); 36 | 37 | user = await Student.findOne({ email: payload['email'] }).exec(); 38 | if ( ! user ) { 39 | user = new Student({ 40 | email: payload['email'], 41 | password: 'default-google-password-to-be-changed' 42 | }); 43 | await user.save().exec(); 44 | console.log('Student created after login with google'); 45 | 46 | } 47 | 48 | } 49 | else { 50 | // find the user in the local db 51 | user = await Student.findOne({ 52 | email: req.body.email 53 | }).exec(); 54 | 55 | // local user not found 56 | if (!user) { 57 | res.status(401).json({ success: false, message: 'Authentication failed. User not found.' }); 58 | return; 59 | } 60 | 61 | // check if password matches 62 | if (user.password != req.body.password) { 63 | res.status(401).json({ success: false, message: 'Authentication failed. Wrong password.' }); 64 | return; 65 | } 66 | } 67 | 68 | // if user is found or created create a token 69 | var payload = { 70 | email: user.email, 71 | id: user._id 72 | // other data encrypted in the token 73 | } 74 | var options = { 75 | expiresIn: 86400 // expires in 24 hours 76 | } 77 | var token = jwt.sign(payload, process.env.SUPER_SECRET, options); 78 | 79 | res.json({ 80 | success: true, 81 | message: 'Enjoy your token!', 82 | token: token, 83 | email: user.email, 84 | id: user._id, 85 | self: "api/v1/" + user._id 86 | }); 87 | 88 | }); 89 | 90 | 91 | 92 | module.exports = router; -------------------------------------------------------------------------------- /app/booklendings.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const Booklending = require('./models/booklending'); // get our mongoose model 4 | const Student = require('./models/student'); // get our mongoose model 5 | const Book = require('./models/book'); // get our mongoose model 6 | 7 | 8 | 9 | /** 10 | * Resource representation based on the following the pattern: 11 | * https://cloud.google.com/blog/products/application-development/api-design-why-you-should-use-links-not-keys-to-represent-relationships-in-apis 12 | */ 13 | router.get('', async (req, res) => { 14 | let booklendings; 15 | 16 | const filter = {}; 17 | if ( req.query.studentId ) 18 | filter.student = req.query.studentId; 19 | 20 | booklendings = await Booklending 21 | .find( filter ) 22 | .populate( 'student', '-password -__v' ) 23 | .populate( 'book', '-__v' ) 24 | .exec(); 25 | 26 | booklendings = booklendings.map( (dbEntry) => { 27 | return { 28 | self: '/api/v1/booklendings/' + dbEntry._id, 29 | student: { 30 | self: '/api/v1/students/' + dbEntry.student._id, 31 | email: dbEntry.student.email 32 | }, 33 | book: { 34 | self: '/api/v1/books/' + dbEntry.book._id, 35 | title: dbEntry.book.title 36 | } 37 | }; 38 | }); 39 | 40 | res.status(200).json(booklendings); 41 | }); 42 | 43 | 44 | 45 | router.post('', async (req, res) => { 46 | let studentUrl = req.body.student; 47 | let bookUrl = req.body.book; 48 | 49 | if (!studentUrl){ 50 | res.status(400).json({ error: 'Student not specified' }); 51 | return; 52 | }; 53 | 54 | if (!bookUrl) { 55 | res.status(400).json({ error: 'Book not specified' }); 56 | return; 57 | }; 58 | 59 | let studentId = studentUrl.substring(studentUrl.lastIndexOf('/') + 1); 60 | let student = null; 61 | try { 62 | student = await Student.findById(studentId); 63 | } catch (error) { 64 | // This catch CastError when studentId cannot be casted to mongoose ObjectId 65 | // CastError: Cast to ObjectId failed for value "11" at path "_id" for model "Student" 66 | } 67 | 68 | if(student == null) { 69 | res.status(400).json({ error: 'Student does not exist' }); 70 | return; 71 | }; 72 | 73 | let bookId = bookUrl.substring(bookUrl.lastIndexOf('/') + 1); 74 | let book = null; 75 | try { 76 | book = await Book.findById(bookId).exec(); 77 | } catch (error) { 78 | // CastError: Cast to ObjectId failed for value "11" at path "_id" for model "Book" 79 | } 80 | 81 | if(book == null) { 82 | res.status(400).json({ error: 'Book does not exist' }); 83 | return; 84 | }; 85 | 86 | if( ( await Booklending.find({book: bookId}).exec() ).length > 0) { 87 | res.status(409).json({ error: 'Book already out' }); 88 | return 89 | } 90 | 91 | let booklending = new Booklending({ 92 | student: studentId, 93 | book: bookId, 94 | }); 95 | 96 | booklending = await booklending.save(); 97 | 98 | let booklendingId = booklending.id; 99 | 100 | res.location("/api/v1/booklendings/" + booklendingId).status(201).send(); 101 | }); 102 | 103 | 104 | 105 | router.delete('/:id', async (req, res) => { 106 | let lending = await Booklending.findById(req.params.id).exec(); 107 | if (!lending) { 108 | res.status(404).send() 109 | console.log('lending not found') 110 | return; 111 | } 112 | await lending.deleteOne() 113 | console.log('lending removed') 114 | res.status(204).send() 115 | }); 116 | 117 | 118 | 119 | module.exports = router; -------------------------------------------------------------------------------- /app/booklendings.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.npmjs.com/package/supertest 3 | */ 4 | const request = require('supertest'); 5 | const app = require('./app'); 6 | const jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens 7 | const mongoose = require('mongoose'); 8 | 9 | describe('GET /api/v1/booklendings', () => { 10 | 11 | let connection; 12 | 13 | beforeAll( async () => { 14 | jest.setTimeout(8000); 15 | jest.unmock('mongoose'); 16 | connection = await mongoose.connect(process.env.DB_URL, {useNewUrlParser: true, useUnifiedTopology: true}); 17 | console.log('Database connected!'); 18 | //return connection; // Need to return the Promise db connection? 19 | }); 20 | 21 | afterAll( () => { 22 | mongoose.connection.close(true); 23 | console.log("Database connection closed"); 24 | }); 25 | 26 | // create a valid token 27 | var token = jwt.sign( 28 | {email: 'John@mail.com'}, 29 | process.env.SUPER_SECRET, 30 | {expiresIn: 86400} 31 | ); 32 | 33 | test('POST /api/v1/booklendings with Student not specified', () => { 34 | return request(app) 35 | .post('/api/v1/booklendings') 36 | .set('x-access-token', token) 37 | .set('Accept', 'application/json') 38 | .expect(400, { error: 'Student not specified' }); 39 | }); 40 | 41 | test('POST /api/v1/booklendings with Book not specified', () => { 42 | return request(app) 43 | .post('/api/v1/booklendings') 44 | .set('x-access-token', token) 45 | .set('Accept', 'application/json') 46 | .send({ student: 'whatever' }) // sends a JSON post body 47 | .expect(400, { error: 'Book not specified' }); 48 | }); 49 | 50 | test('POST /api/v1/booklendings Student does not exist', () => { 51 | return request(app) 52 | .post('/api/v1/booklendings') 53 | .set('x-access-token', token) 54 | .set('Accept', 'application/json') 55 | .send({ student: '/api/v1/students/111', book: '/api/v1/books/0' }) // sends a JSON post body 56 | .expect(400, { error: 'Student does not exist' }); 57 | }); 58 | 59 | test('POST /api/v1/booklendings Book does not exist', () => { 60 | return request(app) 61 | .get('/api/v1/students') 62 | .expect('Content-Type', /json/) 63 | .expect(200) 64 | .then( (res) => { 65 | return request(app) 66 | .post('/api/v1/booklendings') 67 | .set('x-access-token', token) 68 | .set('Accept', 'application/json') 69 | .send({ student: res.body[0].self, book: '/api/v1/books/0' }) // sends a JSON post body 70 | .expect(400, { error: 'Book does not exist' }); 71 | }); 72 | }); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /app/books.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const Book = require('./models/book'); // get our mongoose model 4 | 5 | 6 | 7 | /** 8 | * Resource representation based on the following the pattern: 9 | * https://cloud.google.com/blog/products/application-development/api-design-why-you-should-use-links-not-keys-to-represent-relationships-in-apis 10 | */ 11 | router.get('', async (req, res) => { 12 | // https://mongoosejs.com/docs/api.html#model_Model.find 13 | let books = await Book.find({}); 14 | books = books.map( (book) => { 15 | return { 16 | self: '/api/v1/books/' + book.id, 17 | title: book.title 18 | }; 19 | }); 20 | res.status(200).json(books); 21 | }); 22 | 23 | router.use('/:id', async (req, res, next) => { 24 | // https://mongoosejs.com/docs/api.html#model_Model.findById 25 | let book = await Book.findById(req.params.id).exec(); 26 | if (!book) { 27 | res.status(404).send() 28 | console.log('book not found') 29 | return; 30 | } 31 | req['book'] = book; 32 | next() 33 | }); 34 | 35 | router.get('/:id', async (req, res) => { 36 | let book = req['book']; 37 | res.status(200).json({ 38 | self: '/api/v1/books/' + book.id, 39 | title: book.title 40 | }); 41 | }); 42 | 43 | router.delete('/:id', async (req, res) => { 44 | let book = req['book']; 45 | await Book.deleteOne({ _id: req.params.id }); 46 | console.log('book removed') 47 | res.status(204).send() 48 | }); 49 | 50 | router.post('', async (req, res) => { 51 | 52 | let book = new Book({ 53 | title: req.body.title 54 | }); 55 | 56 | book = await book.save(); 57 | 58 | let bookId = book._id; 59 | 60 | console.log('Book saved successfully'); 61 | 62 | /** 63 | * Link to the newly created resource is returned in the Location header 64 | * https://www.restapitutorial.com/lessons/httpmethods.html 65 | */ 66 | res.location("/api/v1/books/" + bookId).status(201).send(); 67 | }); 68 | 69 | 70 | module.exports = router; 71 | -------------------------------------------------------------------------------- /app/books.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.npmjs.com/package/supertest 3 | */ 4 | const request = require('supertest'); 5 | const app = require('./app'); 6 | 7 | describe('GET /api/v1/books', () => { 8 | 9 | // Moking Book.find method 10 | let bookSpy; 11 | // Moking Book.findById method 12 | let bookSpyFindById; 13 | 14 | beforeAll( () => { 15 | const Book = require('./models/book'); 16 | bookSpy = jest.spyOn(Book, 'find').mockImplementation((criterias) => { 17 | return [{ 18 | id: 1010, 19 | title: 'Software Engineering 2' 20 | }]; 21 | }); 22 | bookSpyFindById = jest.spyOn(Book, 'findById').mockImplementation((id) => { 23 | return { 24 | exec: () => { 25 | if (id==1010) 26 | return { 27 | id: 1010, 28 | title: 'Software Engineering 2' 29 | }; 30 | else 31 | return {}; 32 | } 33 | }; 34 | }) 35 | }); 36 | 37 | afterAll(async () => { 38 | bookSpy.mockRestore(); 39 | bookSpyFindById.mockRestore(); 40 | }); 41 | 42 | test('GET /api/v1/books should respond with an array of books', async () => { 43 | return request(app) 44 | .get('/api/v1/books') 45 | .expect('Content-Type', /json/) 46 | .expect(200) 47 | .then( (res) => { 48 | if(res.body && res.body[0]) { 49 | expect(res.body[0]).toEqual({ 50 | self: '/api/v1/books/1010', 51 | title: 'Software Engineering 2' 52 | }); 53 | } 54 | }); 55 | }); 56 | 57 | 58 | test('GET /api/v1/books/:id should respond with json', async () => { 59 | return request(app) 60 | .get('/api/v1/books/1010') 61 | .expect('Content-Type', /json/) 62 | .expect(200, { 63 | self: '/api/v1/books/1010', 64 | title: 'Software Engineering 2' 65 | }); 66 | }); 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /app/models/book.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | 4 | // set up a mongoose model 5 | module.exports = mongoose.model('Book', new Schema({ 6 | title: String 7 | })); -------------------------------------------------------------------------------- /app/models/booklending.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | 4 | // set up a mongoose model 5 | module.exports = mongoose.model('Booklending', new Schema({ 6 | student: {type: Schema.Types.ObjectId, ref: 'Student'}, 7 | book: {type: Schema.Types.ObjectId, ref: 'Book'} 8 | })); -------------------------------------------------------------------------------- /app/models/student.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | 4 | // set up a mongoose model 5 | module.exports = mongoose.model('Student', new Schema({ 6 | email: String, 7 | password: String 8 | })); -------------------------------------------------------------------------------- /app/students.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const Student = require('./models/student'); // get our mongoose model 4 | 5 | 6 | 7 | router.get('/me', async (req, res) => { 8 | if(!req.loggedUser) { 9 | return; 10 | } 11 | 12 | // https://mongoosejs.com/docs/api.html#model_Model.find 13 | let student = await Student.findOne({email: req.loggedUser.email}); 14 | 15 | res.status(200).json({ 16 | self: '/api/v1/students/' + student.id, 17 | email: student.email 18 | }); 19 | }); 20 | 21 | router.get('', async (req, res) => { 22 | let students; 23 | 24 | if (req.query.email) 25 | // https://mongoosejs.com/docs/api.html#model_Model.find 26 | students = await Student.find({email: req.query.email}).exec(); 27 | else 28 | students = await Student.find().exec(); 29 | 30 | students = students.map( (entry) => { 31 | return { 32 | self: '/api/v1/students/' + entry.id, 33 | email: entry.email 34 | } 35 | }); 36 | 37 | res.status(200).json(students); 38 | }); 39 | 40 | router.post('', async (req, res) => { 41 | 42 | let student = new Student({ 43 | email: req.body.email, 44 | password: req.body.password 45 | }); 46 | 47 | if (!student.email || typeof student.email != 'string' || !checkIfEmailInString(student.email)) { 48 | res.status(400).json({ error: 'The field "email" must be a non-empty string, in email format' }); 49 | return; 50 | } 51 | 52 | student = await student.save(); 53 | 54 | let studentId = student._id; 55 | 56 | /** 57 | * Link to the newly created resource is returned in the Location header 58 | * https://www.restapitutorial.com/lessons/httpmethods.html 59 | */ 60 | res.location("/api/v1/students/" + studentId).status(201).send(); 61 | }); 62 | 63 | 64 | 65 | // https://stackoverflow.com/questions/46155/how-to-validate-an-email-address-in-javascript 66 | function checkIfEmailInString(text) { 67 | // eslint-disable-next-line 68 | var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 69 | return re.test(text); 70 | } 71 | 72 | 73 | 74 | module.exports = router; 75 | -------------------------------------------------------------------------------- /app/students.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.npmjs.com/package/supertest 3 | */ 4 | const request = require('supertest'); 5 | const jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens 6 | const app = require('./app'); 7 | 8 | describe('GET /api/v1/students/me', () => { 9 | 10 | // Moking User.findOne method 11 | let userSpy; 12 | 13 | beforeAll( () => { 14 | const User = require('./models/student'); 15 | userSpy = jest.spyOn(User, 'findOne').mockImplementation((criterias) => { 16 | return { 17 | id: 1212, 18 | email: 'John@mail.com' 19 | }; 20 | }); 21 | }); 22 | 23 | afterAll(async () => { 24 | userSpy.mockRestore(); 25 | }); 26 | 27 | test('GET /api/v1/students/me with no token should return 401', async () => { 28 | const response = await request(app).get('/api/v1/students/me'); 29 | expect(response.statusCode).toBe(401); 30 | }); 31 | 32 | test('GET /api/v1/students/me?token= should return 403', async () => { 33 | const response = await request(app).get('/api/v1/students/me?token=123456'); 34 | expect(response.statusCode).toBe(403); 35 | }); 36 | 37 | // create a valid token 38 | var payload = { 39 | email: 'John@mail.com' 40 | } 41 | var options = { 42 | expiresIn: 86400 // expires in 24 hours 43 | } 44 | var token = jwt.sign(payload, process.env.SUPER_SECRET, options); 45 | 46 | test('GET /api/v1/students/me?token= should return 200', async () => { 47 | expect.assertions(1); 48 | const response = await request(app).get('/api/v1/students/me?token='+token); 49 | expect(response.statusCode).toBe(200); 50 | }); 51 | 52 | test('GET /api/v1/students/me?token= should return user information', async () => { 53 | expect.assertions(2); 54 | const response = await request(app).get('/api/v1/students/me?token='+token); 55 | const user = response.body; 56 | expect(user).toBeDefined(); 57 | expect(user.email).toBe('John@mail.com'); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /app/tokenChecker.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens 2 | 3 | const tokenChecker = function(req, res, next) { 4 | 5 | // check header or url parameters or post parameters for token 6 | var token = req.query.token || req.headers['x-access-token']; 7 | 8 | // if there is no token 9 | if (!token) { 10 | return res.status(401).send({ 11 | success: false, 12 | message: 'No token provided.' 13 | }); 14 | } 15 | 16 | // decode token, verifies secret and checks exp 17 | jwt.verify(token, process.env.SUPER_SECRET, function(err, decoded) { 18 | if (err) { 19 | return res.status(403).send({ 20 | success: false, 21 | message: 'Failed to authenticate token.' 22 | }); 23 | } else { 24 | // if everything is good, save to request for use in other routes 25 | req.loggedUser = decoded; 26 | next(); 27 | } 28 | }); 29 | 30 | }; 31 | 32 | module.exports = tokenChecker -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const app = require('./app/app.js'); 2 | const mongoose = require('mongoose'); 3 | 4 | /** 5 | * https://devcenter.heroku.com/articles/preparing-a-codebase-for-heroku-deployment#4-listen-on-the-correct-port 6 | */ 7 | const port = process.env.PORT || 8080; 8 | 9 | 10 | /** 11 | * Configure mongoose 12 | */ 13 | // mongoose.Promise = global.Promise; 14 | app.locals.db = mongoose.connect( process.env.DB_URL ) 15 | .then ( () => { 16 | 17 | console.log("Connected to Database"); 18 | 19 | app.listen(port, () => { 20 | console.log(`Server listening on port ${port}`); 21 | }); 22 | 23 | }); -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "C:\\Users\\Marco\\AppData\\Local\\Temp\\jest", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | // clearMocks: false, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | collectCoverage: true, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | coverageDirectory: "coverage", 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "\\\\node_modules\\\\" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: "v8", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "json", 77 | // "jsx", 78 | // "ts", 79 | // "tsx", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | // preset: undefined, 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state between every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state between every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | setupFiles: ["/.jest/setEnvVars.js"], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // The number of seconds after which a test is considered as slow and reported as such in the results. 134 | // slowTestThreshold: 5, 135 | 136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 137 | // snapshotSerializers: [], 138 | 139 | // The test environment that will be used for testing 140 | testEnvironment: "node", 141 | 142 | // Options that will be passed to the testEnvironment 143 | // testEnvironmentOptions: {}, 144 | 145 | // Adds a location field to test results 146 | // testLocationInResults: false, 147 | 148 | // The glob patterns Jest uses to detect test files 149 | // testMatch: [ 150 | // "**/__tests__/**/*.[jt]s?(x)", 151 | // "**/?(*.)+(spec|test).[tj]s?(x)" 152 | // ], 153 | 154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 155 | // testPathIgnorePatterns: [ 156 | // "\\\\node_modules\\\\" 157 | // ], 158 | 159 | // The regexp pattern or array of patterns that Jest uses to detect test files 160 | // testRegex: [], 161 | 162 | // This option allows the use of a custom results processor 163 | // testResultsProcessor: undefined, 164 | 165 | // This option allows use of a custom test runner 166 | // testRunner: "jasmine2", 167 | 168 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 169 | // testURL: "http://localhost", 170 | 171 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 172 | // timers: "real", 173 | 174 | // A map from regular expressions to paths to transformers 175 | // transform: undefined, 176 | 177 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 178 | // transformIgnorePatterns: [ 179 | // "\\\\node_modules\\\\", 180 | // "\\.pnp\\.[^\\\\]+$" 181 | // ], 182 | 183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 184 | // unmockedModulePathPatterns: undefined, 185 | 186 | // Indicates whether each individual test should be reported during the run 187 | verbose: true, 188 | 189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 190 | // watchPathIgnorePatterns: [], 191 | 192 | // Whether to use watchman for file crawling 193 | // watchman: true, 194 | }; 195 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true 5 | }, 6 | "exclude": [ 7 | "node_modules", 8 | "**/node_modules/*" 9 | ] 10 | } -------------------------------------------------------------------------------- /oas3.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.4 2 | info: 3 | version: '1.0' 4 | title: "EasyLib OpenAPI 3.0" 5 | description: API for managing book lendings. 6 | license: 7 | name: MIT 8 | servers: 9 | - url: http://localhost:8080/api/v1 10 | description: Localhost 11 | - url: https://easy-lib.onrender.com/api/v1 12 | description: render.com 13 | paths: 14 | 15 | /authentications: 16 | post: 17 | summary: Authenticate a user 18 | description: >- 19 | Authenticates a user and returns a JWT 'token'. 20 | responses: 21 | '200': 22 | description: 'Token created' 23 | content: 24 | application/json: 25 | schema: 26 | type: object 27 | properties: 28 | success: 29 | type: boolean 30 | description: 'True if the authentication was successful' 31 | token: 32 | type: string 33 | description: 'JWT token' 34 | '401': 35 | description: 'Unauthorized. Invalid credentials' 36 | requestBody: 37 | description: >- 38 | The email and password must be passed in the body. 39 | required: true 40 | content: 41 | application/json: 42 | schema: 43 | type: object 44 | required: 45 | - email 46 | - password 47 | properties: 48 | email: 49 | type: string 50 | description: 'Email address of the user' 51 | password: 52 | type: string 53 | description: 'Password of the user' 54 | examples: 55 | example1: 56 | value: 57 | email: 'mario.rossi@unitn.it' 58 | password: 'password' 59 | 60 | 61 | /students: 62 | get: 63 | description: >- 64 | Gets the list of students. 65 | It is possible to show students by their role /students?role={role} 66 | summary: View all students 67 | parameters: 68 | - in: query 69 | name: role 70 | schema: 71 | type: string 72 | enum: [user, poweruser, admin] 73 | responses: 74 | '200': 75 | description: 'Collection of students' 76 | content: 77 | application/json: 78 | schema: 79 | type: array 80 | items: 81 | $ref: '#/components/schemas/Student' 82 | post: 83 | description: >- 84 | Creates a new student in the system. 85 | summary: Register a new student 86 | requestBody: 87 | content: 88 | application/json: 89 | schema: 90 | $ref: '#/components/schemas/Student' 91 | examples: 92 | example1: 93 | value: 94 | email: 'mario.rossi@unitn.it' 95 | responses: 96 | '201': 97 | description: 'Student created. Link in the Location header' 98 | headers: 99 | 'Location': 100 | schema: 101 | type: string 102 | description: Link to the newly created student. 103 | 104 | /books: 105 | get: 106 | description: >- 107 | Gets the list of books. 108 | It is possible to show books by their status /books?status={status} 109 | summary: View all books 110 | parameters: 111 | - in: query 112 | name: status 113 | schema: 114 | type: string 115 | enum: [available, lended] 116 | responses: 117 | '200': 118 | description: 'Collection of books' 119 | content: 120 | application/json: 121 | schema: 122 | type: array 123 | items: 124 | $ref: '#/components/schemas/Book' 125 | post: 126 | description: >- 127 | Creates a new book in the system. 128 | summary: Add a new book 129 | requestBody: 130 | content: 131 | application/json: 132 | schema: 133 | $ref: '#/components/schemas/Book' 134 | examples: 135 | example1: 136 | value: 137 | title: 'The Great Gatsby' 138 | responses: 139 | '201': 140 | description: 'Book created. Link in the Location header' 141 | headers: 142 | 'Location': 143 | schema: 144 | type: string 145 | description: Link to the newly created book. 146 | /books/{bookId}: 147 | get: 148 | description: >- 149 | Gets the book with the given ID. 150 | summary: View a book 151 | parameters: 152 | - in: path 153 | name: bookId 154 | required: true 155 | schema: 156 | type: string 157 | description: 'ID of the book' 158 | responses: 159 | '200': 160 | description: 'Book found' 161 | content: 162 | application/json: 163 | schema: 164 | $ref: '#/components/schemas/Book' 165 | '404': 166 | description: 'Book not found' 167 | delete: 168 | description: >- 169 | Deletes the book with the given ID. 170 | summary: Delete a book 171 | parameters: 172 | - in: path 173 | name: bookId 174 | required: true 175 | schema: 176 | type: string 177 | description: 'ID of the book' 178 | responses: 179 | '204': 180 | description: 'Book deleted' 181 | '404': 182 | description: 'Book not found' 183 | 184 | /booklendings: 185 | get: 186 | description: >- 187 | Gets the list of booklendings. 188 | It is possible to show booklendings by studentId /booklendings?studentId={student} 189 | summary: View all booklendings 190 | security: 191 | - TokenQueryAuth: [] 192 | XAccessTokenHeaderAuth: [] 193 | parameters: 194 | - in: query 195 | name: studentId 196 | schema: 197 | type: string 198 | description: 'ID of the student' 199 | responses: 200 | '200': 201 | description: 'Collection of booklendings' 202 | content: 203 | application/json: 204 | schema: 205 | type: array 206 | items: 207 | $ref: '#/components/schemas/Booklending' 208 | post: 209 | summary: Borrow a book 210 | description: >- 211 | Creates a new booklending. 212 | Token must be passed in the header. 213 | The student and book must already exist in the system. 214 | The book must be available. 215 | The book status will be set to 'lended'. 216 | The booklending will be created with the current date as start date. 217 | The booklending will be created with the end date set to 30 days from now. 218 | The booklending will be created with the status 'active'. 219 | security: 220 | - TokenQueryAuth: [] 221 | XAccessTokenHeaderAuth: [] 222 | responses: 223 | '201': 224 | description: 'Booklending created. Link in the Location header' 225 | headers: 226 | 'Location': 227 | schema: 228 | type: string 229 | description: Link to the newly created booklending. 230 | requestBody: 231 | description: >- 232 | The booklending object to be created. 233 | The student and book must already exist in the system. 234 | The book must be available. 235 | The book status will be set to 'lended'. 236 | The booklending will be created with the current date as start date. 237 | The booklending will be created with the end date set to 30 days from now. 238 | The booklending will be created with the status 'active'. 239 | required: true 240 | content: 241 | application/json: 242 | schema: 243 | $ref: '#/components/schemas/Booklending' 244 | examples: 245 | example1: 246 | value: 247 | student: 'http://localhost:8080/api/v1/students/1' 248 | book: 'http://localhost:8080/api/v1/books/1' 249 | 250 | components: 251 | 252 | securitySchemes: 253 | 254 | XAccessTokenHeaderAuth: # arbitrary name for the security scheme 255 | description: >- 256 | The API authentication. 257 | The API key must be passed in the header 'x-access-token'. 258 | The API key must be a valid JWT token. 259 | type: apiKey 260 | in: header 261 | name: x-access-token 262 | 263 | TokenQueryAuth: # arbitrary name for the security scheme 264 | description: >- 265 | The API authentication. 266 | The API key must be passed in the query string 'token'. 267 | The API key must be a valid JWT token. 268 | type: apiKey 269 | in: query 270 | name: token 271 | 272 | schemas: 273 | 274 | Student: 275 | type: object 276 | required: 277 | - email 278 | properties: 279 | self: 280 | type: string 281 | description: 'Link to the student' 282 | email: 283 | type: string 284 | description: 'Email address of the student' 285 | 286 | Book: 287 | type: object 288 | required: 289 | - title 290 | - author 291 | - ISBN 292 | - status 293 | properties: 294 | self: 295 | type: string 296 | description: 'Link to the book' 297 | title: 298 | type: string 299 | description: 'Title of the book' 300 | author: 301 | type: string 302 | description: 'Author of the book' 303 | ISBN: 304 | type: string 305 | description: 'ISBN of the book' 306 | status: 307 | type: string 308 | enum: [available, lended] 309 | description: 'Tells whether the book is currently available or not' 310 | 311 | Booklending: 312 | type: object 313 | required: 314 | - student 315 | - book 316 | properties: 317 | self: 318 | type: string 319 | description: 'Link to the booklending' 320 | student: 321 | type: string 322 | description: 'Link to the student' 323 | book: 324 | type: string 325 | description: 'Link to the book' 326 | start_date: 327 | type: string 328 | format: date-time 329 | description: 'Start date of the booklending' 330 | default: '${currentDate}' 331 | end_date: 332 | type: string 333 | format: date-time 334 | description: 'End date of the booklending' 335 | status: 336 | type: string 337 | enum: [active, expired] 338 | description: 'Tells whether the booklending is currently active or not' 339 | overdue: 340 | type: boolean 341 | description: >- 342 | Tells whether the booklending is currently overdue or not. 343 | The booklending is overdue if the end date is in the past and the status is 'active'. 344 | The booklending is not overdue if the end date is in the future and the status is 'active'. 345 | The booklending is not overdue if the status is 'expired'. 346 | overdue_days: 347 | type: integer 348 | description: >- 349 | Number of days the booklending is overdue. 350 | The booklending is overdue if the end date is in the past and the status is 'active'. 351 | The booklending is not overdue if the end date is in the future and the status is 'active'. 352 | The booklending is not overdue if the status is 'expired'. 353 | The number of days is calculated as the difference between the current date and the end date. 354 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easylib", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "start": "node index.js", 9 | "start_local": "echo use: $ npm run dev", 10 | "dev": "node -r dotenv/config index.js", 11 | "show-db-url": "dotenv -p DB_URL", 12 | "mongosh": "dotenv -- sh -c 'mongosh $DB_URL '", 13 | "clearBooklendings": "dotenv -- sh -c 'mongosh $DB_URL --eval \"db.booklendings.deleteMany({})\"'", 14 | "clearBooks": "node ./scripts/clearBooks.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/2020-unitn-SE2/EasyLib.git" 19 | }, 20 | "author": "Marco", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/2020-unitn-SE2/EasyLib/issues" 24 | }, 25 | "homepage": "https://github.com/2020-unitn-SE2/EasyLib#readme", 26 | "dependencies": { 27 | "cors": "^2.8.5", 28 | "express": "^4.17.1", 29 | "google-auth-library": "^9.10.0", 30 | "jsonwebtoken": "^9.0.0", 31 | "mongoose": "^8.14.0" 32 | }, 33 | "devDependencies": { 34 | "@types/jest": "^29.5.12", 35 | "@vitejs/plugin-vue": "^5.2.3", 36 | "dotenv": "^8.2.0", 37 | "dotenv-cli": "^8.0.0", 38 | "jest": "^29.7.0", 39 | "supertest": "^6.0.1", 40 | "vite": "^6.3.3" 41 | }, 42 | "engines": { 43 | "node": ">=22.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /scripts/clearBooks.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | var Books = require('../app/models/book'); // get our mongoose model 3 | 4 | 5 | 6 | var mongoose = require('mongoose'); 7 | // connect to database 8 | mongoose.connect(process.env.DB_URL) 9 | .then ( async () => { 10 | console.log("Connected to Database") 11 | 12 | // Clear books 13 | await Books.deleteMany().exec() 14 | console.log("Books removed") 15 | 16 | process.exit(); 17 | }); 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /scripts/setup.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | var Student = require('../app/models/student'); // get our mongoose model 3 | 4 | var mongoose = require('mongoose'); 5 | // connect to database 6 | mongoose.connect(process.env.DB_URL, {useNewUrlParser: true, useUnifiedTopology: true}) 7 | .then ( () => { 8 | console.log("Connected to Database") 9 | }); 10 | 11 | // Clear users 12 | Student.remove().then( () => { 13 | var marco = new Student({ 14 | email: 'marco@unitn.com', 15 | password: '123' 16 | }); 17 | return marco.save(); 18 | }).then( () => { 19 | console.log('User marco@unitn.com saved successfully'); 20 | }).then( () => { 21 | var mario = new Student({ 22 | email: 'mario.rossi@unitn.com', 23 | password: '123' 24 | }); 25 | return mario.save(); 26 | }).then( () => { 27 | console.log('User mario.rossi@unitn.com saved successfully'); 28 | process.exit(); 29 | }); 30 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

EasyLib

10 | 11 |
12 | Logged User: 13 | 14 | 15 | 16 | 17 |
18 | 19 |

Books:

20 |
    21 | 22 |

    Insert new book:

    23 | 24 |
    25 | 26 | 27 |
    28 | 29 |

    Lendings:

    30 |
      31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /static/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This variable stores the logged in user 3 | */ 4 | var loggedUser = {}; 5 | 6 | /** 7 | * This function is called when login button is pressed. 8 | * Note that this does not perform an actual authentication of the user. 9 | * A student is loaded given the specified email, 10 | * if it exists, the studentId is used in future calls. 11 | */ 12 | function login() 13 | { 14 | //get the form object 15 | var email = document.getElementById("loginEmail").value; 16 | var password = document.getElementById("loginPassword").value; 17 | // console.log(email); 18 | 19 | fetch('../api/v1/authentications', { 20 | method: 'POST', 21 | headers: { 'Content-Type': 'application/json' }, 22 | body: JSON.stringify( { email: email, password: password } ), 23 | }) 24 | .then((resp) => resp.json()) // Transform the data into json 25 | .then(function(data) { // Here you get the data to modify as you please 26 | //console.log(data); 27 | loggedUser.token = data.token; 28 | loggedUser.email = data.email; 29 | loggedUser.id = data.id; 30 | loggedUser.self = data.self; 31 | // loggedUser.id = loggedUser.self.substring(loggedUser.self.lastIndexOf('/') + 1); 32 | document.getElementById("loggedUser").textContent = loggedUser.email; 33 | loadLendings(); 34 | return; 35 | }) 36 | .catch( error => console.error(error) ); // If there is any error you will catch them here 37 | 38 | }; 39 | 40 | /** 41 | * This function refresh the list of books 42 | */ 43 | function loadBooks() { 44 | 45 | const ul = document.getElementById('books'); // Get the list where we will place our authors 46 | 47 | ul.textContent = ''; 48 | 49 | fetch('../api/v1/books') 50 | .then((resp) => resp.json()) // Transform the data into json 51 | .then(function(data) { // Here you get the data to modify as you please 52 | 53 | // console.log(data); 54 | 55 | return data.map(function(book) { // Map through the results and for each run the code below 56 | 57 | // let bookId = book.self.substring(book.self.lastIndexOf('/') + 1); 58 | 59 | let li = document.createElement('li'); 60 | let span = document.createElement('span'); 61 | // span.innerHTML = `${book.title}`; 62 | let a = document.createElement('a'); 63 | a.href = book.self 64 | a.textContent = book.title; 65 | // span.innerHTML += `` 66 | let button = document.createElement('button'); 67 | button.type = 'button' 68 | button.onclick = ()=>takeBook(book.self) 69 | button.textContent = 'Take the book'; 70 | 71 | // Append all our elements 72 | span.appendChild(a); 73 | span.appendChild(button); 74 | li.appendChild(span); 75 | ul.appendChild(li); 76 | }) 77 | }) 78 | .catch( error => console.error(error) );// If there is any error you will catch them here 79 | 80 | } 81 | loadBooks(); 82 | 83 | /** 84 | * This function is called by the Take button beside each book. 85 | * It create a new booklendings resource, 86 | * given the book and the logged in student 87 | */ 88 | function takeBook(bookUrl) 89 | { 90 | fetch('../api/v1/booklendings', { 91 | method: 'POST', 92 | headers: { 93 | 'Content-Type': 'application/json', 94 | 'x-access-token': loggedUser.token 95 | }, 96 | body: JSON.stringify( { student: loggedUser.self, book: bookUrl } ), 97 | }) 98 | .then((resp) => { 99 | console.log(resp); 100 | loadLendings(); 101 | return; 102 | }) 103 | .catch( error => console.error(error) ); // If there is any error you will catch them here 104 | 105 | }; 106 | 107 | /** 108 | * This function refresh the list of bookLendings. 109 | * It only load bookLendings given the logged in student. 110 | * It is called every time a book is taken of when the user login. 111 | */ 112 | function loadLendings() { 113 | 114 | const ul = document.getElementById('bookLendings'); // Get the list where we will place our lendings 115 | 116 | ul.innerHTML = ''; 117 | 118 | fetch('../api/v1/booklendings?studentId=' + loggedUser.id + '&token=' + loggedUser.token) 119 | .then((resp) => resp.json()) // Transform the data into json 120 | .then(function(data) { // Here you get the data to modify as you please 121 | 122 | console.log(data); 123 | 124 | return data.map( (entry) => { // Map through the results and for each run the code below 125 | 126 | // let bookId = book.self.substring(book.self.lastIndexOf('/') + 1); 127 | 128 | let li = document.createElement('li'); 129 | let span = document.createElement('span'); 130 | // span.innerHTML = `${entry.book}`; 131 | let a = document.createElement('a'); 132 | a.href = entry.self 133 | a.textContent = entry.book.title; 134 | 135 | // Append all our elements 136 | span.appendChild(a); 137 | li.appendChild(span); 138 | ul.appendChild(li); 139 | }) 140 | }) 141 | .catch( error => console.error(error) );// If there is any error you will catch them here 142 | 143 | } 144 | 145 | 146 | /** 147 | * This function is called by clicking on the "insert book" button. 148 | * It creates a new book given the specified title, 149 | * and force the refresh of the whole list of books. 150 | */ 151 | function insertBook() 152 | { 153 | //get the book title 154 | var bookTitle = document.getElementById("bookTitle").value; 155 | 156 | console.log(bookTitle); 157 | 158 | fetch('../api/v1/books', { 159 | method: 'POST', 160 | headers: { 'Content-Type': 'application/json' }, 161 | body: JSON.stringify( { title: bookTitle } ), 162 | }) 163 | .then((resp) => { 164 | console.log(resp); 165 | loadBooks(); 166 | return; 167 | }) 168 | .catch( error => console.error(error) ); // If there is any error you will catch them here 169 | 170 | }; --------------------------------------------------------------------------------