├── .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 | 
83 |
84 | ## Teaser (MS3)
85 |
86 | 
87 | ### Teaser video
88 | [](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 |
2 |
3 |
4 | Home |
5 | Register |
6 | Login |
7 | Create a post |
8 | User page |
9 |
10 |
11 |
12 |
13 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
28 |
29 |
30 |
31 |
32 |
37 |
38 |
39 |
40 |
41 |
47 |
48 |
49 |
50 | Post
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
69 |
70 |
246 |
--------------------------------------------------------------------------------
/client/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Filter
4 |
5 |
6 | None
12 | Dog
18 | Cat
24 | Landscape
31 | Painting
38 | Delete All
44 |
45 |
46 |
47 |
48 |
57 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
200 |
201 |
254 |
--------------------------------------------------------------------------------
/client/src/views/InsideCollection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 | Update
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
34 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
77 |
78 |
243 |
--------------------------------------------------------------------------------
/client/src/views/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
15 |
16 |
17 |
22 |
23 |
24 |
28 |
29 |
30 |
34 |
39 |
40 |
41 | Log in
47 |
48 |
49 | New to Artsy?
51 | Sign Up
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
116 |
117 |
124 |
--------------------------------------------------------------------------------
/client/src/views/Register.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
15 |
16 |
17 |
22 |
23 |
24 |
30 |
35 |
36 |
42 |
48 |
49 |
55 |
60 |
61 |
66 |
72 |
73 |
74 | Get started!
80 |
81 |
82 | Already signed up?
84 | Log in
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
205 |
206 |
213 |
--------------------------------------------------------------------------------
/client/src/views/User.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 | {{ username }}
16 | Bio: {{ bio }}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
35 |
42 | Enter
43 |
44 |
45 |
46 |
47 |
48 |
49 |
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 | 
18 |
19 | 2. Choose file [tests/server.postman_collection.json](../tests/server.postman_collection.json)
20 |
21 | 
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 | 
28 |
29 | 2. Start your backend server in the terminal via `npm run dev`
30 | 3. Run the collection
31 |
32 | 
33 |
34 | 4. Select your empty environment and click `Run server`
35 |
36 | 
37 |
38 | ## Export Test Collection
39 |
40 | 1. In the extended menu `...` of your collection, click `Export`
41 |
42 | 
43 |
44 | 2. Choose the latest export format (v2.1)
45 |
46 | 
47 |
48 | 3. *Overwrite* the file [tests/server.postman_collection.json](../tests/server.postman_collection.json)
49 |
50 | 
51 |
52 | 4. Run `npm test` in your terminal
53 |
54 | 
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 | 
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 | 
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
--------------------------------------------------------------------------------