├── 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 |
2 |
3 |
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 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/front-end/src/components/movies/Movies.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
3 |
6 | {{ message }}
7 |
8 |
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 |
2 |
6 |
7 |
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 |
2 |
3 |
4 |
5 | {{ value | date }}
6 |
7 |
9 |
10 |
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 |
2 |
3 |
5 |
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
26 |
27 |
28 |
29 |
30 |
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 |
2 |
22 |
23 |
24 |
50 |
51 |
62 |
--------------------------------------------------------------------------------
/front-end/src/components/UserSettings.vue:
--------------------------------------------------------------------------------
1 |
2 |
25 |
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 |
2 |
3 |
4 |
5 | MY HOBBIES
6 | is an
7 | open-source project
8 | for keeping track of books you have read/listened and movies you have watched.
9 |
10 |
11 | After logging in you are able to:
12 |
13 |
14 | - Add an item (book or movie) to the list.
15 | - Modify an existing item.
16 | - Remove an item.
17 | - Perform search for specific item(s).
18 | - Download items as a file in JSON format.
19 | - Upload items from JSON file.
20 |
21 |
22 | Also you have the possibilities to:
23 |
24 |
25 | - Share data with other people.
26 | - Change you personal settings.
27 |
28 |
29 | Unathorized people can see and download the shared data.
30 |
31 |
32 |
Change log
33 |
34 |
35 | -
36 | Sep 22, 2020: Migrate database from mLab to MongoDB Atlas.
37 |
38 | -
39 | Sep 15, 2019: Handle errors from TMDb API. Update dependencies.
40 |
41 | -
42 | Nov 4, 2018: Filter out people without hobbies.
43 |
44 | -
45 | Nov 3, 2018: Update to latest versions of Vue.js and other libraries.
46 |
47 | -
48 | May 25, 2017: Change movie info service from OMDb to TMDb.
49 |
50 | -
51 | Apr 24, 2017: Wire up the OMDb api service to obtain the movie info.
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/front-end/src/components/Pager.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
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 |
2 |
33 |
34 |
35 |
73 |
--------------------------------------------------------------------------------
/front-end/src/components/books/BookList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 | |
14 | Completed |
15 | Kind |
16 | Author |
17 | Title |
18 |
19 |
20 |
21 |
22 | |
23 |
31 | |
32 | {{ book.completed | date }} |
33 | {{ book.mode }} |
34 | {{ book.author }} |
35 | {{ book.title }} |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
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 |
2 |
3 |
4 |
5 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | |
30 |
31 | {{ `${user.name} [${user.total}]` }}
32 | |
33 |
34 |
35 |
36 | {{ `${user.name}'s books [${user.books}]` }}
37 |
38 | |
39 |
40 |
41 |
42 | {{ `${user.name}'s movies [${user.movies}]` }}
43 |
44 | |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
55 |
56 |
57 |
58 |
59 |
60 |
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 |
2 |
61 |
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 |
2 |
3 |
4 |
5 | {{ movieTitle }}
6 |
7 |
Loading...
8 |
{{ movieError }}
9 |
10 |
11 |
12 |
{{ movieInfo.plot }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
27 |
28 |
29 |
30 |
31 | |
32 | Completed |
33 | Year |
34 | Title |
35 | Notes |
36 |
37 |
38 |
39 |
40 | |
41 |
54 | |
55 | {{ movie.completed | date }} |
56 | {{ movie.year }} |
57 | {{ movie.title }} |
58 | {{ movie.notes }} |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
108 |
109 |
118 |
--------------------------------------------------------------------------------
/front-end/src/components/Register.vue:
--------------------------------------------------------------------------------
1 |
2 |
58 |
59 |
60 |
120 |
--------------------------------------------------------------------------------
/front-end/src/components/ItemList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ removeHeader }}
6 |
7 | Are you sure you want to remove {{ itemTitle }}?
8 |
9 |
10 |
11 |
44 |
45 |
48 |
49 |
50 |
51 |
52 |
53 |
57 |
58 |
59 |
60 |
61 |
62 |
124 |
125 |
168 |
--------------------------------------------------------------------------------
/front-end/src/components/movies/Movie.vue:
--------------------------------------------------------------------------------
1 |
2 |
54 |
55 |
56 |
136 |
--------------------------------------------------------------------------------
/front-end/src/components/books/Book.vue:
--------------------------------------------------------------------------------
1 |
2 |
53 |
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
--------------------------------------------------------------------------------