├── back-end ├── .gitignore ├── Procfile ├── public │ ├── favicon.ico │ ├── fonts │ │ ├── glyphicons-halflings-regular.e18bbf61.ttf │ │ ├── glyphicons-halflings-regular.f4769f9b.eot │ │ ├── glyphicons-halflings-regular.fa277232.woff │ │ └── glyphicons-halflings-regular.448c34a5.woff2 │ ├── index.html │ ├── css │ │ └── app.6076211c.css │ └── js │ │ └── app.770a7fbd.js ├── config │ ├── cors.js │ ├── db.js │ └── passport.js ├── users │ ├── router.js │ ├── model.js │ ├── getUsers.js │ └── controller.js ├── items │ ├── book.model.js │ ├── movie.model.js │ ├── router.js │ └── controller.js ├── package.json ├── helpers │ └── paging.js ├── server.js ├── info │ └── movieController.js ├── readme.md └── package-lock.json ├── .gitignore ├── front-end ├── .browserslistrc ├── babel.config.js ├── src │ ├── components │ │ ├── Shared.vue │ │ ├── books │ │ │ ├── Books.vue │ │ │ ├── BookList.vue │ │ │ └── Book.vue │ │ ├── movies │ │ │ ├── Movies.vue │ │ │ ├── MovieList.vue │ │ │ └── Movie.vue │ │ ├── Notifier.vue │ │ ├── ReCaptcha.vue │ │ ├── DateInput.vue │ │ ├── Modal.vue │ │ ├── DropdownMenu.vue │ │ ├── UserSettings.vue │ │ ├── Home.vue │ │ ├── Pager.vue │ │ ├── Login.vue │ │ ├── People.vue │ │ ├── Navbar.vue │ │ ├── Register.vue │ │ └── ItemList.vue │ ├── helpers │ │ ├── config.js │ │ ├── formatters.js │ │ ├── upload.js │ │ └── validators.js │ ├── App.vue │ ├── store │ │ ├── plugins │ │ │ └── localStorage.js │ │ ├── index.js │ │ └── modules │ │ │ ├── notification.js │ │ │ ├── auth.js │ │ │ ├── info.js │ │ │ ├── users.js │ │ │ └── items.js │ ├── router │ │ ├── book-routes.js │ │ ├── movie-routes.js │ │ ├── people-routes.js │ │ └── index.js │ └── main.js ├── postcss.config.js ├── public │ ├── favicon.ico │ └── index.html ├── vue.config.js ├── .gitignore ├── .eslintrc.js ├── README.md └── package.json ├── migrate-to-vue-cli-3.md ├── readme.md └── LICENSE.md /back-end/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /back-end/Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | **/node_modules 3 | **/npm-debug.log 4 | -------------------------------------------------------------------------------- /front-end/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /front-end/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /front-end/src/components/Shared.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /front-end/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /back-end/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iurii-kyrylenko/hobbies/HEAD/back-end/public/favicon.ico -------------------------------------------------------------------------------- /front-end/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iurii-kyrylenko/hobbies/HEAD/front-end/public/favicon.ico -------------------------------------------------------------------------------- /front-end/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runtimeCompiler: false, 3 | outputDir: '../back-end/public' 4 | } 5 | -------------------------------------------------------------------------------- /front-end/src/components/books/Books.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /front-end/src/components/movies/Movies.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /back-end/public/fonts/glyphicons-halflings-regular.e18bbf61.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iurii-kyrylenko/hobbies/HEAD/back-end/public/fonts/glyphicons-halflings-regular.e18bbf61.ttf -------------------------------------------------------------------------------- /back-end/public/fonts/glyphicons-halflings-regular.f4769f9b.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iurii-kyrylenko/hobbies/HEAD/back-end/public/fonts/glyphicons-halflings-regular.f4769f9b.eot -------------------------------------------------------------------------------- /back-end/public/fonts/glyphicons-halflings-regular.fa277232.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iurii-kyrylenko/hobbies/HEAD/back-end/public/fonts/glyphicons-halflings-regular.fa277232.woff -------------------------------------------------------------------------------- /back-end/public/fonts/glyphicons-halflings-regular.448c34a5.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iurii-kyrylenko/hobbies/HEAD/back-end/public/fonts/glyphicons-halflings-regular.448c34a5.woff2 -------------------------------------------------------------------------------- /front-end/src/helpers/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | get apiUrl () { 3 | return process.env.NODE_ENV === 'production' 4 | ? '/api' 5 | : 'http://localhost:3000/api' 6 | }, 7 | get reCaptchaSiteKey () { 8 | return '6LeUuSUTAAAAAElwIcAHk994ErqNeqw7aQxlsw_H' 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /front-end/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /front-end/src/helpers/formatters.js: -------------------------------------------------------------------------------- 1 | export const formatDate = (date, error = 'Invalid date') => { 2 | if (!date) return error 3 | 4 | const months = [ 5 | 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 6 | 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' 7 | ] 8 | 9 | const year = date.getFullYear() 10 | const month = date.getMonth() 11 | const day = date.getDate() 12 | 13 | return `${months[month]} ${day}, ${year}` 14 | } 15 | -------------------------------------------------------------------------------- /front-end/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 13 | }, 14 | parserOptions: { 15 | parser: 'babel-eslint' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /front-end/README.md: -------------------------------------------------------------------------------- 1 | # front-end 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /front-end/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /front-end/src/store/plugins/localStorage.js: -------------------------------------------------------------------------------- 1 | const JWT = 'jwt' 2 | 3 | const init = localStorage[JWT] 4 | 5 | const plugin = store => { 6 | store.subscribe(mutation => { 7 | switch (mutation.type) { 8 | case 'auth/setToken': 9 | localStorage[JWT] = mutation.payload 10 | break 11 | case 'auth/resetToken': 12 | localStorage.removeItem(JWT) 13 | break 14 | } 15 | }) 16 | } 17 | 18 | export default plugin 19 | export { init } 20 | -------------------------------------------------------------------------------- /front-end/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import notification from './modules/notification' 4 | import auth from './modules/auth' 5 | import localStoragePlugin from './plugins/localStorage' 6 | import items from './modules/items' 7 | import users from './modules/users' 8 | import info from './modules/info' 9 | 10 | Vue.use(Vuex) 11 | 12 | export default new Vuex.Store({ 13 | modules: { notification, auth, items, users, info }, 14 | plugins: [localStoragePlugin] 15 | }) 16 | -------------------------------------------------------------------------------- /front-end/src/router/book-routes.js: -------------------------------------------------------------------------------- 1 | import BookList from '@/components/books/BookList' 2 | import Books from '@/components/books/Books' 3 | import Book from '@/components/books/Book' 4 | 5 | export default { 6 | path: '/books', 7 | component: Books, 8 | children: [ 9 | { 10 | path: '', 11 | component: BookList 12 | }, 13 | { 14 | path: 'new', 15 | component: Book 16 | }, 17 | { 18 | path: ':id', 19 | component: Book, 20 | props: true 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /back-end/config/cors.js: -------------------------------------------------------------------------------- 1 | module.exports = (server) => { 2 | server.use((req, res, next) => { 3 | res.header('Access-Control-Allow-Origin', '*'); 4 | res.header('Access-Control-Allow-Methods', 'PUT, DELETE'); 5 | res.header('Access-Control-Allow-Headers', 6 | 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); 7 | 8 | if (req.method === 'OPTIONS') { 9 | res.sendStatus(200); 10 | return; 11 | } 12 | 13 | next(); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /front-end/src/router/movie-routes.js: -------------------------------------------------------------------------------- 1 | import MovieList from '@/components/movies/MovieList' 2 | import Movies from '@/components/movies/Movies' 3 | import Movie from '@/components/movies/Movie' 4 | 5 | export default { 6 | path: '/movies', 7 | component: Movies, 8 | children: [ 9 | { 10 | path: '', 11 | component: MovieList 12 | }, 13 | { 14 | path: 'new', 15 | component: Movie 16 | }, 17 | { 18 | path: ':id', 19 | component: Movie, 20 | props: true 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /front-end/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App' 3 | import router from './router' 4 | import store from './store' 5 | import Vuelidate from 'vuelidate' 6 | import { formatDate } from './helpers/formatters' 7 | import 'bootstrap-css-only/css/bootstrap.css' 8 | 9 | Vue.config.productionTip = false 10 | 11 | // plug-in registration 12 | Vue.use(Vuelidate) 13 | Vue.filter('date', formatDate) 14 | 15 | Vue.config.productionTip = false 16 | 17 | new Vue({ 18 | router, 19 | store, 20 | render: h => h(App) 21 | }).$mount('#app') 22 | -------------------------------------------------------------------------------- /back-end/config/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | mongoose.Promise = global.Promise; 3 | 4 | mongoose.connect(process.env.CONNECTION_STRING, { 5 | useNewUrlParser: true, 6 | useUnifiedTopology: true 7 | }); 8 | 9 | mongoose.connection 10 | .on('connected', () => console.log('mongo connected')) 11 | .on('error', () => console.log('mongo connection error')) 12 | .on('disconnected', () => console.log('mongo disconnectes')); 13 | 14 | require('../items/book.model'); 15 | require('../items/movie.model'); 16 | require('../users/model'); 17 | -------------------------------------------------------------------------------- /front-end/src/components/Notifier.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | -------------------------------------------------------------------------------- /front-end/src/router/people-routes.js: -------------------------------------------------------------------------------- 1 | import Shared from '@/components/Shared' 2 | import People from '@/components/People' 3 | import BookList from '@/components/books/BookList' 4 | import MovieList from '@/components/movies/MovieList' 5 | 6 | export default { 7 | path: '/people', 8 | component: Shared, 9 | children: [ 10 | { 11 | path: '', 12 | component: People 13 | }, 14 | { 15 | path: ':uid/:name/b', 16 | component: BookList, 17 | props: true 18 | }, 19 | { 20 | path: ':uid/:name/m', 21 | component: MovieList, 22 | props: true 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /front-end/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | My Hobbies 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /front-end/src/store/modules/notification.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | msg: '', 3 | type: '', 4 | status: '' 5 | } 6 | 7 | const mutations = { 8 | notify (state, { msg, type }) { 9 | state.msg = msg 10 | state.type = type 11 | }, 12 | remove (state) { 13 | state.msg = '' 14 | state.type = '' 15 | }, 16 | setStatus (state, status) { 17 | state.status = status 18 | } 19 | } 20 | 21 | const getters = { 22 | message: state => state.msg, 23 | alertClass: state => 'alert-' + state.type, 24 | status: state => state.status 25 | } 26 | 27 | export default { 28 | namespaced: true, 29 | state, 30 | mutations, 31 | getters 32 | } 33 | -------------------------------------------------------------------------------- /migrate-to-vue-cli-3.md: -------------------------------------------------------------------------------- 1 | 1. Install latest vue cli: 2 | > npm install -g @vue/cli` 3 | 4 | 2. Generate front-end progect. Select Babel, Router, Vuex, Linter and all default options, except bowser history: 5 | > vue create front-end 6 | 7 | 3. Install dependencies for `front-end`: 8 | > npm i -S axios bootstrap-css-only@^3.3.7 file-saver vuelidate 9 | 10 | 4. Replace `src` folder with `src` from old project. 11 | 12 | 5. Change import path for `file-saver`: 13 | > import { saveAs } from 'file-saver/dist/FileSaver' 14 | 15 | 6. Create `vue.config.js` file: 16 | ```js 17 | module.exports = { 18 | runtimeCompiler: true, 19 | outputDir: '../back-end/public' 20 | } 21 | ``` 22 | -------------------------------------------------------------------------------- /back-end/users/router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const jwt = require('express-jwt'); 4 | const auth = jwt({ secret: process.env.JWT_SECRET, algorithms: ['HS256'] }); 5 | const userController = require('./controller'); 6 | 7 | router.use('/register', userController.validateCaptchaResponse); 8 | router.use('/settings', auth); 9 | 10 | router.get('/', userController.getUsers); 11 | router.post('/register', userController.register); 12 | router.post('/login', userController.login); 13 | router.get('/settings', userController.getSettings); 14 | router.put('/settings', userController.updateSettings); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /front-end/src/components/ReCaptcha.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | -------------------------------------------------------------------------------- /back-end/items/book.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const bookSchema = new Schema({ 5 | userId: Schema.Types.ObjectId, 6 | title: String, 7 | author: String, 8 | completed: Date, 9 | mode: String 10 | }); 11 | 12 | bookSchema.statics.projectionFields = 'completed mode author title'; 13 | bookSchema.statics.sortFields = '-completed'; 14 | bookSchema.statics.searchFields = 'title author'; 15 | 16 | bookSchema.methods.setFromObject = function(object) { 17 | this.title = object.title; 18 | this.author = object.author; 19 | this.completed = object.completed; 20 | this.mode = object.mode; 21 | }; 22 | 23 | mongoose.model('Book', bookSchema); 24 | -------------------------------------------------------------------------------- /back-end/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hobbies", 3 | "version": "1.0.0", 4 | "description": "Books/Movies Management", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "engines": { 10 | "node": "v12.14.0" 11 | }, 12 | "author": "Iurii Kyrylenko", 13 | "license": "MIT", 14 | "devDependencies": {}, 15 | "dependencies": { 16 | "body-parser": "^1.19.0", 17 | "dotenv": "^2.0.0", 18 | "express": "^4.17.1", 19 | "express-jwt": "^6.0.0", 20 | "jsonwebtoken": "^8.5.1", 21 | "mongoose": "^5.10.7", 22 | "multer": "^1.4.2", 23 | "passport": "^0.3.2", 24 | "passport-local": "^1.0.0", 25 | "request": "^2.74.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /front-end/src/helpers/upload.js: -------------------------------------------------------------------------------- 1 | const uploadRequest = (url, file, headers) => { 2 | return new Promise((resolve, reject) => { 3 | const formData = new FormData() 4 | const xhr = new XMLHttpRequest() 5 | formData.append('upload', file, file.name) 6 | 7 | xhr.onreadystatechange = function () { 8 | if (xhr.readyState === 4) { 9 | if (xhr.status === 200) { 10 | resolve(xhr.response) 11 | } else { 12 | reject(xhr.response) 13 | } 14 | } 15 | } 16 | 17 | xhr.open('POST', url, true) 18 | Object.keys(headers).forEach(key => { 19 | xhr.setRequestHeader(key, headers[key]) 20 | }) 21 | xhr.send(formData) 22 | }) 23 | } 24 | 25 | export { uploadRequest } 26 | -------------------------------------------------------------------------------- /back-end/items/movie.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const movieSchema = new Schema({ 5 | userId: Schema.Types.ObjectId, 6 | title: String, 7 | year: String, 8 | notes: String, 9 | completed: Date, 10 | imdbId: String 11 | }); 12 | 13 | movieSchema.statics.projectionFields = 'completed year title notes imdbId'; 14 | movieSchema.statics.sortFields = '-completed'; 15 | movieSchema.statics.searchFields = 'title year notes'; 16 | 17 | movieSchema.methods.setFromObject = function(object) { 18 | this.title = object.title; 19 | this.year = object.year; 20 | this.completed = object.completed; 21 | this.notes = object.notes; 22 | this.imdbId = object.imdbId; 23 | }; 24 | 25 | mongoose.model('Movie', movieSchema); 26 | -------------------------------------------------------------------------------- /front-end/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front-end", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.18.1", 12 | "bootstrap-css-only": "^3.3.7", 13 | "file-saver": "^2.0.2", 14 | "vue": "^2.6.10", 15 | "vue-router": "^3.1.3", 16 | "vuelidate": "^0.7.4", 17 | "vuex": "^3.1.1" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "^3.11.0", 21 | "@vue/cli-plugin-eslint": "^3.11.0", 22 | "@vue/cli-service": "^4.3.1", 23 | "babel-eslint": "^10.0.3", 24 | "eslint": "^5.16.0", 25 | "eslint-plugin-vue": "^5.2.3", 26 | "vue-template-compiler": "^2.6.10" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /back-end/public/index.html: -------------------------------------------------------------------------------- 1 | My Hobbies
-------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## HOBBIES 2 | 3 | HOBBIES is a web application for keeping track of books you have read/listened and movies you have watched. 4 | 5 | ### [Demo](https://ik-hobbies.herokuapp.com) 6 | 7 | After logging in you are able to: 8 | 9 | * Add an item (book or movie) to the list. 10 | * Modify an existing item. 11 | * Remove an item. 12 | * Perform search for specific item(s). 13 | * Download items as a file in JSON format. 14 | * Upload items from JSON file. 15 | 16 | Also you have the possibilities to: 17 | 18 | * Share data with other people. 19 | * Change you personal settings. 20 | 21 | Unathorized people can see and download the shared data. 22 | 23 | ### Technologies 24 | Node.js, Express, MongoDB, JWT, Vue.js, webpack 25 | 26 | ### Detailed descriptions 27 | * [back-end](back-end/readme.md) 28 | * [front-end](front-end/README.md) 29 | 30 | ### License 31 | See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT). 32 | -------------------------------------------------------------------------------- /front-end/src/components/DateInput.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | 25 | 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Iurii Kyrylenko 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /back-end/config/passport.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const LocalStrategy = require('passport-local').Strategy; 3 | const mongoose = require('mongoose'); 4 | const User = mongoose.model('User'); 5 | 6 | const strategy = new LocalStrategy( 7 | { usernameField: 'name' }, 8 | (username, password, done) => { 9 | User.findOne( 10 | { name: username }, 11 | (err, user) => { 12 | if (err) { return done(err); } 13 | // Return if user not found in database 14 | if (!user) { 15 | return done(null, false, { 16 | message: 'User not found' 17 | }); 18 | } 19 | // Return if password is wrong 20 | if (!user.validPassword(password)) { 21 | return done(null, false, { 22 | message: 'Password is wrong' 23 | }); 24 | } 25 | // If credentials are correct, return the user object 26 | return done(null, user); 27 | } 28 | ); 29 | } 30 | ); 31 | 32 | passport.use(strategy); -------------------------------------------------------------------------------- /front-end/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from '@/components/Home' 4 | import Register from '@/components/Register' 5 | import Login from '@/components/Login' 6 | import UserSettings from '@/components/UserSettings' 7 | import peopleRoutes from './people-routes' 8 | import bookRoutes from './book-routes' 9 | import movieRoutes from './movie-routes' 10 | import Store from '@/store' 11 | 12 | Vue.use(Router) 13 | 14 | const router = new Router({ 15 | routes: [ 16 | { 17 | path: '/home', 18 | component: Home 19 | }, 20 | { 21 | path: '/register', 22 | component: Register 23 | }, 24 | { 25 | path: '/login', 26 | component: Login 27 | }, 28 | { ...peopleRoutes }, 29 | { 30 | path: '/profile', 31 | component: UserSettings 32 | }, 33 | { ...bookRoutes }, 34 | { ...movieRoutes }, 35 | { 36 | path: '*', 37 | redirect: '/home' 38 | } 39 | ] 40 | }) 41 | 42 | // Navigation guards 43 | // 44 | router.beforeEach((to, from, next) => { 45 | const isLoggedIn = Store.getters['auth/isLoggedIn'] 46 | switch (to.path) { 47 | case '/register': 48 | case '/login': 49 | next(!isLoggedIn) 50 | break 51 | case '/books': 52 | case '/movies': 53 | case '/profile': 54 | next(isLoggedIn) 55 | break 56 | default: 57 | next(true) 58 | } 59 | }) 60 | 61 | export default router 62 | -------------------------------------------------------------------------------- /front-end/src/store/modules/auth.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { init } from '../plugins/localStorage' 3 | import config from '@/helpers/config' 4 | 5 | const state = { 6 | token: init 7 | } 8 | 9 | const mutations = { 10 | setToken (state, token) { 11 | state.token = token 12 | }, 13 | resetToken (state) { 14 | state.token = '' 15 | } 16 | } 17 | 18 | const actions = { 19 | async login ({ commit }, user) { 20 | const endpoint = config.apiUrl + '/users/login' 21 | const { data } = await axios.post(endpoint, user) 22 | commit('setToken', data.token) 23 | }, 24 | async register ({ commit }, user) { 25 | const endpoint = config.apiUrl + '/users/register' 26 | const { data } = await axios.post(endpoint, user) 27 | commit('setToken', data.token) 28 | }, 29 | logout ({ commit }) { 30 | commit('resetToken') 31 | } 32 | } 33 | 34 | const getPayload = ({ token }) => { 35 | const payload = token.split('.')[1] 36 | return JSON.parse(atob(payload)) 37 | } 38 | 39 | const getters = { 40 | isLoggedIn (state) { 41 | if (!state.token) return false 42 | let payload = getPayload(state) 43 | return payload.exp > (Date.now() / 1000) 44 | }, 45 | currentUser (state) { 46 | if (!state.token) return {} 47 | const { name, email } = getPayload(state) 48 | return { name, email } 49 | } 50 | } 51 | 52 | export default { 53 | namespaced: true, 54 | state, 55 | mutations, 56 | actions, 57 | getters 58 | } 59 | -------------------------------------------------------------------------------- /back-end/public/css/app.6076211c.css: -------------------------------------------------------------------------------- 1 | .dropdown-menu[data-v-3be1bf74]{display:block!important}.drop-hide[data-v-3be1bf74]{display:none}.dropdown-menu .active a[data-v-3be1bf74],.dropdown-menu .active a[data-v-3be1bf74]:hover{background-color:#a7a7a7}a.active[data-v-262ff12d]{color:#000!important;text-shadow:2px 2px #ccc}.status[data-v-262ff12d]{color:#555}#app{margin:20px}.pagination[data-v-a9b50c18]{margin-top:0}.pagination>li>a[data-v-a9b50c18],.pagination>li>a[data-v-a9b50c18]:hover{padding:8px 0 6px;color:#000;width:32px;height:40px;text-align:center}.active>a[data-v-a9b50c18]{background-color:#aaa!important;color:#fff!important;border-color:#aaa!important}td>a[data-v-048db6a0]{color:#000;text-decoration:none;opacity:.6}td>a[data-v-048db6a0]:hover{opacity:1}.modal-mask{position:fixed;z-index:9998;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.5);-webkit-transition:opacity .3s ease;transition:opacity .3s ease}.modal-enter,.modal-leave-active{opacity:0}.table td{vertical-align:middle!important}a>i,label>i{opacity:.5}a:hover>i,label:hover>i{opacity:1}input[type=file]{width:.1px;height:.1px;opacity:0;overflow:hidden;position:absolute;z-index:-1}.my-search{margin-bottom:18px}.table>thead>tr>th{border-bottom:0;font-weight:400;font-style:italic;opacity:.6}.movie-info[data-v-75dd2f41]{overflow-y:auto;max-height:360px}.movie-info img[data-v-75dd2f41]{width:100%}.label[data-v-041fa408]{padding-top:.3em!important;border-radius:.4em!important}.label-my-success[data-v-041fa408]{background-color:green}.label-my-error[data-v-041fa408]{background-color:#a94442} -------------------------------------------------------------------------------- /back-end/users/model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const crypto = require('crypto'); 3 | const jwt = require('jsonwebtoken'); 4 | 5 | const userSchema = new mongoose.Schema({ 6 | email: { 7 | type: String, 8 | unique: true, 9 | required: true 10 | }, 11 | name: { 12 | type: String, 13 | unique: true, 14 | required: true 15 | }, 16 | hash: String, 17 | salt: String, 18 | shareBooks: Boolean, 19 | shareMovies: Boolean 20 | }); 21 | 22 | userSchema.methods.setPassword = function (password) { 23 | this.salt = crypto.randomBytes(16).toString('hex'); 24 | this.hash = crypto.pbkdf2Sync( 25 | password, this.salt, 1000, 64, 'sha1' 26 | ).toString('hex'); 27 | }; 28 | 29 | userSchema.methods.validPassword = function (password) { 30 | const hash = crypto.pbkdf2Sync( 31 | password, this.salt, 1000, 64, 'sha1' 32 | ).toString('hex'); 33 | return this.hash === hash; 34 | }; 35 | 36 | userSchema.methods.generateJwt = function () { 37 | var expiry = new Date(); 38 | expiry.setDate(expiry.getDate() + 7); 39 | 40 | return jwt.sign({ 41 | _id: this._id, 42 | email: this.email, 43 | name: this.name, 44 | exp: parseInt(expiry.getTime() / 1000) 45 | }, process.env.JWT_SECRET); 46 | }; 47 | 48 | userSchema.methods.setFromObject = function (object) { 49 | this.shareBooks = object.shareBooks; 50 | this.shareMovies = object.shareMovies; 51 | }; 52 | 53 | mongoose.model('User', userSchema); 54 | -------------------------------------------------------------------------------- /back-end/items/router.js: -------------------------------------------------------------------------------- 1 | const ItemsControllerFactory = require('../items/controller'); 2 | const usersController = require('../users/controller'); 3 | const jwt = require('express-jwt'); 4 | const multer = require('multer'); 5 | const auth = jwt({ secret: process.env.JWT_SECRET, algorithms: ['HS256'] }); 6 | const express = require('express'); 7 | 8 | const personal = (modelName) => { 9 | // Router should be put inside the exported function. 10 | // Otherwise it is cached and reused when calling 11 | // require('./items/router') again. 12 | // 13 | const router = express.Router(); 14 | // validate JWT 15 | router.use(auth); 16 | // check if the user exists in db 17 | router.use(usersController.checkUser); 18 | 19 | const itemsController = ItemsControllerFactory(modelName); 20 | 21 | router.get('/', itemsController.getItems); 22 | router.get('/:id', itemsController.getItem); 23 | router.post('/', itemsController.addItem); 24 | router.put('/:id', itemsController.changeItem); 25 | router.delete('/:id', itemsController.deleteItem); 26 | router.post( 27 | '/upload', 28 | multer({ inMemory: true }).single('upload'), 29 | itemsController.uploadItems 30 | ); 31 | 32 | return router; 33 | }; 34 | 35 | const shared = (modelName) => { 36 | const router = express.Router(); 37 | router.use(usersController.checkSharedData); 38 | const itemsController = ItemsControllerFactory(modelName); 39 | router.get('/', itemsController.getItems); 40 | return router; 41 | }; 42 | 43 | module.exports = { personal, shared }; 44 | -------------------------------------------------------------------------------- /front-end/src/components/Modal.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 51 | 52 | 75 | -------------------------------------------------------------------------------- /back-end/helpers/paging.js: -------------------------------------------------------------------------------- 1 | const getPageItems = (filter, Model, req, res) => { 2 | const itemsPerPage = 5; 3 | 4 | const queryBuilder = (req) => { 5 | let query = Model 6 | .find(filter(req)) 7 | .select(Model.projectionFields) 8 | .sort(Model.sortFields); 9 | 10 | const term = req.query.term; 11 | 12 | if(term) { 13 | const re = new RegExp(term, 'i'); 14 | const searchFields = Model.searchFields.split(' '); 15 | const searchExpressions = searchFields.map(field => { 16 | var exp = {}; 17 | exp[field] = { $regex: re }; 18 | return exp; 19 | }); 20 | query = query.or(searchExpressions); 21 | } 22 | 23 | return query; 24 | }; 25 | 26 | const getPagesCount = (itemsCount) => { 27 | if(!itemsCount) return 0; 28 | const int = Math.floor(itemsCount / itemsPerPage); 29 | const mod = itemsCount % itemsPerPage; 30 | return mod ? int + 1 : int; 31 | }; 32 | 33 | const callbackBuilder = (res, pages) => { 34 | return (err, items) => { 35 | if(err) { 36 | res.sendStatus(400); 37 | return; 38 | } 39 | res.send({ items, pages }); 40 | }; 41 | }; 42 | 43 | const page = req.query.page; 44 | 45 | if(page) { 46 | queryBuilder(req).count().then(count => { 47 | const itemsToSkip = itemsPerPage * (page - 1); 48 | queryBuilder(req) 49 | .skip(itemsToSkip) 50 | .limit(itemsPerPage) 51 | .exec(callbackBuilder(res, getPagesCount(count))); 52 | }); 53 | } else { 54 | // get all items in one page 55 | queryBuilder(req) 56 | .exec(callbackBuilder(res, 1)); 57 | } 58 | }; 59 | 60 | module.exports = { getPageItems }; 61 | -------------------------------------------------------------------------------- /front-end/src/components/DropdownMenu.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 50 | 51 | 62 | -------------------------------------------------------------------------------- /front-end/src/components/UserSettings.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 59 | -------------------------------------------------------------------------------- /back-end/server.js: -------------------------------------------------------------------------------- 1 | // Uncomment for local environment or start the server using "heroku local" 2 | // require('dotenv').config() 3 | 4 | require('./config/db') 5 | require('./config/passport') 6 | 7 | const allowCORS = require('./config/cors') 8 | const passport = require('passport') 9 | const path = require('path') 10 | const express = require('express') 11 | const bodyParser = require('body-parser') 12 | const usersRouter = require('./users/router') 13 | const itemsRouter = require('./items/router') 14 | const movieInfoController = require('./info/movieController') 15 | 16 | const server = express() 17 | 18 | server.use(express.static(path.join(__dirname, 'public'))) 19 | server.use(bodyParser.json()) 20 | server.use(bodyParser.urlencoded({ extended: true })) 21 | 22 | if(process.env.ALLOW_CORS === 'yes') { 23 | allowCORS(server) 24 | } 25 | 26 | server.use(passport.initialize()) 27 | 28 | // An artificial delay for debug purpose 29 | // 30 | // server.use((req, res, next) => { 31 | // setTimeout(() => next(), 1000) 32 | // }) 33 | 34 | server.use('/api/users', usersRouter) 35 | server.use('/api/books', itemsRouter.personal('Book')) 36 | server.use('/api/movies', itemsRouter.personal('Movie')) 37 | server.use('/api/shared/books', itemsRouter.shared('Book')) 38 | server.use('/api/shared/movies', itemsRouter.shared('Movie')) 39 | // Get info by movie id (qs=imdbId) 40 | server.get('/api/get-movie', movieInfoController.get) 41 | // Get info by movie title (qs=title) 42 | server.get('/api/search-movie', movieInfoController.search) 43 | 44 | // Get extra content 45 | // 46 | // const fs = require("fs") 47 | // server.route(`/${process.env.EXTRA_CONTENT}/:file`) 48 | // .get((req, res, next) => { 49 | // const file = path.join(__dirname, `extra/${req.params.file}`) 50 | // fs.exists(file, exists => { 51 | // if (exists) res.sendFile(file) 52 | // else next() 53 | // }) 54 | // }) 55 | 56 | server.route('/*').get(function(req, res) { 57 | return res.sendFile(path.join(__dirname, 'public/index.html')) 58 | }) 59 | 60 | server.listen(process.env.PORT || 3000) 61 | -------------------------------------------------------------------------------- /back-end/users/getUsers.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const User = mongoose.model('User'); 3 | 4 | const itemsPerPage = 5; 5 | 6 | const getPagesCount = itemsCount => { 7 | if (!itemsCount) return 0; 8 | const int = Math.floor(itemsCount / itemsPerPage); 9 | const mod = itemsCount % itemsPerPage; 10 | return mod ? int + 1 : int; 11 | }; 12 | 13 | const filterConditions = term => { 14 | const expr = { $or: [{ shareBooks: true }, { shareMovies: true }] }; 15 | return term ? { $and: [expr, { name: new RegExp(term, 'i') }] } : expr; 16 | }; 17 | 18 | const getAggregator = match => User.aggregate() 19 | .match(match) 20 | .lookup({ from: "books", localField: "_id", foreignField: "userId", as: "books" }) 21 | .lookup({ from: "movies", localField: "_id", foreignField: "userId", as: "movies" }) 22 | .project({ 23 | name: 1, 24 | shareBooks: 1, 25 | shareMovies: 1, 26 | books: { $size: "$books" }, 27 | movies: { $size: "$movies" }, 28 | total: { $add: [{ $size: '$books' }, { $size: '$movies' }] } 29 | }) 30 | .match({ total: { $gt: 0 } }); 31 | 32 | const fetchUsers = async (term, page) => { 33 | const match = filterConditions(term); 34 | 35 | const aggregator = getAggregator(match) 36 | .sort({ total: -1, name: 1 }); 37 | 38 | // This doesn't work in mongoose: 39 | // getAggregator(match).count("count"); 40 | const aggregatorCount = getAggregator(match) 41 | .append({ $count: "count" }); 42 | 43 | const [{ count }] = await aggregatorCount; 44 | 45 | const pages = getPagesCount(count); 46 | 47 | if (page > 0) { 48 | const itemsToSkip = itemsPerPage * (page - 1); 49 | aggregator.skip(itemsToSkip).limit(itemsPerPage); 50 | } 51 | 52 | const items = await aggregator; 53 | 54 | return { items, pages }; 55 | }; 56 | 57 | const getUsers = async ({ query: { term, page } }, res) => { 58 | try { 59 | const result = await fetchUsers(term, page); 60 | res.send(result); 61 | } 62 | catch (err) { 63 | res.status(400).json(err); 64 | } 65 | }; 66 | 67 | module.exports = { getUsers }; 68 | -------------------------------------------------------------------------------- /front-end/src/store/modules/info.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import config from '@/helpers/config' 3 | 4 | const state = { 5 | movieInfo: { 6 | title: '', 7 | posterUrl: '', 8 | plot: '' 9 | }, 10 | movieProgress: false, 11 | movieError: '' 12 | } 13 | 14 | const mutations = { 15 | setMovieInfo (state, response) { 16 | if (response.found) { 17 | state.movieInfo = { 18 | title: response.title, 19 | posterUrl: response.poster, 20 | plot: response.plot 21 | } 22 | state.movieError = '' 23 | } else { 24 | state.movieError = 'Movie not found (:' 25 | } 26 | }, 27 | setMovieProgress (state, progress) { 28 | state.movieProgress = progress 29 | }, 30 | setMovieError (state, error) { 31 | state.movieError = error 32 | } 33 | } 34 | 35 | const getters = { 36 | movieInfo: state => state.movieInfo, 37 | movieProgress: state => state.movieProgress, 38 | movieError: state => state.movieError 39 | } 40 | 41 | // const mockedResponse = (title) => { 42 | // const promise = new Promise(resolve => { 43 | // setTimeout(() => { 44 | // resolve({ 45 | // found: true, 46 | // title: title, 47 | // poster: 'http://aaa/bbb.jpg', 48 | // plot: 'blah blah blah' 49 | // }) 50 | // }, 1000) 51 | // }) 52 | // return promise 53 | // } 54 | 55 | const actions = { 56 | async getMovieInfo ({ commit }, movie) { 57 | commit('setMovieProgress', true) 58 | 59 | // const data = await mockedResponse(movie.title) 60 | const endpoint = config.apiUrl + (movie.imdbId ? '/get-movie' : '/search-movie') 61 | const params = movie.imdbId ? { imdbId: movie.imdbId } : { title: movie.title } 62 | try { 63 | const { data } = await axios.get(endpoint, { params }) 64 | commit('setMovieInfo', data) 65 | commit('setMovieProgress', false) 66 | } 67 | catch(e) { 68 | commit('setMovieProgress', false) 69 | commit('setMovieError', e.message) 70 | } 71 | } 72 | } 73 | 74 | export default { 75 | namespaced: true, 76 | state, 77 | mutations, 78 | getters, 79 | actions 80 | } 81 | -------------------------------------------------------------------------------- /front-end/src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 58 | -------------------------------------------------------------------------------- /front-end/src/components/Pager.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 68 | 69 | 86 | -------------------------------------------------------------------------------- /front-end/src/helpers/validators.js: -------------------------------------------------------------------------------- 1 | import { required, email as emailRule, sameAs } from 'vuelidate/lib/validators' 2 | 3 | export const userName = [ 4 | { 5 | rule: required, 6 | msg: 'Name is required' 7 | }, 8 | { 9 | rule (value) { 10 | const regexp = /^[A-Za-z_][A-Za-z0-9_-]{4,}$/ 11 | return regexp.test(value) 12 | }, 13 | msg: 'Name requires at least 5 letters or digits and begins with letter' 14 | } 15 | ] 16 | 17 | export const email = [ 18 | { 19 | rule: required, 20 | msg: 'Email address is required' 21 | }, 22 | { 23 | rule: emailRule, 24 | msg: 'Invalid email address' 25 | } 26 | ] 27 | 28 | export const password = [ 29 | { 30 | rule: required, 31 | msg: 'Password is required' 32 | }, 33 | { 34 | rule (value) { 35 | const regexp = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])\S{8,}$/ 36 | return regexp.test(value) 37 | }, 38 | msg: 'Password requires at least 8 characters without spaces, one number, one lowercase and one uppercase letter' 39 | } 40 | ] 41 | 42 | export const confirmation = [ 43 | { 44 | rule: required, 45 | msg: 'Confirmation is required' 46 | }, 47 | { 48 | rule: sameAs('password'), 49 | msg: 'Password mismatch' 50 | } 51 | ] 52 | 53 | export const bookTitle = [ 54 | { 55 | rule: required, 56 | msg: 'Book title is required' 57 | } 58 | ] 59 | 60 | export const author = [ 61 | { 62 | rule: required, 63 | msg: 'Author is required' 64 | } 65 | ] 66 | 67 | export const mode = [ 68 | { 69 | rule: required, 70 | msg: 'Book type is required' 71 | } 72 | ] 73 | 74 | export const movieTitle = [ 75 | { 76 | rule: required, 77 | msg: 'Movie title is required' 78 | } 79 | ] 80 | 81 | export const year = [ 82 | { 83 | rule: required, 84 | msg: 'Year is required' 85 | } 86 | ] 87 | 88 | export const completed = [ 89 | { 90 | rule: required 91 | } 92 | ] 93 | 94 | const vrules = (vdata) => { 95 | return vdata.reduce((acc, cur, i) => { 96 | acc[i] = cur.rule 97 | return acc 98 | }, {}) 99 | } 100 | 101 | const vmsg = (validator, vdata) => { 102 | for (let i = 0; i < vdata.length; i++) { 103 | if (!validator[i]) { return vdata[i].msg } 104 | } 105 | } 106 | 107 | export default { 108 | vrules, 109 | vmsg 110 | } 111 | -------------------------------------------------------------------------------- /back-end/info/movieController.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | 3 | const getPosterUrl = (path) => { 4 | // Supported formats: 5 | // w92, w154, w185, w342, w500, w780, original 6 | return process.env.TMDB_IMAGE_STORE + 'w185' + path 7 | } 8 | 9 | const getResultFromTmdb = (entries) => { 10 | if (!entries.length) { 11 | return { found: false, title: null, plot: null, poster: null } 12 | } 13 | const entry = entries[0] 14 | return { found: true, title: entry.title, plot: entry.overview, poster: getPosterUrl(entry.poster_path) } 15 | } 16 | 17 | const getResultFromTmdbFind = (tmdb) => { 18 | return getResultFromTmdb([...tmdb.movie_results, ...tmdb.tv_results]) 19 | } 20 | 21 | const getResultFromTmdbSearch = (tmdb) => { 22 | return getResultFromTmdb(tmdb.results) 23 | } 24 | 25 | // GET/find/{req.query.imdbId} 26 | // query string: api_key=... & external_source=imdb_id 27 | const get = (req, res) => { 28 | if (!req.query.imdbId) { 29 | res.sendStatus(400) 30 | return 31 | } 32 | 33 | const options = { 34 | method: 'get', 35 | url: process.env.TMDB_API + 'find/' + req.query.imdbId, 36 | qs: { 37 | api_key: process.env.TMDB_API_KEY, 38 | external_source: 'imdb_id' 39 | } 40 | } 41 | 42 | request(options, (err, _, body) => { 43 | if(err) { 44 | res.sendStatus(400) 45 | return 46 | } 47 | try { 48 | const tmdbResult = JSON.parse(body) 49 | const result = getResultFromTmdbFind(tmdbResult) 50 | res.send(result) 51 | } 52 | catch(e) { 53 | res.sendStatus(502) 54 | return 55 | } 56 | }) 57 | } 58 | 59 | // GET /search/movie 60 | // query string: api_key=... & query={req.query.title) 61 | const search = (req, res) => { 62 | if (!req.query.title) { 63 | res.sendStatus(400) 64 | return 65 | } 66 | 67 | const options = { 68 | method: 'get', 69 | url: process.env.TMDB_API + 'search/movie', 70 | qs: { 71 | api_key: process.env.TMDB_API_KEY, 72 | query: req.query.title 73 | } 74 | } 75 | 76 | request(options, (err, _, body) => { 77 | if(err) { 78 | res.sendStatus(400) 79 | return 80 | } 81 | try { 82 | const tmdbResult = JSON.parse(body) 83 | const result = getResultFromTmdbSearch(tmdbResult) 84 | res.send(result) 85 | } 86 | catch(e) { 87 | res.sendStatus(502) 88 | return 89 | } 90 | }) 91 | } 92 | 93 | module.exports = { 94 | get, 95 | search 96 | } 97 | -------------------------------------------------------------------------------- /front-end/src/components/Login.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 73 | -------------------------------------------------------------------------------- /front-end/src/components/books/BookList.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 73 | -------------------------------------------------------------------------------- /back-end/readme.md: -------------------------------------------------------------------------------- 1 | ## REST API, mongoDB, hosting SPA 2 | 3 | #### Endpoints 4 | 5 | | Path | Method | Auth | Notes 6 | |:------------------------------|:-----------------|:-----:|:------ 7 | | /users/login | post | | 8 | | /users/register | post | | 9 | | /users | get | | 10 | | /shared/books, /shared/movies | get | | user id in the query 11 | | /books, /movies | get, post | + | 12 | | /books/id, /movies/id | get, put, delete | + | 13 | | /books/upload, /movies/upload | post | + | 14 | | /users/settings | get, put | + | 15 | 16 | 17 | ##### build/run: 18 | ``` 19 | install: npm i 20 | start server: heroku local 21 | ``` 22 | 23 | #### JSON file to upload books: 24 | ``` 25 | [ 26 | { "title": "t-0001", "author": "a-0001", "completed": "2016-01-01", "mode": "r" }, 27 | { "title": "t-0002", "author": "a-0002", "completed": "2016-01-02", "mode": "a" }, 28 | { "title": "t-0003", "author": "a-0003", "completed": "2016-01-03", "mode": "r-a" } 29 | ] 30 | ``` 31 | 32 | #### JSON file to upload movies: 33 | ``` 34 | [ 35 | { "title": "t-0001", "year": "2001", "completed": "2016-01-01", "notes": "note-001" }, 36 | { "title": "t-0002", "year": "2002", "completed": "2016-01-02", "notes": "note-002" }, 37 | { "title": "t-0003", "year": "2003", "completed": "2016-01-03", "notes": "note-003" } 38 | ] 39 | ``` 40 | 41 | #### local configuration (.env file): 42 | ``` 43 | CONNECTION_STRING = 44 | JWT_SECRET = 45 | CAPTCHA_SECRET = 46 | CAPTCHA_API = https://www.google.com/recaptcha/api/siteverify 47 | TMDB_API = https://api.themoviedb.org/3/ 48 | TMDB_API_KEY = 49 | TMDB_IMAGE_STORE = https://image.tmdb.org/t/p/ 50 | ALLOW_CORS = < yes when the separate server is used for hosting front-end application> 51 | PORT = 3000 52 | ``` 53 | 54 | #### remote configuration (process.env variables): 55 | ``` 56 | CONNECTION_STRING = 57 | JWT_SECRET = 58 | CAPTCHA_SECRET = 59 | CAPTCHA_API = https://www.google.com/recaptcha/api/siteverify 60 | TMDB_API = https://api.themoviedb.org/3/ 61 | TMDB_API_KEY = 62 | TMDB_IMAGE_STORE = https://image.tmdb.org/t/p/ 63 | ALLOW_CORS = no 64 | ``` 65 | 66 | #### deploy back-end to heroku (after building front-end): 67 | ``` 68 | cd ../ 69 | git subtree push --prefix back-end heroku master 70 | ``` 71 | -------------------------------------------------------------------------------- /front-end/src/store/modules/users.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import config from '@/helpers/config' 3 | 4 | const state = { 5 | settings: {}, 6 | users: [], 7 | page: 1, 8 | pageCount: 0, 9 | filter: '' 10 | } 11 | 12 | const getters = { 13 | settings: state => state.settings, 14 | users: state => state.users 15 | .map(({ name, shareBooks, shareMovies, books, movies, total, _id }) => ({ 16 | _id, 17 | name, 18 | shareBooks, 19 | shareMovies, 20 | books, 21 | movies, 22 | total, 23 | tob: _id + '/' + name + '/b', 24 | tom: _id + '/' + name + '/m' 25 | })), 26 | page: state => state.page, 27 | pageCount: state => state.pageCount, 28 | filter: state => state.filter 29 | } 30 | 31 | const mutations = { 32 | updateSettings (state, settings) { 33 | state.settings = settings 34 | }, 35 | setUsers (state, { items, pages }) { 36 | state.users = items 37 | state.pageCount = pages 38 | }, 39 | setPage (state, page) { 40 | state.page = page 41 | }, 42 | setFilter (state, filter) { 43 | state.filter = filter 44 | } 45 | } 46 | 47 | const actions = { 48 | 49 | async getSettings ({ rootState, commit }) { 50 | const endpoint = config.apiUrl + '/users/settings' 51 | const headers = { Authorization: 'Bearer ' + rootState.auth.token } 52 | const { data } = await axios.get(endpoint, { headers }) 53 | commit('updateSettings', data) 54 | }, 55 | 56 | async saveSettings ({ rootState }, settings) { 57 | const endpoint = config.apiUrl + '/users/settings' 58 | const headers = { Authorization: 'Bearer ' + rootState.auth.token } 59 | return await axios.put(endpoint, settings, { headers }) 60 | }, 61 | 62 | async getUsers ({ state, commit }) { 63 | const data = await httpGetUsers({ 64 | page: state.page, 65 | term: state.filter 66 | }) 67 | commit('setUsers', data) 68 | }, 69 | 70 | async changePage ({ state, commit }, page) { 71 | const data = await httpGetUsers({ 72 | page, 73 | term: state.filter 74 | }) 75 | commit('setPage', page) 76 | commit('setUsers', data) 77 | }, 78 | 79 | async applyFilter ({ commit }, filter) { 80 | const data = await httpGetUsers({ 81 | page: 1, 82 | term: filter 83 | }) 84 | commit('setPage', 1) 85 | commit('setFilter', filter) 86 | commit('setUsers', data) 87 | } 88 | } 89 | 90 | const httpGetUsers = async (params) => { 91 | const endpoint = config.apiUrl + '/users' 92 | const { data } = await axios.get(endpoint, { params }) 93 | return data 94 | } 95 | 96 | export default { 97 | namespaced: true, 98 | state, 99 | getters, 100 | mutations, 101 | actions 102 | } 103 | -------------------------------------------------------------------------------- /front-end/src/components/People.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 85 | 86 | 96 | -------------------------------------------------------------------------------- /back-end/items/controller.js: -------------------------------------------------------------------------------- 1 | const paging = require('../helpers/paging'); 2 | 3 | const getItems = (Model, req, res) => 4 | paging.getPageItems(req => ({ userId: req.user._id }), Model, req, res); 5 | 6 | const getItem = (Model, req, res) => { 7 | Model.findById(req.params.id, (err, item) => { 8 | if(err) { 9 | res.sendStatus(400); 10 | return; 11 | } 12 | if(!item || !item.userId.equals(req.user._id)) { 13 | res.sendStatus(404); 14 | return; 15 | } 16 | res.send(item); 17 | }); 18 | }; 19 | 20 | const addItem = (Model, req, res) => { 21 | const item = new Model(req.body); 22 | item.userId = req.user._id; 23 | item.save((err) => { 24 | if(err) { 25 | res.sendStatus(400); 26 | return; 27 | } 28 | res.sendStatus(201); 29 | }); 30 | }; 31 | 32 | const changeItem = (Model, req, res) => { 33 | Model.findById(req.params.id, (err, item) => { 34 | if(err) { 35 | res.sendStatus(400); 36 | return; 37 | } 38 | if(!item || !item.userId.equals(req.user._id)) { 39 | res.sendStatus(404); 40 | return; 41 | } 42 | item.setFromObject(req.body); 43 | item.save((err) => { 44 | if(err) { 45 | res.sendStatus(400); 46 | return; 47 | } 48 | res.sendStatus(204); 49 | }); 50 | }); 51 | }; 52 | 53 | const deleteItem = (Model, req, res) => { 54 | Model.findById(req.params.id, (err, item) => { 55 | if(err) { 56 | res.sendStatus(400); 57 | return; 58 | } 59 | if(!item || !item.userId.equals(req.user._id)) { 60 | res.sendStatus(404); 61 | return; 62 | } 63 | item.remove((err) => { 64 | if(err) { 65 | res.sendStatus(400); 66 | return; 67 | } 68 | res.sendStatus(204); 69 | }); 70 | }); 71 | }; 72 | 73 | const uploadItems = (Model, req, res) => { 74 | const data = JSON.parse(req.file.buffer); 75 | const tasks = []; 76 | for(var i = 0; i < data.length; i++) { 77 | var item = new Model(data[i]); 78 | item.userId = req.user._id; 79 | tasks.push(item.save()); 80 | } 81 | Promise.all(tasks) 82 | .then(() => res.sendStatus(200)) 83 | .catch(() => res.sendStatus(400)); 84 | }; 85 | 86 | const mongoose = require('mongoose'); 87 | 88 | module.exports = (modelName) => { 89 | const Model = mongoose.model(modelName); 90 | return { 91 | getItems: (req, res) => getItems(Model, req, res), 92 | getItem: (req, res) => getItem(Model, req, res), 93 | addItem: (req, res) => addItem(Model, req, res), 94 | changeItem: (req, res) => changeItem(Model, req, res), 95 | deleteItem: (req, res) => deleteItem(Model, req, res), 96 | uploadItems: (req, res) => uploadItems(Model, req, res) 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /front-end/src/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 105 | 106 | 115 | -------------------------------------------------------------------------------- /back-end/users/controller.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const mongoose = require('mongoose'); 3 | const User = mongoose.model('User'); 4 | const request = require('request'); 5 | const { getUsers } = require('./getUsers'); 6 | 7 | const getSettings = (req, res) => { 8 | User.findById(req.user._id, User.projectionFields, (err, user) => { 9 | if (err) { 10 | res.sendStatus(400); 11 | return; 12 | } 13 | if (!user) { 14 | res.sendStatus(404); 15 | return; 16 | } 17 | res.send(user); 18 | }); 19 | }; 20 | 21 | const updateSettings = (req, res) => { 22 | User.findById(req.user._id, (err, user) => { 23 | if (err) { 24 | res.sendStatus(400); 25 | return; 26 | } 27 | if (!user) { 28 | res.sendStatus(404); 29 | return; 30 | } 31 | user.setFromObject(req.body); 32 | user.save((err) => { 33 | if (err) { 34 | res.sendStatus(400); 35 | return; 36 | } 37 | res.sendStatus(204); 38 | }); 39 | }); 40 | }; 41 | 42 | 43 | const validateCaptchaResponse = (req, res, next) => { 44 | const postData = { 45 | response: req.body.captchaResponse, 46 | secret: process.env.CAPTCHA_SECRET 47 | }; 48 | 49 | const options = { 50 | method: 'post', 51 | form: postData, 52 | url: process.env.CAPTCHA_API 53 | }; 54 | 55 | request(options, (err, _, body) => { 56 | if(err) { 57 | res.sendStatus(400); 58 | return; 59 | } 60 | const success = JSON.parse(body).success; 61 | if(!success) { 62 | console.log('validateCaptcha:', body); 63 | res.sendStatus(403); 64 | return; 65 | } 66 | next(); 67 | }); 68 | }; 69 | 70 | const register = (req, res) => { 71 | const user = new User(); 72 | 73 | user.name = req.body.name; 74 | user.email = req.body.email; 75 | user.setPassword(req.body.password); 76 | 77 | user.save((err) => { 78 | // Validations error 79 | if (err) { 80 | res.status(400).json(err); 81 | return; 82 | } 83 | 84 | const token = user.generateJwt(); 85 | res.status(200).json({ token }); 86 | }); 87 | }; 88 | 89 | const login = (req, res) => { 90 | const authFn = passport.authenticate('local', (err, user, info) => { 91 | // If Passport throws/catches an error 92 | if (err) { 93 | res.status(404).json(err); 94 | return; 95 | } 96 | 97 | // If a user is found 98 | if(user) { 99 | const token = user.generateJwt(); 100 | res.status(200).json({ token }); 101 | } else { 102 | // If user is not found 103 | res.status(401).json(info); 104 | } 105 | }); 106 | authFn(req, res); 107 | }; 108 | 109 | // Use this middleware after authorization. It addresses following use cases: 110 | // User is deleted after login. 111 | // Or valid JWT for deleted user is used for authorization. 112 | const checkUser = (req, res, next) => { 113 | User.findById(req.user._id, (err, user) => { 114 | if(err) { 115 | res.sendStatus(400); 116 | return; 117 | } 118 | if(!user) { 119 | res.sendStatus(401); 120 | return; 121 | } 122 | next(); 123 | }); 124 | }; 125 | 126 | // Use this middleware in the public routes. 127 | // It gets user's id from request query and places it in request's body 128 | const checkSharedData = (req, res, next) => { 129 | // To do: check user settings against the resourse name 130 | // and decline request if resourse is not shared 131 | if (req.query.user) { 132 | req.user = { _id: req.query.user } 133 | } 134 | next(); 135 | }; 136 | 137 | module.exports = { 138 | getUsers, 139 | getSettings, 140 | updateSettings, 141 | validateCaptchaResponse, 142 | register, 143 | login, 144 | checkUser, 145 | checkSharedData 146 | }; 147 | -------------------------------------------------------------------------------- /front-end/src/components/movies/MovieList.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 108 | 109 | 118 | -------------------------------------------------------------------------------- /front-end/src/components/Register.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 120 | -------------------------------------------------------------------------------- /front-end/src/components/ItemList.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 124 | 125 | 168 | -------------------------------------------------------------------------------- /front-end/src/components/movies/Movie.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 136 | -------------------------------------------------------------------------------- /front-end/src/components/books/Book.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 135 | -------------------------------------------------------------------------------- /front-end/src/store/modules/items.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import config from '@/helpers/config' 3 | import { saveAs } from 'file-saver/dist/FileSaver' 4 | import { uploadRequest } from '@/helpers/upload' 5 | 6 | const state = { 7 | hobby: 'books', 8 | selector: 'shared', 9 | uid: '', 10 | books: { 11 | items: [], 12 | page: 1, 13 | pageCount: 0, 14 | filter: '' 15 | }, 16 | movies: { 17 | items: [], 18 | page: 1, 19 | pageCount: 0, 20 | filter: '' 21 | }, 22 | shared: { 23 | items: [], 24 | page: 1, 25 | pageCount: 0, 26 | filter: '' 27 | } 28 | } 29 | 30 | const getters = { 31 | my: state => !state.uid, 32 | items: state => state[state.selector].items, 33 | page: state => state[state.selector].page, 34 | pageCount: state => state[state.selector].pageCount, 35 | filter: state => state[state.selector].filter, 36 | item: state => id => { 37 | const res = state[state.selector].items.filter(({ _id }) => _id === id) 38 | return res.length ? res[0] : null 39 | } 40 | } 41 | 42 | const mutations = { 43 | clear (state, selector) { 44 | const ss = state[selector] 45 | ss.items = [] 46 | ss.page = 1 47 | ss.pageCount = 0 48 | ss.filter = '' 49 | }, 50 | select (state, { hobby, uid = '' }) { 51 | state.hobby = hobby 52 | state.uid = uid 53 | state.selector = uid ? 'shared' : hobby 54 | }, 55 | setItems (state, { items, pages }) { 56 | // deserialize date 57 | state[state.selector].items = items.map(item => { 58 | item.completed = new Date(item.completed) 59 | return item 60 | }) 61 | state[state.selector].pageCount = pages 62 | }, 63 | setPage (state, page) { 64 | state[state.selector].page = page 65 | }, 66 | setFilter (state, filter) { 67 | state[state.selector].filter = filter 68 | } 69 | } 70 | 71 | const httpGetItems = async (rootState, state, params) => { 72 | const endpoint = state.uid 73 | ? config.apiUrl + '/shared/' + state.hobby 74 | : config.apiUrl + '/' + state.hobby 75 | const headers = state.uid 76 | ? {} 77 | : { Authorization: 'Bearer ' + rootState.auth.token } 78 | params = state.uid 79 | ? { user: state.uid, ...params } 80 | : params 81 | const { data } = await axios.get(endpoint, { headers, params }) 82 | return data 83 | } 84 | 85 | const replaceForDownload = (key, value) => { 86 | if (key === '_id') return undefined 87 | if (value === '') return undefined 88 | if (key === 'completed') return value.split(/T/)[0] 89 | return value 90 | } 91 | 92 | const actions = { 93 | 94 | async getItems ({ state, rootState, commit }) { 95 | const data = await httpGetItems(rootState, state, { 96 | page: state[state.selector].page, 97 | term: state[state.selector].filter 98 | }) 99 | commit('setItems', data) 100 | }, 101 | 102 | async changePage ({ state, rootState, commit }, page) { 103 | const data = await httpGetItems(rootState, state, { 104 | page, 105 | term: state[state.selector].filter 106 | }) 107 | commit('setPage', page) 108 | commit('setItems', data) 109 | }, 110 | 111 | async applyFilter ({ state, rootState, commit }, filter) { 112 | const data = await httpGetItems(rootState, state, { 113 | page: 1, 114 | term: filter 115 | }) 116 | commit('setPage', 1) 117 | commit('setFilter', filter) 118 | commit('setItems', data) 119 | }, 120 | 121 | async download ({ rootState, state }) { 122 | const data = await httpGetItems(rootState, state, { 123 | term: state[state.selector].filter 124 | }) 125 | const blob = new Blob( 126 | [JSON.stringify(data.items, replaceForDownload, 1)], 127 | { type: 'application/json' }) 128 | saveAs(blob, state.hobby + '.json') 129 | }, 130 | 131 | async upload ({ rootState, state, dispatch }, file) { 132 | const endpoint = `${config.apiUrl}/${state.selector}/upload` 133 | const headers = { Authorization: 'Bearer ' + rootState.auth.token } 134 | await uploadRequest(endpoint, file, headers) 135 | return dispatch('applyFilter', '') 136 | }, 137 | 138 | async delete ({ rootState, state, dispatch }, id) { 139 | const endpoint = `${config.apiUrl}/${state.selector}/${id}` 140 | const headers = { Authorization: 'Bearer ' + rootState.auth.token } 141 | await axios.delete(endpoint, { headers }) 142 | return dispatch('changePage', 1) 143 | }, 144 | 145 | async create ({ rootState, state, dispatch }, item) { 146 | const endpoint = config.apiUrl + '/' + state.selector 147 | const headers = { Authorization: 'Bearer ' + rootState.auth.token } 148 | await axios.post(endpoint, item, { headers }) 149 | return dispatch('changePage', 1) 150 | }, 151 | 152 | async modify ({ rootState, state, dispatch }, { item, id }) { 153 | const endpoint = `${config.apiUrl}/${state.selector}/${id}` 154 | const headers = { Authorization: 'Bearer ' + rootState.auth.token } 155 | await axios.put(endpoint, item, { headers }) 156 | return dispatch('changePage', 1) 157 | } 158 | } 159 | 160 | export default { 161 | namespaced: true, 162 | state, 163 | getters, 164 | mutations, 165 | actions 166 | } 167 | -------------------------------------------------------------------------------- /back-end/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hobbies", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.7", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 10 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 11 | "requires": { 12 | "mime-types": "~2.1.24", 13 | "negotiator": "0.6.2" 14 | } 15 | }, 16 | "ajv": { 17 | "version": "6.12.0", 18 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", 19 | "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", 20 | "requires": { 21 | "fast-deep-equal": "^3.1.1", 22 | "fast-json-stable-stringify": "^2.0.0", 23 | "json-schema-traverse": "^0.4.1", 24 | "uri-js": "^4.2.2" 25 | } 26 | }, 27 | "append-field": { 28 | "version": "1.0.0", 29 | "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", 30 | "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" 31 | }, 32 | "array-flatten": { 33 | "version": "1.1.1", 34 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 35 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 36 | }, 37 | "asn1": { 38 | "version": "0.2.4", 39 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 40 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 41 | "requires": { 42 | "safer-buffer": "~2.1.0" 43 | } 44 | }, 45 | "assert-plus": { 46 | "version": "1.0.0", 47 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 48 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 49 | }, 50 | "async": { 51 | "version": "1.5.2", 52 | "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", 53 | "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" 54 | }, 55 | "asynckit": { 56 | "version": "0.4.0", 57 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 58 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 59 | }, 60 | "aws-sign2": { 61 | "version": "0.7.0", 62 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 63 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 64 | }, 65 | "aws4": { 66 | "version": "1.9.1", 67 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", 68 | "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" 69 | }, 70 | "bcrypt-pbkdf": { 71 | "version": "1.0.2", 72 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 73 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 74 | "requires": { 75 | "tweetnacl": "^0.14.3" 76 | } 77 | }, 78 | "bl": { 79 | "version": "2.2.1", 80 | "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", 81 | "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", 82 | "requires": { 83 | "readable-stream": "^2.3.5", 84 | "safe-buffer": "^5.1.1" 85 | } 86 | }, 87 | "bluebird": { 88 | "version": "3.5.1", 89 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", 90 | "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" 91 | }, 92 | "body-parser": { 93 | "version": "1.19.0", 94 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 95 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 96 | "requires": { 97 | "bytes": "3.1.0", 98 | "content-type": "~1.0.4", 99 | "debug": "2.6.9", 100 | "depd": "~1.1.2", 101 | "http-errors": "1.7.2", 102 | "iconv-lite": "0.4.24", 103 | "on-finished": "~2.3.0", 104 | "qs": "6.7.0", 105 | "raw-body": "2.4.0", 106 | "type-is": "~1.6.17" 107 | } 108 | }, 109 | "bson": { 110 | "version": "1.1.5", 111 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", 112 | "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" 113 | }, 114 | "buffer-equal-constant-time": { 115 | "version": "1.0.1", 116 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 117 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 118 | }, 119 | "buffer-from": { 120 | "version": "1.1.1", 121 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 122 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 123 | }, 124 | "busboy": { 125 | "version": "0.2.14", 126 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", 127 | "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", 128 | "requires": { 129 | "dicer": "0.2.5", 130 | "readable-stream": "1.1.x" 131 | }, 132 | "dependencies": { 133 | "isarray": { 134 | "version": "0.0.1", 135 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 136 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" 137 | }, 138 | "readable-stream": { 139 | "version": "1.1.14", 140 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 141 | "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", 142 | "requires": { 143 | "core-util-is": "~1.0.0", 144 | "inherits": "~2.0.1", 145 | "isarray": "0.0.1", 146 | "string_decoder": "~0.10.x" 147 | } 148 | }, 149 | "string_decoder": { 150 | "version": "0.10.31", 151 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 152 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" 153 | } 154 | } 155 | }, 156 | "bytes": { 157 | "version": "3.1.0", 158 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 159 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 160 | }, 161 | "caseless": { 162 | "version": "0.12.0", 163 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 164 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 165 | }, 166 | "combined-stream": { 167 | "version": "1.0.8", 168 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 169 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 170 | "requires": { 171 | "delayed-stream": "~1.0.0" 172 | } 173 | }, 174 | "concat-stream": { 175 | "version": "1.6.2", 176 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 177 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 178 | "requires": { 179 | "buffer-from": "^1.0.0", 180 | "inherits": "^2.0.3", 181 | "readable-stream": "^2.2.2", 182 | "typedarray": "^0.0.6" 183 | } 184 | }, 185 | "content-disposition": { 186 | "version": "0.5.3", 187 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 188 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 189 | "requires": { 190 | "safe-buffer": "5.1.2" 191 | } 192 | }, 193 | "content-type": { 194 | "version": "1.0.4", 195 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 196 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 197 | }, 198 | "cookie": { 199 | "version": "0.4.0", 200 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 201 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 202 | }, 203 | "cookie-signature": { 204 | "version": "1.0.6", 205 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 206 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 207 | }, 208 | "core-util-is": { 209 | "version": "1.0.2", 210 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 211 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 212 | }, 213 | "dashdash": { 214 | "version": "1.14.1", 215 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 216 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 217 | "requires": { 218 | "assert-plus": "^1.0.0" 219 | } 220 | }, 221 | "debug": { 222 | "version": "2.6.9", 223 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 224 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 225 | "requires": { 226 | "ms": "2.0.0" 227 | } 228 | }, 229 | "delayed-stream": { 230 | "version": "1.0.0", 231 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 232 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 233 | }, 234 | "denque": { 235 | "version": "1.4.1", 236 | "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", 237 | "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" 238 | }, 239 | "depd": { 240 | "version": "1.1.2", 241 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 242 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 243 | }, 244 | "destroy": { 245 | "version": "1.0.4", 246 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 247 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 248 | }, 249 | "dicer": { 250 | "version": "0.2.5", 251 | "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", 252 | "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", 253 | "requires": { 254 | "readable-stream": "1.1.x", 255 | "streamsearch": "0.1.2" 256 | }, 257 | "dependencies": { 258 | "isarray": { 259 | "version": "0.0.1", 260 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 261 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" 262 | }, 263 | "readable-stream": { 264 | "version": "1.1.14", 265 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 266 | "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", 267 | "requires": { 268 | "core-util-is": "~1.0.0", 269 | "inherits": "~2.0.1", 270 | "isarray": "0.0.1", 271 | "string_decoder": "~0.10.x" 272 | } 273 | }, 274 | "string_decoder": { 275 | "version": "0.10.31", 276 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 277 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" 278 | } 279 | } 280 | }, 281 | "dotenv": { 282 | "version": "2.0.0", 283 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-2.0.0.tgz", 284 | "integrity": "sha1-vXWcNXqqcDZeAclrewvsCKbg2Uk=" 285 | }, 286 | "ecc-jsbn": { 287 | "version": "0.1.2", 288 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 289 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 290 | "requires": { 291 | "jsbn": "~0.1.0", 292 | "safer-buffer": "^2.1.0" 293 | } 294 | }, 295 | "ecdsa-sig-formatter": { 296 | "version": "1.0.11", 297 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 298 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 299 | "requires": { 300 | "safe-buffer": "^5.0.1" 301 | } 302 | }, 303 | "ee-first": { 304 | "version": "1.1.1", 305 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 306 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 307 | }, 308 | "encodeurl": { 309 | "version": "1.0.2", 310 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 311 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 312 | }, 313 | "escape-html": { 314 | "version": "1.0.3", 315 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 316 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 317 | }, 318 | "etag": { 319 | "version": "1.8.1", 320 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 321 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 322 | }, 323 | "express": { 324 | "version": "4.17.1", 325 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 326 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 327 | "requires": { 328 | "accepts": "~1.3.7", 329 | "array-flatten": "1.1.1", 330 | "body-parser": "1.19.0", 331 | "content-disposition": "0.5.3", 332 | "content-type": "~1.0.4", 333 | "cookie": "0.4.0", 334 | "cookie-signature": "1.0.6", 335 | "debug": "2.6.9", 336 | "depd": "~1.1.2", 337 | "encodeurl": "~1.0.2", 338 | "escape-html": "~1.0.3", 339 | "etag": "~1.8.1", 340 | "finalhandler": "~1.1.2", 341 | "fresh": "0.5.2", 342 | "merge-descriptors": "1.0.1", 343 | "methods": "~1.1.2", 344 | "on-finished": "~2.3.0", 345 | "parseurl": "~1.3.3", 346 | "path-to-regexp": "0.1.7", 347 | "proxy-addr": "~2.0.5", 348 | "qs": "6.7.0", 349 | "range-parser": "~1.2.1", 350 | "safe-buffer": "5.1.2", 351 | "send": "0.17.1", 352 | "serve-static": "1.14.1", 353 | "setprototypeof": "1.1.1", 354 | "statuses": "~1.5.0", 355 | "type-is": "~1.6.18", 356 | "utils-merge": "1.0.1", 357 | "vary": "~1.1.2" 358 | } 359 | }, 360 | "express-jwt": { 361 | "version": "6.0.0", 362 | "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-6.0.0.tgz", 363 | "integrity": "sha512-C26y9myRjx7CyhZ+BAT3p+gQyRCoDZ7qo8plCvLDaRT6je6ALIAQknT6XLVQGFKwIy/Ux7lvM2MNap5dt0T7gA==", 364 | "requires": { 365 | "async": "^1.5.0", 366 | "express-unless": "^0.3.0", 367 | "jsonwebtoken": "^8.1.0", 368 | "lodash.set": "^4.0.0" 369 | } 370 | }, 371 | "express-unless": { 372 | "version": "0.3.1", 373 | "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-0.3.1.tgz", 374 | "integrity": "sha1-JVfBRudb65A+LSR/m1ugFFJpbiA=" 375 | }, 376 | "extend": { 377 | "version": "3.0.2", 378 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 379 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 380 | }, 381 | "extsprintf": { 382 | "version": "1.3.0", 383 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 384 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 385 | }, 386 | "fast-deep-equal": { 387 | "version": "3.1.1", 388 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", 389 | "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" 390 | }, 391 | "fast-json-stable-stringify": { 392 | "version": "2.1.0", 393 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 394 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 395 | }, 396 | "finalhandler": { 397 | "version": "1.1.2", 398 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 399 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 400 | "requires": { 401 | "debug": "2.6.9", 402 | "encodeurl": "~1.0.2", 403 | "escape-html": "~1.0.3", 404 | "on-finished": "~2.3.0", 405 | "parseurl": "~1.3.3", 406 | "statuses": "~1.5.0", 407 | "unpipe": "~1.0.0" 408 | } 409 | }, 410 | "forever-agent": { 411 | "version": "0.6.1", 412 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 413 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 414 | }, 415 | "form-data": { 416 | "version": "2.3.3", 417 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 418 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 419 | "requires": { 420 | "asynckit": "^0.4.0", 421 | "combined-stream": "^1.0.6", 422 | "mime-types": "^2.1.12" 423 | } 424 | }, 425 | "forwarded": { 426 | "version": "0.1.2", 427 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 428 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 429 | }, 430 | "fresh": { 431 | "version": "0.5.2", 432 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 433 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 434 | }, 435 | "getpass": { 436 | "version": "0.1.7", 437 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 438 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 439 | "requires": { 440 | "assert-plus": "^1.0.0" 441 | } 442 | }, 443 | "har-schema": { 444 | "version": "2.0.0", 445 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 446 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 447 | }, 448 | "har-validator": { 449 | "version": "5.1.3", 450 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 451 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 452 | "requires": { 453 | "ajv": "^6.5.5", 454 | "har-schema": "^2.0.0" 455 | } 456 | }, 457 | "http-errors": { 458 | "version": "1.7.2", 459 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 460 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 461 | "requires": { 462 | "depd": "~1.1.2", 463 | "inherits": "2.0.3", 464 | "setprototypeof": "1.1.1", 465 | "statuses": ">= 1.5.0 < 2", 466 | "toidentifier": "1.0.0" 467 | } 468 | }, 469 | "http-signature": { 470 | "version": "1.2.0", 471 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 472 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 473 | "requires": { 474 | "assert-plus": "^1.0.0", 475 | "jsprim": "^1.2.2", 476 | "sshpk": "^1.7.0" 477 | } 478 | }, 479 | "iconv-lite": { 480 | "version": "0.4.24", 481 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 482 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 483 | "requires": { 484 | "safer-buffer": ">= 2.1.2 < 3" 485 | } 486 | }, 487 | "inherits": { 488 | "version": "2.0.3", 489 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 490 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 491 | }, 492 | "ipaddr.js": { 493 | "version": "1.9.1", 494 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 495 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 496 | }, 497 | "is-typedarray": { 498 | "version": "1.0.0", 499 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 500 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 501 | }, 502 | "isarray": { 503 | "version": "1.0.0", 504 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 505 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 506 | }, 507 | "isstream": { 508 | "version": "0.1.2", 509 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 510 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 511 | }, 512 | "jsbn": { 513 | "version": "0.1.1", 514 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 515 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 516 | }, 517 | "json-schema": { 518 | "version": "0.2.3", 519 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 520 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 521 | }, 522 | "json-schema-traverse": { 523 | "version": "0.4.1", 524 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 525 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 526 | }, 527 | "json-stringify-safe": { 528 | "version": "5.0.1", 529 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 530 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 531 | }, 532 | "jsonwebtoken": { 533 | "version": "8.5.1", 534 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", 535 | "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", 536 | "requires": { 537 | "jws": "^3.2.2", 538 | "lodash.includes": "^4.3.0", 539 | "lodash.isboolean": "^3.0.3", 540 | "lodash.isinteger": "^4.0.4", 541 | "lodash.isnumber": "^3.0.3", 542 | "lodash.isplainobject": "^4.0.6", 543 | "lodash.isstring": "^4.0.1", 544 | "lodash.once": "^4.0.0", 545 | "ms": "^2.1.1", 546 | "semver": "^5.6.0" 547 | }, 548 | "dependencies": { 549 | "ms": { 550 | "version": "2.1.2", 551 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 552 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 553 | } 554 | } 555 | }, 556 | "jsprim": { 557 | "version": "1.4.1", 558 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 559 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 560 | "requires": { 561 | "assert-plus": "1.0.0", 562 | "extsprintf": "1.3.0", 563 | "json-schema": "0.2.3", 564 | "verror": "1.10.0" 565 | } 566 | }, 567 | "jwa": { 568 | "version": "1.4.1", 569 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 570 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 571 | "requires": { 572 | "buffer-equal-constant-time": "1.0.1", 573 | "ecdsa-sig-formatter": "1.0.11", 574 | "safe-buffer": "^5.0.1" 575 | } 576 | }, 577 | "jws": { 578 | "version": "3.2.2", 579 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 580 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 581 | "requires": { 582 | "jwa": "^1.4.1", 583 | "safe-buffer": "^5.0.1" 584 | } 585 | }, 586 | "kareem": { 587 | "version": "2.3.1", 588 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.1.tgz", 589 | "integrity": "sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw==" 590 | }, 591 | "lodash.includes": { 592 | "version": "4.3.0", 593 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 594 | "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" 595 | }, 596 | "lodash.isboolean": { 597 | "version": "3.0.3", 598 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 599 | "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" 600 | }, 601 | "lodash.isinteger": { 602 | "version": "4.0.4", 603 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 604 | "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" 605 | }, 606 | "lodash.isnumber": { 607 | "version": "3.0.3", 608 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 609 | "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" 610 | }, 611 | "lodash.isplainobject": { 612 | "version": "4.0.6", 613 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 614 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 615 | }, 616 | "lodash.isstring": { 617 | "version": "4.0.1", 618 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 619 | "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" 620 | }, 621 | "lodash.once": { 622 | "version": "4.1.1", 623 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 624 | "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" 625 | }, 626 | "lodash.set": { 627 | "version": "4.3.2", 628 | "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", 629 | "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" 630 | }, 631 | "media-typer": { 632 | "version": "0.3.0", 633 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 634 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 635 | }, 636 | "memory-pager": { 637 | "version": "1.5.0", 638 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 639 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", 640 | "optional": true 641 | }, 642 | "merge-descriptors": { 643 | "version": "1.0.1", 644 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 645 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 646 | }, 647 | "methods": { 648 | "version": "1.1.2", 649 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 650 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 651 | }, 652 | "mime": { 653 | "version": "1.6.0", 654 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 655 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 656 | }, 657 | "mime-db": { 658 | "version": "1.43.0", 659 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", 660 | "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" 661 | }, 662 | "mime-types": { 663 | "version": "2.1.26", 664 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", 665 | "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", 666 | "requires": { 667 | "mime-db": "1.43.0" 668 | } 669 | }, 670 | "minimist": { 671 | "version": "1.2.5", 672 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 673 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 674 | }, 675 | "mkdirp": { 676 | "version": "0.5.5", 677 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 678 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 679 | "requires": { 680 | "minimist": "^1.2.5" 681 | } 682 | }, 683 | "mongodb": { 684 | "version": "3.6.2", 685 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.2.tgz", 686 | "integrity": "sha512-sSZOb04w3HcnrrXC82NEh/YGCmBuRgR+C1hZgmmv4L6dBz4BkRse6Y8/q/neXer9i95fKUBbFi4KgeceXmbsOA==", 687 | "requires": { 688 | "bl": "^2.2.1", 689 | "bson": "^1.1.4", 690 | "denque": "^1.4.1", 691 | "require_optional": "^1.0.1", 692 | "safe-buffer": "^5.1.2", 693 | "saslprep": "^1.0.0" 694 | } 695 | }, 696 | "mongoose": { 697 | "version": "5.10.7", 698 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.10.7.tgz", 699 | "integrity": "sha512-oiofFrD4I5p3PhJXn49QyrU1nX5CY01qhPkfMMrXYPhkfGLEJVwFVO+0PsCxD91A2kQP+d/iFyk5U8e86KI8eQ==", 700 | "requires": { 701 | "bson": "^1.1.4", 702 | "kareem": "2.3.1", 703 | "mongodb": "3.6.2", 704 | "mongoose-legacy-pluralize": "1.0.2", 705 | "mpath": "0.7.0", 706 | "mquery": "3.2.2", 707 | "ms": "2.1.2", 708 | "regexp-clone": "1.0.0", 709 | "safe-buffer": "5.2.1", 710 | "sift": "7.0.1", 711 | "sliced": "1.0.1" 712 | }, 713 | "dependencies": { 714 | "ms": { 715 | "version": "2.1.2", 716 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 717 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 718 | }, 719 | "safe-buffer": { 720 | "version": "5.2.1", 721 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 722 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 723 | } 724 | } 725 | }, 726 | "mongoose-legacy-pluralize": { 727 | "version": "1.0.2", 728 | "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", 729 | "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" 730 | }, 731 | "mpath": { 732 | "version": "0.7.0", 733 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.7.0.tgz", 734 | "integrity": "sha512-Aiq04hILxhz1L+f7sjGyn7IxYzWm1zLNNXcfhDtx04kZ2Gk7uvFdgZ8ts1cWa/6d0TQmag2yR8zSGZUmp0tFNg==" 735 | }, 736 | "mquery": { 737 | "version": "3.2.2", 738 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.2.tgz", 739 | "integrity": "sha512-XB52992COp0KP230I3qloVUbkLUxJIu328HBP2t2EsxSFtf4W1HPSOBWOXf1bqxK4Xbb66lfMJ+Bpfd9/yZE1Q==", 740 | "requires": { 741 | "bluebird": "3.5.1", 742 | "debug": "3.1.0", 743 | "regexp-clone": "^1.0.0", 744 | "safe-buffer": "5.1.2", 745 | "sliced": "1.0.1" 746 | }, 747 | "dependencies": { 748 | "debug": { 749 | "version": "3.1.0", 750 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 751 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 752 | "requires": { 753 | "ms": "2.0.0" 754 | } 755 | } 756 | } 757 | }, 758 | "ms": { 759 | "version": "2.0.0", 760 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 761 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 762 | }, 763 | "multer": { 764 | "version": "1.4.2", 765 | "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", 766 | "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", 767 | "requires": { 768 | "append-field": "^1.0.0", 769 | "busboy": "^0.2.11", 770 | "concat-stream": "^1.5.2", 771 | "mkdirp": "^0.5.1", 772 | "object-assign": "^4.1.1", 773 | "on-finished": "^2.3.0", 774 | "type-is": "^1.6.4", 775 | "xtend": "^4.0.0" 776 | } 777 | }, 778 | "negotiator": { 779 | "version": "0.6.2", 780 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 781 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 782 | }, 783 | "oauth-sign": { 784 | "version": "0.9.0", 785 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 786 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 787 | }, 788 | "object-assign": { 789 | "version": "4.1.1", 790 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 791 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 792 | }, 793 | "on-finished": { 794 | "version": "2.3.0", 795 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 796 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 797 | "requires": { 798 | "ee-first": "1.1.1" 799 | } 800 | }, 801 | "parseurl": { 802 | "version": "1.3.3", 803 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 804 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 805 | }, 806 | "passport": { 807 | "version": "0.3.2", 808 | "resolved": "https://registry.npmjs.org/passport/-/passport-0.3.2.tgz", 809 | "integrity": "sha1-ndAJ+RXo/glbASSgG4+C2gdRAQI=", 810 | "requires": { 811 | "passport-strategy": "1.x.x", 812 | "pause": "0.0.1" 813 | } 814 | }, 815 | "passport-local": { 816 | "version": "1.0.0", 817 | "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", 818 | "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", 819 | "requires": { 820 | "passport-strategy": "1.x.x" 821 | } 822 | }, 823 | "passport-strategy": { 824 | "version": "1.0.0", 825 | "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", 826 | "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" 827 | }, 828 | "path-to-regexp": { 829 | "version": "0.1.7", 830 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 831 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 832 | }, 833 | "pause": { 834 | "version": "0.0.1", 835 | "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", 836 | "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" 837 | }, 838 | "performance-now": { 839 | "version": "2.1.0", 840 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 841 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 842 | }, 843 | "process-nextick-args": { 844 | "version": "2.0.1", 845 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 846 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 847 | }, 848 | "proxy-addr": { 849 | "version": "2.0.6", 850 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", 851 | "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", 852 | "requires": { 853 | "forwarded": "~0.1.2", 854 | "ipaddr.js": "1.9.1" 855 | } 856 | }, 857 | "psl": { 858 | "version": "1.8.0", 859 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", 860 | "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" 861 | }, 862 | "punycode": { 863 | "version": "2.1.1", 864 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 865 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 866 | }, 867 | "qs": { 868 | "version": "6.7.0", 869 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 870 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 871 | }, 872 | "range-parser": { 873 | "version": "1.2.1", 874 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 875 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 876 | }, 877 | "raw-body": { 878 | "version": "2.4.0", 879 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 880 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 881 | "requires": { 882 | "bytes": "3.1.0", 883 | "http-errors": "1.7.2", 884 | "iconv-lite": "0.4.24", 885 | "unpipe": "1.0.0" 886 | } 887 | }, 888 | "readable-stream": { 889 | "version": "2.3.7", 890 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 891 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 892 | "requires": { 893 | "core-util-is": "~1.0.0", 894 | "inherits": "~2.0.3", 895 | "isarray": "~1.0.0", 896 | "process-nextick-args": "~2.0.0", 897 | "safe-buffer": "~5.1.1", 898 | "string_decoder": "~1.1.1", 899 | "util-deprecate": "~1.0.1" 900 | } 901 | }, 902 | "regexp-clone": { 903 | "version": "1.0.0", 904 | "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", 905 | "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==" 906 | }, 907 | "request": { 908 | "version": "2.88.2", 909 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", 910 | "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", 911 | "requires": { 912 | "aws-sign2": "~0.7.0", 913 | "aws4": "^1.8.0", 914 | "caseless": "~0.12.0", 915 | "combined-stream": "~1.0.6", 916 | "extend": "~3.0.2", 917 | "forever-agent": "~0.6.1", 918 | "form-data": "~2.3.2", 919 | "har-validator": "~5.1.3", 920 | "http-signature": "~1.2.0", 921 | "is-typedarray": "~1.0.0", 922 | "isstream": "~0.1.2", 923 | "json-stringify-safe": "~5.0.1", 924 | "mime-types": "~2.1.19", 925 | "oauth-sign": "~0.9.0", 926 | "performance-now": "^2.1.0", 927 | "qs": "~6.5.2", 928 | "safe-buffer": "^5.1.2", 929 | "tough-cookie": "~2.5.0", 930 | "tunnel-agent": "^0.6.0", 931 | "uuid": "^3.3.2" 932 | }, 933 | "dependencies": { 934 | "qs": { 935 | "version": "6.5.2", 936 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 937 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 938 | } 939 | } 940 | }, 941 | "require_optional": { 942 | "version": "1.0.1", 943 | "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", 944 | "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", 945 | "requires": { 946 | "resolve-from": "^2.0.0", 947 | "semver": "^5.1.0" 948 | } 949 | }, 950 | "resolve-from": { 951 | "version": "2.0.0", 952 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", 953 | "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" 954 | }, 955 | "safe-buffer": { 956 | "version": "5.1.2", 957 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 958 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 959 | }, 960 | "safer-buffer": { 961 | "version": "2.1.2", 962 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 963 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 964 | }, 965 | "saslprep": { 966 | "version": "1.0.3", 967 | "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", 968 | "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", 969 | "optional": true, 970 | "requires": { 971 | "sparse-bitfield": "^3.0.3" 972 | } 973 | }, 974 | "semver": { 975 | "version": "5.7.1", 976 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 977 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 978 | }, 979 | "send": { 980 | "version": "0.17.1", 981 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 982 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 983 | "requires": { 984 | "debug": "2.6.9", 985 | "depd": "~1.1.2", 986 | "destroy": "~1.0.4", 987 | "encodeurl": "~1.0.2", 988 | "escape-html": "~1.0.3", 989 | "etag": "~1.8.1", 990 | "fresh": "0.5.2", 991 | "http-errors": "~1.7.2", 992 | "mime": "1.6.0", 993 | "ms": "2.1.1", 994 | "on-finished": "~2.3.0", 995 | "range-parser": "~1.2.1", 996 | "statuses": "~1.5.0" 997 | }, 998 | "dependencies": { 999 | "ms": { 1000 | "version": "2.1.1", 1001 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 1002 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 1003 | } 1004 | } 1005 | }, 1006 | "serve-static": { 1007 | "version": "1.14.1", 1008 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 1009 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 1010 | "requires": { 1011 | "encodeurl": "~1.0.2", 1012 | "escape-html": "~1.0.3", 1013 | "parseurl": "~1.3.3", 1014 | "send": "0.17.1" 1015 | } 1016 | }, 1017 | "setprototypeof": { 1018 | "version": "1.1.1", 1019 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 1020 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 1021 | }, 1022 | "sift": { 1023 | "version": "7.0.1", 1024 | "resolved": "https://registry.npmjs.org/sift/-/sift-7.0.1.tgz", 1025 | "integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g==" 1026 | }, 1027 | "sliced": { 1028 | "version": "1.0.1", 1029 | "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", 1030 | "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" 1031 | }, 1032 | "sparse-bitfield": { 1033 | "version": "3.0.3", 1034 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 1035 | "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", 1036 | "optional": true, 1037 | "requires": { 1038 | "memory-pager": "^1.0.2" 1039 | } 1040 | }, 1041 | "sshpk": { 1042 | "version": "1.16.1", 1043 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 1044 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 1045 | "requires": { 1046 | "asn1": "~0.2.3", 1047 | "assert-plus": "^1.0.0", 1048 | "bcrypt-pbkdf": "^1.0.0", 1049 | "dashdash": "^1.12.0", 1050 | "ecc-jsbn": "~0.1.1", 1051 | "getpass": "^0.1.1", 1052 | "jsbn": "~0.1.0", 1053 | "safer-buffer": "^2.0.2", 1054 | "tweetnacl": "~0.14.0" 1055 | } 1056 | }, 1057 | "statuses": { 1058 | "version": "1.5.0", 1059 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 1060 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 1061 | }, 1062 | "streamsearch": { 1063 | "version": "0.1.2", 1064 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", 1065 | "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" 1066 | }, 1067 | "string_decoder": { 1068 | "version": "1.1.1", 1069 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 1070 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 1071 | "requires": { 1072 | "safe-buffer": "~5.1.0" 1073 | } 1074 | }, 1075 | "toidentifier": { 1076 | "version": "1.0.0", 1077 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 1078 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 1079 | }, 1080 | "tough-cookie": { 1081 | "version": "2.5.0", 1082 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", 1083 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 1084 | "requires": { 1085 | "psl": "^1.1.28", 1086 | "punycode": "^2.1.1" 1087 | } 1088 | }, 1089 | "tunnel-agent": { 1090 | "version": "0.6.0", 1091 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1092 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 1093 | "requires": { 1094 | "safe-buffer": "^5.0.1" 1095 | } 1096 | }, 1097 | "tweetnacl": { 1098 | "version": "0.14.5", 1099 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 1100 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 1101 | }, 1102 | "type-is": { 1103 | "version": "1.6.18", 1104 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1105 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1106 | "requires": { 1107 | "media-typer": "0.3.0", 1108 | "mime-types": "~2.1.24" 1109 | } 1110 | }, 1111 | "typedarray": { 1112 | "version": "0.0.6", 1113 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 1114 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" 1115 | }, 1116 | "unpipe": { 1117 | "version": "1.0.0", 1118 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1119 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 1120 | }, 1121 | "uri-js": { 1122 | "version": "4.2.2", 1123 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 1124 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 1125 | "requires": { 1126 | "punycode": "^2.1.0" 1127 | } 1128 | }, 1129 | "util-deprecate": { 1130 | "version": "1.0.2", 1131 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1132 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 1133 | }, 1134 | "utils-merge": { 1135 | "version": "1.0.1", 1136 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1137 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1138 | }, 1139 | "uuid": { 1140 | "version": "3.4.0", 1141 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 1142 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" 1143 | }, 1144 | "vary": { 1145 | "version": "1.1.2", 1146 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1147 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1148 | }, 1149 | "verror": { 1150 | "version": "1.10.0", 1151 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 1152 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 1153 | "requires": { 1154 | "assert-plus": "^1.0.0", 1155 | "core-util-is": "1.0.2", 1156 | "extsprintf": "^1.2.0" 1157 | } 1158 | }, 1159 | "xtend": { 1160 | "version": "4.0.2", 1161 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 1162 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 1163 | } 1164 | } 1165 | } 1166 | -------------------------------------------------------------------------------- /back-end/public/js/app.770a7fbd.js: -------------------------------------------------------------------------------- 1 | (function(t){function e(e){for(var n,s,i=e[0],c=e[1],u=e[2],p=0,m=[];p1?r("li",[r("a",{attrs:{href:""},on:{click:function(e){return e.preventDefault(),t.previous(e)}}},[r("span",{attrs:{"aria-hidden":"true"}},[t._v("«")])])]):t._e(),t._l(t.pages,(function(e){return r("li",{key:e,class:{active:e==t.page}},[r("a",{attrs:{href:""},on:{click:function(r){return r.preventDefault(),t.select(e)}}},[t._v(t._s(e))])])})),t.page=n&&(a=n-r);var o=a+this.frame-1;return{min:a,max:o}},select:function(t){this.$emit("change",t)},previous:function(){this.$emit("change",this.page-1)},next:function(){this.$emit("change",this.page+1)}},computed:{pages:function(){for(var t=this.getPagerLimits(this.pageCount,this.page),e=t.min,r=t.max,n=[],a=e;a<=r;a++)n.push(a);return n}}}),Ht=Yt,zt=(r("3983"),Object(f["a"])(Ht,qt,Lt,!1,null,"a9b50c18",null)),Jt=zt.exports;function Kt(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),r.push.apply(r,n)}return r}function Vt(t){for(var e=1;eDate.now()/1e3},currentUser:function(t){if(!t.token)return{};var e=jr(t),r=e.name,n=e.email;return{name:r,email:n}}},kr={namespaced:!0,state:wr,mutations:Or,actions:_r,getters:Cr},$r=r("1d79"),Pr=function(t,e,r){return new Promise((function(n,a){var o=new FormData,s=new XMLHttpRequest;o.append("upload",e,e.name),s.onreadystatechange=function(){4===s.readyState&&(200===s.status?n(s.response):a(s.response))},s.open("POST",t,!0),Object.keys(r).forEach((function(t){s.setRequestHeader(t,r[t])})),s.send(o)}))};function xr(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),r.push.apply(r,n)}return r}function Sr(t){for(var e=1;e1&&void 0!==arguments[1]?arguments[1]:"Invalid date";if(!t)return e;var r=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],n=t.getFullYear(),a=t.getMonth(),o=t.getDate();return"".concat(r[a]," ").concat(o,", ").concat(n)};r("924b");n["a"].config.productionTip=!1,n["a"].use(tn.a),n["a"].filter("date",en),n["a"].config.productionTip=!1,new n["a"]({router:Wr,store:Vr,render:function(t){return t(I)}}).$mount("#app")},5858:function(t,e,r){"use strict";var n=r("b98b"),a=r.n(n);a.a},"85ec":function(t,e,r){},8857:function(t,e,r){},"8c51":function(t,e,r){},"8fc6":function(t,e,r){},9953:function(t,e,r){"use strict";var n=r("8fc6"),a=r.n(n);a.a},a707:function(t,e,r){},b98b:function(t,e,r){},be87:function(t,e,r){},e869:function(t,e,r){"use strict";var n=r("f60d"),a=r.n(n);a.a},f44f:function(t,e,r){"use strict";var n=r("8c51"),a=r.n(n);a.a},f60d:function(t,e,r){}}); 2 | //# sourceMappingURL=app.770a7fbd.js.map --------------------------------------------------------------------------------