├── .gitignore ├── .gitlab-ci.yml ├── Procfile ├── README.md ├── backend-frontend.code-workspace ├── client ├── .editorconfig ├── .env.sample ├── .eslintrc.js ├── .prettierrc ├── .vscode │ ├── launch.json │ └── settings.json ├── README.md ├── jsconfig.json ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── Api.js │ ├── App.vue │ ├── Images │ │ ├── Artsy-text-top.png │ │ ├── Artsy.png │ │ ├── DefaultPostImagePreview.png │ │ ├── cat.png │ │ ├── dog.png │ │ ├── landscape.png │ │ ├── painting.png │ │ └── trash.png │ ├── main.js │ ├── router.js │ └── views │ │ ├── CreatePost.vue │ │ ├── Home.vue │ │ ├── InsideCollection.vue │ │ ├── Login.vue │ │ ├── Register.vue │ │ └── User.vue └── vue.config.js ├── docs ├── DEPLOYMENT.md └── LOCAL_DEPLOYMENT.md ├── images ├── er_diagram.png └── teaser.png ├── package-lock.json ├── package.json └── server ├── .eslintrc.js ├── .vscode ├── launch.json └── settings.json ├── README.md ├── app.js ├── controllers ├── collections.js ├── posts.js ├── ratings.js ├── userAuth.js └── users.js ├── docs ├── FAQ.md ├── POSTMAN.md ├── TROUBLESHOOTING.md └── img │ ├── postman_choose.png │ ├── postman_env.png │ ├── postman_export.png │ ├── postman_export_format.png │ ├── postman_export_save.png │ ├── postman_import.png │ ├── postman_run.png │ ├── postman_runner.png │ ├── postman_variable_save.png │ ├── postman_variable_use.png │ └── test_run.png ├── icons └── .gitkeep ├── image_handling ├── imageDeleteHandler.js └── imageUploadHandler.js ├── models ├── collection.js ├── post.js ├── rating.js └── user.js ├── package-lock.json ├── package.json ├── tests ├── dropdb.js ├── invalid_file_format_test.txt ├── package.json ├── server.postman_collection.json ├── test_greater_than_30_image.jpg ├── test_icon.jpg ├── test_image.jpg └── test_thumnail.jpg ├── thumbnails └── .gitkeep └── uploads └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,vuejs,visualstudiocode,linux,macos,windows 3 | # Edit at https://www.gitignore.io/?templates=node,vuejs,visualstudiocode,linux,macos,windows 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### macOS ### 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | # Thumbnails 30 | ._* 31 | 32 | # Files that might appear in the root of a volume 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | 41 | # Directories potentially created on remote AFP share 42 | .AppleDB 43 | .AppleDesktop 44 | Network Trash Folder 45 | Temporary Items 46 | .apdisk 47 | 48 | ### Node ### 49 | # Logs 50 | logs 51 | *.log 52 | npm-debug.log* 53 | yarn-debug.log* 54 | yarn-error.log* 55 | lerna-debug.log* 56 | 57 | # Diagnostic reports (https://nodejs.org/api/report.html) 58 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 59 | 60 | # Runtime data 61 | pids 62 | *.pid 63 | *.seed 64 | *.pid.lock 65 | 66 | # Directory for instrumented libs generated by jscoverage/JSCover 67 | lib-cov 68 | 69 | # Coverage directory used by tools like istanbul 70 | coverage 71 | 72 | # nyc test coverage 73 | .nyc_output 74 | 75 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 76 | .grunt 77 | 78 | # Bower dependency directory (https://bower.io/) 79 | bower_components 80 | 81 | # node-waf configuration 82 | .lock-wscript 83 | 84 | # Compiled binary addons (https://nodejs.org/api/addons.html) 85 | build/Release 86 | 87 | # Dependency directories 88 | node_modules/ 89 | jspm_packages/ 90 | 91 | # TypeScript v1 declaration files 92 | typings/ 93 | 94 | # Optional npm cache directory 95 | .npm 96 | 97 | # Optional eslint cache 98 | .eslintcache 99 | 100 | # Optional REPL history 101 | .node_repl_history 102 | 103 | # Output of 'npm pack' 104 | *.tgz 105 | 106 | # Yarn Integrity file 107 | .yarn-integrity 108 | 109 | # dotenv environment variables file 110 | .env 111 | .env.test 112 | 113 | # parcel-bundler cache (https://parceljs.org/) 114 | .cache 115 | 116 | # next.js build output 117 | .next 118 | 119 | # nuxt.js build output 120 | .nuxt 121 | 122 | # vuepress build output 123 | .vuepress/dist 124 | 125 | # Serverless directories 126 | .serverless/ 127 | 128 | # FuseBox cache 129 | .fusebox/ 130 | 131 | # DynamoDB Local files 132 | .dynamodb/ 133 | 134 | ### VisualStudioCode ### 135 | .vscode/* 136 | !.vscode/settings.json 137 | !.vscode/tasks.json 138 | !.vscode/launch.json 139 | !.vscode/extensions.json 140 | 141 | ### VisualStudioCode Patch ### 142 | # Ignore all local history of files 143 | .history 144 | 145 | ### Vuejs ### 146 | # Recommended template: Node.gitignore 147 | 148 | dist/ 149 | npm-debug.log 150 | yarn-error.log 151 | 152 | ### Windows ### 153 | # Windows thumbnail cache files 154 | Thumbs.db 155 | ehthumbs.db 156 | ehthumbs_vista.db 157 | 158 | # Dump file 159 | *.stackdump 160 | 161 | # Folder config file 162 | [Dd]esktop.ini 163 | 164 | # Recycle Bin used on file shares 165 | $RECYCLE.BIN/ 166 | 167 | # Windows Installer files 168 | *.cab 169 | *.msi 170 | *.msix 171 | *.msm 172 | *.msp 173 | 174 | # Windows shortcuts 175 | *.lnk 176 | 177 | # End of https://www.gitignore.io/api/node,vuejs,visualstudiocode,linux,macos,windows 178 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:14-alpine 2 | 3 | # Cache modules in between jobs per-branch 4 | cache: 5 | key: ${CI_COMMIT_REF_SLUG} 6 | paths: 7 | - server/node_modules/ 8 | 9 | stages: 10 | - build 11 | - test 12 | - deploy 13 | 14 | build: 15 | stage: build 16 | tags: 17 | - docker 18 | script: 19 | - cd server 20 | - npm install 21 | 22 | test: 23 | stage: test 24 | tags: 25 | - docker 26 | services: 27 | - name: mvertes/alpine-mongo:latest 28 | alias: mongo 29 | variables: 30 | MONGODB_URI: "mongodb://mongo:27017/serverTestDB" 31 | script: 32 | - cd server 33 | - npm run ci-test 34 | 35 | deploy: 36 | stage: deploy 37 | tags: 38 | - docker 39 | image: ruby:alpine 40 | script: 41 | - apk update && apk add git curl 42 | - gem install dpl 43 | - dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_API_KEY 44 | environment: 45 | name: production 46 | url: https://$HEROKU_APP_NAME.herokuapp.com/ 47 | only: 48 | refs: 49 | - master 50 | variables: 51 | - $HEROKU_APP_NAME 52 | - $HEROKU_API_KEY 53 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server/app.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backend and Frontend Template 2 | 3 | Latest version: https://git.ita.chalmers.se/courses/dit341/group-00-web (public Github [mirror](https://github.com/dit341/group-00-web)) 4 | 5 | ## Project Structure 6 | 7 | | File | Purpose | What you do? | 8 | | ---------------------------------------------------- | --------------------------------- | ----------------------------------------- | 9 | | `server/` | Backend server code | All your server code | 10 | | [server/README.md](server/README.md) | Everything about the server | **READ ME** carefully! | 11 | | `client/` | Frontend client code | All your client code | 12 | | [client/README.md](client/README.md) | Everything about the client | **READ ME** carefully! | 13 | | [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | Free online production deployment | Deploy your app online in production mode | 14 | | [docs/LOCAL_DEPLOYMENT.md](docs/LOCAL_DEPLOYMENT.md) | Local production deployment | Deploy your app local in production mode | 15 | 16 | ## Requirements 17 | 18 | The version numbers in brackets indicate the tested versions but feel free to use more recent versions. 19 | You can also use alternative tools if you know how to configure them (e.g., Firefox instead of Chrome). 20 | 21 | - [Git](https://git-scm.com/) (v2) => [installation instructions](https://www.atlassian.com/git/tutorials/install-git) 22 | - [Add your Git username and set your email](https://docs.gitlab.com/ce/gitlab-basics/start-using-git.html#add-your-git-username-and-set-your-email) 23 | - `git config --global user.name "YOUR_USERNAME"` => check `git config --global user.name` 24 | - `git config --global user.email "email@example.com"` => check `git config --global user.email` 25 | - > **Windows users**: We recommend to use the [Git Bash](https://www.atlassian.com/git/tutorials/git-bash) shell from your Git installation or the Bash shell from the [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/install-win10) to run all shell commands for this project. 26 | - [Chalmers GitLab](https://git.ita.chalmers.se/) => Login with your **Chalmers CID** choosing "Sign in with" **Chalmers Login**. (contact [support@chalmers.se](mailto:support@chalmers.se) if you don't have one) 27 | - DIT341 course group: https://git.ita.chalmers.se/courses/dit341 28 | - [Setup SSH key with Gitlab](https://docs.gitlab.com/ee/ssh/) 29 | - Create an SSH key pair `ssh-keygen -t ed25519 -C "email@example.com"` (skip if you already have one) 30 | - Add your public SSH key to your Gitlab profile under https://git.ita.chalmers.se/profile/keys 31 | - Make sure the email you use to commit is registered under https://git.ita.chalmers.se/profile/emails 32 | - Checkout the [Backend-Frontend](https://git.ita.chalmers.se/courses/dit341/group-00-web) template `git clone git@git.ita.chalmers.se:courses/dit341/group-00-web.git` 33 | - [Server Requirements](./server/README.md#Requirements) 34 | - [Client Requirements](./client/README.md#Requirements) 35 | 36 | ## Getting started 37 | 38 | ```bash 39 | # Clone repository 40 | git clone git@git.ita.chalmers.se:courses/dit341/group-00-web.git 41 | 42 | # Change into the directory 43 | cd group-00-web 44 | 45 | # Setup backend 46 | cd server && npm install 47 | npm run dev 48 | 49 | # Setup frontend 50 | cd client && npm install 51 | npm run serve 52 | ``` 53 | 54 | > Check out the detailed instructions for [backend](./server/README.md) and [frontend](./client/README.md). 55 | 56 | ## Visual Studio Code (VSCode) 57 | 58 | Open the `server` and `client` in separate VSCode workspaces or open the combined [backend-frontend.code-workspace](./backend-frontend.code-workspace). Otherwise, workspace-specific settings don't work properly. 59 | 60 | ## System Definition (MS0) 61 | 62 | ### Purpose 63 | 64 | With this website, the focus orients around displaying images/artwork in a curated environment. Users are able to upload images/artwork, add images to collections, and also search by tag in order to build up their own collections of favorite pieces. 65 | 66 | ### Pages 67 | 68 | - **Registration page:** On the registration page creation of a user is done by creating a username, password, bio about themselves, and uploading a user icon. Upon creation, the user's two default collections (MyPhotos and FavoritedImages) are added to the user. After creating an account the user is then redirected to the “Log-in page.” 69 | 70 | - **Log-in:** On the log-in page, the user can input the username and password that they have previously created. Upon successfully logging in a JWT WebToken is set to the user to initiate a session and then the user is redirected to the “Home page.” If the user does not have an account, they have the option to be redirected to the registration page. 71 | 72 | - **Home page:** On the home page, a user is displayed with all the user-uploaded images. Below each image, there is a heart icon which upon user click will add the selected image to their FavoritedImages collection. At the top of the homepage, the user is presented with an image filtering system. The filtering system works using the user-selected image tags when uploading an image. Therefore, the filtering options are as follows: none, cat, dog, landscape, and painting photos. Upon a filtering option selected the user view is updated to only show the images that are related to the associated selected tag. Lastly, in the top right, the user has also the option to delete all photos to clear the homepage view if wanted. 73 | 74 | - **User page:** The user page contains details on the user, such as their username, user icon, and bio. The user specific collections are shown on their page. Each user has default collections one containing all their uploaded posts and another collection containing their favorite posts. The collections have a default folder icon presented on them. 75 | 76 | - **Post creation page:** In the post creation page, a user is able to upload an image; after which, the uploaded image is displayed/previewed to the user. Next, users must add post criteria to submit their new post which include: add a title, add a description, add related tags. After uploading an image the image is automatically added to the users MyPhotos collections in the user page. 77 | 78 | - **Inside a collection page:** When a user enters a collection from the user page, posts within that collection will be visible. In the MyPhotos collection that contains posts that the user uploaded, the user can select to delete their posts. The FavoritedImages collection will contain posts that the user has favorited. The user is not able to delete posts from the FavoritedImages collection, since they may be posts from other users. Functionality regarding editing a user created collection has also been implemented. This would mainly enable for and support future implementations of allowing users to create their own collections, and involves ensuring that the current logged in user actually has permissions to modify that particular collection. 79 | 80 | ### Entity-Relationship (ER) Diagram 81 | 82 | ![ER Diagram](./images/er_diagram.png) 83 | 84 | ## Teaser (MS3) 85 | 86 | ![Teaser](./images/teaser.png) 87 | ### Teaser video 88 | [![Teaser video](https://img.youtube.com/vi/EP0KQcu8TLc/0.jpg)](https://youtu.be/EP0KQcu8TLc) 89 | -------------------------------------------------------------------------------- /backend-frontend.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | { 7 | "path": "client" 8 | }, 9 | { 10 | "path": "server" 11 | } 12 | ], 13 | "settings": {} 14 | } -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /client/.env.sample: -------------------------------------------------------------------------------- 1 | VUE_APP_API_ENDPOINT=http://localhost:5000/api 2 | -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | rules: { 11 | 'space-before-function-paren': [2, { anonymous: 'always', named: 'never' }], 12 | 'no-console': 'off', 13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 14 | }, 15 | parserOptions: { 16 | parser: 'babel-eslint' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /client/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "chrome", 6 | "request": "launch", 7 | "name": "Debug Client with Chrome", 8 | "url": "http://localhost:8080", 9 | "webRoot": "${workspaceFolder}/src", 10 | "breakOnLoad": true, 11 | "sourceMapPathOverrides": { 12 | "webpack:///src/*": "${webRoot}/*" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /client/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vetur.validation.template": false, 3 | "vetur.format.enable": true, 4 | // Vetur automatically prefers .prettierrc 5 | // https://github.com/vuejs/vetur/blob/master/docs/formatting.md 6 | "eslint.validate": ["vue", "html", "javascript"], 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Client – Vue.js Frontend 2 | 3 | This [Vue.js](https://vuejs.org/) template provides sample code how to connect to the ExpressJS backend. 4 | 5 | ## Client Structure 6 | 7 | | File | Purpose | What you do? | 8 | | ------------- | ------------- | ----- | 9 | | [README.md](./README.md) | Everything about the client | **READ ME** carefully! | 10 | | [public/favicon.ico](public/favicon.ico) | [Favicon](https://en.wikipedia.org/wiki/Favicon) website icon | — | 11 | | [public/index.html](public/index.html) | Static HTML entry point page | — | 12 | | `src/` | src (i.e., source code) | All your code goes in here | 13 | | [src/Api.js](src/Api.js) | Configures HTTP library to communicate with backend | — | 14 | | [src/App.vue](src/App.vue) | Main Vue layout template for all view (or pages) | Change your global template for all views | 15 | | `src/assets/` | Graphical resources | Add your images, logos, etc | 16 | | `src/components/` | Vue components that are reusable LEGO blocks | Add your custom components here | 17 | | [src/main.js](src/main.js) | Main JavaScript entry point | — | 18 | | [src/router.js](src/router.js) | Vue routes configuration | Register new routes/pages/views | 19 | | `src/views/` | Vue components that are separate pages/views | Add new routes/pages/views | 20 | | [src/views/Home.vue](src/views/Home.vue) | Home page/view | Replace with your home page/view | 21 | | [package.json](package.json) | Project meta-information | —| 22 | | [vue.config.js](vue.config.js) | Vue configuration | — | 23 | 24 | > NOTE: The (mandatory) exercises are essential for understanding this template and will *save* you time! 25 | 26 | Optional: Learn how to create such a project template in this [tutorial](https://www.vuemastery.com/courses/real-world-vue-js/vue-cli). 27 | 28 | ## Requirements 29 | 30 | * [Server](../server/README.md) backend running on `http://localhost:3000` 31 | * [Node.js](https://nodejs.org/en/download/) (v14) => installation instructions for [Linux](https://github.com/nodesource/distributions) 32 | * [Visual Studio Code (VSCode)](https://code.visualstudio.com/) as IDE 33 | * [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) plugin for Vue tooling 34 | * [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) plugin for linting Vue, JS, and HTML code 35 | * [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) plugin for debugging 36 | * [Google Chrome](https://www.google.com/chrome/) as web browser 37 | * [Vue.js devtools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd?hl=en) plugin for debugging 38 | 39 | ## Project setup 40 | 41 | Make sure, you are in the client directory `cd client` 42 | 43 | Installs all project dependencies specified in [package.json](./package.json). 44 | 45 | ```sh 46 | npm install 47 | ``` 48 | 49 | ### Compiles and hot-reloads for development 50 | 51 | Automatically recompiles and refreshes the browser tab if you save any changes to local files. 52 | 53 | ```sh 54 | npm run serve 55 | ``` 56 | 57 | ### Compiles and minifies for production 58 | 59 | Builds the production-ready website into the `dist` directory. 60 | 61 | ```sh 62 | npm run build 63 | ``` 64 | 65 | ### Lints and fixes files 66 | 67 | ```sh 68 | npm run lint 69 | ``` 70 | 71 | * [JavaScript Standard Style](https://standardjs.com/rules-en.html) 72 | * [Are Semicolons Necessary in JavaScript? (8' background explanation)](https://youtu.be/gsfbh17Ax9I) 73 | 74 | > The Vue.js community [favors](https://forum.vuejs.org/t/semicolon-less-code-my-thoughts/4229) omitting optional semicolons `;` in Javascript. 75 | 76 | ## Axios HTTP Library 77 | 78 | * [Documentation with Examples](https://github.com/axios/axios#axios) 79 | 80 | ## Bootstrap 4 and BootstrapVue 81 | 82 | * [BootstrapVue Components](https://bootstrap-vue.js.org/docs/components) 83 | * [Layout and Grid System](https://bootstrap-vue.js.org/docs/components/layout/) 84 | * [Link](https://bootstrap-vue.js.org/docs/components/link) 85 | * [Button](https://bootstrap-vue.js.org/docs/components/button) 86 | * [Form](https://bootstrap-vue.js.org/docs/components/form) 87 | * [BootstrapVue Online Playground](https://bootstrap-vue.js.org/play/) 88 | 89 | > Plain [Bootstrap 4](https://getbootstrap.com/) uses a popular JS library called [jQuery](http://jquery.com/) for dynamic components (e.g., dropdowns). However, using jQuery with Vue is [problematic](https://vuejsdevelopers.com/2017/05/20/vue-js-safely-jquery-plugin/) and therefore we use BootstrapVue here. 90 | 91 | ## Debug in VSCode with Chrome 92 | 93 | 1. **[VSCode]** Set a breakpoint in your Javascript code 94 | 2. **[Terminal]** Run `npm run serve` to serve the client 95 | 3. **[VSCode]** Select *Debug > Start Debugging (F5)* to automatically start a debug session in Chrome[1](#1) 96 | 4. **[Chrome]** Browse in Chrome to trigger your breakpoint and the focus will jump back to VSCode 97 | 98 | Find illustrated instructions in the [Vuejs Debug Docs](https://vuejs.org/v2/cookbook/debugging-in-vscode.html). 99 | 100 | 1 Chrome will launch with a separate user profile (not to mess up with your familiar daily Chrome profile) in a temp folder as described in the VSCode [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome). It is recommended to install the [vue-devtools](https://github.com/vuejs/vue-devtools) [Chrome Extension](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) there. 101 | -------------------------------------------------------------------------------- /client/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 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.21.1", 12 | "bootstrap": "^4.6.0", 13 | "bootstrap-vue": "^2.21.2", 14 | "vue": "^2.6.14", 15 | "vue-router": "^3.5.2" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-eslint": "^4.5.13", 19 | "@vue/cli-service": "^4.5.13", 20 | "@vue/eslint-config-standard": "^6.1.0", 21 | "babel-eslint": "^10.1.0", 22 | "eslint": "^7.32.0", 23 | "eslint-plugin-import": "^2.24.1", 24 | "eslint-plugin-node": "^11.1.0", 25 | "eslint-plugin-promise": "^5.1.0", 26 | "eslint-plugin-standard": "^5.0.0", 27 | "eslint-plugin-vue": "^7.16.0", 28 | "vue-template-compiler": "^2.6.14" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | client 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /client/src/Api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const Api = axios.create({ 4 | baseURL: process.env.VUE_APP_API_ENDPOINT || 'http://localhost:3000/api' 5 | }) 6 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 24 | -------------------------------------------------------------------------------- /client/src/Images/Artsy-text-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/client/src/Images/Artsy-text-top.png -------------------------------------------------------------------------------- /client/src/Images/Artsy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/client/src/Images/Artsy.png -------------------------------------------------------------------------------- /client/src/Images/DefaultPostImagePreview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/client/src/Images/DefaultPostImagePreview.png -------------------------------------------------------------------------------- /client/src/Images/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/client/src/Images/cat.png -------------------------------------------------------------------------------- /client/src/Images/dog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/client/src/Images/dog.png -------------------------------------------------------------------------------- /client/src/Images/landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/client/src/Images/landscape.png -------------------------------------------------------------------------------- /client/src/Images/painting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/client/src/Images/painting.png -------------------------------------------------------------------------------- /client/src/Images/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/client/src/Images/trash.png -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue' 5 | 6 | import 'bootstrap/dist/css/bootstrap.css' 7 | import 'bootstrap-vue/dist/bootstrap-vue.css' 8 | 9 | Vue.use(BootstrapVue) 10 | Vue.use(BootstrapVueIcons) 11 | 12 | Vue.config.productionTip = false 13 | 14 | new Vue({ 15 | router, 16 | render: function (h) { return h(App) } 17 | }).$mount('#app') 18 | -------------------------------------------------------------------------------- /client/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from './views/Home.vue' 4 | import CreatePost from './views/CreatePost.vue' 5 | import Login from './views/Login.vue' 6 | import Register from './views/Register.vue' 7 | import User from './views/User.vue' 8 | import InsideCollection from './views/InsideCollection.vue' 9 | 10 | Vue.use(Router) 11 | 12 | export default new Router({ 13 | mode: 'history', 14 | base: process.env.BASE_URL, 15 | routes: [ 16 | { 17 | path: '/', 18 | name: 'home', 19 | component: Home 20 | }, 21 | { 22 | path: '/posts/create', 23 | name: 'create a post', 24 | component: CreatePost 25 | }, 26 | { 27 | path: '/login', 28 | name: 'login', 29 | component: Login 30 | }, 31 | { 32 | path: '/register', 33 | name: 'register', 34 | component: Register 35 | }, 36 | { 37 | path: '/user', 38 | name: 'user', 39 | component: User 40 | }, 41 | { 42 | path: '/users/:Uid/collection/:Cid', 43 | name: 'inside a collection', 44 | component: InsideCollection 45 | } 46 | ] 47 | }) 48 | -------------------------------------------------------------------------------- /client/src/views/CreatePost.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 69 | 70 | 246 | -------------------------------------------------------------------------------- /client/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 200 | 201 | 254 | -------------------------------------------------------------------------------- /client/src/views/InsideCollection.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 77 | 78 | 243 | -------------------------------------------------------------------------------- /client/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 116 | 117 | 124 | -------------------------------------------------------------------------------- /client/src/views/Register.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 205 | 206 | 213 | -------------------------------------------------------------------------------- /client/src/views/User.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 123 | 124 | 142 | -------------------------------------------------------------------------------- /client/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | configureWebpack: { 3 | devtool: 'source-map' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | These steps describe how you can deploy your app online for free (**NO** credit card required). 4 | 5 | ## Requirements 6 | 7 | * Free [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) account 8 | * Free [Heroku](https://www.heroku.com/) account 9 | * [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) (Windows users might need to restart their VSCode and/or computer to ensure that Heroku is in their PATH. For `bash: heroku: command not found`, checkout [this](https://stackoverflow.com/a/38746507/6875981) StackOverflow answer.) 10 | 11 | > All these services have a **free** tier and can be used **WITHOUT** a credit card. 12 | 13 | ## Setup Hosted MongoDB 14 | 15 | 1. Sign up for a free [MongoDB Atlas account](https://www.mongodb.com/cloud/atlas/register) 16 | 2. You will be forwarded to the *Create New Cluster* view. Otherwise, navigate to Clusters > Build a Cluster.
NOTE: Maybe you need to create an organization and project first. 17 | 3. Choose the cloud provider *aws* and the region *Ireland (eu-west-1)* (important for compatibility with Heroku!). Keep all other default settings (e.g., M0 Sandbox free tier, cluster name *Cluster0*) and click *Create Cluster* (takes a few minutes). 18 | 4. Click the *Connect* button 19 | 5. Click the *Add a Different IP Address* button, enter `0.0.0.0/0` for the IP Address and click *Add IP Address* button.
(**Warning:** limit IP addresses in real production deployments!) 20 | 6. Create a new Database user by entering *Username* and *Password* (avoid special characters for mongoose compatibility) and clicking the button *Create Database User*. 21 | 7. Continue with *Choose a connection method* 22 | 8. Choose *Connect Your Application* 23 | 9. Keep the default driver version (Node.js, 3.6 or later) and click the *Copy* button for the Connection String only. 24 | 10. Replace the placeholders `` with your Database user password (created in step 6.) and the database name `` with a sensible name for your application domain. Example: 25 | 26 | ```none 27 | mongodb+srv://myUser:mySecurePassword@cluster0-a1bc2.mongodb.net/animalProductionDB?retryWrites=true&w=majority 28 | ``` 29 | 30 | Find a more detailed tutorial [here](https://developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs/mongoose#Setting_up_the_MongoDB_database). 31 | 32 | ## Deploy to Heroku 33 | 34 | ### Setup 35 | 36 | Sign up for a free [Heroku account](https://signup.heroku.com/). 37 | 38 | Install the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli), login via `heroku login`, and follow these steps: 39 | 40 | ```bash 41 | cd group-00-web 42 | # Optional app name: heroku apps:create my-app-name --region eu 43 | heroku apps:create --region eu 44 | heroku config:set MONGODB_URI="mongodb+srv://myUser:mySecurePassword@cluster0-a1bc2.mongodb.net/animalProductionDB?retryWrites=true&w=majority" 45 | heroku config:set NODE_ENV="production" 46 | 47 | # MacOS, Linux 48 | export API_ENDPOINT="$(heroku apps:info -s | grep web_url | cut -d= -f2)api" 49 | heroku config:set VUE_APP_API_ENDPOINT=$API_ENDPOINT 50 | # Windows 51 | heroku config:set VUE_APP_API_ENDPOINT=web_url_to_your_herokuapp/api 52 | ``` 53 | 54 | To set the API_ENDPOINT, you can also manually extract the `web_url` from `heroku apps:info -s`. For example: `API_ENDPOINT=https://aqueous-crag-12345.herokuapp.com/api` 55 | 56 | > The app needs to be re-deployed whenever `VUE_APP_API_ENDPOINT` is updated (e.g., when the Heroku app name changed). Therefore, you might need to push a new commit to master to trigger a full re-deployment. 57 | 58 | ### Deploy 59 | 60 | ```bash 61 | git push heroku master 62 | heroku open 63 | ``` 64 | 65 | ### Debugging Heroku 66 | 67 | ```bash 68 | heroku logs # Show current logs 69 | heroku logs --tail # Show current logs and keep updating with any new results 70 | heroku ps # Display dyno status 71 | ``` 72 | 73 | ## Automatically Deploy to Heroku with GitLab 74 | 75 | This setup automatically deploys the latest commit in the master branch to Heroku if the tests succeed: 76 | 77 | 1. Open the GitLab settings `Settings > CI / CD > Variables` (e.g., https://git.chalmers.se/courses/dit341/group-00-web/-/settings/ci_cd) 78 | 2. Add a variable with the key `HEROKU_APP_NAME`. As value, enter the name of your app shown on the first line when running `heroku apps:info`. It is also visible in the Heroku app settings (https://dashboard.heroku.com/apps/your-app-name/settings). Example above: `aqueous-crag-12345` 79 | 3. Add a variable with the key `HEROKU_API_KEY`. As value, enter the `API Key` of your Heroku account in the Heroku account settings (https://dashboard.heroku.com/account). You can enable "Mask variable" to protect your secret API key. 80 | 81 | > Deployment will only be triggered for the master branch and when both HEROKU variables are configured. 82 | 83 | A HelloWorld tutorial can be found here: [Deploy Node.js App with GitLab CI/CD](https://medium.com/@seulkiro/deploy-node-js-app-with-gitlab-ci-cd-214d12bfeeb5) 84 | 85 | ## Further Readings on Production Deployment 86 | 87 | * [Express Tutorial Part 7: Deploying to production](https://developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs/deployment) 88 | * [Vue.js Production Deployment](https://vuejs.org/v2/guide/deployment.html) 89 | -------------------------------------------------------------------------------- /docs/LOCAL_DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # Local Deployment 2 | 3 | These steps describe how you can deploy your app locally in production mode if you do not want to deploy your app online for free (see [Deployment](./DEPLOYMENT.md)). 4 | 5 | ## Requirements 6 | 7 | * [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) 8 | * [MongoDB](https://www.mongodb.com/download-center/community?jmp=nav) (v4) must be running locally on port 27017 9 | 10 | ## Deploy Locally 11 | 12 | > Make sure you have all dependencies installed for the server and client (using `npm install`). 13 | 14 | 1. Change into the root directory `cd group-00-web` 15 | 2. Set the environment variable `NODE_ENV` for the server: 16 | * macOS/Linux: `export NODE_ENV=production` (check with `echo $NODE_ENV`) 17 | * Windows: `set NODE_ENV "production"` (check with `echo %NODE_ENV%`) 18 | 3. Set the environment variable `MONGODB_URI` for the server (change the database name "animals" according to your project): 19 | * macOS/Linux: `export MONGODB_URI=mongodb://localhost:27017/animalProductionDB` 20 | * Windows: `set MONGODB_URI "mongodb://localhost:27017/animalProductionDB"` 21 | 4. Set the environment variable `VUE_APP_API_ENDPOINT` for the client production build: 22 | * macOS/Linux: `export VUE_APP_API_ENDPOINT=http://localhost:5000/api` 23 | * Windows: `set VUE_APP_API_ENDPOINT "http://localhost:5000/api"` 24 | 5. Build the minified Vue.js production assets via `npm run build --prefix client` 25 | 6. [Run the Heroku app locally](https://devcenter.heroku.com/articles/heroku-local) via `heroku local` 26 | 27 | The terminal output should look like this (Heroku uses port 5000 by default): 28 | 29 | ```none 30 | ➜ group-00-web git:(master) ✗ heroku local 31 | 3:03:38 PM web.1 | Express server listening on port 5000, in production mode 32 | 3:03:38 PM web.1 | Backend: http://localhost:5000/api/ 33 | 3:03:38 PM web.1 | Frontend (production): http://localhost:5000/ 34 | 3:03:38 PM web.1 | Connected to MongoDB with URI: mongodb://localhost:27017/animals-production 35 | ``` 36 | -------------------------------------------------------------------------------- /images/er_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/images/er_diagram.png -------------------------------------------------------------------------------- /images/teaser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/images/teaser.png -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend-frontend", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend-frontend", 3 | "version": "0.1.0", 4 | "description": "This collection package integrates the server and client for deployment.", 5 | "main": "server/app.js", 6 | "scripts": { 7 | "postinstall": "npm install --prefix server && npm install --prefix client && npm install --only=dev --prefix client && npm run build --prefix client" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": "eslint:recommended", 6 | "parserOptions": { 7 | "ecmaVersion": 6 8 | }, 9 | "rules": { 10 | "no-console": "off", 11 | "indent": [ 12 | "error", 13 | 4 14 | ], 15 | "linebreak-style": [ 16 | "error", 17 | "unix" 18 | ], 19 | "quotes": [ 20 | "error", 21 | "single" 22 | ], 23 | "semi": [ 24 | "error", 25 | "always" 26 | ] 27 | } 28 | }; -------------------------------------------------------------------------------- /server/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug Server", 11 | "program": "${workspaceFolder}/app.js" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /server/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["javascript"], 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Server – ExpressJS Backend 2 | 3 | This [ExpressJS](https://expressjs.com/) template provides the basic infrastructure for a JSON API with MongoDB persistency with [Mongoose](https://mongoosejs.com/). 4 | 5 | ## Server Structure 6 | 7 | | File | Purpose | What you do? | 8 | | ------------- | ------------- | ----- | 9 | | [README.md](./README.md) | Everything about the server | **READ ME** carefully! | 10 | | [app.js](./app.js) | JavaScript entry point for Express application | Import new routes/controllers | 11 | | `controllers/` | Implementation of Express endpoints | Define new route handlers | 12 | | `models/` | [Mongoose](https://mongoosejs.com/) models | Define data schema | 13 | | [tests/server.postman_collection.json](tests/server.postman_collection.json) | [Postman test scripts](https://learning.postman.com/docs/postman/scripts/test-scripts/) | Replace with your exported Postman test collection | 14 | | [docs/FAQ.md](docs/FAQ.md) | List of FAQs | Find answers to common questions | 15 | | [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | List of problems and solutions | Find solutions for common error messages | 16 | | [package.json](package.json) | Project meta-information | — | 17 | 18 | > NOTE: The (mandatory) exercises are essential for understanding this template and will *save* you time! 19 | 20 | Optional: Learn how to create such a project template in this [tutorial](https://developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs/skeleton_website). 21 | 22 | ## Requirements 23 | 24 | * [Node.js](https://nodejs.org/en/download/) (v14) => installation instructions for [Linux](https://github.com/nodesource/distributions), use installers for macOS and Windows (don't forget to restart your Bash shell) 25 | * [MongoDB](https://www.mongodb.com/download-center/community?jmp=nav) (v4.4) must be running locally on port 27017 => installation instructions for [macOS](https://github.com/joe4dev/dit032-setup/blob/master/macOS.md#mongodb), [Windows](https://github.com/joe4dev/dit032-setup/blob/master/Windows.md#mongodb), [Linux](https://github.com/joe4dev/dit032-setup/blob/master/Linux.md#mongodb) 26 | * [Postman](https://www.getpostman.com/downloads/) (v8) for API testing 27 | 28 | ## Project setup 29 | 30 | Make sure, you are in the server directory `cd server` 31 | 32 | Installs all project dependencies specified in [package.json](./package.json). 33 | 34 | ```bash 35 | npm install 36 | ``` 37 | 38 | ## Start the server with auto-restarts for development 39 | 40 | Automatically restarts your server if you save any changes to local files. 41 | 42 | ```bash 43 | npm run dev 44 | ``` 45 | 46 | ## Start the server 47 | 48 | ```bash 49 | npm start 50 | ``` 51 | 52 | ## Run the Postman Tests 53 | 54 | Starts a new server on another port (default `3001`) and runs the `server` postman test collection against a test database (default `serverTestDB`). 55 | 56 | ```bash 57 | npm test 58 | ``` 59 | 60 | > The test database is dropped before each test execution. Adjust your tests to support this clean state. 61 | 62 | ## Postman Tests 63 | 64 | We use the API testing tool Postman to define example HTTP requests and test assertions. Your tests will be automatically executed in GitLab pipelines whenever you push to the `master` branch. Try to do that as often as possible. 65 | 66 | * [Set up Postman for your project](./docs/POSTMAN.md) 67 | 68 | > Remember to **export and commit** any test changes back to `tests/server.postman_collection.json` and make sure `npm test` succeeds for your final submission! 69 | 70 | ## Error Handling 71 | 72 | * [Error Handling in Node.js](https://www.joyent.com/node-js/production/design/errors) 73 | * [Error Handling in Express.js](https://expressjs.com/en/guide/error-handling.html) 74 | 75 | ## Debugging with VSCode 76 | 77 | 1. Set a breakpoint by clicking on the left side of a line number 78 | 2. Click *Run > Start Debugging* (Choose the "Debug Server" config if you opened the combined workspace) 79 | 80 | > Learn more in the [VSCode Debugging Docs](https://code.visualstudio.com/docs/editor/debugging). 81 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var mongoose = require('mongoose'); 3 | var morgan = require('morgan'); 4 | var path = require('path'); 5 | var cors = require('cors'); 6 | var history = require('connect-history-api-fallback'); 7 | var passport = require('passport'); 8 | var userController = require('./controllers/users'); 9 | var ratingController = require('./controllers/ratings'); 10 | var collectionController = require('./controllers/collections'); 11 | var multer = require('multer'); 12 | var userAuthentication = require('./controllers/userAuth'); 13 | 14 | 15 | // Variables 16 | var mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/animalDevelopmentDB'; 17 | var port = process.env.PORT || 3000; 18 | postController = require('./controllers/posts'); 19 | 20 | // Connect to MongoDB 21 | mongoose.connect(mongoURI, { useNewUrlParser: true, useUnifiedTopology: true }, function (err) { 22 | if (err) { 23 | console.error(`Failed to connect to MongoDB with URI: ${mongoURI}`); 24 | console.error(err.stack); 25 | process.exit(1); 26 | } 27 | console.log(`Connected to MongoDB with URI: ${mongoURI}`); 28 | }); 29 | 30 | // Create Express app 31 | var app = express(); 32 | 33 | 34 | 35 | app.use('/uploads', express.static('uploads')); // makes uploads folder public 36 | app.use('/icons', express.static('icons')); 37 | app.use('/thumbnails', express.static('thumbnails')); 38 | 39 | // Parse requests of content-type 'application/json' 40 | app.use(express.urlencoded({ extended: true })); 41 | app.use(express.json()); 42 | // HTTP request logger 43 | app.use(morgan('dev')); 44 | // Enable cross-origin resource sharing for frontend must be registered before api 45 | app.options('*', cors()); 46 | app.use(cors()); 47 | 48 | // Import routes 49 | app.get('/api', function (req, res) { 50 | res.json({ 'message': 'Welcome to your DIT341 backend ExpressJS project!' }); 51 | }); 52 | 53 | app.use(postController); 54 | app.use(ratingController); 55 | app.use(userController); 56 | app.use(collectionController); 57 | app.use(userAuthentication); 58 | 59 | //Passport middleware 60 | app.use(passport.initialize()); 61 | 62 | // Catch all non-error handler for api (i.e., 404 Not Found) 63 | app.use('/api/*', function (req, res) { 64 | res.status(404).json({ 'message': 'Not Found' }); 65 | }); 66 | 67 | // Configuration for serving frontend in production mode 68 | // Support Vuejs HTML 5 history mode 69 | app.use(history()); 70 | // Serve static assets 71 | var root = path.normalize(__dirname + '/..'); 72 | var client = path.join(root, 'client', 'dist'); 73 | app.use(express.static(client)); 74 | 75 | 76 | 77 | // Error handler (i.e., when exception is thrown) must be registered last 78 | var env = app.get('env'); 79 | // eslint-disable-next-line no-unused-vars 80 | app.use(function (err, req, res, next) { 81 | console.error(err.stack); 82 | var err_res = { 83 | 'message': err.message, 84 | 'error': {} 85 | }; 86 | if (env === 'development') { 87 | // Return sensitive stack trace only in dev mode 88 | err_res['error'] = err.stack; 89 | } 90 | // Check for multer image handling errors 91 | if (err instanceof multer.MulterError) { 92 | if (err.code === 'LIMIT_FILE_SIZE') { 93 | err.status = 413; 94 | } 95 | if (err.code === 'LIMIT_UNEXPECTED_FILE') { 96 | err.status = 422; 97 | } 98 | } 99 | console.log('error name: ' + err.name); 100 | res.status(err.status || 500); 101 | res.json(err_res); 102 | }); 103 | 104 | app.listen(port, function (err) { 105 | if (err) throw err; 106 | console.log(`Express server listening on port ${port}, in ${env} mode`); 107 | console.log(`Backend: http://localhost:${port}/api/`); 108 | console.log(`Frontend (production): http://localhost:${port}/`); 109 | }); 110 | 111 | module.exports = app; -------------------------------------------------------------------------------- /server/controllers/collections.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var imgUpload = require('../image_handling/imageUploadHandler'); 4 | var imgDelete = require('../image_handling/imageDeleteHandler'); 5 | var Collection = require('../models/collection'); 6 | var User = require('../models/user') 7 | var mongoose = require('mongoose'); 8 | 9 | router.use(express.json()); 10 | 11 | router.post('/api/users/:userID/collections', imgUpload.none(), function (req, res, next) { 12 | var userID = req.params.userID; 13 | var collection = new Collection(req.body); 14 | User.findById(userID, function (err, user) { 15 | if (err) { 16 | if (err instanceof mongoose.CastError) { 17 | err.status = 400; 18 | err.message = 'Invalid user ID'; 19 | } 20 | return next(err); 21 | } 22 | if (user == null) { 23 | var err = new Error('No User found'); 24 | err.status = 404; 25 | return next(err); 26 | } 27 | collection.save(function (err, collection) { 28 | if (err) { 29 | if (err.name == 'ValidationError') { 30 | err.message = 'ValidationError. Incorrect data input.'; 31 | err.status = 422; 32 | } 33 | return next(err); 34 | } 35 | user.collections.push(collection._id); 36 | user.save(); 37 | console.log('Collection created'); 38 | return res.status(201).json(collection); 39 | }) 40 | }) 41 | }); 42 | 43 | router.get("/api/users/:userID/collections", function (req, res, next) { 44 | var userID = req.params.userID; 45 | User.findById(userID, function (err, user) { 46 | if (err) { 47 | return next(err); 48 | } 49 | }).populate('collections').exec(function (err, user) { 50 | if (err) { 51 | if (err instanceof mongoose.CastError) { 52 | err.status = 400; 53 | err.message = 'Invalid user ID'; 54 | } 55 | return next(err); 56 | } 57 | if (user == null) { 58 | var err = new Error('No User found'); 59 | err.status = 404; 60 | return next(err); 61 | } 62 | if (user.collections.length == 0) { 63 | var err = new Error('No user collections found'); 64 | err.status = 404; 65 | return next(err); 66 | } 67 | console.log(`User collections retrieved`); 68 | res.status(200).json(user); 69 | }); 70 | }); 71 | 72 | router.get("/api/users/:userID/collections/:collectionID", function (req, res, next) { 73 | var userID = req.params.userID; 74 | var collectionID = req.params.collectionID; 75 | User.findOne({ _id: userID }, { "collections": collectionID }) 76 | .populate("collections").exec(function (err, user) { 77 | if (err) { 78 | if (err instanceof mongoose.CastError) { 79 | err.status = 400; 80 | err.message = 'Invalid user ID or collection ID'; 81 | } 82 | return next(err); 83 | } 84 | if (user == null) { 85 | var err = new Error('No User found'); 86 | err.status = 404; 87 | return next(err); 88 | } 89 | console.log('User specific collection retreived'); 90 | res.status(200).json(user); 91 | }); 92 | }); 93 | 94 | router.put("/api/collections/:id", imgUpload.single('thumbnail'), function (req, res, next) { 95 | var id = req.params.id; 96 | Collection.findById(id, function (err, collection) { 97 | if (err) { 98 | if (err instanceof mongoose.CastError) { 99 | err.status = 400; 100 | err.message = 'Invalid collection ID'; 101 | } 102 | return next(err); 103 | } 104 | if (collection == null) { 105 | var err = new Error('No collection found'); 106 | err.status = 404; 107 | return next(err); 108 | } 109 | collection.title = req.body.title; 110 | collection.event = req.body.event; 111 | try { 112 | collection.thumbnail = req.file.path; 113 | } catch (err) { 114 | if (err instanceof TypeError) { 115 | err.status = 422; 116 | err.message = 'Input error, Thumbnail was not found'; 117 | return next(err); 118 | } 119 | } 120 | collection.save(function (err, collection) { 121 | if (err) { 122 | if (err.name == 'ValidationError') { 123 | err.message = 'ValidationError. Incorrect data input.'; 124 | err.status = 422; 125 | } 126 | return next(err); 127 | } 128 | res.status(200).json(collection); 129 | console.log("collection saved"); 130 | }); 131 | }); 132 | }); 133 | 134 | router.patch("/api/collections/:id", function (req, res, next) { 135 | var id = req.params.id; 136 | Collection.findById(id, function (err, collection) { 137 | if (err) { 138 | if (err instanceof mongoose.CastError) { 139 | err.status = 400; 140 | err.message = 'Invalid collection ID'; 141 | } 142 | return next(err); 143 | } 144 | if (collection == null) { 145 | var err = new Error('No collection found'); 146 | err.status = 404; 147 | return next(err); 148 | } 149 | collection.title = (req.body.title || collection.title); 150 | var postId = (req.body.post_id || null) 151 | if (postId != null) { 152 | collection.post_id.push(postId) 153 | } 154 | collection.save(function (err, collection) { 155 | if (err) { 156 | if (err.name == 'ValidationError') { 157 | err.message = 'ValidationError. Incorrect data input.'; 158 | err.status = 422; 159 | } 160 | return next(err); 161 | } 162 | res.status(200).json(collection); 163 | console.log("collection updated"); 164 | }); 165 | }); 166 | }); 167 | 168 | router.delete("/api/collections/:id", async function (req, res, next) { 169 | var id = req.params.id; 170 | Collection.findOneAndDelete({ _id: id }, async function (err, collection) { 171 | if (err) { 172 | if (err instanceof mongoose.CastError) { 173 | err.status = 400; 174 | err.message = 'Invalid collection ID'; 175 | } 176 | return next(err); 177 | } 178 | if (collection == null) { 179 | var err = new Error('No collection found'); 180 | err.status = 404; 181 | return next(err); 182 | } 183 | try { 184 | collection.remove(); 185 | await imgDelete.deleteSingleImage(collection.thumbnail); 186 | res.status(200).json(collection); 187 | console.log('specific collection deleted'); 188 | } catch (err) { 189 | next(err); 190 | } 191 | }); 192 | }); 193 | 194 | //DELETE ALL COLLECTIONS FOR TESTING PURPOSES 195 | router.delete("/api/collections", async function (req, res, next) { 196 | Collection.deleteMany({}, async function (err, deleteInformation) { 197 | if (err) { 198 | return next(err); 199 | } 200 | if (deleteInformation.n == 0) { 201 | var err = new Error('No collections were found'); 202 | err.status = 404; 203 | return next(err); 204 | } 205 | try { 206 | await imgDelete.deleteAllImages('./thumbnails/'); 207 | res.status(200).json(deleteInformation); 208 | console.log('all collections deleted'); 209 | } catch (err) { 210 | next(err); 211 | } 212 | }); 213 | }); 214 | 215 | module.exports = router; -------------------------------------------------------------------------------- /server/controllers/posts.js: -------------------------------------------------------------------------------- 1 | /** NOTE: Image handling was referenced from the following links: 2 | * https://www.youtube.com/watch?v=srPXMt1Q0nY 3 | * https://github.com/expressjs/multer#error-handling 4 | * https://stackoverflow.com/questions/27072866/how-to-remove-all-files-from-directory-without-removing-directory-in-node-js/49125621 5 | * https://stackoverflow.com/questions/31592726/how-to-store-a-file-with-file-extension-with-multer 6 | * https://www.tabnine.com/code/javascript/functions/fs%2Fpromises/unlink 7 | * https://stackoverflow.com/questions/48842006/to-use-multer-to-save-files-in-different-folders 8 | */ 9 | 10 | var express = require('express'); 11 | var router = express.Router(); 12 | var Post = require('../models/post'); 13 | var Rating = require('../models/rating'); 14 | var imgUpload = require('../image_handling/imageUploadHandler'); 15 | var imgDelete = require('../image_handling/imageDeleteHandler'); 16 | var mongoose = require('mongoose'); 17 | var multer = require('multer'); 18 | 19 | 20 | router.use(express.json()); 21 | 22 | function queryByTag(tag, req, res, next) { 23 | Post.find({ tags: { $all: tag } }, function (err, posts) { 24 | if (err) { return next(err); } 25 | }).populate('user_id').exec(function (err, posts) { 26 | if (err) { return next(err); } 27 | if (posts.length == 0) { 28 | var err = new Error('No post with tag: ' + tag + ' found'); 29 | err.status = 404; 30 | return next(err); 31 | } 32 | console.log('Posts with specified tag retreived'); 33 | res.status(200).json({ "posts": posts }); 34 | });; 35 | } 36 | 37 | router.post('/api/posts', imgUpload.single('image') ,function (req, res, next) { 38 | //NOTE: When creating a post, the event variable has to be passed before the image! 39 | var post = new Post(req.body); 40 | post.post_id = mongoose.Types.ObjectId(); 41 | try { 42 | post.image = req.file.path; 43 | } catch (err){ 44 | if (err instanceof TypeError){ 45 | err.status = 422; 46 | err.message = 'Input error, Image was not found'; 47 | return next(err); 48 | } 49 | } 50 | // This check is necessary to ensure that there is a user_id when creating a post, since a post 51 | // needs a user when creating it. If the user is deleted, the post will still remain (hence the manual check). 52 | if (!req.body.user_id){ 53 | var err = new Error('ValidationError: Missing user_id when creating a post'); 54 | err.status = 422; 55 | return next(err); 56 | } 57 | post.save(function (err, post) { 58 | if (err) { 59 | if ( err.name == 'ValidationError' ) { 60 | err.message = 'ValidationError. Incorrect data input.'; 61 | err.status = 422; 62 | } 63 | return next(err); 64 | } 65 | res.status(201).json(post); 66 | }); 67 | }); 68 | 69 | router.get('/api/posts', function (req, res, next) { 70 | if (req.query.tag != null) { 71 | var tag = req.query.tag; 72 | queryByTag(tag, req, res, next); 73 | } else { 74 | Post.find(function (err, posts) { 75 | if (err) { return next(err); } 76 | }).populate('user_id').exec(function (err, posts) { 77 | if (err) { return next(err); } 78 | if (posts.length == 0) { 79 | var err = new Error('No posts found'); 80 | err.status = 404; 81 | return next(err); 82 | } 83 | console.log('posts retreived'); 84 | res.status(200).json({ "posts": posts }); 85 | }); 86 | } 87 | }); 88 | 89 | router.get('/api/posts/:id', function (req, res, next) { 90 | var id = req.params.id; 91 | Post.findById(id, function (err, post) { 92 | if (err) { return next(err); } 93 | }).populate('user_id').exec(function (err, post) { 94 | if (err) { 95 | if (err instanceof mongoose.CastError){ 96 | err.status = 400; 97 | err.message = 'Invalid post ID'; 98 | } 99 | return next(err); 100 | } 101 | if (post == null) { 102 | var err = new Error('No Post found'); 103 | err.status = 404; 104 | return next(err); 105 | } 106 | console.log('Post with specified id retreived'); 107 | res.status(200).json(post); 108 | }); 109 | }); 110 | 111 | router.put('/api/posts/:id', function (req, res, next) { 112 | var id = req.params.id; 113 | Post.findById(id, function (err, post) { 114 | if (err) { 115 | if (err instanceof mongoose.CastError){ 116 | err.status = 400; 117 | err.message = 'Invalid post ID'; 118 | } 119 | return next(err); 120 | } 121 | if (post == null) { 122 | var err = new Error('Post not found'); 123 | err.status = 404; 124 | return next(err) 125 | } 126 | post.title = req.body.title; 127 | post.description = req.body.description; 128 | post.numberOfFavorites = req.body.numberOfFavorites; 129 | post.tags = req.body.tags; 130 | post.save(function(err, post) { 131 | if (err) { 132 | if ( err.name == 'ValidationError' ) { 133 | err.message = 'ValidationError. Incorrect data input.'; 134 | err.status = 422; 135 | } 136 | return next(err); 137 | } 138 | res.status(200).json(post); 139 | console.log('post saved'); 140 | }); 141 | }); 142 | }); 143 | 144 | router.patch('/api/posts/:id', function (req, res, next) { 145 | var id = req.params.id; 146 | Post.findById(id, function (err, post) { 147 | if (err) { 148 | if (err instanceof mongoose.CastError){ 149 | err.status = 400; 150 | err.message = 'Invalid post ID'; 151 | } 152 | return next(err); 153 | } 154 | if (post == null) { 155 | var err = new Error('Post not found'); 156 | err.status = 404; 157 | return next(err) 158 | } 159 | post.title = (req.body.title || post.title); 160 | post.description = (req.body.description || post.description); 161 | post.numberOfFavorites = (req.body.numberOfFavorites || post.numberOfFavorites); 162 | post.tags = (req.body.tags || post.tags); 163 | post.save( function (err, post) { 164 | if (err) { 165 | if ( err.name == 'ValidationError' ) { 166 | err.message = 'ValidationError. Incorrect data input.'; 167 | err.status = 422; 168 | } 169 | return next(err); 170 | } 171 | res.status(200).json(post); 172 | console.log('post saved'); 173 | }); 174 | }); 175 | }); 176 | 177 | router.delete('/api/posts/:id', async function (req, res, next) { 178 | var id = req.params.id; 179 | Post.findOneAndDelete({ _id: id }, async function (err, post) { 180 | if (err) { 181 | if (err instanceof mongoose.CastError){ 182 | err.status = 400; 183 | err.message = 'Invalid post ID'; 184 | } 185 | return next(err); 186 | } 187 | if (post == null) { 188 | var err = new Error('Post not found'); 189 | err.status = 404; 190 | return next(err); 191 | } 192 | try { 193 | await imgDelete.deleteSingleImage(post.image); 194 | post.remove(); 195 | res.status(200).json(post); 196 | console.log('specific post deleted'); 197 | } catch (err) { 198 | next(err); 199 | } 200 | }); 201 | }); 202 | 203 | //DELETE ALL POSTS FOR TESTING PURPOSES 204 | router.delete('/api/posts', async function (req, res, next) { 205 | Post.deleteMany({}, async function (err, deleteInformation) { 206 | if (err) { return next(err); } 207 | if (deleteInformation.n == 0) { 208 | var err = new Error('No posts were found'); 209 | err.status = 404; 210 | return next(err); 211 | } 212 | try { 213 | await imgDelete.deleteAllImages('./uploads/'); 214 | await Rating.deleteMany(); 215 | res.status(200).json(deleteInformation); 216 | console.log('All posts deleted'); 217 | } catch (err) { 218 | next(err); 219 | } 220 | }); 221 | }); 222 | 223 | module.exports = router; 224 | 225 | -------------------------------------------------------------------------------- /server/controllers/ratings.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var Post = require('../models/post'); 4 | var Rating = require('../models/rating'); 5 | var User = require('../models/user') 6 | var mongoose = require('mongoose'); 7 | 8 | router.use(express.json()); 9 | 10 | router.post('/api/posts/:id/ratings', function (req, res, next) { 11 | var postId = req.params.id; 12 | var userId = req.body.user; 13 | var rating = new Rating(req.body); 14 | User.findById(userId, function(err, user){ 15 | if (err) { return next(err) } 16 | if (user === null) { return res.status(404).json({ message: "User not found" }); } 17 | Post.findById(postId, function(err, post){ 18 | if (err) { return next(err) } 19 | if (post === null) { return res.status(404).json({ message: "Post not found" }); } 20 | rating.post = postId; 21 | rating.save(function (err, rating) { 22 | if (err) { return next(err) } 23 | post.ratings.push(rating._id); 24 | post.save(); 25 | console.log('Rating created'); 26 | return res.status(201).json(rating); 27 | }); 28 | }) 29 | }) 30 | }); 31 | 32 | router.patch('/api/ratings/:id', function (req, res, next) { 33 | var id = req.params.id; 34 | Rating.findById(id, function (err, rating) { 35 | if (err) { return next(err); } 36 | if (rating == null) { return res.status(404).json({ message: "Rating not found"}); } 37 | // It makes sense to patch only the score, rather than which post or user the rating belongs to 38 | rating.starRating = (req.body.starRating || rating.starRating); 39 | rating.save(); 40 | console.log('Rating saved'); 41 | return res.status(200).json(rating); 42 | }); 43 | }); 44 | 45 | router.get('/api/ratings', function (req, res, next) { 46 | Rating.find(function (err, ratings) { 47 | if (err) { return next(err); } 48 | if (ratings.length == 0) { return res.status(404).json({ message: "Ratings not found"}); } 49 | console.log('Ratings retreived'); 50 | return res.status(200).json({"ratings": ratings}); 51 | }) 52 | .populate('user') 53 | .populate('post'); 54 | }); 55 | 56 | router.delete('/api/posts/:postId/ratings/:ratingId', function (req, res, next) { 57 | var postId = req.params.postId; 58 | var ratingId = req.params.ratingId; 59 | Post.findById(postId, function(err, post){ 60 | if (err) { return next(err) } 61 | if (post === null) { return res.status(404).json({ message: "Post not found" }); } 62 | Rating.findOneAndDelete({ _id: ratingId}, async function(err, rating) { 63 | if (err) { return next(err); } 64 | if (rating == null) { return res.status(404).json({ message: "Rating not found" }); } 65 | try { 66 | await Post.updateOne({_id: post._id}, { $pullAll: {ratings: [rating._id] }} ); 67 | res.status(200).json(rating); 68 | console.log('Specific rating deleted'); 69 | } catch (err) { 70 | next(err); 71 | } 72 | }); 73 | }) 74 | }); 75 | 76 | module.exports = router; 77 | -------------------------------------------------------------------------------- /server/controllers/userAuth.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var User = require('../models/user'); 3 | var router = express.Router(); 4 | var imgUpload = require('../image_handling/imageUploadHandler'); 5 | var passportJWT = require('passport-jwt'); 6 | var jwt = require('jsonwebtoken'); 7 | var ExtractJwt = passportJWT.ExtractJwt; 8 | var jwtOptions = {}; 9 | jwtOptions.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme('jwt'); 10 | jwtOptions.secretOrKey = 'thisisthesecretkey'; 11 | 12 | //Register a user 13 | router.post('/api/usersAuth/register', imgUpload.single('icon'), (req, res, next) => { 14 | var username = req.body.username; 15 | var bio = req.body.bio; 16 | var password = req.body.password; 17 | var event = req.body.event; 18 | console.log(`here ${process.cwd()}`) 19 | try { 20 | var icon = req.file.path; 21 | } catch (err) { 22 | if (err instanceof TypeError) { 23 | err.status = 422; 24 | err.message = 'Input error, Icon was not found'; 25 | return next(err); 26 | } 27 | } 28 | var collections = req.body.collections; 29 | var newUser = new User({ 30 | username, 31 | bio, 32 | password, 33 | event, 34 | icon, 35 | collections 36 | }); 37 | User.createUser(newUser, (error, user) => { 38 | if (error) { 39 | res.status(422).json({ 40 | message: 'Something went wrong. Please try again after some time!', 41 | }); 42 | } 43 | res.status(201).json(user); 44 | }); 45 | }); 46 | 47 | //User login 48 | router.post('/api/usersAuth/login', (req, res) => { 49 | if (req.body.username && req.body.password) { 50 | var username = req.body.username; 51 | var password = req.body.password; 52 | User.getUserByUsername(username, (err, user) => { 53 | if (!user) { 54 | res.status(404).json({ message: 'The user does not exist!' }); 55 | } else { 56 | User.comparePassword(password, user.password, (error, isMatch) => { 57 | if (error) console.log(error); 58 | if (isMatch) { 59 | var payload = { id: user }; 60 | var token = jwt.sign(payload, jwtOptions.secretOrKey); 61 | res.json({ message: 'ok', token }); 62 | } else { 63 | res.status(401).json({ message: 'The password is incorrect!' }); 64 | } 65 | }); 66 | } 67 | }); 68 | } 69 | }); 70 | 71 | const checkToken = (req, res, next) => { 72 | const header = req.headers['authorization']; 73 | 74 | if (typeof header !== 'undefined') { 75 | const bearer = header.split(' '); 76 | const token = bearer[1]; 77 | 78 | req.token = token; 79 | next(); 80 | } else { 81 | res.sendStatus(403); 82 | } 83 | } 84 | 85 | router.get('/api/usersAuth/data', checkToken, (req, res) => { 86 | jwt.verify(req.token, jwtOptions.secretOrKey, (err, authorizedData) => { 87 | if (err) { 88 | console.log('ERROR: Could not connect to the protect route'); 89 | res.sendStatus(403); 90 | } else { 91 | res.json({ 92 | message: 'Successful log in', 93 | authorizedData 94 | }); 95 | console.log('SUCCESS: Connected to the protected route'); 96 | } 97 | }) 98 | }) 99 | 100 | module.exports = router; -------------------------------------------------------------------------------- /server/controllers/users.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var User = require('../models/user'); 4 | var imgUpload = require('../image_handling/imageUploadHandler'); 5 | var imgDelete = require('../image_handling/imageDeleteHandler'); 6 | var mongoose = require('mongoose'); 7 | var Collection = require('../models/collection'); 8 | 9 | router.use(express.json()); 10 | 11 | router.post("/api/users", imgUpload.single('icon'), function (req, res, next) { 12 | //NOTE: When creating a user, the event variable has to be passed before the image! 13 | var user = new User(req.body); 14 | try { 15 | user.icon = req.file.path; 16 | } catch (err) { 17 | if (err instanceof TypeError) { 18 | err.status = 422; 19 | err.message = 'Input error, Icon was not found'; 20 | return next(err); 21 | } 22 | } 23 | user.save(function (err, user) { 24 | if (err) { 25 | if (err.name == 'ValidationError') { 26 | err.message = 'ValidationError. Incorrect data input.'; 27 | err.status = 422; 28 | } else if (err.code === 11000) { 29 | err.status = 409; 30 | err.message = 'Username already exists!' 31 | } 32 | return next(err); 33 | } 34 | console.log('user created'); 35 | res.status(201).json(user); 36 | }); 37 | }); 38 | 39 | router.get("/api/users", function (req, res, next) { 40 | User.find(function (err, user) { 41 | if (err) { 42 | return next(err); 43 | } 44 | console.log('user retreived'); 45 | }).populate('collections').exec(function (err, user) { 46 | if (err) { 47 | return next(err); 48 | } 49 | if (user.length == 0) { 50 | var err = new Error('No users found'); 51 | err.status = 404; 52 | return next(err); 53 | } 54 | console.log(`User Collections`); 55 | res.status(200).json({ "users": user }); 56 | }); 57 | }); 58 | 59 | router.get("/api/users/:id", function (req, res, next) { 60 | var id = req.params.id; 61 | User.findById(id, function (err, user) { 62 | if (err) { 63 | if (err instanceof mongoose.CastError) { 64 | err.status = 400; 65 | err.message = 'Invalid user ID'; 66 | } 67 | return next(err); 68 | } 69 | if (user == null) { 70 | var err = new Error(`No user found`); 71 | err.status = 404; 72 | return next(err); 73 | } 74 | console.log('user with specified id retreived'); 75 | res.status(200).json(user); 76 | }); 77 | }); 78 | 79 | router.put("/api/users/:id", function (req, res, next) { 80 | var id = req.params.id; 81 | User.findById(id, function (err, user) { 82 | if (err) { 83 | if (err instanceof mongoose.CastError) { 84 | err.status = 400; 85 | err.message = 'Invalid user ID'; 86 | } 87 | return next(err); 88 | } 89 | if (user == null) { 90 | var err = new Error('User not found'); 91 | err.status = 404; 92 | return next(err); 93 | } 94 | user.username = req.body.username; 95 | user.password = req.body.password; 96 | user.bio = req.body.bio; 97 | user.event = req.body.event 98 | user.collections = req.body.collections; 99 | user.save(async function (err, user) { 100 | if (err) { 101 | if (err.name == 'ValidationError') { 102 | err.message = 'ValidationError. Incorrect data input.'; 103 | err.status = 422; 104 | } else if (err.code === 11000) { 105 | err.status = 409; 106 | err.message = 'Username already exists!' 107 | } 108 | return next(err); 109 | } 110 | if (user.collections == req.body.collections && !(!(user.collections))) { 111 | var error = null; 112 | for (var i = 0; i < user.collections.length; i++) { 113 | await Collection.findById(user.collections[i], async function (err, collection) { 114 | if (collection == null) { 115 | error = new Error('Collection not found!'); 116 | error.status = 404; 117 | } 118 | }); 119 | } 120 | if (error != null) { 121 | return next(error) 122 | } 123 | } 124 | res.status(200).json(user); 125 | console.log('user saved'); 126 | }); 127 | }); 128 | }); 129 | 130 | router.patch("/api/users/:id", function (req, res, next) { 131 | var id = req.params.id; 132 | User.findById(id, function (err, user) { 133 | if (err) { 134 | if (err instanceof mongoose.CastError) { 135 | err.status = 400; 136 | err.message = 'Invalid user ID'; 137 | } 138 | return next(err); 139 | } 140 | if (user == null) { 141 | var err = new Error('User not found'); 142 | err.status = 404; 143 | return next(err) 144 | } 145 | user.username = (req.body.username || user.username); 146 | user.password = (req.body.password || user.password); 147 | user.bio = (req.body.bio || user.bio); 148 | var collectionID = (req.body.collections || null); 149 | if (collectionID != null) { 150 | try { 151 | user.collections.push(collectionID); 152 | } catch (err) { 153 | if (err instanceof mongoose.CastError) { 154 | err.status = 422; 155 | err.message = 'ValidationError. Incorrect data input.'; 156 | return next(err); 157 | } 158 | } 159 | } 160 | user.save(async function (err, user) { 161 | if (err) { 162 | if (err.name == 'ValidationError') { 163 | err.message = 'ValidationError. Incorrect data input.'; 164 | err.status = 422; 165 | } else if (err.code === 11000) { 166 | err.status = 409; 167 | err.message = 'Username already exists!' 168 | } 169 | return next(err); 170 | } 171 | if ((collectionID != null)) { 172 | var error = null; 173 | await Collection.findById(collectionID, async function (err, collection) { 174 | if (collection == null) { 175 | error = new Error('Collection not found!'); 176 | error.status = 404; 177 | } 178 | }); 179 | if (error != null) { 180 | return next(error) 181 | } 182 | } 183 | res.status(200).json(user); 184 | console.log('user updated'); 185 | }); 186 | }); 187 | }); 188 | 189 | router.delete("/api/users/:id", async function (req, res, next) { 190 | var id = req.params.id; 191 | User.findOneAndDelete({ _id: id }, async function (err, user) { 192 | if (err) { 193 | if (err instanceof mongoose.CastError) { 194 | err.status = 400; 195 | err.message = 'Invalid post ID'; 196 | } 197 | return next(err); 198 | } 199 | if (user == null) { 200 | var err = new Error('User not found'); 201 | err.status = 404; 202 | return next(err); 203 | } 204 | try { 205 | await imgDelete.deleteSingleImage(user.icon); 206 | user.remove(); 207 | res.status(200).json(user); 208 | console.log('User with specific ID removed'); 209 | } catch (err) { 210 | next(err); 211 | } 212 | }); 213 | }); 214 | 215 | // DELETE ALL USERS FOR TESTING PURPOSES 216 | router.delete("/api/users", async function (req, res, next) { 217 | User.deleteMany({}, async function (err, deleteInformation) { 218 | if (err) { 219 | return next(err); 220 | } 221 | if (deleteInformation.n == 0) { 222 | var err = new Error('No users were found'); 223 | err.status = 404; 224 | return next(err); 225 | } 226 | try { 227 | await imgDelete.deleteAllImages('./icons/') 228 | res.status(200).json(deleteInformation); 229 | console.log('all users deleted'); 230 | } catch (err) { 231 | next(err); 232 | } 233 | }); 234 | }); 235 | 236 | module.exports = router; -------------------------------------------------------------------------------- /server/docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## How to parse URI query string parameters? 4 | 5 | ```none 6 | GET /shoes?order=desc&shoe[color]=blue&shoe[type]=converse 7 | ``` 8 | 9 | The Express parser provides the query string parameters in `req.query` 10 | 11 | ```js 12 | req.query.order 13 | // => "desc" 14 | 15 | req.query.shoe.color 16 | // => "blue" 17 | 18 | req.query.shoe.type 19 | // => "converse" 20 | ``` 21 | -------------------------------------------------------------------------------- /server/docs/POSTMAN.md: -------------------------------------------------------------------------------- 1 | # Postman: What you need to know for your project 2 | 3 | ## Getting Started 4 | 5 | * [Postman tutorial](https://www.guru99.com/postman-tutorial.html) 6 | * [Postman docs](https://learning.postman.com/docs/getting-started/sending-the-first-request/) 7 | 8 | ## Free Online HTTP Test Services (to play around) 9 | 10 | * [JSONPlaceholder](https://jsonplaceholder.typicode.com/) 11 | * [Postman Echo](https://docs.postman-echo.com/) 12 | 13 | ## Import a Test Collection 14 | 15 | 1. File > Import ... 16 | 17 | ![Postman import](./img/postman_import.png) 18 | 19 | 2. Choose file [tests/server.postman_collection.json](../tests/server.postman_collection.json) 20 | 21 | ![Postman choose file](./img/postman_choose.png) 22 | 23 | ## Run a Test Collection 24 | 25 | 1. Create a new empty environment in Postman in the top right corner via `Add` 26 | 27 | ![Postman new environment](./img/postman_env.png) 28 | 29 | 2. Start your backend server in the terminal via `npm run dev` 30 | 3. Run the collection 31 | 32 | ![Postman run collection](./img/postman_run.png) 33 | 34 | 4. Select your empty environment and click `Run server` 35 | 36 | ![Postman collection runner](./img/postman_runner.png) 37 | 38 | ## Export Test Collection 39 | 40 | 1. In the extended menu `...` of your collection, click `Export` 41 | 42 | ![Postman export button](./img/postman_export.png) 43 | 44 | 2. Choose the latest export format (v2.1) 45 | 46 | ![Postman export format](./img/postman_export_format.png) 47 | 48 | 3. *Overwrite* the file [tests/server.postman_collection.json](../tests/server.postman_collection.json) 49 | 50 | ![Postman overwrite file](./img/postman_export_save.png) 51 | 52 | 4. Run `npm test` in your terminal 53 | 54 | ![Postman run tests](./img/test_run.png) 55 | 56 | ## Test script assertions 57 | 58 | * [Test script examples](https://learning.postman.com/docs/writing-scripts/script-references/test-examples/) 59 | 60 | ## Chaining Requests via Postman Variables 61 | 62 | > Make sure your test collection works on an empty database! 63 | 64 | Whenever you create an object that you want to use later (e.g., to retrieve, update, delete), you need to save its `_id` to a Postman environment variable for later re-use: 65 | 66 | 1. Save the `_id` of created objects for later request via `pm.environment.set("camel_id", jsonData._id);` 67 | 68 | ![Postman save id](./img/postman_variable_save.png) 69 | 70 | 2. Use variables in *later* requests in Postman via `{{camel_id}}` or in Postman scripts or via `pm.variables.get("camel_id");` 71 | 72 | ![Postman use variable](./img/postman_variable_use.png) 73 | 74 | Check out the following documentation for more info: 75 | 76 | * [Extracting data from responses and chaining requests](http://blog.getpostman.com/2014/01/27/extracting-data-from-responses-and-chaining-requests/) 77 | * NOTE: Uses the deprecated environment variable syntax. Replace `postman.setEnvironmentVariable(...)` with `pm.environment.set(...)` 78 | * [Example in the express-rest-api tutorial used in the lecture](https://git.chalmers.se/courses/dit341/express-rest-api) 79 | * [Postman detailed docs about variables](https://learning.postman.com/docs/sending-requests/variables/) 80 | -------------------------------------------------------------------------------- /server/docs/TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | This page summarizes common issues and their solutions. 4 | 5 | ## ENOENT: no such file or directory package.json 6 | 7 | When trying to `npm install` 8 | 9 | ```none 10 | npm WARN saveError ENOENT: no such file or directory, open '/Users/joe/Projects/Web/package.json' 11 | npm notice created a lockfile as package-lock.json. You should commit this file. 12 | npm WARN enoent ENOENT: no such file or directory, open '/Users/joe/Projects/Web/package.json' 13 | npm WARN Web No description 14 | npm WARN Web No repository field. 15 | npm WARN Web No README data 16 | npm WARN Web No license field. 17 | 18 | up to date in 1.44s 19 | found 0 vulnerabilities 20 | ``` 21 | 22 | When trying to `npm start` 23 | 24 | ```none 25 | npm ERR! path /Users/joe/Projects/Web/package.json 26 | npm ERR! code ENOENT 27 | npm ERR! errno -2 28 | npm ERR! syscall open 29 | npm ERR! enoent ENOENT: no such file or directory, open '/Users/joe/Projects/Web/package.json' 30 | npm ERR! enoent This is related to npm not being able to find a file. 31 | npm ERR! enoent 32 | 33 | npm ERR! A complete log of this run can be found in: 34 | npm ERR! /Users/joe/.npm/_logs/2018-09-04T17_29_33_231Z-debug.log 35 | ``` 36 | 37 | > **Solution:** You are most likely in the wrong directoy. Change into the template directory via `cd express-template` and try again. 38 | 39 | ## Error: listen EADDRINUSE :::3000 40 | 41 | When trying to `npm start` 42 | 43 | ```none 44 | > express-template@0.1.0 start /Users/joe/Projects/Web/express-template 45 | > node ./server/app.js 46 | 47 | events.js:167 48 | throw er; // Unhandled 'error' event 49 | ^ 50 | 51 | Error: listen EADDRINUSE :::3000 52 | at Server.setupListenHandle [as _listen2] (net.js:1336:14) 53 | at listenInCluster (net.js:1384:12) 54 | at Server.listen (net.js:1471:7) 55 | at Function.listen (/Users/joe/Projects/Web/express-template/node_modules/express/lib/application.js:618:24) 56 | at Object. (/Users/joe/Projects/Web/express-template/server/app.js:42:5) 57 | at Module._compile (internal/modules/cjs/loader.js:689:30) 58 | at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10) 59 | at Module.load (internal/modules/cjs/loader.js:599:32) 60 | at tryModuleLoad (internal/modules/cjs/loader.js:538:12) 61 | at Function.Module._load (internal/modules/cjs/loader.js:530:3) 62 | at Function.Module.runMain (internal/modules/cjs/loader.js:742:12) 63 | at startup (internal/bootstrap/node.js:266:19) 64 | at bootstrapNodeJSCore (internal/bootstrap/node.js:596:3) 65 | Emitted 'error' event at: 66 | at emitErrorNT (net.js:1363:8) 67 | at process._tickCallback (internal/process/next_tick.js:63:19) 68 | at Function.Module.runMain (internal/modules/cjs/loader.js:745:11) 69 | at startup (internal/bootstrap/node.js:266:19) 70 | at bootstrapNodeJSCore (internal/bootstrap/node.js:596:3) 71 | npm ERR! code ELIFECYCLE 72 | npm ERR! errno 1 73 | npm ERR! express-template@0.1.0 start: `node ./server/app.js` 74 | npm ERR! Exit status 1 75 | npm ERR! 76 | npm ERR! Failed at the express-template@0.1.0 start script. 77 | npm ERR! This is probably not a problem with npm. There is likely additional logging output above. 78 | 79 | npm ERR! A complete log of this run can be found in: 80 | npm ERR! /Users/joe/.npm/_logs/2018-09-04T17_34_11_215Z-debug.log 81 | ``` 82 | 83 | > **Solution:** You are most likely already running another instance of the server. Stop the other instance or application that uses port 3000 and try again. 84 | 85 | ## MongoNetworkError: failed to connect to server [localhost:27017] 86 | 87 | When trying to `npm start` 88 | 89 | ```none 90 | > express-template@0.1.0 start /Users/joe/Projects/Web/express-template 91 | > node ./server/app.js 92 | 93 | Express server listening on port 3000, in development mode 94 | (node:42678) UnhandledPromiseRejectionWarning: MongoNetworkError: failed to connect to server [localhost:27017] on first connect [MongoNetworkError: connect ECONNREFUSED 127.0.0.1:27017] 95 | at Pool. (/Users/joe/Projects/Web/express-template/node_modules/mongodb-core/lib/topologies/server.js:564:11) 96 | at Pool.emit (events.js:182:13) 97 | at Connection. (/Users/joe/Projects/Web/express-template/node_modules/mongodb-core/lib/connection/pool.js:317:12) 98 | at Object.onceWrapper (events.js:273:13) 99 | at Connection.emit (events.js:182:13) 100 | at Socket. (/Users/joe/Projects/Web/express-template/node_modules/mongodb-core/lib/connection/connection.js:246:50) 101 | at Object.onceWrapper (events.js:273:13) 102 | at Socket.emit (events.js:182:13) 103 | at emitErrorNT (internal/streams/destroy.js:82:8) 104 | at emitErrorAndCloseNT (internal/streams/destroy.js:50:3) 105 | at process._tickCallback (internal/process/next_tick.js:63:19) 106 | (node:42678) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1) 107 | (node:42678) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. 108 | ``` 109 | 110 | > **Solution:** Your MongoDB server is not up and running. Start `mongod` and try again. You can go back to the Data Managment course and check your [dit032-setup](https://github.com/joe4dev/dit032-setup). 111 | 112 | 113 | ## GET http://localhost:3000/api [errored] connect ECONNREFUSED 127.0.0.1:3000 114 | 115 | When trying to `npm test` 116 | 117 | ```none 118 | 119 | > express-template@0.1.0 test /Users/joe/Projects/Web/express-template 120 | > newman run ./tests/api-tests.postman.json 121 | 122 | newman 123 | 124 | express-template 125 | 126 | → http://localhost:3000/api 127 | GET http://localhost:3000/api [errored] 128 | connect ECONNREFUSED 127.0.0.1:3000 129 | 2⠄ JSONError in test-script 130 | 131 | → http://localhost:3000/api/camels 132 | GET http://localhost:3000/api/camels [errored] 133 | connect ECONNREFUSED 127.0.0.1:3000 134 | 4⠄ JSONError in test-script 135 | 136 | ┌─────────────────────────┬──────────┬──────────┐ 137 | │ │ executed │ failed │ 138 | ├─────────────────────────┼──────────┼──────────┤ 139 | │ iterations │ 1 │ 0 │ 140 | ├─────────────────────────┼──────────┼──────────┤ 141 | │ requests │ 2 │ 2 │ 142 | ├─────────────────────────┼──────────┼──────────┤ 143 | │ test-scripts │ 2 │ 2 │ 144 | ├─────────────────────────┼──────────┼──────────┤ 145 | │ prerequest-scripts │ 0 │ 0 │ 146 | ├─────────────────────────┼──────────┼──────────┤ 147 | │ assertions │ 0 │ 0 │ 148 | ├─────────────────────────┴──────────┴──────────┤ 149 | │ total run duration: 77ms │ 150 | ├───────────────────────────────────────────────┤ 151 | │ total data received: 0B (approx) │ 152 | ├───────────────────────────────────────────────┤ 153 | │ average response time: 0ms │ 154 | └───────────────────────────────────────────────┘ 155 | 156 | # failure detail 157 | 158 | 1. Error connect ECONNREFUSED 127.0.0.1:3000 159 | at request 160 | inside "http://localhost:3000/api" 161 | 162 | 2. JSONError Unexpected token u in JSON at position 0 163 | at test-script 164 | inside "http://localhost:3000/api" 165 | 166 | 3. Error connect ECONNREFUSED 127.0.0.1:3000 167 | at request 168 | inside "http://localhost:3000/api/camels" 169 | 170 | 4. JSONError Unexpected token u in JSON at position 0 171 | at test-script 172 | inside "http://localhost:3000/api/camels" 173 | npm ERR! Test failed. See above for more details. 174 | ``` 175 | 176 | > **Solution:** Your Nodejs server is not running. Start your Nodejs server and try again. 177 | -------------------------------------------------------------------------------- /server/docs/img/postman_choose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/docs/img/postman_choose.png -------------------------------------------------------------------------------- /server/docs/img/postman_env.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/docs/img/postman_env.png -------------------------------------------------------------------------------- /server/docs/img/postman_export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/docs/img/postman_export.png -------------------------------------------------------------------------------- /server/docs/img/postman_export_format.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/docs/img/postman_export_format.png -------------------------------------------------------------------------------- /server/docs/img/postman_export_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/docs/img/postman_export_save.png -------------------------------------------------------------------------------- /server/docs/img/postman_import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/docs/img/postman_import.png -------------------------------------------------------------------------------- /server/docs/img/postman_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/docs/img/postman_run.png -------------------------------------------------------------------------------- /server/docs/img/postman_runner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/docs/img/postman_runner.png -------------------------------------------------------------------------------- /server/docs/img/postman_variable_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/docs/img/postman_variable_save.png -------------------------------------------------------------------------------- /server/docs/img/postman_variable_use.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/docs/img/postman_variable_use.png -------------------------------------------------------------------------------- /server/docs/img/test_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/docs/img/test_run.png -------------------------------------------------------------------------------- /server/icons/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/icons/.gitkeep -------------------------------------------------------------------------------- /server/image_handling/imageDeleteHandler.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | var imageDeleteHandler = { 5 | deleteSingleImage: async function (path) { 6 | await fs.promises.unlink(path); 7 | }, 8 | deleteAllImages: async function (imageDirectory) { 9 | const images = await fs.promises.readdir(imageDirectory); 10 | await Promise.all(images.map(image => fs.promises.unlink(path.join(imageDirectory, image)))); 11 | } 12 | }; 13 | 14 | 15 | module.exports = imageDeleteHandler; 16 | -------------------------------------------------------------------------------- /server/image_handling/imageUploadHandler.js: -------------------------------------------------------------------------------- 1 | var multer = require('multer'); 2 | var path = require('path'); 3 | const postImageDirectory = './uploads/'; 4 | const iconImageDirectory = './icons/'; 5 | const thumbnailImageDirectory = './thumbnails/'; 6 | 7 | 8 | // Allows us to define how files are stored. 9 | var storage = multer.diskStorage({ 10 | destination: function (req, file, cb) { // function defines where incoming image should be stored. 11 | if (req.body.event == 'post') { 12 | cb(null, postImageDirectory); 13 | } 14 | else if (req.body.event == 'icon') { 15 | cb(null, iconImageDirectory); 16 | } 17 | else if (req.body.event == 'thumbnail') { 18 | cb(null, thumbnailImageDirectory); 19 | } 20 | else { 21 | var err = new Error('Invalid event'); 22 | err.status = 422; 23 | cb(err); 24 | } 25 | }, 26 | filename: function (req, file, cb) { 27 | cb(null, Date.now() + path.extname(file.originalname)); 28 | } 29 | }); 30 | 31 | var imageFilter = function (req, image, cb) { 32 | if (image.mimetype === 'image/jpeg' || image.mimetype === 'image/png' || image.mimetype === 'image/jpg') { 33 | //accepts image 34 | cb(null, true); 35 | } else { 36 | var err = new Error('ERROR: Image file type is not supported'); 37 | err.status = 415; 38 | cb(err, false); // Error message added here due to this being the fail/rejected case 39 | } 40 | }; 41 | 42 | var imgUpload = multer({ 43 | storage: storage, 44 | fileFilter: imageFilter, 45 | limits: { 46 | fileSize: 1024 * 1024 * 25 // accepts file sizes up to 25mb 47 | } 48 | }); 49 | 50 | module.exports = imgUpload; -------------------------------------------------------------------------------- /server/models/collection.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | 4 | var collectionSchema = new Schema({ 5 | title: { type: String, required: true }, 6 | event: { type: String }, 7 | thumbnail: { type: String }, 8 | post_id: [{ type: Schema.Types.ObjectId, ref: 'posts' }] 9 | }); 10 | 11 | module.exports = mongoose.model("collections", collectionSchema); -------------------------------------------------------------------------------- /server/models/post.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Rating = require('./rating'); 3 | var Schema = mongoose.Schema; 4 | var Collection = require('./collection') 5 | 6 | var postSchema = new Schema({ 7 | post_id: { type: Schema.Types.ObjectId, required: true }, 8 | title: { type: String, required: true }, 9 | description: { type: String, default: function() {return ''} }, 10 | numberOfFavorites: { type: Number, default: function() { return 0}, required: true }, 11 | tags: { type: [String], required: true }, 12 | user_id: { type: Schema.Types.ObjectId, ref: 'users' }, 13 | event: { type: String, required: true }, 14 | image: { type: String, required: true }, 15 | ratings: [{ type: Schema.Types.ObjectId, ref: 'ratings' }] 16 | }); 17 | 18 | postSchema.pre('remove', async function (next) { 19 | try { 20 | await Collection.updateMany({ post_id: this._id }, 21 | { $pull: { post_id: this._id } }, 22 | { multi: true }).exec(); 23 | await Rating.deleteMany({ _id: { $in: this.ratings }}); 24 | next(); 25 | } catch (err) { 26 | next(err); 27 | } 28 | }); 29 | 30 | module.exports = mongoose.model("posts", postSchema); -------------------------------------------------------------------------------- /server/models/rating.js: -------------------------------------------------------------------------------- 1 | var mongoose = require("mongoose"); 2 | 3 | var Schema = mongoose.Schema; 4 | 5 | var ratingSchema = new Schema({ 6 | starRating: { type: Number, required: true }, 7 | user: { type: Schema.Types.ObjectId, ref: 'users' }, 8 | post: { type: Schema.Types.ObjectId, ref: 'posts' } 9 | }); 10 | 11 | module.exports = mongoose.model("ratings", ratingSchema); -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | var mongoose = require("mongoose"); 2 | var Rating = require('./rating'); 3 | var Schema = mongoose.Schema; 4 | var bcryptjs = require('bcryptjs'); 5 | var Collection = require('./collection') 6 | var Post = require('./post'); 7 | 8 | var userSchema = new Schema({ 9 | username: { type: String, unique: true, required: true }, 10 | password: { type: String, required: true }, 11 | bio: { type: String }, 12 | event: { type: String, required: true }, 13 | icon: { type: String, required: true }, 14 | collections: [{ type: Schema.Types.ObjectId, ref: "collections" }] 15 | }); 16 | 17 | userSchema.pre('remove', async function (next) { 18 | try { 19 | await Collection.remove({ 20 | "_id": { 21 | $in: this.collections 22 | } 23 | }); 24 | await Post.updateMany({ user_id: this._id }, 25 | { user_id: null }).exec(); 26 | next(); 27 | } catch (err) { 28 | next(err); 29 | } 30 | }); 31 | 32 | const users = mongoose.model("users", userSchema); 33 | module.exports = users; 34 | 35 | module.exports.createUser = (newUser, callback) => { 36 | bcryptjs.genSalt(10, (err, salt) => { 37 | bcryptjs.hash(newUser.password, salt, (error, hash) => { 38 | // Here we store the hashed password 39 | const newUserResource = newUser; 40 | newUserResource.password = hash; 41 | newUserResource.save(callback); 42 | }); 43 | }); 44 | }; 45 | 46 | module.exports.getUserByUsername = (username, callback) => { 47 | const query = { username }; 48 | users.findOne(query, callback); 49 | }; 50 | 51 | module.exports.comparePassword = (candidatePassword, hash, callback) => { 52 | bcryptjs.compare(candidatePassword, hash, (err, isMatch) => { 53 | if (err) throw err; 54 | callback(null, isMatch); 55 | }); 56 | }; -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.1.0", 4 | "engines": { 5 | "node": "12.x" 6 | }, 7 | "private": true, 8 | "description": "Template for ExpressJS API with Mongoose for Web and Mobile Engineering (DIT341)", 9 | "main": "./app.js", 10 | "scripts": { 11 | "start": "node ./app.js", 12 | "dev": "nodemon ./app.js", 13 | "lint": "eslint .", 14 | "test": "cross-env-shell MONGODB_URI=mongodb://localhost:27017/serverTestDB \"npm run newman-server\"", 15 | "ci-test": "npm run newman-server", 16 | "newman-server": "cross-env-shell PORT=3001 \"npm run dropdb && run-p --race start newman-wait\"", 17 | "newman-wait": "wait-on http://localhost:3001/api && npm run newman", 18 | "newman": "newman run ./tests/server.postman_collection.json --env-var host=http://localhost:3001", 19 | "dropdb": "node ./tests/dropdb.js" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://gitlab.com/dit341/group-00-web.git" 24 | }, 25 | "dependencies": { 26 | "bcryptjs": "^2.4.3", 27 | "body-parser": "^1.19.0", 28 | "connect-history-api-fallback": "^1.6.0", 29 | "cors": "^2.8.5", 30 | "express": "^4.17.1", 31 | "jsonwebtoken": "^8.5.1", 32 | "mongoose": "^5.13.8", 33 | "morgan": "^1.10.0", 34 | "multer": "^1.4.3", 35 | "passport": "^0.5.0", 36 | "passport-jwt": "^4.0.0", 37 | "passport-local": "^1.0.0" 38 | }, 39 | "devDependencies": { 40 | "cross-env": "^7.0.3", 41 | "newman": "^5.2.4", 42 | "nodemon": "^2.0.13", 43 | "npm-run-all": "^4.1.5", 44 | "wait-on": "^5.3.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/tests/dropdb.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | // Variables 4 | var mongoURI = process.env.MONGODB_URI; 5 | 6 | if (!mongoURI) { 7 | console.error('Missing MONGODB_URI for dropping test database.'); 8 | process.exit(1); 9 | } 10 | 11 | // Drop database 12 | mongoose.connect(mongoURI, { useNewUrlParser: true, useUnifiedTopology: true }, function (err) { 13 | if (err) { 14 | console.error(`Failed to connect to MongoDB with URI: ${mongoURI}`); 15 | console.error(err.stack); 16 | process.exit(1); 17 | } 18 | mongoose.connection.db.dropDatabase(function () { 19 | console.log(`Dropped database: ${mongoURI}`); 20 | process.exit(0); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /server/tests/invalid_file_format_test.txt: -------------------------------------------------------------------------------- 1 | invalid file format test file 2 | -------------------------------------------------------------------------------- /server/tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "50509aa5-0e41-49d1-8b67-61b442613f8e", 4 | "name": "server", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Create a post", 10 | "event": [ 11 | { 12 | "listen": "test", 13 | "script": { 14 | "exec": [ 15 | "var jsonData = pm.response.json();", 16 | "var postId = jsonData._id;", 17 | "var postTag = jsonData.tags;", 18 | "", 19 | "pm.environment.set(\"post_id\", postId);", 20 | "pm.environment.set(\"post_tag\", postTag);", 21 | "", 22 | "pm.test('Status code is 201', function () {", 23 | " pm.response.to.have.status(201);", 24 | "});", 25 | "", 26 | "pm.test('Body has correct title, description, number of favourits, and tags', function () {", 27 | " pm.expect(jsonData.title).to.eql('test_title');", 28 | " pm.expect(jsonData.description).to.eql('eg_description');", 29 | " pm.expect(jsonData.numberOfFavorites).to.eql(3);", 30 | " pm.expect(jsonData.tags).to.eql(['testtag']);", 31 | "});", 32 | "", 33 | "pm.test('Body has an image url', function () {", 34 | " pm.expect(jsonData.image).to.be.a('String');", 35 | "});", 36 | "", 37 | "pm.test('Body has _id', function () {", 38 | " pm.expect(jsonData._id).to.be.a('String');", 39 | "});", 40 | "", 41 | "pm.test('Body has a post_id', function() {", 42 | " pm.expect(jsonData.post_id).to.be.a('String');", 43 | "})" 44 | ], 45 | "type": "text/javascript" 46 | } 47 | } 48 | ], 49 | "protocolProfileBehavior": { 50 | "disabledSystemHeaders": {} 51 | }, 52 | "request": { 53 | "method": "POST", 54 | "header": [], 55 | "body": { 56 | "mode": "formdata", 57 | "formdata": [ 58 | { 59 | "key": "", 60 | "type": "file", 61 | "src": [], 62 | "disabled": true 63 | }, 64 | { 65 | "key": "title", 66 | "value": "test_title", 67 | "type": "text" 68 | }, 69 | { 70 | "key": "description", 71 | "value": "eg_description", 72 | "type": "text" 73 | }, 74 | { 75 | "key": "numberOfFavorites", 76 | "value": "3", 77 | "type": "text" 78 | }, 79 | { 80 | "key": "tags", 81 | "value": "testtag", 82 | "contentType": "", 83 | "type": "text" 84 | }, 85 | { 86 | "key": "image", 87 | "type": "file", 88 | "src": "/home/younis/Pictures/cat_test.jpg" 89 | } 90 | ] 91 | }, 92 | "url": { 93 | "raw": "{{host}}/api/posts", 94 | "host": [ 95 | "{{host}}" 96 | ], 97 | "path": [ 98 | "api", 99 | "posts" 100 | ] 101 | } 102 | }, 103 | "response": [] 104 | }, 105 | { 106 | "name": "Create second post to show in next test", 107 | "event": [ 108 | { 109 | "listen": "test", 110 | "script": { 111 | "exec": [ 112 | "var jsonData = pm.response.json();", 113 | "var postId = jsonData._id;", 114 | "var postTag = jsonData.tags;", 115 | "", 116 | "pm.environment.set(\"post_id2\", postId);", 117 | "", 118 | "pm.test('Status code is 201', function () {", 119 | " pm.response.to.have.status(201);", 120 | "});", 121 | "", 122 | "pm.test('Body has correct title, description, number of favourits, and tags', function () {", 123 | " pm.expect(jsonData.title).to.eql('test_titlecopy');", 124 | " pm.expect(jsonData.description).to.eql('eg_descriptioncopy');", 125 | " pm.expect(jsonData.numberOfFavorites).to.eql(3);", 126 | " pm.expect(jsonData.tags).to.eql(['testtagcopy']);", 127 | "});", 128 | "", 129 | "pm.test('Body has an image url', function () {", 130 | " pm.expect(jsonData.image).to.be.a('String');", 131 | "});", 132 | "", 133 | "pm.test('Body has _id', function () {", 134 | " pm.expect(jsonData._id).to.be.a('String');", 135 | "});", 136 | "", 137 | "pm.test('Body has a post_id', function() {", 138 | " pm.expect(jsonData.post_id).to.be.a('String');", 139 | "})" 140 | ], 141 | "type": "text/javascript" 142 | } 143 | } 144 | ], 145 | "protocolProfileBehavior": { 146 | "disabledSystemHeaders": {} 147 | }, 148 | "request": { 149 | "method": "POST", 150 | "header": [], 151 | "body": { 152 | "mode": "formdata", 153 | "formdata": [ 154 | { 155 | "key": "", 156 | "type": "file", 157 | "src": [], 158 | "disabled": true 159 | }, 160 | { 161 | "key": "title", 162 | "value": "test_titlecopy", 163 | "type": "text" 164 | }, 165 | { 166 | "key": "description", 167 | "value": "eg_descriptioncopy", 168 | "type": "text" 169 | }, 170 | { 171 | "key": "numberOfFavorites", 172 | "value": "3", 173 | "type": "text" 174 | }, 175 | { 176 | "key": "tags", 177 | "value": "testtagcopy", 178 | "contentType": "", 179 | "type": "text" 180 | }, 181 | { 182 | "key": "image", 183 | "type": "file", 184 | "src": "/home/younis/Pictures/cat_test.jpg" 185 | } 186 | ] 187 | }, 188 | "url": { 189 | "raw": "{{host}}/api/posts", 190 | "host": [ 191 | "{{host}}" 192 | ], 193 | "path": [ 194 | "api", 195 | "posts" 196 | ] 197 | } 198 | }, 199 | "response": [] 200 | }, 201 | { 202 | "name": "Get all posts", 203 | "event": [ 204 | { 205 | "listen": "test", 206 | "script": { 207 | "exec": [ 208 | "var jsonDataPostOne = pm.response.json().posts[0];", 209 | "var jsonDataPostTwo = pm.response.json().posts[1];", 210 | "", 211 | "var getPost_IdOne = pm.variables.get('post_id');", 212 | "var getPost_IdTwo = pm.variables.get('post_id2');", 213 | "", 214 | "", 215 | "pm.test('Status code is 200', function () {", 216 | " pm.response.to.have.status(200);", 217 | "});", 218 | "", 219 | "", 220 | "pm.test('First Post in body has correct id', function () {", 221 | " pm.expect(jsonDataPostOne._id).to.eql(getPost_IdOne);", 222 | "});", 223 | "", 224 | "pm.test('Second Post in body has correct id', function () {", 225 | " pm.expect(jsonDataPostTwo._id).to.eql(getPost_IdTwo);", 226 | "});", 227 | "", 228 | "pm.test('Body has correct title, description, number of favourites, and tags', function () {", 229 | " pm.expect(jsonDataPostOne.title).to.eql('test_title');", 230 | " pm.expect(jsonDataPostOne.description).to.eql('eg_description');", 231 | " pm.expect(jsonDataPostOne.numberOfFavorites).to.eql(3);", 232 | " pm.expect(jsonDataPostOne.tags).to.eql(['testtag']);", 233 | " pm.expect(jsonDataPostTwo.title).to.eql('test_titlecopy');", 234 | " pm.expect(jsonDataPostTwo.description).to.eql('eg_descriptioncopy');", 235 | " pm.expect(jsonDataPostTwo.numberOfFavorites).to.eql(3);", 236 | " pm.expect(jsonDataPostTwo.tags).to.eql(['testtagcopy']);", 237 | "});", 238 | "", 239 | "pm.test('Body has an image url', function () {", 240 | " pm.expect(jsonDataPostOne.image).to.be.a('String');", 241 | " pm.expect(jsonDataPostTwo.image).to.be.a('String');", 242 | "});", 243 | "", 244 | "", 245 | "pm.test('Body has _id', function () {", 246 | " pm.expect(jsonDataPostOne._id).to.be.a('String');", 247 | " pm.expect(jsonDataPostTwo._id).to.be.a('String');", 248 | "});", 249 | "", 250 | "pm.test('Body has a post_id', function() {", 251 | " pm.expect(jsonDataPostOne.post_id).to.be.a('String');", 252 | " pm.expect(jsonDataPostTwo.post_id).to.be.a('String');", 253 | "})" 254 | ], 255 | "type": "text/javascript" 256 | } 257 | } 258 | ], 259 | "protocolProfileBehavior": { 260 | "disableBodyPruning": true 261 | }, 262 | "request": { 263 | "method": "GET", 264 | "header": [], 265 | "body": { 266 | "mode": "raw", 267 | "raw": "", 268 | "options": { 269 | "raw": { 270 | "language": "json" 271 | } 272 | } 273 | }, 274 | "url": { 275 | "raw": "{{host}}/api/posts", 276 | "host": [ 277 | "{{host}}" 278 | ], 279 | "path": [ 280 | "api", 281 | "posts" 282 | ] 283 | } 284 | }, 285 | "response": [] 286 | }, 287 | { 288 | "name": "Get post with specific ID", 289 | "event": [ 290 | { 291 | "listen": "test", 292 | "script": { 293 | "exec": [ 294 | "var jsonData = pm.response.json();", 295 | "", 296 | "var getPost_Id = pm.variables.get(\"post_id\");", 297 | "", 298 | "pm.test('Status code is 200', function () {", 299 | " pm.response.to.have.status(200);", 300 | "});", 301 | "", 302 | "pm.test('Body has _id', function () {", 303 | " pm.expect(jsonData._id).to.be.a('String');", 304 | "});", 305 | "", 306 | "pm.test('Body has correct _id ', function () {", 307 | " pm.expect(jsonData._id).to.eql(getPost_Id);", 308 | "});", 309 | "", 310 | "pm.test('Body has correct title, description, number of favourites, and tags', function () {", 311 | " pm.expect(jsonData.title).to.eql('test_title');", 312 | " pm.expect(jsonData.description).to.eql('eg_description');", 313 | " pm.expect(jsonData.numberOfFavorites).to.eql(3);", 314 | " pm.expect(jsonData.tags).to.eql(['testtag']);", 315 | "});", 316 | "", 317 | "pm.test('Body has an image url', function () {", 318 | " pm.expect(jsonData.image).to.be.a('String');", 319 | "});", 320 | "", 321 | "", 322 | "pm.test('Body has a post_id', function() {", 323 | " pm.expect(jsonData.post_id).to.be.a('String');", 324 | "})" 325 | ], 326 | "type": "text/javascript" 327 | } 328 | } 329 | ], 330 | "request": { 331 | "method": "GET", 332 | "header": [], 333 | "url": { 334 | "raw": "{{host}}/api/posts/{{post_id}}", 335 | "host": [ 336 | "{{host}}" 337 | ], 338 | "path": [ 339 | "api", 340 | "posts", 341 | "{{post_id}}" 342 | ] 343 | } 344 | }, 345 | "response": [] 346 | }, 347 | { 348 | "name": "Get post with specific tag", 349 | "event": [ 350 | { 351 | "listen": "test", 352 | "script": { 353 | "exec": [ 354 | "var jsonData = pm.response.json().posts[0];", 355 | "", 356 | "var getPostTag = pm.variables.get('post_tag');", 357 | "", 358 | "pm.test('Status code is 200', function () {", 359 | " pm.response.to.have.status(200);", 360 | "});", 361 | "", 362 | "", 363 | "pm.test('Body has correct tag', function () {", 364 | " pm.expect(jsonData.tags).to.eql(getPostTag);", 365 | "});", 366 | "", 367 | "pm.test('Body has correct title, description, number of favourites, and tags', function () {", 368 | " pm.expect(jsonData.title).to.eql('test_title');", 369 | " pm.expect(jsonData.description).to.eql('eg_description');", 370 | " pm.expect(jsonData.numberOfFavorites).to.eql(3);", 371 | " pm.expect(jsonData.tags).to.eql(['testtag']);", 372 | "});", 373 | "", 374 | "pm.test('Body has an image url', function () {", 375 | " pm.expect(jsonData.image).to.be.a('String');", 376 | "});", 377 | "", 378 | "", 379 | "pm.test('Body has _id', function () {", 380 | " pm.expect(jsonData._id).to.be.a('String');", 381 | "});", 382 | "", 383 | "pm.test('Body has a post_id', function() {", 384 | " pm.expect(jsonData.post_id).to.be.a('String');", 385 | "})" 386 | ], 387 | "type": "text/javascript" 388 | } 389 | } 390 | ], 391 | "request": { 392 | "method": "GET", 393 | "header": [], 394 | "url": { 395 | "raw": "{{host}}/api/posts/tag/{{post_tag}}", 396 | "host": [ 397 | "{{host}}" 398 | ], 399 | "path": [ 400 | "api", 401 | "posts", 402 | "tag", 403 | "{{post_tag}}" 404 | ] 405 | } 406 | }, 407 | "response": [] 408 | }, 409 | { 410 | "name": "Put test", 411 | "event": [ 412 | { 413 | "listen": "test", 414 | "script": { 415 | "exec": [ 416 | "var jsonData = pm.response.json();", 417 | "", 418 | "var getPost_Id = pm.variables.get(\"post_id\");", 419 | "", 420 | "pm.test('Status code is 200', function () {", 421 | " pm.response.to.have.status(200);", 422 | "});", 423 | "", 424 | "pm.test('Body has _id', function () {", 425 | " pm.expect(jsonData._id).to.be.a('String');", 426 | "});", 427 | "", 428 | "pm.test('Body has correct _id', function () {", 429 | " pm.expect(jsonData._id).to.eql(getPost_Id);", 430 | "});", 431 | "", 432 | "pm.test('Body has correct title, description, number of favourites, and tags', function () {", 433 | " pm.expect(jsonData.title).to.eql('new title test');", 434 | " pm.expect(jsonData.description).to.eql('new description test');", 435 | " pm.expect(jsonData.numberOfFavorites).to.eql(10);", 436 | " pm.expect(jsonData.tags).to.eql(['new tag 1', 'new tag 2']);", 437 | "});", 438 | "", 439 | "pm.test('Body has an image url', function () {", 440 | " pm.expect(jsonData.image).to.be.a('String');", 441 | "});", 442 | "", 443 | "pm.test('Body has a post_id', function() {", 444 | " pm.expect(jsonData.post_id).to.be.a('String');", 445 | "})", 446 | "" 447 | ], 448 | "type": "text/javascript" 449 | } 450 | } 451 | ], 452 | "request": { 453 | "method": "PUT", 454 | "header": [], 455 | "body": { 456 | "mode": "raw", 457 | "raw": "{\n \"title\": \"new title test\",\n \"description\": \"new description test\",\n \"numberOfFavorites\": 10,\n \"tags\": [\n \"new tag 1\",\n \"new tag 2\"\n ]\n}", 458 | "options": { 459 | "raw": { 460 | "language": "json" 461 | } 462 | } 463 | }, 464 | "url": { 465 | "raw": "{{host}}/api/posts/{{post_id}}", 466 | "host": [ 467 | "{{host}}" 468 | ], 469 | "path": [ 470 | "api", 471 | "posts", 472 | "{{post_id}}" 473 | ] 474 | } 475 | }, 476 | "response": [] 477 | }, 478 | { 479 | "name": "Patch test", 480 | "event": [ 481 | { 482 | "listen": "test", 483 | "script": { 484 | "exec": [ 485 | "var jsonData = pm.response.json();", 486 | "", 487 | "var getPost_Id = pm.variables.get(\"post_id\");", 488 | "", 489 | "pm.test('Status code is 200', function () {", 490 | " pm.response.to.have.status(200);", 491 | "});", 492 | "", 493 | "pm.test('Body has _id', function () {", 494 | " pm.expect(jsonData._id).to.be.a('String');", 495 | "});", 496 | "", 497 | "pm.test('Body has correct _id', function () {", 498 | " pm.expect(jsonData._id).to.eql(getPost_Id);", 499 | "});", 500 | "", 501 | "pm.test('Body has correct title, description, number of favourites, and tags', function () {", 502 | " pm.expect(jsonData.title).to.eql('new title test');", 503 | " pm.expect(jsonData.description).to.eql('This is a new patch description');", 504 | " pm.expect(jsonData.numberOfFavorites).to.eql(10);", 505 | " pm.expect(jsonData.tags).to.eql(['new tag 1', 'new tag 2']);", 506 | "});", 507 | "", 508 | "pm.test('Body has an image url', function () {", 509 | " pm.expect(jsonData.image).to.be.a('String');", 510 | "});", 511 | "", 512 | "pm.test('Body has a post_id', function() {", 513 | " pm.expect(jsonData.post_id).to.be.a('String');", 514 | "})", 515 | "" 516 | ], 517 | "type": "text/javascript" 518 | } 519 | } 520 | ], 521 | "request": { 522 | "method": "PATCH", 523 | "header": [], 524 | "body": { 525 | "mode": "raw", 526 | "raw": "{\n \"description\": \"This is a new patch description\"\n}", 527 | "options": { 528 | "raw": { 529 | "language": "json" 530 | } 531 | } 532 | }, 533 | "url": { 534 | "raw": "{{host}}/api/posts/{{post_id}}", 535 | "host": [ 536 | "{{host}}" 537 | ], 538 | "path": [ 539 | "api", 540 | "posts", 541 | "{{post_id}}" 542 | ] 543 | } 544 | }, 545 | "response": [] 546 | }, 547 | { 548 | "name": "Delete specific post", 549 | "event": [ 550 | { 551 | "listen": "test", 552 | "script": { 553 | "exec": [ 554 | "var jsonData = pm.response.json();", 555 | "", 556 | "var getPost_Id = pm.variables.get(\"post_id\");", 557 | "", 558 | "pm.test('Status code is 200', function () {", 559 | " pm.response.to.have.status(200);", 560 | "});", 561 | "", 562 | "pm.test('Body has _id', function () {", 563 | " pm.expect(jsonData._id).to.be.a('String');", 564 | "});", 565 | "", 566 | "pm.test('Body has correct _id', function () {", 567 | " pm.expect(jsonData._id).to.eql(getPost_Id);", 568 | "});", 569 | "", 570 | "pm.test('Body has correct title, description, number of favourites, and tags', function () {", 571 | " pm.expect(jsonData.title).to.eql('new title test');", 572 | " pm.expect(jsonData.description).to.eql('This is a new patch description');", 573 | " pm.expect(jsonData.numberOfFavorites).to.eql(10);", 574 | " pm.expect(jsonData.tags).to.eql(['new tag 1', 'new tag 2']);", 575 | "});", 576 | "", 577 | "pm.test('Body has an image url', function () {", 578 | " pm.expect(jsonData.image).to.be.a('String');", 579 | "});", 580 | "", 581 | "pm.test('Body has a post_id', function() {", 582 | " pm.expect(jsonData.post_id).to.be.a('String');", 583 | "})", 584 | "" 585 | ], 586 | "type": "text/javascript" 587 | } 588 | } 589 | ], 590 | "request": { 591 | "method": "DELETE", 592 | "header": [], 593 | "url": { 594 | "raw": "{{host}}/api/posts/{{post_id}}", 595 | "host": [ 596 | "{{host}}" 597 | ], 598 | "path": [ 599 | "api", 600 | "posts", 601 | "{{post_id}}" 602 | ] 603 | } 604 | }, 605 | "response": [] 606 | }, 607 | { 608 | "name": "Post to show that delete all posts works", 609 | "event": [ 610 | { 611 | "listen": "test", 612 | "script": { 613 | "exec": [ 614 | "var jsonData = pm.response.json();", 615 | "var postId = jsonData._id;", 616 | "var postTag = jsonData.tags;", 617 | "", 618 | "pm.environment.set(\"post_id\", postId);", 619 | "pm.environment.set(\"post_tag\", postTag);", 620 | "", 621 | "pm.test('Status code is 201', function () {", 622 | " pm.response.to.have.status(201);", 623 | "});", 624 | "", 625 | "pm.test('Body has correct title, description, number of favourits, and tags', function () {", 626 | " pm.expect(jsonData.title).to.eql('test_title2');", 627 | " pm.expect(jsonData.description).to.eql('eg_description2');", 628 | " pm.expect(jsonData.numberOfFavorites).to.eql(1);", 629 | " pm.expect(jsonData.tags).to.eql(['testtag']);", 630 | "});", 631 | "", 632 | "pm.test('Body has an image url', function () {", 633 | " pm.expect(jsonData.image).to.be.a('String');", 634 | "});", 635 | "", 636 | "pm.test('Body has _id', function () {", 637 | " pm.expect(jsonData._id).to.be.a('String');", 638 | "});", 639 | "", 640 | "pm.test('Body has a post_id', function() {", 641 | " pm.expect(jsonData.post_id).to.be.a('String');", 642 | "})" 643 | ], 644 | "type": "text/javascript" 645 | } 646 | } 647 | ], 648 | "protocolProfileBehavior": { 649 | "disabledSystemHeaders": {} 650 | }, 651 | "request": { 652 | "method": "POST", 653 | "header": [], 654 | "body": { 655 | "mode": "formdata", 656 | "formdata": [ 657 | { 658 | "key": "", 659 | "type": "file", 660 | "src": [], 661 | "disabled": true 662 | }, 663 | { 664 | "key": "title", 665 | "value": "test_title2", 666 | "type": "text" 667 | }, 668 | { 669 | "key": "description", 670 | "value": "eg_description2", 671 | "type": "text" 672 | }, 673 | { 674 | "key": "numberOfFavorites", 675 | "value": "1", 676 | "type": "text" 677 | }, 678 | { 679 | "key": "tags", 680 | "value": "testtag", 681 | "contentType": "", 682 | "type": "text" 683 | }, 684 | { 685 | "key": "image", 686 | "type": "file", 687 | "src": "/home/younis/Pictures/cat_test.jpg" 688 | } 689 | ] 690 | }, 691 | "url": { 692 | "raw": "{{host}}/api/posts", 693 | "host": [ 694 | "{{host}}" 695 | ], 696 | "path": [ 697 | "api", 698 | "posts" 699 | ] 700 | } 701 | }, 702 | "response": [] 703 | }, 704 | { 705 | "name": "Second post to show that delete all posts works Copy", 706 | "event": [ 707 | { 708 | "listen": "test", 709 | "script": { 710 | "exec": [ 711 | "var jsonData = pm.response.json();", 712 | "var postId = jsonData._id;", 713 | "var postTag = jsonData.tags;", 714 | "", 715 | "pm.environment.set(\"post_id\", postId);", 716 | "pm.environment.set(\"post_tag\", postTag);", 717 | "", 718 | "pm.test('Status code is 201', function () {", 719 | " pm.response.to.have.status(201);", 720 | "});", 721 | "", 722 | "pm.test('Body has correct title, description, number of favourits, and tags', function () {", 723 | " pm.expect(jsonData.title).to.eql('test_title3');", 724 | " pm.expect(jsonData.description).to.eql('eg_description3');", 725 | " pm.expect(jsonData.numberOfFavorites).to.eql(3);", 726 | " pm.expect(jsonData.tags).to.eql(['testtag', 'testtag3']);", 727 | "});", 728 | "", 729 | "pm.test('Body has an image url', function () {", 730 | " pm.expect(jsonData.image).to.be.a('String');", 731 | "});", 732 | "", 733 | "pm.test('Body has _id', function () {", 734 | " pm.expect(jsonData._id).to.be.a('String');", 735 | "});", 736 | "", 737 | "pm.test('Body has a post_id', function() {", 738 | " pm.expect(jsonData.post_id).to.be.a('String');", 739 | "})" 740 | ], 741 | "type": "text/javascript" 742 | } 743 | } 744 | ], 745 | "protocolProfileBehavior": { 746 | "disabledSystemHeaders": {} 747 | }, 748 | "request": { 749 | "method": "POST", 750 | "header": [], 751 | "body": { 752 | "mode": "formdata", 753 | "formdata": [ 754 | { 755 | "key": "", 756 | "type": "file", 757 | "src": [], 758 | "disabled": true 759 | }, 760 | { 761 | "key": "title", 762 | "value": "test_title3", 763 | "type": "text" 764 | }, 765 | { 766 | "key": "description", 767 | "value": "eg_description3", 768 | "type": "text" 769 | }, 770 | { 771 | "key": "numberOfFavorites", 772 | "value": "3", 773 | "type": "text" 774 | }, 775 | { 776 | "key": "tags[0]", 777 | "value": "testtag", 778 | "contentType": "", 779 | "type": "text" 780 | }, 781 | { 782 | "key": "image", 783 | "type": "file", 784 | "src": "/home/younis/Pictures/cat_test.jpg" 785 | }, 786 | { 787 | "key": "tags[1]", 788 | "value": "testtag3", 789 | "type": "text" 790 | } 791 | ] 792 | }, 793 | "url": { 794 | "raw": "{{host}}/api/posts", 795 | "host": [ 796 | "{{host}}" 797 | ], 798 | "path": [ 799 | "api", 800 | "posts" 801 | ] 802 | } 803 | }, 804 | "response": [] 805 | }, 806 | { 807 | "name": "Delete all posts", 808 | "event": [ 809 | { 810 | "listen": "test", 811 | "script": { 812 | "exec": [ 813 | "var jsonData = pm.response.json();", 814 | "", 815 | "var numberOfMatchedDocuments = jsonData.n;", 816 | "var isDeleteSuccesful = jsonData.ok;", 817 | "var deletedCount = jsonData.deletedCount;", 818 | "", 819 | "", 820 | "pm.test('Status code is 200', function () {", 821 | " pm.response.to.have.status(200);", 822 | "});", 823 | "", 824 | "pm.test('Has deleted sucesfully', function() {", 825 | " pm.expect(isDeleteSuccesful).to.eql(1);", 826 | "});", 827 | "", 828 | "pm.test('Correct number of documents deleted', function() {", 829 | " pm.expect(numberOfMatchedDocuments).to.eql(3);", 830 | "});", 831 | "", 832 | "pm.test('Correct number of deletes have occured', function() {", 833 | " pm.expect(deletedCount).to.eql(3);", 834 | "});" 835 | ], 836 | "type": "text/javascript" 837 | } 838 | } 839 | ], 840 | "request": { 841 | "method": "DELETE", 842 | "header": [], 843 | "url": { 844 | "raw": "{{host}}/api/posts/", 845 | "host": [ 846 | "{{host}}" 847 | ], 848 | "path": [ 849 | "api", 850 | "posts", 851 | "" 852 | ] 853 | } 854 | }, 855 | "response": [] 856 | } 857 | ], 858 | "event": [ 859 | { 860 | "listen": "prerequest", 861 | "script": { 862 | "type": "text/javascript", 863 | "exec": [ 864 | "" 865 | ] 866 | } 867 | }, 868 | { 869 | "listen": "test", 870 | "script": { 871 | "type": "text/javascript", 872 | "exec": [ 873 | "" 874 | ] 875 | } 876 | } 877 | ], 878 | "variable": [ 879 | { 880 | "key": "host", 881 | "value": "http://localhost:3000", 882 | "type": "string" 883 | } 884 | ] 885 | } -------------------------------------------------------------------------------- /server/tests/test_greater_than_30_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/tests/test_greater_than_30_image.jpg -------------------------------------------------------------------------------- /server/tests/test_icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/tests/test_icon.jpg -------------------------------------------------------------------------------- /server/tests/test_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/tests/test_image.jpg -------------------------------------------------------------------------------- /server/tests/test_thumnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/tests/test_thumnail.jpg -------------------------------------------------------------------------------- /server/thumbnails/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/thumbnails/.gitkeep -------------------------------------------------------------------------------- /server/uploads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marble879/Web-Development-Project/70070b7d10d6d0100391c2c9d0bcf644e82d4699/server/uploads/.gitkeep --------------------------------------------------------------------------------