├── .gitignore ├── images └── profile.png ├── public ├── image │ └── profile │ │ └── profile.png ├── stylesheet │ └── app.css └── assets │ ├── js │ └── popper.min.js │ └── css │ └── font-awesome.min.css ├── .env ├── utils └── delete_image.js ├── models ├── book.js ├── comment.js ├── issue.js ├── activity.js └── user.js ├── views ├── partials │ ├── footer.html │ ├── alerts.html │ ├── header.html │ ├── adminNav.html │ └── userNav.html ├── admin │ ├── adminLogin.html │ ├── book.html │ ├── addBook.html │ ├── notification.html │ ├── user.html │ ├── bookInventory.html │ ├── activities.html │ ├── users.html │ ├── profile.html │ └── index.html ├── landing.html ├── user │ ├── userLogin.html │ ├── notification.html │ ├── userSignup.html │ ├── return-renew.html │ ├── bookDetails.html │ ├── profile.html │ └── index.html ├── signup.html └── books.html ├── routes ├── books.js ├── auth.js ├── users.js ├── index.js └── admin.js ├── middleware └── index.js ├── resize.js ├── LICENSE ├── seed.js ├── package.json ├── controllers ├── auth.js ├── books.js ├── admin.js └── user.js ├── README.md └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | TODO.md -------------------------------------------------------------------------------- /images/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azad71/Library-Management-System/HEAD/images/profile.png -------------------------------------------------------------------------------- /public/image/profile/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azad71/Library-Management-System/HEAD/public/image/profile/profile.png -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | SESSION_SECRET=$2y$12$nNYIs5iStm9gAsdgDGv3l.OyZN3Reav7U.YfwYW/L/171cjIHgjbm 2 | ADMIN_SECRET=opensesame 3 | DB_URL= mongodb://127.0.0.1:27017/library 4 | PORT=5005 5 | -------------------------------------------------------------------------------- /utils/delete_image.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | const deleteImage = (imagePath, next) => { 4 | fs.unlink(imagePath, (err) => { 5 | if (err) { 6 | console.log("Failed to delete image at delete profile"); 7 | return; 8 | } 9 | }); 10 | }; 11 | 12 | module.exports = deleteImage; 13 | -------------------------------------------------------------------------------- /models/book.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const bookSchema = new mongoose.Schema({ 4 | title: String, 5 | ISBN: String, 6 | stock: Number, 7 | author: String, 8 | description: String, 9 | category: String, 10 | comments: [ 11 | { 12 | type: mongoose.Schema.Types.ObjectId, 13 | ref: "Comment", 14 | }, 15 | ], 16 | }); 17 | 18 | module.exports = mongoose.model("Book", bookSchema); 19 | -------------------------------------------------------------------------------- /views/partials/footer.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/stylesheet/app.css: -------------------------------------------------------------------------------- 1 | .btn-group-xs > .btn, .btn-xs { 2 | padding: .40rem .40rem; 3 | font-size: .8rem; 4 | line-height: .5; 5 | border-radius: .2rem; 6 | } 7 | 8 | .wrapper { 9 | display: flex; 10 | justify-content: center; 11 | } 12 | 13 | footer { 14 | flex: 0 0 auto; 15 | display: flex; 16 | color: #ccc; 17 | margin: 1em; 18 | padding: 1em; 19 | width: 80%; 20 | border-top: 1px solid blue; 21 | justify-content: center; 22 | } 23 | 24 | .table-text { 25 | color: red; 26 | font-weight: bold; 27 | } -------------------------------------------------------------------------------- /routes/books.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | // Importing controller 5 | const bookController = require("../controllers/books"); 6 | 7 | // Browse books 8 | router.get("/books/:filter/:value/:page", bookController.getBooks); 9 | 10 | // Fetch books by search value 11 | router.post("/books/:filter/:value/:page", bookController.findBooks); 12 | 13 | // Fetch individual book details 14 | router.get("/books/details/:book_id", bookController.getBookDetails); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /models/comment.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const commentSchema = new mongoose.Schema({ 4 | text: String, 5 | author: { 6 | id: { 7 | type: mongoose.Schema.Types.ObjectId, 8 | ref: "User", 9 | }, 10 | username: String, 11 | }, 12 | 13 | book: { 14 | id: { 15 | type: mongoose.Schema.Types.ObjectId, 16 | ref: "Book", 17 | }, 18 | title: String, 19 | }, 20 | 21 | date: { type: Date, default: Date.now() }, 22 | }); 23 | 24 | module.exports = mongoose.model("Comment", commentSchema); 25 | -------------------------------------------------------------------------------- /views/partials/alerts.html: -------------------------------------------------------------------------------- 1 |
2 | <% if(error && error.length > 0){ %> 3 | 6 | <% } %> 7 | 8 | <% if(success && success.length > 0){ %> 9 | 12 | <% } %> 13 | 14 | <% if(warning && warning.length > 0){ %> 15 | 18 | <% } %> 19 |
-------------------------------------------------------------------------------- /views/partials/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Library Manangement System -------------------------------------------------------------------------------- /middleware/index.js: -------------------------------------------------------------------------------- 1 | const multer = require("multer"); 2 | 3 | const middleware = {}; 4 | 5 | middleware.isLoggedIn = function(req, res, next) { 6 | if(req.isAuthenticated()) { 7 | return next(); 8 | } 9 | req.flash("error", "You need to be logged in first"); 10 | res.redirect("/"); 11 | }; 12 | 13 | middleware.isAdmin = function(req, res, next) { 14 | if(req.isAuthenticated() && req.user.isAdmin) { 15 | return next(); 16 | } 17 | req.flash("error", "Sorry, this route is allowed for admin only"); 18 | res.redirect("/"); 19 | }; 20 | 21 | middleware.upload = multer({ 22 | limits: { 23 | fileSize: 4 * 1024 * 1024, 24 | } 25 | }); 26 | 27 | module.exports = middleware; -------------------------------------------------------------------------------- /resize.js: -------------------------------------------------------------------------------- 1 | const sharp = require('sharp'); 2 | const uuidv4 = require('uuid/v4'); 3 | const path = require('path'); 4 | 5 | class Resize { 6 | constructor(folder) { 7 | this.folder = folder; 8 | } 9 | save(buffer) { 10 | const filename = Resize.filename(); 11 | const filepath = this.filepath(filename); 12 | 13 | sharp(buffer) 14 | .resize(200, 200, { 15 | fit: sharp.fit.inside, 16 | withoutEnlargement: true 17 | }) 18 | .toFile(filepath); 19 | 20 | return filename; 21 | } 22 | static filename() { 23 | return `${uuidv4()}.png`; 24 | } 25 | filepath(filename) { 26 | return path.resolve(`${this.folder}/${filename}`); 27 | } 28 | } 29 | module.exports = Resize; -------------------------------------------------------------------------------- /models/issue.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const issueSchema = new mongoose.Schema({ 4 | book_info: { 5 | id: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: "Book", 8 | }, 9 | title: String, 10 | author: String, 11 | ISBN: String, 12 | category: String, 13 | stock: Number, 14 | issueDate: { type: Date, default: Date.now() }, 15 | returnDate: { type: Date, default: Date.now() + 7 * 24 * 60 * 60 * 1000 }, 16 | isRenewed: { type: Boolean, default: false }, 17 | }, 18 | 19 | user_id: { 20 | id: { 21 | type: mongoose.Schema.Types.ObjectId, 22 | ref: "User", 23 | }, 24 | 25 | username: String, 26 | }, 27 | }); 28 | 29 | module.exports = mongoose.model("Issue", issueSchema); 30 | -------------------------------------------------------------------------------- /models/activity.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const activitySchema = new mongoose.Schema({ 4 | info: { 5 | id: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: "Book", 8 | }, 9 | title: String, 10 | }, 11 | 12 | category: String, 13 | 14 | time: { 15 | id: { 16 | type: mongoose.Schema.Types.ObjectId, 17 | ref: "Issue", 18 | }, 19 | returnDate: Date, 20 | issueDate: Date, 21 | }, 22 | 23 | user_id: { 24 | id: { 25 | type: mongoose.Schema.Types.ObjectId, 26 | ref: "User", 27 | }, 28 | username: String, 29 | }, 30 | 31 | fine: { 32 | amount: Number, 33 | date: Date, 34 | }, 35 | 36 | entryTime: { 37 | type: Date, 38 | default: Date.now(), 39 | }, 40 | }); 41 | 42 | module.exports = mongoose.model("Activity", activitySchema); 43 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const passportLocalMongoose = require("passport-local-mongoose"); 3 | 4 | const userSchema = new mongoose.Schema({ 5 | firstName: { 6 | type: String, 7 | trim: true, 8 | }, 9 | lastName: { 10 | type: String, 11 | trim: true, 12 | }, 13 | username: { 14 | type: String, 15 | trim: true, 16 | }, 17 | email: { 18 | type: String, 19 | trim: true, 20 | }, 21 | password: String, 22 | joined: { type: Date, default: Date.now() }, 23 | bookIssueInfo: [ 24 | { 25 | book_info: { 26 | id: { 27 | type: mongoose.Schema.Types.ObjectId, 28 | ref: "Issue", 29 | }, 30 | }, 31 | }, 32 | ], 33 | gender: String, 34 | address: String, 35 | image: { 36 | type: String, 37 | default: "", 38 | }, 39 | violationFlag: { type: Boolean, default: false }, 40 | fines: { type: Number, default: 0 }, 41 | isAdmin: { type: Boolean, default: false }, 42 | }); 43 | 44 | userSchema.plugin(passportLocalMongoose); 45 | 46 | module.exports = mongoose.model("User", userSchema); 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Md. Abul Hasanat Azad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /seed.js: -------------------------------------------------------------------------------- 1 | const Book = require('./models/book.js'); 2 | const faker = require('faker'); 3 | const category = ["Science", "Biology", "Physics", "Chemistry", "Novel", "Travel", "Cooking", "Philosophy", "Mathematics", "Ethics", "Technology"]; 4 | 5 | const author = []; 6 | for(let i = 0; i < 11; i++) { 7 | author.push(faker.name.findName()); 8 | } 9 | async function seed(limit) { 10 | for(let i = 0; i < 11; i++) { 11 | author.push(faker.name.findName()); 12 | } 13 | for(let i = 0; i < limit; i++) { 14 | let index1 = Math.floor(Math.random() * Math.floor(11)); 15 | let index2 = Math.floor(Math.random() * Math.floor(11)); 16 | try { 17 | const book = new Book({ 18 | title: faker.lorem.words(), 19 | ISBN: faker.random.uuid(), 20 | stock: 10, 21 | author: author[index2], 22 | description: faker.lorem.paragraphs(3), 23 | category: category[index1], 24 | }); 25 | await book.save(); 26 | } catch(err) { 27 | console.log("Error at creating books"); 28 | } 29 | } 30 | } 31 | 32 | module.exports = seed; -------------------------------------------------------------------------------- /views/admin/adminLogin.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 |
11 |
12 |
13 |
14 |

Admin Login

15 |
16 |
17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 | 27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 | <% include ('../partials/footer.html') %> 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "library_management_system", 3 | "version": "1.0.0", 4 | "description": "Basic library management system", 5 | "main": "server.js", 6 | "keywords": [ 7 | "Javascript", 8 | "NodeJS", 9 | "expressjs" 10 | ], 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "start": "node server.js", 14 | "dev:start": "nodemon server.js", 15 | "dev:linux": "export NODE_ENV=dev && nodemon server.js", 16 | "dev:windows": "SET NODE_ENV=dev && nodemon server.js" 17 | }, 18 | "author": "Azad Mamun", 19 | "license": "ISC", 20 | "dependencies": { 21 | "connect-flash": "^0.1.1", 22 | "connect-mongodb-session": "^3.1.1", 23 | "dotenv": "^8.2.0", 24 | "ejs": "^3.1.8", 25 | "express": "^4.16.4", 26 | "express-sanitizer": "^1.0.5", 27 | "express-session": "^1.17.1", 28 | "method-override": "^3.0.0", 29 | "mongoose": "^5.13.15", 30 | "multer": "^1.4.1", 31 | "passport": "^0.6.0", 32 | "passport-local": "^1.0.0", 33 | "passport-local-mongoose": "^5.0.1", 34 | "sharp": "^0.30.5", 35 | "uid": "^1.0.0", 36 | "uuid": "^3.4.0" 37 | }, 38 | "devDependencies": { 39 | "faker": "^4.1.0", 40 | "nodemon": "^2.0.16" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /views/landing.html: -------------------------------------------------------------------------------- 1 | <%- include ('./partials/header.html') %> 2 | 3 | 4 | 5 |
6 |

Welcome To Library Management System

7 | 8 | <%- include ('./partials/alerts.html') %> 9 |
10 |
11 |
12 | Admin Login 13 |
14 | 15 |
16 | User Login 17 |
18 | 19 |
20 | User Sign Up 21 |
22 | 23 |
24 | Browse Books 25 |
26 |
27 |
28 |
29 | 30 | <%- include ('./partials/footer.html') %> -------------------------------------------------------------------------------- /views/user/userLogin.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | <%- include ('../partials/alerts.html') %> 7 | 8 |
9 |
10 |
11 |
12 |
13 |
14 |

User Login

15 |
16 |
17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 | 27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 | <% include ('../partials/footer.html') %> 36 | -------------------------------------------------------------------------------- /views/user/notification.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | <%- include ('../partials/userNav.html') %> 8 | 9 | 10 |
11 |
12 |
13 |
14 |

Notifications

15 |
16 |
17 |
18 |
19 | 20 | 21 |
22 |
23 |
24 | 27 | 28 |
29 |
30 | 31 | 32 | 33 | 34 |
35 |
36 |
37 |
38 |
39 | 40 | 41 |

This route is still under development. will be added in next version

42 | 43 | <% include ('../partials/footer.html') %> 44 | -------------------------------------------------------------------------------- /routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const passport = require("passport"); 4 | 5 | // Import index controller 6 | const authController = require("../controllers/auth"); 7 | 8 | // Import models 9 | const User = require("../models/user"); 10 | 11 | //landing page 12 | router.get("/", authController.getLandingPage); 13 | 14 | //admin login handler 15 | router.get("/auth/admin-login", authController.getAdminLoginPage); 16 | 17 | router.post( 18 | "/auth/admin-login", 19 | passport.authenticate("local", { 20 | successRedirect: "/admin", 21 | failureRedirect: "/auth/admin-login", 22 | }), 23 | (req, res) => {} 24 | ); 25 | 26 | //admin logout handler 27 | router.get("/auth/admin-logout", authController.getAdminLogout); 28 | 29 | // admin sign up handler 30 | router.get("/auth/admin-signup", authController.getAdminSignUp); 31 | 32 | router.post("/auth/admin-signup", authController.postAdminSignUp); 33 | 34 | //user login handler 35 | router.get("/auth/user-login", authController.getUserLoginPage); 36 | 37 | router.post( 38 | "/auth/user-login", 39 | passport.authenticate("local", { 40 | successRedirect: "/user/1", 41 | failureRedirect: "/auth/user-login", 42 | }), 43 | (req, res) => {} 44 | ); 45 | 46 | //user -> user logout handler 47 | router.get("/auth/user-logout", authController.getUserLogout); 48 | 49 | //user sign up handler 50 | router.get("/auth/user-signUp", authController.getUserSignUp); 51 | 52 | router.post("/auth/user-signup", authController.postUserSignUp); 53 | 54 | module.exports = router; 55 | -------------------------------------------------------------------------------- /views/partials/adminNav.html: -------------------------------------------------------------------------------- 1 | 50 | -------------------------------------------------------------------------------- /views/signup.html: -------------------------------------------------------------------------------- 1 | <%- include ('./partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%- include ('./partials/alerts.html') %> 9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |

Admin Sign Up

17 |
18 |
19 |
20 |
21 | 22 | 23 |
24 | 25 |
26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 | 41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | 49 | <% include ('./partials/footer.html') %> 50 | -------------------------------------------------------------------------------- /views/partials/userNav.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/admin/book.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%- include ('../partials/adminNav.html') %> 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 |
24 | 25 |
26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 |
44 | 45 | 46 |
47 |
48 |
49 |
50 | 51 | 52 | 55 | <% include ('../partials/footer.html') %> 56 | -------------------------------------------------------------------------------- /views/admin/addBook.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%- include ('../partials/adminNav.html') %> <% include ('../partials/alerts.html') %> 9 | 10 |
11 |
12 |
13 |
14 |
15 |

Add Book

16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 | 24 |
25 | 26 | 27 |
28 | 29 |
30 | 31 | 32 |
33 | 34 |
35 | 36 | 37 |
38 | 39 |
40 | 41 | 42 |
43 | 44 |
45 | 46 | 47 |
48 | 49 |
50 |
51 |
52 |
53 |
54 |
55 | 56 | 57 | 60 | 61 | <% include ('../partials/footer.html') %> 62 | -------------------------------------------------------------------------------- /controllers/auth.js: -------------------------------------------------------------------------------- 1 | // importing libraries 2 | const passport = require("passport"); 3 | 4 | if (process.env.NODE_ENV !== "production") require("dotenv").config(); 5 | 6 | // importing models 7 | const User = require("../models/user"); 8 | 9 | exports.getLandingPage = async (_req, res) => { 10 | return res.render("landing.html"); 11 | }; 12 | 13 | exports.getAdminLoginPage = (req, res, next) => { 14 | res.render("admin/adminLogin"); 15 | }; 16 | 17 | exports.getAdminLogout = (req, res, next) => { 18 | req.logout(); 19 | res.redirect("/"); 20 | }; 21 | 22 | exports.getAdminSignUp = (req, res, next) => { 23 | res.render("signup"); 24 | }; 25 | 26 | exports.postAdminSignUp = async (req, res, next) => { 27 | try { 28 | if (req.body.adminCode === process.env.ADMIN_SECRET) { 29 | const newAdmin = new User({ 30 | username: req.body.username, 31 | email: req.body.email, 32 | isAdmin: true, 33 | }); 34 | 35 | const user = await User.register(newAdmin, req.body.password); 36 | await passport.authenticate("local")(req, res, () => { 37 | req.flash( 38 | "success", 39 | "Hello, " + user.username + " Welcome to Admin Dashboard" 40 | ); 41 | res.redirect("/admin"); 42 | }); 43 | } else { 44 | req.flash("error", "Secret code does not matching!"); 45 | return res.redirect("back"); 46 | } 47 | } catch (err) { 48 | req.flash( 49 | "error", 50 | "Given info matches someone registered as User. Please provide different info for registering as Admin" 51 | ); 52 | return res.render("signup"); 53 | } 54 | }; 55 | 56 | exports.getUserLoginPage = (req, res, next) => { 57 | res.render("user/userLogin"); 58 | }; 59 | 60 | exports.getUserLogout = async (req, res, next) => { 61 | await req.session.destroy(); 62 | req.logout(); 63 | res.redirect("/"); 64 | }; 65 | 66 | exports.getUserSignUp = (req, res, next) => { 67 | res.render("user/userSignup"); 68 | }; 69 | 70 | exports.postUserSignUp = async (req, res, next) => { 71 | try { 72 | const newUser = new User({ 73 | firstName: req.body.firstName, 74 | lastName: req.body.lastName, 75 | username: req.body.username, 76 | email: req.body.email, 77 | gender: req.body.gender, 78 | address: req.body.address, 79 | }); 80 | 81 | await User.register(newUser, req.body.password); 82 | await passport.authenticate("local")(req, res, () => { 83 | res.redirect("/user/1"); 84 | }); 85 | } catch (err) { 86 | console.log(err); 87 | return res.render("user/userSignup"); 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | // importing modules 2 | const express = require("express"); 3 | const router = express.Router(); 4 | const middleware = require("../middleware"); 5 | 6 | // importing controller 7 | const userController = require("../controllers/user"); 8 | 9 | // user -> dashboard 10 | router.get( 11 | "/user/:page", 12 | middleware.isLoggedIn, 13 | userController.getUserDashboard 14 | ); 15 | 16 | // user -> profile 17 | router.get( 18 | "/user/:page/profile", 19 | middleware.isLoggedIn, 20 | userController.getUserProfile 21 | ); 22 | 23 | //user -> upload image 24 | router.post( 25 | "/user/1/image", 26 | middleware.isLoggedIn, 27 | userController.postUploadUserImage 28 | ); 29 | 30 | //user -> update password 31 | router.put( 32 | "/user/1/update-password", 33 | middleware.isLoggedIn, 34 | userController.putUpdatePassword 35 | ); 36 | 37 | //user -> update profile 38 | router.put( 39 | "/user/1/update-profile", 40 | middleware.isLoggedIn, 41 | userController.putUpdateUserProfile 42 | ); 43 | 44 | //user -> notification 45 | router.get( 46 | "/user/1/notification", 47 | middleware.isLoggedIn, 48 | userController.getNotification 49 | ); 50 | 51 | //user -> issue a book 52 | router.post( 53 | "/books/:book_id/issue/:user_id", 54 | middleware.isLoggedIn, 55 | userController.postIssueBook 56 | ); 57 | 58 | //user -> show return-renew page 59 | router.get( 60 | "/books/return-renew", 61 | middleware.isLoggedIn, 62 | userController.getShowRenewReturn 63 | ); 64 | 65 | //user -> renew book 66 | router.post( 67 | "/books/:book_id/renew", 68 | middleware.isLoggedIn, 69 | middleware.isLoggedIn, 70 | userController.postRenewBook 71 | ); 72 | 73 | // user -> return book 74 | 75 | router.post( 76 | "/books/:book_id/return", 77 | middleware.isLoggedIn, 78 | userController.postReturnBook 79 | ); 80 | 81 | //user -> create new comment 82 | router.post( 83 | "/books/details/:book_id/comment", 84 | middleware.isLoggedIn, 85 | userController.postNewComment 86 | ); 87 | 88 | //user -> update existing comment 89 | router.post( 90 | "/books/details/:book_id/:comment_id", 91 | middleware.isLoggedIn, 92 | userController.postUpdateComment 93 | ); 94 | 95 | //user -> delete existing comment 96 | router.delete( 97 | "/books/details/:book_id/:comment_id", 98 | middleware.isLoggedIn, 99 | userController.deleteComment 100 | ); 101 | 102 | // user -> delete user account 103 | router.delete( 104 | "/user/1/delete-profile", 105 | middleware.isLoggedIn, 106 | userController.deleteUserAccount 107 | ); 108 | 109 | module.exports = router; 110 | -------------------------------------------------------------------------------- /views/user/userSignup.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 |
10 |
11 |
12 |
13 |

User Sign Up

14 |
15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 | 30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 |
38 | 39 | 40 |
41 | 42 |
43 | 44 | 49 |
50 | 51 |
52 | 53 | 54 |
55 | 56 | 57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | 65 | <% include ('../partials/footer.html') %> 66 | -------------------------------------------------------------------------------- /controllers/books.js: -------------------------------------------------------------------------------- 1 | const Book = require("../models/book"); 2 | const PER_PAGE = 16; 3 | 4 | exports.getBooks = async (req, res, next) => { 5 | var page = req.params.page || 1; 6 | const filter = req.params.filter; 7 | const value = req.params.value; 8 | let searchObj = {}; 9 | 10 | // constructing search object 11 | if (filter != "all" && value != "all") { 12 | // fetch books by search value and filter 13 | searchObj[filter] = value; 14 | } 15 | 16 | try { 17 | // Fetch books from database 18 | const books = await Book.find(searchObj) 19 | .skip(PER_PAGE * page - PER_PAGE) 20 | .limit(PER_PAGE); 21 | 22 | // Get the count of total available book of given filter 23 | const count = await Book.find(searchObj).countDocuments(); 24 | 25 | res.render("books", { 26 | books: books, 27 | current: page, 28 | pages: Math.ceil(count / PER_PAGE), 29 | filter: filter, 30 | value: value, 31 | user: req.user, 32 | }); 33 | } catch (err) { 34 | console.log(err); 35 | } 36 | }; 37 | 38 | exports.findBooks = async (req, res, next) => { 39 | var page = req.params.page || 1; 40 | const filter = req.body.filter.toLowerCase(); 41 | const value = req.body.searchName; 42 | 43 | // show flash message if empty search field is sent to backend 44 | if (value == "") { 45 | req.flash( 46 | "error", 47 | "Search field is empty. Please fill the search field in order to get a result" 48 | ); 49 | return res.redirect("back"); 50 | } 51 | 52 | const searchObj = {}; 53 | searchObj[filter] = value; 54 | 55 | try { 56 | // Fetch books from database 57 | const books = await Book.find(searchObj) 58 | .skip(PER_PAGE * page - PER_PAGE) 59 | .limit(PER_PAGE); 60 | 61 | // Get the count of total available book of given filter 62 | const count = await Book.find(searchObj).countDocuments(); 63 | 64 | res.render("books", { 65 | books: books, 66 | current: page, 67 | pages: Math.ceil(count / PER_PAGE), 68 | filter: filter, 69 | value: value, 70 | user: req.user, 71 | }); 72 | } catch (err) { 73 | console.log(err); 74 | } 75 | }; 76 | 77 | // find book details working procedure 78 | /* 79 | 1. fetch book from db by id 80 | 2. populate book with associated comments 81 | 3. render user/bookDetails template and send the fetched book 82 | */ 83 | 84 | exports.getBookDetails = async (req, res, next) => { 85 | try { 86 | const book_id = req.params.book_id; 87 | const book = await Book.findById(book_id).populate("comments"); 88 | res.render("user/bookDetails", { book: book }); 89 | } catch (err) { 90 | console.log(err); 91 | return res.redirect("back"); 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /views/user/return-renew.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%- include ('../partials/userNav.html') %> 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 |

Renew/Return

17 |
18 |
19 |
20 |
21 | 22 | 23 |
24 |
25 |
26 | 27 |
28 |
29 |

All Renewables/Returnables

30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | <% user.forEach(book => { %> 44 | 45 | 46 | 47 | 48 | 49 | 50 | 66 | 67 | <% }) %> 68 | 69 |
TitleAuthorIssue dateReturn dateCategory
You have issued <%=book.book_info.title%><%=book.book_info.author%><%=book.book_info.issueDate.toDateString()%><%=book.book_info.returnDate.toDateString()%><%=book.book_info.category%> 51 | 52 | <% if(book.book_info.isRenewed) { %> 53 | Renewed! 54 | <% } else if(currentUser.violationFlag && book.book_info.returnDate < Date.now()) { %> 55 | Renew 56 | <% } else { %> 57 |
58 | 59 |
60 | <% } %> 61 | 62 |
63 | 64 |
65 |
70 | 71 |
72 |
73 |
74 |
75 | 76 | <% include ('../partials/footer.html') %> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repo is under construction. You may find some parts not working as expected. 2 | 3 | ### Checkout "dev" branch to see which part I'm working on right now 4 | 5 | ### Following feature will be added in this repo 6 | 7 | 1. Server side caching 8 | 2. Server side form validation 9 | 3. Test coverage 10 | 4. Scalable image upload 11 | 5. Wiring up CI/CD 12 | 13 | # Library-Management-System 14 | 15 | A simple online library management system built with MongodDB, Express.js and Node.js. [Click here](https://demo-library-system.herokuapp.com/) to see the application 16 | 17 | ## Techonologies used in this application 18 | 19 | ### Front-end 20 | 21 | 1. HTML5 22 | 2. CSS3 23 | 3. BOOTSTRAP 4 24 | 4. jQuery 25 | 26 | ### Back-end 27 | 28 | 1. MongoDB 29 | 2. Express.js 30 | 3. Node.js 31 | 4. Passport.js 32 | 33 | ## Install dependencies 34 | 35 | Open git bash or command line tools at application file and run following npm command or if you know what to do, just look at `package.json` file :) 36 | 37 | `npm install passport passport-local passport-local-mongoose body-parser connect-flash ejs express express-santizer express-session method-override mongoose multer sharp uuid --save` 38 | 39 | #### Install dev dependencies if needed 40 | 41 | `npm install nodemon faker --save-dev` 42 | 43 | ## Run the application 44 | 45 | - create a `.env` file in app directory 46 | - add `SESSION_SECRET=`, `ADMIN_SECRET=` and `DB_URL=` into that file. 47 | - run `npm run dev` 48 | - App will open at [http://localhost:3000](http://localhost:3000) 49 | 50 | ## Functionalitites 51 | 52 | Whole app is divided into three modules. 53 | 54 | - Admin 55 | - User 56 | - Browse books 57 | 58 | ### Admin module functionalities 59 | 60 | - Sign up (This route is hidden. only accessible by typing the route manually and when admin log in) 61 | - Login 62 | - Logout 63 | - Track all users activities 64 | - Add books 65 | - Update books 66 | - Delete books 67 | - Search books by category, title, author, ISBN 68 | - Find users by firstname, lastname, email and username 69 | - Delete user acount 70 | - Restrict individual user if violate any terms and conditions 71 | - Send notification to all/individual/filtered user (not ready yet, will be added as soon as I learn socket.io) 72 | - Browse books showcase 73 | - Update admin profile and password 74 | - Add new admin 75 | - Delete currently logged in admin profile 76 | 77 | ### User module functionalities 78 | 79 | - Sign up 80 | - Login 81 | - Logout 82 | - Track own activities 83 | - Issue books 84 | - Renew books 85 | - Return books 86 | - Pay fines (not ready yet, will be added asap) 87 | - Browse books showcase 88 | - Add, edit and delete comment on any books comment section 89 | - Upload/Update profile picture 90 | - Update profile and password 91 | - Delete account 92 | 93 | ### Browse books module functionalities 94 | 95 | This module can be accessed by anyone 96 | 97 | - Show all books 98 | - Find books on filtered search 99 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const passport = require("passport"); 4 | 5 | // Import models 6 | const User = require("../models/user"); 7 | 8 | //landing page 9 | router.get("/", (req, res) => { 10 | res.render("landing"); 11 | }); 12 | 13 | //admin login handler 14 | router.get("/adminLogin", (req, res) => { 15 | res.render("admin/adminLogin"); 16 | }); 17 | 18 | router.post( 19 | "/adminLogin", 20 | passport.authenticate("local", { 21 | successRedirect: "/admin", 22 | failureRedirect: "/adminLogin", 23 | }), 24 | (req, res) => {} 25 | ); 26 | 27 | //admin logout handler 28 | router.get("/adminLogout", (req, res) => { 29 | req.logout(); 30 | res.redirect("/"); 31 | }); 32 | 33 | // sign up 34 | router.get("/adminSignup", (req, res) => { 35 | res.render("signup"); 36 | }); 37 | 38 | router.post("/adminSignup", (req, res) => { 39 | if (req.body.adminCode == "Open Sesame") { 40 | const newAdmin = new User({ 41 | username: req.body.username, 42 | email: req.body.email, 43 | isAdmin: true, 44 | }); 45 | 46 | User.register(newAdmin, req.body.password, (err, user) => { 47 | if (err) { 48 | req.flash( 49 | "error", 50 | "Given info matches someone registered as User. Please provide different info for registering as Admin" 51 | ); 52 | return res.render("signup"); 53 | } 54 | passport.authenticate("local")(req, res, function () { 55 | req.flash( 56 | "success", 57 | "Hello, " + user.username + " Welcome to Admin Dashboard" 58 | ); 59 | res.redirect("/admin"); 60 | }); 61 | }); 62 | } else { 63 | req.flash("error", "Secret word doesn't match!"); 64 | return res.redirect("back"); 65 | } 66 | }); 67 | 68 | //user login handler 69 | router.get("/userLogin", (req, res) => { 70 | res.render("user/userLogin"); 71 | }); 72 | 73 | router.post( 74 | "/userLogin", 75 | passport.authenticate("local", { 76 | successRedirect: "/user/1", 77 | failureRedirect: "/userLogin", 78 | }), 79 | (req, res) => {} 80 | ); 81 | 82 | //user -> user logout handler 83 | router.get("/userLogout", (req, res) => { 84 | req.logout(); 85 | res.redirect("/"); 86 | }); 87 | 88 | //user sign up handler 89 | router.get("/signUp", (req, res) => { 90 | res.render("user/userSignup"); 91 | }); 92 | 93 | router.post("/signUp", (req, res) => { 94 | const newUser = new User({ 95 | firstName: req.body.firstName, 96 | lastName: req.body.lastName, 97 | username: req.body.username, 98 | email: req.body.email, 99 | gender: req.body.gender, 100 | address: req.body.address, 101 | }); 102 | 103 | User.register(newUser, req.body.password, (err, user) => { 104 | if (err) { 105 | return res.render("user/userSignup"); 106 | } 107 | passport.authenticate("local")(req, res, () => { 108 | res.redirect("/user/1"); 109 | }); 110 | }); 111 | }); 112 | 113 | module.exports = router; 114 | -------------------------------------------------------------------------------- /views/user/bookDetails.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | <%- include ('../partials/userNav.html') %> 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |

<%= book.title %>

17 |
18 | Author : <%= book.author %> 19 | ISBN : <%= book.ISBN %> 20 | Category : <%= book.category %> 21 | In Stock : <%= book.stock %> 22 |
23 |

<%- book.description %>

24 |
25 |
26 | 27 |
28 | <% if(currentUser) { %> 29 |

30 | 40 |

41 |
42 |
43 | 44 | 45 |
46 |
47 | 48 | <% } %> 49 | 50 |
    51 | <%book.comments.forEach((comment) => { %> 52 |
  • 53 | <%=comment.author.username%> 54 | at <%=comment.date.toDateString()%> 55 |

    <%=comment.text%>

    56 | 57 | <% if(currentUser && comment.author.id.equals(currentUser._id)) {%> 58 | 59 | 62 | 63 |
    64 | 67 |
    68 | <% } %> 69 | 70 |
    71 |
    72 | 73 | 74 |
    75 |
    76 |
  • 77 | <% }); %> 78 |
79 |
80 |
81 |
82 |
83 | 84 | 85 | 91 | <% include ('../partials/footer.html') %> 92 | -------------------------------------------------------------------------------- /routes/admin.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const middleware = require("../middleware"); 4 | 5 | // importing controller 6 | const adminController = require("../controllers/admin"); 7 | 8 | //admin -> dashboard 9 | router.get("/admin", middleware.isAdmin, adminController.getDashboard); 10 | 11 | //admin -> find activities of all users on admin dashboard 12 | router.post("/admin", middleware.isAdmin, adminController.postDashboard); 13 | 14 | //admin -> delete profile 15 | router.delete( 16 | "/admin/delete-profile", 17 | middleware.isAdmin, 18 | adminController.deleteAdminProfile 19 | ); 20 | 21 | //admin book inventory 22 | router.get( 23 | "/admin/bookInventory/:filter/:value/:page", 24 | middleware.isAdmin, 25 | adminController.getAdminBookInventory 26 | ); 27 | 28 | // admin -> show searched books 29 | router.post( 30 | "/admin/bookInventory/:filter/:value/:page", 31 | middleware.isAdmin, 32 | adminController.postAdminBookInventory 33 | ); 34 | 35 | //admin -> show books to be updated 36 | router.get( 37 | "/admin/book/update/:book_id", 38 | middleware.isAdmin, 39 | adminController.getUpdateBook 40 | ); 41 | 42 | //admin -> update book 43 | router.post( 44 | "/admin/book/update/:book_id", 45 | middleware.isAdmin, 46 | adminController.postUpdateBook 47 | ); 48 | 49 | //admin -> delete book 50 | router.get( 51 | "/admin/book/delete/:book_id", 52 | middleware.isAdmin, 53 | adminController.getDeleteBook 54 | ); 55 | 56 | //admin -> users list 57 | router.get( 58 | "/admin/users/:page", 59 | middleware.isAdmin, 60 | adminController.getUserList 61 | ); 62 | 63 | //admin -> show searched user 64 | router.post( 65 | "/admin/users/:page", 66 | middleware.isAdmin, 67 | adminController.postShowSearchedUser 68 | ); 69 | 70 | //admin -> flag/unflag user 71 | router.get( 72 | "/admin/users/flagged/:user_id", 73 | middleware.isAdmin, 74 | adminController.getFlagUser 75 | ); 76 | 77 | //admin -> show one user 78 | router.get( 79 | "/admin/users/profile/:user_id", 80 | middleware.isAdmin, 81 | adminController.getUserProfile 82 | ); 83 | 84 | //admin -> show all activities of one user 85 | router.get( 86 | "/admin/users/activities/:user_id", 87 | middleware.isAdmin, 88 | adminController.getUserAllActivities 89 | ); 90 | 91 | //admin -> show activities by category 92 | router.post( 93 | "/admin/users/activities/:user_id", 94 | middleware.isAdmin, 95 | adminController.postShowActivitiesByCategory 96 | ); 97 | 98 | // admin -> delete a user 99 | router.get( 100 | "/admin/users/delete/:user_id", 101 | middleware.isAdmin, 102 | adminController.getDeleteUser 103 | ); 104 | 105 | //admin -> add new book 106 | router.get( 107 | "/admin/books/add", 108 | middleware.isAdmin, 109 | adminController.getAddNewBook 110 | ); 111 | 112 | router.post( 113 | "/admin/books/add", 114 | middleware.isAdmin, 115 | adminController.postAddNewBook 116 | ); 117 | 118 | //admin -> profile 119 | router.get( 120 | "/admin/profile", 121 | middleware.isAdmin, 122 | adminController.getAdminProfile 123 | ); 124 | 125 | //admin -> update profile 126 | router.post( 127 | "/admin/profile", 128 | middleware.isAdmin, 129 | adminController.postUpdateAdminProfile 130 | ); 131 | 132 | //admin -> update password 133 | router.put( 134 | "/admin/update-password", 135 | middleware.isAdmin, 136 | adminController.putUpdateAdminPassword 137 | ); 138 | 139 | // //admin -> notifications 140 | // router.get("/admin/notifications", (req, res) => { 141 | // res.send("This route is still under development. will be added in next version"); 142 | // }); 143 | 144 | module.exports = router; 145 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const mongoose = require("mongoose"); 4 | const ejs = require("ejs"); 5 | const session = require("express-session"); 6 | const passport = require("passport"); 7 | const multer = require("multer"); 8 | const path = require("path"); 9 | const sanitizer = require("express-sanitizer"); 10 | const methodOverride = require("method-override"); 11 | const localStrategy = require("passport-local"); 12 | const MongoStore = require("connect-mongodb-session")(session); 13 | const flash = require("connect-flash"); 14 | const crypto = require("crypto"); 15 | const User = require("./models/user"); 16 | const userRoutes = require("./routes/users"); 17 | const adminRoutes = require("./routes/admin"); 18 | const bookRoutes = require("./routes/books"); 19 | const authRoutes = require("./routes/auth"); 20 | 21 | // const Seed = require("./seed"); 22 | 23 | // uncomment below line for first time to seed database; 24 | // Seed(1000); 25 | 26 | if (process.env.NODE_ENV !== "production") require("dotenv").config(); 27 | 28 | // app config 29 | app.engine(".html", ejs.renderFile); 30 | app.set("view engine", "html"); 31 | app.set("views", path.join(__dirname, "views")); 32 | 33 | app.use(methodOverride("_method")); 34 | 35 | app.use(express.static(__dirname + "/public")); 36 | 37 | app.use(express.json()); 38 | app.use(express.urlencoded({ extended: true })); 39 | app.use(sanitizer()); 40 | 41 | // db config 42 | mongoose 43 | .connect(process.env.DB_URL, { 44 | useNewUrlParser: true, 45 | useUnifiedTopology: true, 46 | useCreateIndex: true, 47 | useFindAndModify: false, 48 | }) 49 | .then(() => console.log("MongoDB is connected")) 50 | .catch((error) => console.log(error)); 51 | 52 | //PASSPORT CONFIGURATION 53 | 54 | const store = new MongoStore({ 55 | uri: process.env.DB_URL, 56 | collection: "sessions", 57 | }); 58 | 59 | app.use( 60 | session({ 61 | //must be declared before passport session and initialize method 62 | secret: process.env.SESSION_SECRET, 63 | saveUninitialized: false, 64 | resave: false, 65 | store, 66 | }) 67 | ); 68 | 69 | app.use(flash()); 70 | 71 | app.use(passport.initialize()); //must declared before passport.session() 72 | app.use(passport.session()); 73 | 74 | passport.use(new localStrategy(User.authenticate())); 75 | passport.serializeUser(User.serializeUser()); 76 | passport.deserializeUser(User.deserializeUser()); 77 | 78 | // configure image file storage 79 | const fileStorage = multer.diskStorage({ 80 | destination: (req, file, cb) => { 81 | cb(null, "images"); 82 | }, 83 | filename: (req, file, cb) => { 84 | cb(null, `${crypto.randomBytes(12).toString("hex")}-${file.originalname}`); 85 | }, 86 | }); 87 | 88 | const filefilter = (req, file, cb) => { 89 | if ( 90 | file.mimetype === "image/png" || 91 | file.mimetype === "image/jpg" || 92 | file.mimetype === "image/jpeg" 93 | ) { 94 | cb(null, true); 95 | } else { 96 | cb(null, false); 97 | } 98 | }; 99 | 100 | app.use( 101 | multer({ storage: fileStorage, fileFilter: filefilter }).single("image") 102 | ); 103 | app.use("/images", express.static(path.join(__dirname, "images"))); 104 | 105 | app.use((req, res, next) => { 106 | res.locals.currentUser = req.user; 107 | res.locals.error = req.flash("error"); 108 | res.locals.success = req.flash("success"); 109 | res.locals.warning = req.flash("warning"); 110 | next(); 111 | }); 112 | 113 | //Routes 114 | app.use(userRoutes); 115 | app.use(adminRoutes); 116 | app.use(bookRoutes); 117 | app.use(authRoutes); 118 | 119 | const PORT = process.env.PORT || 3000; 120 | 121 | app.listen(PORT, () => { 122 | console.log(`server is running at http://localhost:${PORT}`); 123 | }); 124 | -------------------------------------------------------------------------------- /views/admin/notification.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%- include ('../partials/adminNav.html') %> 9 | 10 | 11 |
12 |
13 |
14 |
15 |

Notification

16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 | 30 | 31 |
32 |
33 | 34 | 35 | 36 | 37 |
38 |
39 | 40 |
41 |
42 |
43 | 44 | 45 |
46 |
47 |
48 | 49 |
50 |
51 |

Recent Notifications

52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 94 | 95 | 96 | 97 |
InfoCategoryDate
User1 due $5.00 fines. Notify to pay finesFineJuly 11, 2017 68 | 69 | 70 | 71 |
User2's return date for The old man and the sea is about to overReturnJuly 09, 2017 79 | 80 | 81 | 82 |
User3 has violated a terms and conditionsViolationJuly 10, 2017 90 | 91 | 92 | 93 |
98 | 99 | 108 | 109 |
110 |
111 |
112 |
113 | 114 | <% include ('../partials/footer.html') %> -------------------------------------------------------------------------------- /views/admin/user.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%- include ('../partials/adminNav.html') %> 9 | 10 |
11 |
12 |
" class="card-img-top">
13 |
14 |

Personal Informations

15 | 16 |
    17 |
  • First Name : <%=user.firstName%>
  • 18 |
  • Last Name : <%=user.lastName%>
  • 19 |
  • Username : <%=user.username%>
  • 20 |
  • Joined : <%=user.joined.toDateString()%>
  • 21 |
  • Email : <%=user.email%>
  • 22 |
  • 23 | Issued books : <% issues.forEach(issue => { %> <%=issue.book_info.title + ", "%> <% }) %> 24 |
  • 25 |
  • Address : <%=user.address%>
  • 26 |
  • Violation Flag : <%=user.violationFlag%>
  • 27 |
  • Due Fines : $<%=user.fines%>
  • 28 |
29 |
30 |
31 |
32 |
33 | Recent activities 34 |
35 |
    36 | <% for(var i = 0; i < activities.length; i++) { %> <%if(i > 5) break; %> <% if(activities[i].category 37 | =="Issue") { %> 38 |
  • 39 | <%=user.username%> issued <%=activities[i].info.title%> at <%=activities[i].entryTime.toDateString()%> 40 |
  • 41 | <% } else if(activities[i].category =="Return") { %> 42 |
  • 43 | <%=user.username%> returned <%=activities[i].info.title%> at <%=activities[i].entryTime.toDateString()%> 44 |
  • 45 | <% } else if(activities[i].category =="Renew") { %> 46 |
  • 47 | <%=user.username%> renewed <%=activities[i].info.title%> at <%=activities[i].entryTime.toDateString()%> 48 |
  • 49 | <% } else if(activities[i].category =="Update Profile") { %> 50 |
  • 51 | <%=user.username%> updated profile at <%=activities[i].entryTime.toDateString()%> 52 |
  • 53 | <% } else if(activities[i].category =="Update Password") { %> 54 |
  • 55 | <%=user.username%> updated password at <%=activities[i].entryTime.toDateString()%> 56 |
  • 57 | <% } else if(activities[i].category =="Upload Photo") { %> 58 |
  • 59 | <%=user.username%> uploaded photo at <%=activities[i].entryTime.toDateString()%> 60 |
  • 61 | <% } else if(activities[i].category =="Update Password") { %> 62 |
  • 63 | <%=user.username%> updated password at <%=activities[i].entryTime.toDateString()%> 64 |
  • 65 | <% } else if(activities[i].category =="Comment") { %> 66 |
  • 67 | <%=user.username%> commented on <%=activities[i].info.title%> at <%=activities[i].entryTime.toDateString()%> 68 |
  • 69 | <% } else if(activities[i].category =="Update Comment") { %> 70 |
  • 71 | <%=user.username%> updated comment on <%=activities[i].info.title%> at 72 | <%=activities[i].entryTime.toDateString()%> 73 |
  • 74 | <% } else if(activities[i].category =="Delete Comment") { %> 75 |
  • 76 | <%=user.username%> deleteted comment on <%=activities[i].info.title%> at 77 | <%=activities[i].entryTime.toDateString()%> 78 |
  • 79 | <% } %> <% } %> <%if(activities.length > 6) { %> 80 | See all... 81 | <% } %> 82 |
83 |
84 |
85 |
86 |
87 | 88 | <% include ('../partials/footer.html') %> 89 | -------------------------------------------------------------------------------- /views/admin/bookInventory.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%- include ('../partials/adminNav.html') %> 9 | 10 |
11 |
12 |
13 |
14 |

Book Inventory

15 |
16 |
17 |
18 |
19 | 20 | 21 | 46 | 47 | <%- include ('../partials/alerts.html') %> 48 | 49 | 50 |
51 |
52 |
53 |
54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | <% books.forEach(book => { %> 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 81 | 82 | <% }); %> 83 | 84 |
TitleAuthorISBNCatergoryIn StockEdit
<%= book.title %><%= book.author %><%= book.ISBN %><%= book.category %><%= book.stock %> 76 | 77 | Update 78 | Delete 79 | 80 |
85 | 86 | <% if (pages > 0) { %> 87 | 114 | <% } %> 115 |
116 |
117 |
118 |
119 |
120 | 121 | <% include ('../partials/footer.html') %> 122 | -------------------------------------------------------------------------------- /views/admin/activities.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <% include ('../partials/adminNav.html') %> 9 | 10 |
11 |
12 |
13 |
14 |

User Activities

15 |
16 |
17 |
18 |
19 | 20 | 21 |
22 |
23 |
24 | 27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 |
35 |
36 |
37 |
38 |
39 |
40 | 41 | 42 |
43 |
44 |
45 |
46 |
47 |
48 |

Recent Activities

49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | <%for(let i=0; i < activities.length; i++) { %> 60 | 61 | 62 | <% if(activities[i].category=="Issue" ) { %> 63 | 64 | 65 | 66 | 67 | 68 | <% } else if(activities[i].category=="Return" ) { %> 69 | 70 | 71 | 72 | 73 | 74 | <% } else if(activities[i].category=="Renew" ) { %> 75 | 76 | 77 | 78 | 79 | 80 | <% } else if(activities[i].category=="Update Profile" ) { %> 81 | 82 | 83 | 84 | 85 | 86 | <% } else if(activities[i].category=="Update Password" ) { %> 87 | 88 | 89 | 90 | 91 | 92 | <% } else if(activities[i].category=="Upload Photo" ) { %> 93 | 94 | 95 | 96 | 97 | 98 | <% } else if(activities[i].category=="Comment" ) { %> 99 | 100 | 101 | 102 | 103 | 104 | <% } else if(activities[i].category=="Update Comment" ) { %> 105 | 106 | 109 | 110 | 111 | 112 | <% } else if(activities[i].category=="Delete Comment" ) { %> 113 | 114 | 117 | 118 | 119 | 120 | <% } %> 121 | 122 | 123 | <% } %> 124 | 125 |
InfoCategoryDate Posted
<%=activities[i].user_id.username || 'This user' %> issued <%=activities[i].info.title%>Issue<%=activities[i].entryTime.toDateString()%><%=activities[i].user_id.username || 'This user' %> returned <%=activities[i].info.title%>Return<%=activities[i].entryTime.toDateString()%><%=activities[i].user_id.username || 'This user' %> renewed <%=activities[i].info.title%>Renew<%=activities[i].entryTime.toDateString()%><%=activities[i].user_id.username || 'This user' %> updated profileUpdate Profile<%=activities[i].entryTime.toDateString()%><%=activities[i].user_id.username || 'This user' %> updated passwordUpdate Password<%=activities[i].entryTime.toDateString()%><%=activities[i].user_id.username || 'This user' %> update/upload profileUpdate/Upload Profile<%=activities[i].entryTime.toDateString()%><%=activities[i].user_id.username || 'This user' %> commented on <%=activities[i].info.title%>Comment<%=activities[i].entryTime.toDateString()%> 107 | <%= activities[i].user_id.username || 'This user' %> updated comment on <%=activities[i].info.title %> 108 | Update Comment<%=activities[i].entryTime.toDateString()%> 115 | <%=activities[i].user_id.username || 'This user' %> deleted comment on <%=activities[i].info.title%> 116 | Delete Comment<%=activities[i].entryTime.toDateString()%>
126 |
127 |
128 |
129 |
130 |
131 | 132 | <% include ('../partials/footer.html') %> 133 | -------------------------------------------------------------------------------- /views/admin/users.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%- include ('../partials/adminNav.html') %> 9 | 10 |
11 |
12 |
13 |
14 |

Users

15 |
16 |
17 |
18 |
19 | 20 | 21 |
22 |
23 |
24 | 27 |
28 |
29 |
30 | 36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 |
44 |
45 | 46 | <% include ("../partials/alerts.html") %> 47 | 48 | 49 |
50 |
51 |
52 |
53 |
54 |
55 |

Users

56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | <% users.forEach(user=> { %> <%if (user.isAdmin) return; %> 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 103 | 104 | <% }); %> 105 | 106 |
NameUser nameEmailGenderDate RegiseteredViolation flagFine
<%=user.firstName + " " + user.lastName%> <%=user.username%> <%=user.email%><%=user.gender%><%=user.joined.toDateString()%><%=user.violationFlag%>$<%=user.fines%> 81 | <%if(user.bookIssueInfo.length> 0) { %> 82 | 87 | 88 | 89 | <% } else { %> 90 | 91 | 92 | 93 | <% } %> <% if(user.violationFlag) { %> 94 | 95 | 96 | 97 | <% } else { %> 98 | 99 | 100 | 101 | <% } %> 102 |
107 | 108 | <% if (pages> 0) { %> 109 | 130 | <% } %> 131 |
132 |
133 |
134 |
135 |
136 | 137 | <% include ('../partials/footer.html') %> 138 | -------------------------------------------------------------------------------- /views/admin/profile.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%- include ('../partials/adminNav.html') %> 9 | 10 | 11 |
12 |
13 |
14 |
15 |

Profile

16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 | 30 | 31 | 36 | 37 | 42 | 43 | 48 | 49 | 54 | 55 |
56 |
57 |
58 | 59 | 60 | 61 |
62 |
63 |
64 |
65 |
66 |
67 |

Admin Info

68 |
69 |
70 |

User name : <%=currentUser.username%>

71 |

Email : <%=currentUser.email%>

72 |
73 |
74 |
75 |
76 | 77 | 78 | 79 | 110 | 111 | 112 | 138 | 139 | 140 | 160 | 161 | 162 | 174 | 175 | <% include ('../partials/footer.html') %> -------------------------------------------------------------------------------- /views/books.html: -------------------------------------------------------------------------------- 1 | <%- include ('./partials/header.html') %> 2 | 3 | 4 | 5 | 70 | 71 | 72 | 97 | 98 | <%- include ('./partials/alerts.html') %> 99 | 100 |
101 |
102 | 103 |
104 | <% for(var i=0; i < books.length; i++) { %> 105 |
106 |
107 |
108 | <%=books[i].title%> 109 |
110 |

111 | Author : <%=books[i].author%> 112 |

113 |

114 | Category : <%=books[i].category%> 115 |

116 |

117 | In stock : <%=books[i].stock%> 118 |

119 | 120 | 121 | <% if(currentUser && books[i].stock> 0) { var match = false%> 122 | <% user.bookIssueInfo.forEach(book_info=> { %> 123 | <% if(book_info._id.equals(books[i]._id)) { %> 124 | Issued! 125 | Return/Renew 126 | <% match=true; } %> 127 | <% }) %> 128 | 129 | <% if(!match) {%> 130 |
132 | 133 |
134 | <% } %> 135 | <% } %> 136 | Details 137 |
138 |
139 | <% } %> 140 |
141 | <% if (pages> 0) { %> 142 | 178 | <% } %> 179 |
180 |
181 | 182 | <% include ('./partials/footer.html') %> -------------------------------------------------------------------------------- /views/admin/index.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%- include ('../partials/adminNav.html') %> 9 | 10 | 11 |
12 |
13 |
14 |
15 |

Dashboard

16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 |
28 | 34 | 35 | 36 | 37 |
38 |
39 |
40 |
41 |
42 |
43 | 44 | <%- include ('../partials/alerts.html')%> 45 | 46 | 47 |
48 |
49 |
50 |
51 |
52 |
53 |

Recent User Activities

54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | <%for(let i = 0; i < activities.length; i++) { %> 65 | 66 | 67 | <% if(activities[i].category =="Issue") { %> 68 | 74 | 75 | 76 | <% } else if(activities[i].category =="Return") { %> 77 | 83 | 84 | 85 | <% } else if(activities[i].category =="Renew") { %> 86 | 92 | 93 | 94 | 95 | <% } else if(activities[i].category =="Update Profile") { %> 96 | 102 | 103 | 104 | 105 | <% } else if(activities[i].category =="Update Password") { %> 106 | 112 | 113 | 114 | 115 | <% } else if(activities[i].category =="Upload Photo") { %> 116 | 122 | 123 | 124 | 125 | <% } else if(activities[i].category =="Comment") { %> 126 | 132 | 133 | 134 | <% } else if(activities[i].category =="Update Comment") { %> 135 | 141 | 142 | 143 | <% } else if(activities[i].category =="Delete Comment") { %> 144 | 150 | 151 | 152 | <% } %> 153 | 154 | 155 | <% } %> 156 | 157 |
InfoCategoryDate Posted
69 | 70 | <%=activities[i].user_id.username || 'This user'%> 72 | issued <%=activities[i].info.title%> 73 | Issue<%=activities[i].entryTime.toDateString()%> 78 | 79 | <%=activities[i].user_id.username || 'This user'%> 80 | 81 | returned <%=activities[i].info.title%> 82 | Return<%=activities[i].entryTime.toDateString()%> 87 | 88 | <%=activities[i].user_id.username || 'This user'%> 89 | 90 | renewed <%=activities[i].info.title%> 91 | Renew<%=activities[i].entryTime.toDateString()%> 97 | 98 | <%=activities[i].user_id.username || 'This user'%> 99 | 100 | updated profile 101 | Update Profile<%=activities[i].entryTime.toDateString()%> 107 | 108 | <%=activities[i].user_id.username || 'This user'%> 109 | 110 | updated password 111 | Update Password<%=activities[i].entryTime.toDateString()%> 117 | 118 | <%=activities[i].user_id.username || 'This user'%> 119 | 120 | uploaded profile photo 121 | Upload Photo<%=activities[i].entryTime.toDateString()%> 127 | 128 | <%=activities[i].user_id.username || 'This user'%> 129 | 130 | commented <%=activities[i].info.title%> 131 | Comment<%=activities[i].entryTime.toDateString()%> 136 | 137 | <%=activities[i].user_id.username || 'This user'%> 138 | 139 | updated comment on <%=activities[i].info.title%> 140 | Update Comment<%=activities[i].entryTime.toDateString()%> 145 | 146 | <%=activities[i].user_id.username || 'This user'%> 147 | 148 | deleted comment on <%=activities[i].info.title%> 149 | Delete Comment<%=activities[i].entryTime.toDateString()%>
158 | 159 | <% if (pages > 0) { %> 160 | 181 | <% } %> 182 |
183 |
184 | 185 |
186 |
187 |
188 |

Books

189 |

<%=books_count%>

190 | View 191 |
192 |
193 | 194 |
195 |
196 |

Users

197 |

<%=users_count%>

198 | Users 199 |
200 |
201 |
202 |
203 |
204 |
205 | 206 | <% include ('../partials/footer.html') %> 207 | -------------------------------------------------------------------------------- /views/user/profile.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%- include ('../partials/userNav.html') %> 9 | 10 | 11 |
12 |
13 |
14 |
15 |

Profile

16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 | 28 | 29 | 34 | 35 | 40 | 41 | <% if(currentUser.bookIssueInfo.length > 0) { %> 42 | 47 | <% } else { %> 48 | 49 | 54 | <% } %> 55 |
56 |
57 |
58 | 59 | <% include ../partials/alerts %> 60 | 61 | 62 |
63 |
64 |
65 |
66 | " class="card-img-top rounded-circle"> 67 | Change Photo 70 |
71 |
72 |

Personal Informations

73 | 74 |
    75 |
  • First Name : <%=currentUser.firstName%>
  • 76 |
  • Last Name : <%=currentUser.lastName%>
  • 77 |
  • Username : <%=currentUser.username%>
  • 78 |
  • Joined : <%=currentUser.joined.toDateString()%>
  • 79 |
  • Email : <%=currentUser.email%>
  • 80 |
  • Address : <%=currentUser.address%>
  • 81 |
  • Violation Flag : <%=currentUser.violationFlag%>
  • 82 |
  • Due Fines : $<%=currentUser.fines%>
  • 83 |
84 |
85 | 86 |
87 |

Terms & Conditions

88 |
    89 |
  • Rule x should be abided by everyone
  • 90 |
  • Rule x should be abided by everyone
  • 91 |
  • Rule x should be abided by everyone
  • 92 |
  • Rule x should be abided by everyone
  • 93 |
94 |
95 |
96 |
97 |
98 | 99 | 100 | 118 | 119 | 120 | 149 | 150 | 151 | 198 | 199 | 200 | 219 | 220 | 221 | 232 | 233 | <% include ('../partials/footer.html') %> 234 | -------------------------------------------------------------------------------- /views/user/index.html: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header.html') %> 2 | 3 | 4 | 5 | 6 | 7 | <%- include ('../partials/userNav.html') %> 8 | 9 | 10 |
11 |
12 |
13 |
14 |

Dashboard

15 |
16 |
17 |
18 |
19 | 20 | <%- include ('../partials/alerts.html') %> 21 | 22 |
23 |
24 |
25 | 30 | 31 | 32 | 37 | 38 | 39 | 44 |
45 |
46 |
47 | 48 | 49 |
50 |
51 |
52 |
53 |
54 | " class="card-img-top p-1" alt="..."> 55 |
56 |

Name : <%=user.firstName%> <%=user.lastName%>

57 |

Email : <%=user.email%>

58 |

Book Issued : <%=user.bookIssueInfo.length%>

59 |

Due : $<%=user.fines%>

60 |

Flagged : <%=user.violationFlag%>

61 |

Joined : <%=user.joined.toDateString() %>

62 |
63 |
64 |
65 |
66 | 67 |
68 |
69 |

Recent Activities

70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | <% activities.forEach(activity => { %> 82 | <% if(activity.category == 'Issue') { %> 83 | 84 | 85 | 86 | 90 | 91 | <% } else if(activity.category == 'Return') { %> 92 | 93 | 94 | 95 | 99 | 100 | <% } else if(activity.category == 'Comment') { %> 101 | 102 | 103 | 104 | 105 | 108 | 109 | <% } else if(activity.category == 'Fine') {%> 110 | 111 | 112 | 113 | 114 | 119 | 120 | <% } else if(activity.category == 'Renew') { %> 121 | 122 | 123 | 124 | 128 | 129 | <% } else if(activity.category == 'Update Profile') { %> 130 | 131 | 132 | 133 | 136 | 139 | 140 | <% } else if(activity.category == 'Update Password') { %> 141 | 142 | 143 | 144 | 147 | 150 | 151 | <% } else if(activity.category == 'Upload Photo') { %> 152 | 153 | 154 | 155 | 158 | 161 | 162 | <% } else if(activity.category == 'Update Comment') { %> 163 | 164 | 165 | 166 | 167 | 170 | 171 | <% } else if(activity.category == 'Delete Comment') { %> 172 | 173 | 174 | 175 | 176 | 179 | 180 | <% } %> 181 | <% }); %> 182 | 183 | 184 |
InfoCategoryDate
You have issued <%=activity.info.title%><%=activity.category%> 87 |

Issue : <%=activity.time.issueDate.toDateString()%>

88 |

Return : <%=activity.time.returnDate.toDateString()%>

89 |
You have returned <%=activity.info.title%><%=activity.category%> 96 |

Issue : <%=activity.time.issueDate.toDateString()%>

97 |

Return : <%=activity.time.returnDate.toDateString()%>

98 |
You commented on <%=activity.info.title%><%= activity.category %><%=activity.entryTime.toDateString()%> 106 | Details 107 |
You paid $<%=activity.fine.amount%> fines<%=activity.category%><%=activity.fine.date%> 115 | 118 |
You have renewed <%=activity.info.title%><%=activity.category%> 125 |

Issue : <%=activity.time.issueDate.toDateString()%>

126 |

Return : <%=activity.time.returnDate.toDateString()%>

127 |
You have recently updated your profile info <%=activity.category%> 134 | <%=activity.entryTime.toDateString()%> 135 | 137 | Visit Profile 138 |
You have recently updated your password<%=activity.category%> 145 | <%=activity.entryTime.toDateString()%> 146 | 148 | 149 |
You have recently updated your profile picture<%=activity.category%> 156 | <%=activity.entryTime.toDateString()%> 157 | 159 | 160 |
You have updated your comment on <%=activity.info.title%><%= activity.category %><%=activity.entryTime.toDateString()%> 168 | Details 169 |
You have deleted your comment on <%=activity.info.title%><%= activity.category %><%=activity.entryTime.toDateString()%> 177 | Details 178 |
185 | 186 | <% if (pages > 0) { %> 187 | 217 | <% } %> 218 | 219 | 220 | 221 |
222 |
223 |
224 |
225 |
226 | 227 | 228 | 240 | 241 | <% include ('../partials/footer.html') %> -------------------------------------------------------------------------------- /controllers/admin.js: -------------------------------------------------------------------------------- 1 | // importing dependencies 2 | const fs = require("fs"); 3 | 4 | // importing models 5 | const Book = require("../models/book"); 6 | const User = require("../models/user"); 7 | const Activity = require("../models/activity"); 8 | const Issue = require("../models/issue"); 9 | const Comment = require("../models/comment"); 10 | 11 | // importing utilities 12 | const deleteImage = require("../utils/delete_image"); 13 | 14 | // GLOBAL_VARIABLES 15 | const PER_PAGE = 10; 16 | 17 | // admin -> show dashboard working procedure 18 | /* 19 | 1. Get user, book and activity count 20 | 2. Fetch all activities in chunk (for pagination) 21 | 3. Render admin/index 22 | */ 23 | exports.getDashboard = async (req, res, next) => { 24 | var page = req.query.page || 1; 25 | try { 26 | const users_count = (await User.find().countDocuments()) - 1; 27 | const books_count = await Book.find().countDocuments(); 28 | const activity_count = await Activity.find().countDocuments(); 29 | const activities = await Activity.find() 30 | .sort("-entryTime") 31 | .skip(PER_PAGE * page - PER_PAGE) 32 | .limit(PER_PAGE); 33 | 34 | res.render("admin/index", { 35 | users_count: users_count, 36 | books_count: books_count, 37 | activities: activities, 38 | current: page, 39 | pages: Math.ceil(activity_count / PER_PAGE), 40 | }); 41 | } catch (err) { 42 | console.log(err); 43 | } 44 | }; 45 | 46 | // admin -> search activities working procedure 47 | /* 48 | 1. Get user and book count 49 | 2. Fetch activities by search query 50 | 3. Render admin/index 51 | **pagination is not done 52 | */ 53 | exports.postDashboard = async (req, res, next) => { 54 | try { 55 | const search_value = req.body.searchUser; 56 | 57 | // getting user and book count 58 | const books_count = await Book.find().countDocuments(); 59 | const users_count = await User.find().countDocuments(); 60 | 61 | // fetching activities by search query 62 | const activities = await Activity.find({ 63 | $or: [{ "user_id.username": search_value }, { category: search_value }], 64 | }); 65 | 66 | // rendering 67 | res.render("admin/index", { 68 | users_count: users_count, 69 | books_count: books_count, 70 | activities: activities, 71 | current: 1, 72 | pages: 0, 73 | }); 74 | } catch (err) { 75 | console.log(err); 76 | return res.redirect("back"); 77 | } 78 | }; 79 | 80 | // admin -> delete profile working procedure 81 | /* 82 | 1. Find admin by user_id and remove 83 | 2. Redirect back to / 84 | */ 85 | exports.deleteAdminProfile = async (req, res, next) => { 86 | try { 87 | await User.findByIdAndRemove(req.user._id); 88 | res.redirect("/"); 89 | } catch (err) { 90 | console.log(err); 91 | return res.redirect("back"); 92 | } 93 | }; 94 | 95 | // admin -> get book inventory working procedure 96 | /* 97 | 1. Construct search object 98 | 2. Fetch books by search object 99 | 3. Render admin/bookInventory 100 | */ 101 | exports.getAdminBookInventory = async (req, res, next) => { 102 | try { 103 | let page = req.params.page || 1; 104 | const filter = req.params.filter; 105 | const value = req.params.value; 106 | 107 | // console.log(filter, value); 108 | // // constructing search object 109 | let searchObj = {}; 110 | if (filter !== "all" && value !== "all") { 111 | // fetch books by search value and filter 112 | searchObj[filter] = value; 113 | } 114 | 115 | // get the book counts 116 | const books_count = await Book.find(searchObj).countDocuments(); 117 | 118 | // fetching books 119 | const books = await Book.find(searchObj) 120 | .skip(PER_PAGE * page - PER_PAGE) 121 | .limit(PER_PAGE); 122 | 123 | // rendering admin/bookInventory 124 | res.render("admin/bookInventory", { 125 | books: books, 126 | current: page, 127 | pages: Math.ceil(books_count / PER_PAGE), 128 | filter: filter, 129 | value: value, 130 | }); 131 | } catch (err) { 132 | // console.log(err.messge); 133 | return res.redirect("back"); 134 | } 135 | }; 136 | 137 | // admin -> return book inventory by search query working procedure 138 | /* 139 | same as getAdminBookInventory method 140 | */ 141 | exports.postAdminBookInventory = async (req, res, next) => { 142 | try { 143 | let page = req.params.page || 1; 144 | const filter = req.body.filter.toLowerCase(); 145 | const value = req.body.searchName; 146 | 147 | if (value == "") { 148 | req.flash( 149 | "error", 150 | "Search field is empty. Please fill the search field in order to get a result" 151 | ); 152 | return res.redirect("back"); 153 | } 154 | const searchObj = {}; 155 | searchObj[filter] = value; 156 | 157 | // get the books count 158 | const books_count = await Book.find(searchObj).countDocuments(); 159 | 160 | // fetch the books by search query 161 | const books = await Book.find(searchObj) 162 | .skip(PER_PAGE * page - PER_PAGE) 163 | .limit(PER_PAGE); 164 | 165 | // rendering admin/bookInventory 166 | res.render("admin/bookInventory", { 167 | books: books, 168 | current: page, 169 | pages: Math.ceil(books_count / PER_PAGE), 170 | filter: filter, 171 | value: value, 172 | }); 173 | } catch (err) { 174 | // console.log(err.message); 175 | return res.redirect("back"); 176 | } 177 | }; 178 | 179 | // admin -> get the book to be updated 180 | exports.getUpdateBook = async (req, res, next) => { 181 | try { 182 | const book_id = req.params.book_id; 183 | const book = await Book.findById(book_id); 184 | 185 | res.render("admin/book", { 186 | book: book, 187 | }); 188 | } catch (err) { 189 | console.log(err); 190 | return res.redirect("back"); 191 | } 192 | }; 193 | 194 | // admin -> post update book 195 | exports.postUpdateBook = async (req, res, next) => { 196 | try { 197 | const description = req.sanitize(req.body.book.description); 198 | const book_info = req.body.book; 199 | const book_id = req.params.book_id; 200 | 201 | await Book.findByIdAndUpdate(book_id, book_info); 202 | 203 | res.redirect("/admin/bookInventory/all/all/1"); 204 | } catch (err) { 205 | console.log(err); 206 | res.redirect("back"); 207 | } 208 | }; 209 | 210 | // admin -> delete book 211 | exports.getDeleteBook = async (req, res, next) => { 212 | try { 213 | const book_id = req.params.book_id; 214 | 215 | const book = await Book.findById(book_id); 216 | await book.remove(); 217 | 218 | req.flash("success", `A book named ${book.title} is just deleted!`); 219 | res.redirect("back"); 220 | } catch (err) { 221 | console.log(err); 222 | res.redirect("back"); 223 | } 224 | }; 225 | 226 | // admin -> get user list 227 | exports.getUserList = async (req, res, next) => { 228 | try { 229 | const page = req.params.page || 1; 230 | 231 | const users = await User.find() 232 | .sort("-joined") 233 | .skip(PER_PAGE * page - PER_PAGE) 234 | .limit(PER_PAGE); 235 | 236 | const users_count = await User.find().countDocuments(); 237 | 238 | res.render("admin/users", { 239 | users: users, 240 | current: page, 241 | pages: Math.ceil(users_count / PER_PAGE), 242 | }); 243 | } catch (err) { 244 | console.log(err); 245 | res.redirect("back"); 246 | } 247 | }; 248 | 249 | // admin -> show searched user 250 | exports.postShowSearchedUser = async (req, res, next) => { 251 | try { 252 | const page = req.params.page || 1; 253 | const search_value = req.body.searchUser; 254 | 255 | const users = await User.find({ 256 | $or: [ 257 | { firstName: search_value }, 258 | { lastName: search_value }, 259 | { username: search_value }, 260 | { email: search_value }, 261 | ], 262 | }); 263 | 264 | if (users.length <= 0) { 265 | req.flash("error", "User not found!"); 266 | return res.redirect("back"); 267 | } else { 268 | res.render("admin/users", { 269 | users: users, 270 | current: page, 271 | pages: 0, 272 | }); 273 | } 274 | } catch (err) { 275 | console.log(err); 276 | res.redirect("back"); 277 | } 278 | }; 279 | 280 | // admin -> flag/unflag user 281 | exports.getFlagUser = async (req, res, next) => { 282 | try { 283 | const user_id = req.params.user_id; 284 | 285 | const user = await User.findById(user_id); 286 | 287 | if (user.violationFlag) { 288 | user.violationFlag = false; 289 | await user.save(); 290 | req.flash( 291 | "success", 292 | `An user named ${user.firstName} ${user.lastName} is just unflagged!` 293 | ); 294 | } else { 295 | user.violationFlag = true; 296 | await user.save(); 297 | req.flash( 298 | "warning", 299 | `An user named ${user.firstName} ${user.lastName} is just flagged!` 300 | ); 301 | } 302 | 303 | res.redirect("/admin/users/1"); 304 | } catch (err) { 305 | console.log(err); 306 | res.redirect("back"); 307 | } 308 | }; 309 | 310 | // admin -> show one user 311 | exports.getUserProfile = async (req, res, next) => { 312 | try { 313 | const user_id = req.params.user_id; 314 | 315 | const user = await User.findById(user_id); 316 | const issues = await Issue.find({ "user_id.id": user_id }); 317 | const comments = await Comment.find({ "author.id": user_id }); 318 | const activities = await Activity.find({ "user_id.id": user_id }).sort( 319 | "-entryTime" 320 | ); 321 | 322 | res.render("admin/user", { 323 | user: user, 324 | issues: issues, 325 | activities: activities, 326 | comments: comments, 327 | }); 328 | } catch (err) { 329 | console.log(err); 330 | res.redirect("back"); 331 | } 332 | }; 333 | 334 | // admin -> show all activities of one user 335 | exports.getUserAllActivities = async (req, res, next) => { 336 | try { 337 | const user_id = req.params.user_id; 338 | 339 | const activities = await Activity.find({ "user_id.id": user_id }).sort( 340 | "-entryTime" 341 | ); 342 | res.render("admin/activities", { 343 | activities: activities, 344 | }); 345 | } catch (err) { 346 | console.log(err); 347 | res.redirect("back"); 348 | } 349 | }; 350 | 351 | // admin -> show activities by category 352 | exports.postShowActivitiesByCategory = async (req, res, next) => { 353 | try { 354 | const category = req.body.category; 355 | const activities = await Activity.find({ category: category }); 356 | 357 | res.render("admin/activities", { 358 | activities: activities, 359 | }); 360 | } catch (err) { 361 | console.log(err); 362 | res.redirect("back"); 363 | } 364 | }; 365 | 366 | // admin -> delete a user 367 | exports.getDeleteUser = async (req, res, next) => { 368 | try { 369 | const user_id = req.params.user_id; 370 | const user = await User.findById(user_id); 371 | await user.remove(); 372 | 373 | let imagePath = `images/${user.image}`; 374 | if (fs.existsSync(imagePath)) { 375 | deleteImage(imagePath); 376 | } 377 | 378 | await Issue.deleteMany({ "user_id.id": user_id }); 379 | await Comment.deleteMany({ "author.id": user_id }); 380 | await Activity.deleteMany({ "user_id.id": user_id }); 381 | 382 | res.redirect("/admin/users/1"); 383 | } catch (err) { 384 | console.log(err); 385 | res.redirect("back"); 386 | } 387 | }; 388 | 389 | // admin -> add new book 390 | exports.getAddNewBook = (req, res, next) => { 391 | res.render("admin/addBook"); 392 | }; 393 | 394 | exports.postAddNewBook = async (req, res, next) => { 395 | try { 396 | const book_info = req.body.book; 397 | book_info.description = req.sanitize(book_info.description); 398 | 399 | const isDuplicate = await Book.find(book_info); 400 | 401 | if (isDuplicate.length > 0) { 402 | req.flash("error", "This book is already registered in inventory"); 403 | return res.redirect("back"); 404 | } 405 | 406 | const new_book = new Book(book_info); 407 | await new_book.save(); 408 | req.flash( 409 | "success", 410 | `A new book named ${new_book.title} is added to the inventory` 411 | ); 412 | res.redirect("/admin/bookInventory/all/all/1"); 413 | } catch (err) { 414 | console.log(err); 415 | res.redirect("back"); 416 | } 417 | }; 418 | 419 | // admin -> get profile 420 | exports.getAdminProfile = (req, res, next) => { 421 | res.render("admin/profile"); 422 | }; 423 | 424 | // admin -> update profile 425 | exports.postUpdateAdminProfile = async (req, res, next) => { 426 | try { 427 | const user_id = req.user._id; 428 | const update_info = req.body.admin; 429 | 430 | await User.findByIdAndUpdate(user_id, update_info); 431 | 432 | res.redirect("/admin/profile"); 433 | } catch (err) { 434 | console.log(err); 435 | res.redirect("back"); 436 | } 437 | }; 438 | 439 | // admin -> update password 440 | exports.putUpdateAdminPassword = async (req, res, next) => { 441 | try { 442 | const user_id = req.user._id; 443 | const old_password = req.body.oldPassword; 444 | const new_password = req.body.password; 445 | 446 | const admin = await User.findById(user_id); 447 | await admin.changePassword(old_password, new_password); 448 | await admin.save(); 449 | 450 | req.flash( 451 | "success", 452 | "Your password is changed recently. Please login again to confirm" 453 | ); 454 | res.redirect("/auth/admin-login"); 455 | } catch (err) { 456 | console.log(err); 457 | res.redirect("back"); 458 | } 459 | }; 460 | -------------------------------------------------------------------------------- /controllers/user.js: -------------------------------------------------------------------------------- 1 | // importing dependencies 2 | const sharp = require("sharp"); 3 | const uid = require("uid"); 4 | const fs = require("fs"); 5 | 6 | // importing models 7 | const User = require("../models/user"); 8 | const Activity = require("../models/activity"); 9 | const Book = require("../models/book"); 10 | const Issue = require("../models/issue"); 11 | const Comment = require("../models/comment"); 12 | 13 | // importing utilities 14 | const deleteImage = require("../utils/delete_image"); 15 | 16 | // GLOBAL_VARIABLES 17 | const PER_PAGE = 5; 18 | 19 | //user -> dashboard 20 | exports.getUserDashboard = async (req, res, next) => { 21 | var page = req.params.page || 1; 22 | const user_id = req.user._id; 23 | 24 | try { 25 | // fetch user info from db and populate it with related book issue 26 | const user = await User.findById(user_id); 27 | 28 | if (user.bookIssueInfo.length > 0) { 29 | const issues = await Issue.find({ "user_id.id": user._id }); 30 | 31 | for (let issue of issues) { 32 | if (issue.book_info.returnDate < Date.now()) { 33 | user.violatonFlag = true; 34 | user.save(); 35 | req.flash( 36 | "warning", 37 | "You are flagged for not returning " + 38 | issue.book_info.title + 39 | " in time" 40 | ); 41 | break; 42 | } 43 | } 44 | } 45 | const activities = await Activity.find({ "user_id.id": req.user._id }) 46 | .sort({ _id: -1 }) 47 | .skip(PER_PAGE * page - PER_PAGE) 48 | .limit(PER_PAGE); 49 | 50 | const activity_count = await Activity.find({ 51 | "user_id.id": req.user._id, 52 | }).countDocuments(); 53 | 54 | res.render("user/index", { 55 | user: user, 56 | current: page, 57 | pages: Math.ceil(activity_count / PER_PAGE), 58 | activities: activities, 59 | }); 60 | } catch (err) { 61 | console.log(err); 62 | return res.redirect("back"); 63 | } 64 | }; 65 | 66 | // user -> profile 67 | exports.getUserProfile = (req, res, next) => { 68 | res.render("user/profile"); 69 | }; 70 | 71 | // user -> update/change password 72 | exports.putUpdatePassword = async (req, res, next) => { 73 | const username = req.user.username; 74 | const oldPassword = req.body.oldPassword; 75 | const newPassword = req.body.password; 76 | 77 | try { 78 | const user = await User.findByUsername(username); 79 | await user.changePassword(oldPassword, newPassword); 80 | await user.save(); 81 | 82 | // logging activity 83 | const activity = new Activity({ 84 | category: "Update Password", 85 | user_id: { 86 | id: req.user._id, 87 | username: req.user.username, 88 | }, 89 | }); 90 | await activity.save(); 91 | 92 | req.flash( 93 | "success", 94 | "Your password is recently updated. Please log in again to confirm" 95 | ); 96 | res.redirect("/auth/user-login"); 97 | } catch (err) { 98 | console.log(err); 99 | return res.redirect("back"); 100 | } 101 | }; 102 | 103 | // user -> update profile 104 | exports.putUpdateUserProfile = async (req, res, next) => { 105 | try { 106 | const userUpdateInfo = { 107 | firstName: req.body.firstName, 108 | lastName: req.body.lastName, 109 | email: req.body.email, 110 | gender: req.body.gender, 111 | address: req.body.address, 112 | }; 113 | await User.findByIdAndUpdate(req.user._id, userUpdateInfo); 114 | 115 | // logging activity 116 | const activity = new Activity({ 117 | category: "Update Profile", 118 | user_id: { 119 | id: req.user._id, 120 | username: req.user.username, 121 | }, 122 | }); 123 | await activity.save(); 124 | 125 | res.redirect("back"); 126 | } catch (err) { 127 | console.log(err); 128 | return res.redirect("back"); 129 | } 130 | }; 131 | 132 | // upload image 133 | exports.postUploadUserImage = async (req, res, next) => { 134 | try { 135 | const user_id = req.user._id; 136 | const user = await User.findById(user_id); 137 | 138 | let imageUrl; 139 | if (req.file) { 140 | imageUrl = `${uid()}__${req.file.originalname}`; 141 | let filename = `images/${imageUrl}`; 142 | let previousImagePath = `images/${user.image}`; 143 | 144 | const imageExist = fs.existsSync(previousImagePath); 145 | if (imageExist) { 146 | deleteImage(previousImagePath); 147 | } 148 | await sharp(req.file.path).rotate().resize(500, 500).toFile(filename); 149 | 150 | fs.unlink(req.file.path, (err) => { 151 | if (err) { 152 | console.log(err); 153 | } 154 | }); 155 | } else { 156 | imageUrl = "profile.png"; 157 | } 158 | 159 | user.image = imageUrl; 160 | await user.save(); 161 | 162 | const activity = new Activity({ 163 | category: "Upload Photo", 164 | user_id: { 165 | id: req.user._id, 166 | username: user.username, 167 | }, 168 | }); 169 | await activity.save(); 170 | 171 | res.redirect("/user/1/profile"); 172 | } catch (err) { 173 | console.log(err); 174 | res.redirect("back"); 175 | } 176 | }; 177 | 178 | //user -> notification 179 | exports.getNotification = async (req, res, next) => { 180 | res.render("user/notification"); 181 | }; 182 | 183 | //user -> issue a book 184 | exports.postIssueBook = async (req, res, next) => { 185 | if (req.user.violationFlag) { 186 | req.flash( 187 | "error", 188 | "You are flagged for violating rules/delay on returning books/paying fines. Untill the flag is lifted, You can't issue any books" 189 | ); 190 | return res.redirect("back"); 191 | } 192 | 193 | if (req.user.bookIssueInfo.length >= 5) { 194 | req.flash("warning", "You can't issue more than 5 books at a time"); 195 | return res.redirect("back"); 196 | } 197 | 198 | try { 199 | const book = await Book.findById(req.params.book_id); 200 | const user = await User.findById(req.params.user_id); 201 | 202 | // registering issue 203 | book.stock -= 1; 204 | const issue = new Issue({ 205 | book_info: { 206 | id: book._id, 207 | title: book.title, 208 | author: book.author, 209 | ISBN: book.ISBN, 210 | category: book.category, 211 | stock: book.stock, 212 | }, 213 | user_id: { 214 | id: user._id, 215 | username: user.username, 216 | }, 217 | }); 218 | 219 | // putting issue record on individual user document 220 | user.bookIssueInfo.push(book._id); 221 | 222 | // logging the activity 223 | const activity = new Activity({ 224 | info: { 225 | id: book._id, 226 | title: book.title, 227 | }, 228 | category: "Issue", 229 | time: { 230 | id: issue._id, 231 | issueDate: issue.book_info.issueDate, 232 | returnDate: issue.book_info.returnDate, 233 | }, 234 | user_id: { 235 | id: user._id, 236 | username: user.username, 237 | }, 238 | }); 239 | 240 | // await ensure to synchronously save all database alteration 241 | await issue.save(); 242 | await user.save(); 243 | await book.save(); 244 | await activity.save(); 245 | 246 | res.redirect("/books/all/all/1"); 247 | } catch (err) { 248 | console.log(err); 249 | return res.redirect("back"); 250 | } 251 | }; 252 | 253 | // user -> show return-renew page 254 | exports.getShowRenewReturn = async (req, res, next) => { 255 | const user_id = req.user._id; 256 | try { 257 | const issue = await Issue.find({ "user_id.id": user_id }); 258 | res.render("user/return-renew", { user: issue }); 259 | } catch (err) { 260 | console.log(err); 261 | return res.redirect("back"); 262 | } 263 | }; 264 | 265 | // user -> renew book working procedure 266 | /* 267 | 1. construct the search object 268 | 2. fetch issues based on search object 269 | 3. increament return date by 7 days set isRenewed = true 270 | 4. Log the activity 271 | 5. save all db alteration 272 | 6. redirect to /books/return-renew 273 | */ 274 | exports.postRenewBook = async (req, res, next) => { 275 | try { 276 | const searchObj = { 277 | "user_id.id": req.user._id, 278 | "book_info.id": req.params.book_id, 279 | }; 280 | const issue = await Issue.findOne(searchObj); 281 | // adding extra 7 days to that issue 282 | let time = issue.book_info.returnDate.getTime(); 283 | issue.book_info.returnDate = time + 7 * 24 * 60 * 60 * 1000; 284 | issue.book_info.isRenewed = true; 285 | 286 | // logging the activity 287 | const activity = new Activity({ 288 | info: { 289 | id: issue._id, 290 | title: issue.book_info.title, 291 | }, 292 | category: "Renew", 293 | time: { 294 | id: issue._id, 295 | issueDate: issue.book_info.issueDate, 296 | returnDate: issue.book_info.returnDate, 297 | }, 298 | user_id: { 299 | id: req.user._id, 300 | username: req.user.username, 301 | }, 302 | }); 303 | 304 | await activity.save(); 305 | await issue.save(); 306 | 307 | res.redirect("/books/return-renew"); 308 | } catch (err) { 309 | console.log(err); 310 | return res.redirect("back"); 311 | } 312 | }; 313 | 314 | // user -> return book working procedure 315 | /* 316 | 1. Find the position of the book to be returned from user.bookIssueInfo 317 | 2. Fetch the book from db and increament its stock by 1 318 | 3. Remove issue record from db 319 | 4. Pop bookIssueInfo from user by position 320 | 5. Log the activity 321 | 6. refirect to /books/return-renew 322 | */ 323 | exports.postReturnBook = async (req, res, next) => { 324 | try { 325 | // finding the position 326 | const book_id = req.params.book_id; 327 | const pos = req.user.bookIssueInfo.indexOf(req.params.book_id); 328 | 329 | // fetching book from db and increament 330 | const book = await Book.findById(book_id); 331 | book.stock += 1; 332 | await book.save(); 333 | 334 | // removing issue 335 | const issue = await Issue.findOne({ "user_id.id": req.user._id }); 336 | await issue.remove(); 337 | 338 | // popping book issue info from user 339 | req.user.bookIssueInfo.splice(pos, 1); 340 | await req.user.save(); 341 | 342 | // logging the activity 343 | const activity = new Activity({ 344 | info: { 345 | id: issue.book_info.id, 346 | title: issue.book_info.title, 347 | }, 348 | category: "Return", 349 | time: { 350 | id: issue._id, 351 | issueDate: issue.book_info.issueDate, 352 | returnDate: issue.book_info.returnDate, 353 | }, 354 | user_id: { 355 | id: req.user._id, 356 | username: req.user.username, 357 | }, 358 | }); 359 | await activity.save(); 360 | 361 | // redirecting 362 | res.redirect("/books/return-renew"); 363 | } catch (err) { 364 | console.log(err); 365 | return res.redirect("back"); 366 | } 367 | }; 368 | 369 | // user -> create new comment working procedure 370 | /* 371 | 1. Find the book to be commented by id 372 | 2. Create new Comment instance and fill information inside it 373 | 3. Log the activity 374 | 4. Redirect to /books/details/:book_id 375 | */ 376 | exports.postNewComment = async (req, res, next) => { 377 | try { 378 | const comment_text = req.body.comment; 379 | const user_id = req.user._id; 380 | const username = req.user.username; 381 | 382 | // fetching the book to be commented by id 383 | const book_id = req.params.book_id; 384 | const book = await Book.findById(book_id); 385 | 386 | // creating new comment instance 387 | const comment = new Comment({ 388 | text: comment_text, 389 | author: { 390 | id: user_id, 391 | username: username, 392 | }, 393 | book: { 394 | id: book._id, 395 | title: book.title, 396 | }, 397 | }); 398 | await comment.save(); 399 | 400 | // pushing the comment id to book 401 | book.comments.push(comment._id); 402 | await book.save(); 403 | 404 | // logging the activity 405 | const activity = new Activity({ 406 | info: { 407 | id: book._id, 408 | title: book.title, 409 | }, 410 | category: "Comment", 411 | user_id: { 412 | id: user_id, 413 | username: username, 414 | }, 415 | }); 416 | await activity.save(); 417 | 418 | res.redirect("/books/details/" + book_id); 419 | } catch (err) { 420 | console.log(err); 421 | return res.redirect("back"); 422 | } 423 | }; 424 | 425 | // user -> update existing comment working procedure 426 | /* 427 | 1. Fetch the comment to be updated from db and update 428 | 2. Fetch the book to be commented for logging book id, title in activity 429 | 3. Log the activity 430 | 4. Redirect to /books/details/"+book_id 431 | */ 432 | exports.postUpdateComment = async (req, res, next) => { 433 | const comment_id = req.params.comment_id; 434 | const comment_text = req.body.comment; 435 | const book_id = req.params.book_id; 436 | const username = req.user.username; 437 | const user_id = req.user._id; 438 | 439 | try { 440 | // fetching the comment by id 441 | await Comment.findByIdAndUpdate(comment_id, comment_text); 442 | 443 | // fetching the book 444 | const book = await Book.findById(book_id); 445 | 446 | // logging the activity 447 | const activity = new Activity({ 448 | info: { 449 | id: book._id, 450 | title: book.title, 451 | }, 452 | category: "Update Comment", 453 | user_id: { 454 | id: user_id, 455 | username: username, 456 | }, 457 | }); 458 | await activity.save(); 459 | 460 | // redirecting 461 | res.redirect("/books/details/" + book_id); 462 | } catch (err) { 463 | console.log(err); 464 | return res.redirect("back"); 465 | } 466 | }; 467 | 468 | // user -> delete existing comment working procedure 469 | /* 470 | 1. Fetch the book info for logging info 471 | 2. Find the position of comment id in book.comments array in Book model 472 | 3. Pop the comment id by position from Book 473 | 4. Find the comment and remove it from Comment 474 | 5. Log the activity 475 | 6. Redirect to /books/details/" + book_id 476 | */ 477 | exports.deleteComment = async (req, res, next) => { 478 | const book_id = req.params.book_id; 479 | const comment_id = req.params.comment_id; 480 | const user_id = req.user._id; 481 | const username = req.user.username; 482 | try { 483 | // fetching the book 484 | const book = await Book.findById(book_id); 485 | 486 | // finding the position and popping comment_id 487 | const pos = book.comments.indexOf(comment_id); 488 | book.comments.splice(pos, 1); 489 | await book.save(); 490 | 491 | // removing comment from Comment 492 | await Comment.findByIdAndRemove(comment_id); 493 | 494 | // logging the activity 495 | const activity = new Activity({ 496 | info: { 497 | id: book._id, 498 | title: book.title, 499 | }, 500 | category: "Delete Comment", 501 | user_id: { 502 | id: user_id, 503 | username: username, 504 | }, 505 | }); 506 | await activity.save(); 507 | 508 | // redirecting 509 | res.redirect("/books/details/" + book_id); 510 | } catch (err) { 511 | console.log(err); 512 | return res.redirect("back"); 513 | } 514 | }; 515 | 516 | // user -> delete user account 517 | exports.deleteUserAccount = async (req, res, next) => { 518 | try { 519 | const user_id = req.user._id; 520 | 521 | const user = await User.findById(user_id); 522 | await user.remove(); 523 | 524 | let imagePath = `images/${user.image}`; 525 | if (fs.existsSync(imagePath)) { 526 | deleteImage(imagePath); 527 | } 528 | 529 | await Issue.deleteMany({ "user_id.id": user_id }); 530 | await Comment.deleteMany({ "author.id": user_id }); 531 | await Activity.deleteMany({ "user_id.id": user_id }); 532 | 533 | res.redirect("/"); 534 | } catch (err) { 535 | console.log(err); 536 | res.redirect("back"); 537 | } 538 | }; 539 | -------------------------------------------------------------------------------- /public/assets/js/popper.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) Federico Zivolo 2017 3 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). 4 | */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=getComputedStyle(e,null);return t?o[t]:o}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll)/.test(r+s+p)?e:n(o(e))}function r(e){var o=e&&e.offsetParent,i=o&&o.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TD','TABLE'].indexOf(o.nodeName)&&'static'===t(o,'position')?r(o):o:e?e.ownerDocument.documentElement:document.documentElement}function p(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||r(e.firstElementChild)===e)}function s(e){return null===e.parentNode?e:s(e.parentNode)}function d(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,i=o?e:t,n=o?t:e,a=document.createRange();a.setStart(i,0),a.setEnd(n,0);var l=a.commonAncestorContainer;if(e!==l&&t!==l||i.contains(n))return p(l)?l:r(l);var f=s(e);return f.host?d(f.host,t):d(e,s(t).host)}function a(e){var t=1=o.clientWidth&&i>=o.clientHeight}),l=0i[e]&&!t.escapeWithReference&&(n=_(p[o],i[e]-('right'===e?p.width:p.height))),pe({},o,n)}};return n.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';p=se({},p,s[t](e))}),e.offsets.popper=p,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,i=t.reference,n=e.placement.split('-')[0],r=X,p=-1!==['top','bottom'].indexOf(n),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(i[s])&&(e.offsets.popper[d]=r(i[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var i;if(!F(e.instance.modifiers,'arrow','keepTogether'))return e;var n=o.element;if('string'==typeof n){if(n=e.instance.popper.querySelector(n),!n)return e;}else if(!e.instance.popper.contains(n))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',g=a?'bottom':'right',u=L(n)[l];d[g]-us[g]&&(e.offsets.popper[m]+=d[m]+u-s[g]),e.offsets.popper=c(e.offsets.popper);var b=d[m]+d[l]/2-u/2,w=t(e.instance.popper),y=parseFloat(w['margin'+f],10),E=parseFloat(w['border'+f+'Width'],10),v=b-e.offsets.popper[m]-y-E;return v=J(_(s[l]-u,v),0),e.arrowElement=n,e.offsets.arrow=(i={},pe(i,m,Math.round(v)),pe(i,h,''),i),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(k(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=y(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement),i=e.placement.split('-')[0],n=x(i),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case le.FLIP:p=[i,n];break;case le.CLOCKWISE:p=q(i);break;case le.COUNTERCLOCKWISE:p=q(i,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(i!==s||p.length===d+1)return e;i=e.placement.split('-')[0],n=x(i);var a=e.offsets.popper,l=e.offsets.reference,f=X,m='left'===i&&f(a.right)>f(l.left)||'right'===i&&f(a.left)f(l.top)||'bottom'===i&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===i&&h||'right'===i&&c||'top'===i&&g||'bottom'===i&&u,w=-1!==['top','bottom'].indexOf(i),y=!!t.flipVariations&&(w&&'start'===r&&h||w&&'end'===r&&c||!w&&'start'===r&&g||!w&&'end'===r&&u);(m||b||y)&&(e.flipped=!0,(m||b)&&(i=p[d+1]),y&&(r=K(r)),e.placement=i+(r?'-'+r:''),e.offsets.popper=se({},e.offsets.popper,S(e.instance.popper,e.offsets.reference,e.placement)),e=C(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],i=e.offsets,n=i.popper,r=i.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return n[p?'left':'top']=r[o]-(s?n[p?'width':'height']:0),e.placement=x(t),e.offsets.popper=c(n),e}},hide:{order:800,enabled:!0,fn:function(e){if(!F(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=T(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.rightli{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} --------------------------------------------------------------------------------