├── .gitignore
├── CNAME
├── LICENSE
├── README.md
├── _config.yml
├── app
└── README.md
├── checklist
└── README.md
├── contribute
└── README.md
├── faq
└── README.md
├── guidelines
└── README.md
├── signup-1.png
├── signup-2.png
└── tasks
├── precourse
└── README.md
├── task-1
├── README.md
├── Solution.md
├── code
│ ├── index.js
│ ├── package-lock.json
│ └── package.json
└── images
│ └── post-api-postman.png
├── task-10
├── README.md
├── Solution.md
├── code
│ ├── backend
│ │ ├── .travis.yml
│ │ ├── db
│ │ │ └── index.js
│ │ ├── index.js
│ │ ├── models
│ │ │ ├── User.js
│ │ │ ├── Website.js
│ │ │ └── index.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── passport
│ │ │ └── index.js
│ │ ├── routes
│ │ │ ├── users.js
│ │ │ └── websites.js
│ │ ├── test
│ │ │ └── test.js
│ │ └── workers
│ │ │ └── uptime.js
│ └── frontend
│ │ ├── .gitignore
│ │ ├── .storybook
│ │ └── config.js
│ │ ├── README.md
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ └── manifest.json
│ │ └── src
│ │ ├── App.css
│ │ ├── App.js
│ │ ├── actions
│ │ └── index.js
│ │ ├── components
│ │ ├── Login.js
│ │ ├── Signup.js
│ │ ├── Website.js
│ │ ├── WebsiteForm.js
│ │ └── WebsiteList.js
│ │ ├── configureStore.js
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── logo.svg
│ │ ├── reducers
│ │ ├── index.js
│ │ └── user.js
│ │ ├── registerServiceWorker.js
│ │ └── test
│ │ ├── __snapshots__
│ │ └── storyshots.test.js.snap
│ │ ├── stories
│ │ └── index.stories.js
│ │ └── storyshots.test.js
└── images
│ ├── first-worker.png
│ └── worker-test.png
├── task-11
├── README.md
├── Solution.md
├── code
│ └── backend
│ │ ├── .travis.yml
│ │ ├── db
│ │ └── index.js
│ │ ├── index.js
│ │ ├── models
│ │ ├── User.js
│ │ ├── Website.js
│ │ └── index.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── passport
│ │ └── index.js
│ │ ├── routes
│ │ ├── users.js
│ │ └── websites.js
│ │ ├── test
│ │ └── test.js
│ │ └── workers
│ │ └── uptime.js
└── images
│ └── test-sms.png
├── task-12
├── README.md
├── Solution.md
├── code
│ ├── backend
│ │ ├── .dockerignore
│ │ ├── .travis.yml
│ │ ├── Dockerfile
│ │ ├── db
│ │ │ └── index.js
│ │ ├── docker-compose.yml
│ │ ├── index.js
│ │ ├── init
│ │ │ └── init.sql
│ │ ├── models
│ │ │ ├── User.js
│ │ │ ├── Website.js
│ │ │ └── index.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── passport
│ │ │ └── index.js
│ │ ├── routes
│ │ │ ├── users.js
│ │ │ └── websites.js
│ │ ├── test
│ │ │ └── test.js
│ │ └── workers
│ │ │ └── uptime.js
│ └── frontend
│ │ ├── .gitignore
│ │ ├── .storybook
│ │ └── config.js
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ ├── docker-compose.yml
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ └── manifest.json
│ │ └── src
│ │ ├── App.css
│ │ ├── App.js
│ │ ├── actions
│ │ └── index.js
│ │ ├── components
│ │ ├── Login.js
│ │ ├── Signup.js
│ │ ├── Website.js
│ │ ├── WebsiteForm.js
│ │ └── WebsiteList.js
│ │ ├── configureStore.js
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── logo.svg
│ │ ├── reducers
│ │ ├── index.js
│ │ └── user.js
│ │ ├── registerServiceWorker.js
│ │ └── test
│ │ ├── __snapshots__
│ │ └── storyshots.test.js.snap
│ │ ├── stories
│ │ └── index.stories.js
│ │ └── storyshots.test.js
└── images
│ └── docker-version.png
├── task-13
└── README.md
├── task-14
└── README.md
├── task-15
└── README.md
├── task-2
├── README.md
├── Solution.md
├── code
│ ├── db
│ │ └── index.js
│ ├── index.js
│ ├── models
│ │ └── User.js
│ ├── package-lock.json
│ ├── package.json
│ └── routes
│ │ └── users.js
└── images
│ ├── postman-api-login.png
│ └── postman-api.png
├── task-3
├── README.md
├── Solution.md
├── code
│ ├── task-1
│ │ ├── index.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ └── test
│ │ │ └── test.js
│ └── task-2
│ │ ├── .travis.yml
│ │ ├── db
│ │ └── index.js
│ │ ├── index.js
│ │ ├── models
│ │ └── User.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── routes
│ │ └── users.js
│ │ └── test
│ │ └── test.js
└── images
│ └── test-task-3.png
├── task-4
├── README.md
├── Solution.md
├── code
│ ├── backend
│ │ ├── db
│ │ │ └── index.js
│ │ ├── index.js
│ │ ├── models
│ │ │ └── User.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── routes
│ │ │ └── users.js
│ │ └── test
│ │ │ └── test.js
│ ├── frontend
│ │ ├── README.md
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── public
│ │ │ ├── favicon.ico
│ │ │ ├── index.html
│ │ │ └── manifest.json
│ │ └── src
│ │ │ ├── App.css
│ │ │ ├── App.js
│ │ │ ├── App.test.js
│ │ │ ├── actions
│ │ │ └── index.js
│ │ │ ├── components
│ │ │ ├── Login.js
│ │ │ ├── Signup.js
│ │ │ └── simple-component.js
│ │ │ ├── index.css
│ │ │ ├── index.js
│ │ │ ├── logo.svg
│ │ │ ├── reducers
│ │ │ ├── default-reducer.js
│ │ │ └── index.js
│ │ │ └── registerServiceWorker.js
│ └── package.json
└── images
│ ├── signup-1.png
│ ├── signup-2.png
│ └── signup-login.png
├── task-5
├── README.md
├── Solution.md
├── code
│ ├── .storybook
│ │ └── config.js
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ └── manifest.json
│ └── src
│ │ ├── App.css
│ │ ├── App.js
│ │ ├── actions
│ │ └── index.js
│ │ ├── components
│ │ ├── Login.js
│ │ ├── Signup.js
│ │ └── simple-component.js
│ │ ├── configureStore.js
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── logo.svg
│ │ ├── reducers
│ │ ├── default-reducer.js
│ │ └── index.js
│ │ ├── registerServiceWorker.js
│ │ └── test
│ │ ├── __snapshots__
│ │ └── storyshots.test.js.snap
│ │ ├── stories
│ │ └── index.stories.js
│ │ └── storyshots.test.js
└── images
│ ├── snapshot-1.png
│ └── storybook-1.png
├── task-6
├── README.md
├── Solution.md
└── code
│ ├── db
│ └── index.js
│ ├── index.js
│ ├── models
│ ├── User.js
│ ├── Website.js
│ └── index.js
│ ├── package-lock.json
│ ├── package.json
│ ├── passport
│ └── index.js
│ └── routes
│ ├── users.js
│ └── websites.js
├── task-7
├── README.md
├── Solution.md
├── code
│ ├── .travis.yml
│ ├── db
│ │ └── index.js
│ ├── index.js
│ ├── models
│ │ ├── User.js
│ │ ├── Website.js
│ │ └── index.js
│ ├── package-lock.json
│ ├── package.json
│ ├── passport
│ │ └── index.js
│ ├── routes
│ │ ├── users.js
│ │ └── websites.js
│ └── test
│ │ └── test.js
└── images
│ ├── test-result-1.png
│ ├── test-result-2.png
│ ├── test-result-3.png
│ └── test-result-4.png
├── task-8
├── README.md
├── Screen Shot 2018-08-12 at 11.57.25 AM.png
├── Screen Shot 2018-08-12 at 12.01.27 PM.png
├── Screen Shot 2018-08-12 at 12.01.31 PM.png
├── Screen Shot 2018-08-12 at 12.01.37 PM.png
├── Solution.md
├── code
│ ├── backend
│ │ ├── .travis.yml
│ │ ├── db
│ │ │ └── index.js
│ │ ├── index.js
│ │ ├── models
│ │ │ ├── User.js
│ │ │ ├── Website.js
│ │ │ └── index.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── passport
│ │ │ └── index.js
│ │ ├── routes
│ │ │ ├── users.js
│ │ │ └── websites.js
│ │ └── test
│ │ │ └── test.js
│ └── frontend
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ └── manifest.json
│ │ └── src
│ │ ├── App.css
│ │ ├── App.js
│ │ ├── App.test.js
│ │ ├── actions
│ │ └── index.js
│ │ ├── components
│ │ ├── Login.js
│ │ ├── Signup.js
│ │ ├── Website.js
│ │ ├── WebsiteForm.js
│ │ └── WebsiteList.js
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── logo.svg
│ │ ├── reducers
│ │ ├── index.js
│ │ └── user.js
│ │ └── registerServiceWorker.js
└── images
│ ├── folders.png
│ ├── form-and-list.png
│ ├── signup-form.png
│ └── website-form.png
└── task-9
├── README.md
├── Solution.md
└── code
├── backend
├── .travis.yml
├── db
│ └── index.js
├── index.js
├── models
│ ├── User.js
│ ├── Website.js
│ └── index.js
├── package-lock.json
├── package.json
├── passport
│ └── index.js
├── routes
│ ├── users.js
│ └── websites.js
└── test
│ └── test.js
└── frontend
├── .gitignore
├── .storybook
└── config.js
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
└── src
├── App.css
├── App.js
├── actions
└── index.js
├── components
├── Login.js
├── Signup.js
├── Website.js
├── WebsiteForm.js
└── WebsiteList.js
├── configureStore.js
├── index.css
├── index.js
├── logo.svg
├── reducers
├── index.js
└── user.js
├── registerServiceWorker.js
└── test
├── __snapshots__
└── storyshots.test.js.snap
├── stories
└── index.stories.js
└── storyshots.test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | jest-test-results.json*
8 |
9 | # Runtime data
10 | pids
11 | *.pid
12 | *.seed
13 | *.pid.lock
14 |
15 | # Directory for instrumented libs generated by jscoverage/JSCover
16 | lib-cov
17 |
18 | # Coverage directory used by tools like istanbul
19 | coverage
20 |
21 | # nyc test coverage
22 | .nyc_output
23 |
24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
25 | .grunt
26 |
27 | # Bower dependency directory (https://bower.io/)
28 | bower_components
29 |
30 | # node-waf configuration
31 | .lock-wscript
32 |
33 | # Compiled binary addons (https://nodejs.org/api/addons.html)
34 | build/Release
35 |
36 | # Dependency directories
37 | node_modules/
38 | jspm_packages/
39 |
40 | # TypeScript v1 declaration files
41 | typings/
42 |
43 | # Optional npm cache directory
44 | .npm
45 |
46 | # Optional eslint cache
47 | .eslintcache
48 |
49 | # Optional REPL history
50 | .node_repl_history
51 |
52 | # Output of 'npm pack'
53 | *.tgz
54 |
55 | # Yarn Integrity file
56 | .yarn-integrity
57 |
58 | # dotenv environment variables file
59 | .env
60 |
61 | # next.js build output
62 | .next
63 |
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | course.hackerbayuniversity.com
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 HackerBay University
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-slate
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | # App we're building
2 |
3 | We're working on a server monitoring software. For a production example, please check out [fyipe.com](https://fyipe.com).
4 |
5 | **The app will:**
6 |
7 | - Let users sign up and login.
8 | - When the user logs in, he is navigated to his dashboard.
9 | - User can add one or more websites to monitor.
10 | - The app will check the website(s) every minute and will let the user know when it's down.
11 | - The user will be alerted via email, call, SMS.
12 | - User can also attach his twitter account and the updates about the website(s) will be posted there.
13 | - Dashboard will show the uptime percent from the day the `websites to monitor` were added and will also show the current status of the website(s) - If its `Online` or `Offline`
14 |
15 | ### Why server monitoring?
16 |
17 | It covers most concepts to build an app - from relationships in the database, to workers, to third party API integration and more. You can finish the app fairly quickly and get a good understanding of all the concepts from `Web Development`
18 |
19 | ### FAQ
20 |
21 | **How will the app look like?**
22 | If you have any questions on how the app will look like, please check out [fyipe.com](https://fyipe.com). Your mentor should give you a demo account for you to test things out.
23 |
24 | **Will I be building the exact clone?**
25 | No. That's months or years of work. You'll be building a very simple clone, with a simple UI and few API's.
26 |
27 | **I have more questions, who do I ask?**
28 | If you're enrolled in the program, the best person to ask any question would be your mentor. If you're not, please write to us at university@hackerbay.io
29 |
30 | **Can I build something else?**
31 | Yes. Please talk to your mentor and confirm your idea with him.
32 |
--------------------------------------------------------------------------------
/checklist/README.md:
--------------------------------------------------------------------------------
1 | # Onboarding Checklist
2 | - If you were selected and have not been invited to Slack, please email us at university@hackerbay.io
3 | - When you've joined Hackerbay University Slack, make sure you:
4 | - Upload your real profile picture.
5 | - Download Slack Apps for Mobile, and Desktop and sign in there.
6 | - You will be paired with a mentor. If you're not paired yet, please email university@hackerbay.io or talk to `Kumar Abhishek` on Slack.
7 | - `Kumar Abhishek` is the admin on Slack. He is also managing this program. If there are any feedback / issues, please do let him know.
8 |
9 | ... and that's quite about it! Welcome aboard, and let's partner up in helping you grow.
10 |
11 |
--------------------------------------------------------------------------------
/contribute/README.md:
--------------------------------------------------------------------------------
1 | # Contribute
2 |
3 | This course and free and open for everyone in the world. If you wish to make a change and improve it. We welcome your changes. Please send a [Pull Request](https://help.github.com/articles/about-pull-requests/) to `master` for review and we will have ti merged in sooner than you expect.
4 |
--------------------------------------------------------------------------------
/faq/README.md:
--------------------------------------------------------------------------------
1 | # FAQ
2 |
3 | **Why does this course use PostgreSQL and not MongoDB or MySQL?**
4 |
5 | Most apps are not high scale as Twitter or Facebook and SQL Databases are used for 99.99% of those use cases. PostgreSQL does JSON storage (like MongoDB) and gives you all the best features of SQL (like relations, joins and more). It's the best of both worlds.
6 |
--------------------------------------------------------------------------------
/signup-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/signup-1.png
--------------------------------------------------------------------------------
/signup-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/signup-2.png
--------------------------------------------------------------------------------
/tasks/task-1/code/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 |
4 | const app = express();
5 | app.use(bodyParser.json());
6 |
7 | app.get('/', (req, res) => {
8 | res.json({ status: 'success' });
9 | });
10 |
11 | // variable should be outside of the API's scope.
12 | let data = null;
13 |
14 | app.post('/data', (req, res) => {
15 | // We assign the body of the request to the data variable.
16 | data = req.body;
17 | // We send the data variable in the response.
18 | res.json(data);
19 | });
20 |
21 | app.get('/data', (req, res) => {
22 | //data comes from the variable we declared and mutared earlier
23 | res.json(data);
24 | })
25 |
26 | app.listen(3000, () => console.log('Listening on port 3000'));
--------------------------------------------------------------------------------
/tasks/task-1/code/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "first-api",
3 | "version": "1.0.0",
4 | "description": "Learn how server programming works and create your first get and post API",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "body-parser": "^1.18.3",
13 | "express": "^4.16.3"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tasks/task-1/images/post-api-postman.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-1/images/post-api-postman.png
--------------------------------------------------------------------------------
/tasks/task-10/README.md:
--------------------------------------------------------------------------------
1 | # Task 10 - Workers
2 |
3 | ## Objectives
4 |
5 | - Learn about background workers.
6 | - Learn how to implement workers in your NodeJS application.
7 |
8 | ## Learning Resources
9 |
10 | Here are the list of learning resources for this task.
11 |
12 | Topic | Resource
13 | ------------ | -------------
14 | How to run NodeJS Cron Jobs | [Link to this resource](https://www.youtube.com/watch?v=ppFqkXJmwS0)
15 | Node Cron Documentation | [Link to this resource](https://github.com/kelektiv/node-cron)
16 |
17 |
18 | ## Tasks
19 |
20 | #### Step 1: Set up Cron Job
21 |
22 | - Create a new folder on our backend called `workers`, and create a new file in that folder called `uptime.js`.
23 | - NPM install the `node-cron` module.
24 | - Create a new job and schedule it to run every minute.
25 |
26 |
27 | #### Step 2: Check Uptime.
28 |
29 | - In that job, get a list of all the websites that you have on the database for all customers.
30 | - Check uptime using the `request` module. (There are better ways to do it, but lets make this simpler for now.)
31 | - If the website is down. Update the status to `offline` in the table. If the website is up, update the status to `online`. This should reflect on the dashboard when you reload the dashboard.
32 |
33 | #### Step 3: Test
34 |
35 | - Unit test the worker by adding few test websites (test cases). When you run the test, it should wait for a minute and then check the results. Validate if the results are correct.
36 |
37 | ## Deliverable
38 |
39 | - Push changes to Git, make sure the build passes.
40 |
41 |
42 |
--------------------------------------------------------------------------------
/tasks/task-10/code/backend/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | node_js:
4 | - "node"
5 | services:
6 | - postgresql
7 | install:
8 | - "npm install"
9 | script:
10 | - "npm test"
--------------------------------------------------------------------------------
/tasks/task-10/code/backend/db/index.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 |
3 | let dbUrl = null;
4 |
5 | if(process.env.NODE_ENV === 'test') dbUrl = 'postgres://utvdnzdb:hxLot3tUXkVkmG66z7uQlO05f-N1rjun@packy.db.elephantsql.com:5432/utvdnzdb';
6 | else {
7 | dbUrl = 'postgres://ivan:123456@localhost:5432/tutorial_web_monitor';
8 | }
9 |
10 | const sequelize = new Sequelize(dbUrl, {
11 | host: 'localhost',
12 | dialect: 'postgres',
13 | operatorsAliases: false,
14 | logging: false,
15 |
16 | pool: {
17 | max: 5,
18 | min: 0,
19 | acquire: 30000,
20 | idle: 10000
21 | }
22 | });
23 |
24 | module.exports = sequelize;
--------------------------------------------------------------------------------
/tasks/task-10/code/backend/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 | const logger = require('morgan');
4 | const passport = require('passport');
5 |
6 | const users = require('./routes/users');
7 | const websites = require('./routes/websites');
8 |
9 | const app = express();
10 |
11 | app.use(bodyParser.json());
12 |
13 | if(process.env.NODE_ENV !== 'test') app.use(logger('tiny'));
14 |
15 | app.use(passport.initialize());
16 |
17 | require('./passport/')(passport);
18 |
19 | app.use('/users', users);
20 | app.use('/websites', websites);
21 |
22 | app.get('/', (req, res) => {
23 | res.json({ success: true })
24 | });
25 |
26 | app.listen(3001, () => console.log('Listening on Port 3001'));
27 |
28 | module.exports = app;
--------------------------------------------------------------------------------
/tasks/task-10/code/backend/models/User.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | const User = sequelize.define('User', {
5 | email: DataTypes.STRING,
6 | password: DataTypes.STRING
7 | });
8 |
9 | User.associate = function (models) {
10 | models.User.hasMany(models.Website);
11 | };
12 |
13 | User.beforeCreate((user, options) => {
14 | let salt = bcrypt.genSaltSync(10);
15 | let hash = bcrypt.hashSync(user.password, salt);
16 | return user.password = hash;
17 | })
18 |
19 | return User;
20 | };
--------------------------------------------------------------------------------
/tasks/task-10/code/backend/models/Website.js:
--------------------------------------------------------------------------------
1 | module.exports = (sequelize, DataTypes) => {
2 | const Website = sequelize.define('Website', {
3 | name: DataTypes.STRING,
4 | url: {
5 | type: DataTypes.STRING,
6 | validate: {
7 | isUrl: {
8 | msg: "URL is not valid"
9 | }
10 | }
11 | },
12 | status: {
13 | type: DataTypes.STRING,
14 | defaultValue: 'online'
15 | }
16 | });
17 |
18 | Website.associate = function (models) {
19 | models.Website.belongsTo(models.User, {
20 | foreignKey: { allowNull: false },
21 | onDelete: "CASCADE"
22 | });
23 | };
24 |
25 | return Website;
26 | };
--------------------------------------------------------------------------------
/tasks/task-10/code/backend/models/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const Sequelize = require('sequelize');
4 | const basename = path.basename(__filename);
5 | const sequelize = require('../db');
6 |
7 | let db = {};
8 |
9 | fs
10 | .readdirSync(__dirname)
11 | .filter(file => {
12 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
13 | })
14 | .forEach(file => {
15 | var model = sequelize['import'](path.join(__dirname, file));
16 | db[model.name] = model;
17 | });
18 |
19 | Object.keys(db).forEach(modelName => {
20 | if (db[modelName].associate) {
21 | db[modelName].associate(db);
22 | }
23 | });
24 |
25 | db.sequelize = sequelize;
26 | db.Sequelize = Sequelize;
27 |
28 | module.exports = db;
--------------------------------------------------------------------------------
/tasks/task-10/code/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "task-6",
3 | "version": "1.0.0",
4 | "description": "Tutorial for task 6",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha --timeout 10000 --exit",
8 | "start": "node index.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "bcryptjs": "^2.4.3",
14 | "body-parser": "^1.18.3",
15 | "cron": "^1.4.1",
16 | "express": "^4.16.3",
17 | "jsonwebtoken": "^8.3.0",
18 | "morgan": "^1.9.1",
19 | "passport": "^0.4.0",
20 | "passport-jwt": "^4.0.0",
21 | "pg": "^7.4.3",
22 | "pg-hstore": "^2.3.2",
23 | "request": "^2.88.0",
24 | "sequelize": "^4.38.1"
25 | },
26 | "devDependencies": {
27 | "chai": "^4.1.2",
28 | "chai-http": "^4.2.0",
29 | "mocha": "^5.2.0",
30 | "sinon": "^6.3.5"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tasks/task-10/code/backend/passport/index.js:
--------------------------------------------------------------------------------
1 | const JwtStrategy = require('passport-jwt').Strategy;
2 | const ExtractJwt = require('passport-jwt').ExtractJwt;
3 | const models = require('../models')
4 |
5 | let opts = {};
6 |
7 | opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken()
8 | opts.secretOrKey = 'secret';
9 |
10 | module.exports = passport => {
11 | passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
12 | models.User.findById(jwt_payload.id)
13 | .then(user => {
14 | if (user) return done(null, user);
15 | return done(null, false)
16 | })
17 | .catch(err => console.log(err));
18 | }));
19 | }
--------------------------------------------------------------------------------
/tasks/task-10/code/backend/routes/users.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const jwt = require('jsonwebtoken');
3 | const bcrypt = require('bcryptjs');
4 |
5 | const models = require('../models');
6 | const router = express.Router();
7 |
8 | router.post('/signup', (req, res) => {
9 | let newUser = {
10 | email: req.body.email,
11 | password: req.body.password
12 | }
13 |
14 | models.User.findOne({ where: { email: newUser.email }})
15 | .then(user => {
16 | if(user) return res.status(401).json({ msg: 'Email already exists' })
17 | else {
18 | models.User.create(newUser)
19 | .then(user => {
20 | let payload = {
21 | email: user.email,
22 | id: user.id
23 | }
24 | jwt.sign(payload, 'secret', { expiresIn: '1h'}, (err, token) => {
25 | // Here we send the token to the client.
26 | res.json({session: token})
27 | })
28 | })
29 | }
30 | })
31 | .catch(err => res.status(401).json(err));
32 | });
33 |
34 | router.post('/login', (req, res) => {
35 | let newUser = {
36 | email: req.body.email,
37 | password: req.body.password
38 | }
39 |
40 | models.User.findOne({ where: { email: newUser.email }})
41 | .then(user => {
42 | if(user) {
43 | bcrypt.compare(newUser.password, user.password)
44 | .then(isMatch => {
45 | if(isMatch) {
46 | let payload = { email: user.email, id: user.id }
47 | jwt.sign(payload, 'secret', { expiresIn: '1h'}, (err, token) => {
48 | res.json({session: token})
49 | })
50 | }
51 | else {
52 | res.status(401).json({error: 'Invalid Password'})
53 | }
54 | })
55 | }
56 | else {
57 | res.status(404).json({error: 'User does not exist.'})
58 | }
59 | })
60 | .catch(err => res.status(401).json(err));
61 | })
62 |
63 | module.exports = router;
--------------------------------------------------------------------------------
/tasks/task-10/code/backend/routes/websites.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const models = require('../models/');
4 | const passport = require('passport');
5 | const jwt = require('jsonwebtoken');
6 |
7 | router.get('/list',
8 | passport.authenticate('jwt', { session: false }),
9 | (req, res) => {
10 | let pureToken = req.get('Authorization').slice(7)
11 | let currentUser = jwt.verify(pureToken, 'secret');
12 |
13 | models.Website.findAll({
14 | where: {
15 | UserId: currentUser.id
16 | }
17 | })
18 | .then(websites => res.json(websites))
19 | .catch(err => res.status(400).json({ msg: "Error" }))
20 | })
21 |
22 | router.post('/add',
23 | passport.authenticate('jwt', { session: false }),
24 | (req, res) => {
25 |
26 | let pureToken = req.get('Authorization').slice(7)
27 | let currentUser = jwt.verify(pureToken, 'secret');
28 |
29 | if(req.body.constructor === Object && Object.keys(req.body).length === 0) return res.status(400).json({ msg: 'Invalid data' })
30 |
31 | let newWebsite = {
32 | name: req.body.name,
33 | url: req.body.url
34 | }
35 |
36 | models.User.findOne({
37 | where: {
38 | id: currentUser.id
39 | }
40 | })
41 | .then(user => {
42 | if(!user) res.status(401).json({ msg:"User not found, please log in" })
43 | models.Website.find({
44 | where: {
45 | UserId: currentUser.id,
46 | url: newWebsite.url
47 | }
48 | })
49 | .then(website => {
50 | if(website) res.status(400).json({ msg: "Website already added" })
51 | else {
52 | models.Website.create(newWebsite)
53 | .then(website => {
54 | website.setUser(currentUser.id)
55 | res.json(website)
56 | })
57 | .catch(err => res.status(400).json(err))
58 | }
59 | })
60 | })
61 | .catch(err => res.status(401).send(err))
62 | })
63 |
64 | module.exports = router;
--------------------------------------------------------------------------------
/tasks/task-10/code/backend/workers/uptime.js:
--------------------------------------------------------------------------------
1 | const request = require('request');
2 | const models = require('../models');
3 |
4 | module.exports = function(cron){
5 |
6 | function changeStatus(){
7 | models.Website.findAll({})
8 | .then(websites => {
9 | websites.map(website => {
10 | request(`http://${website.dataValues.url}`, function (error, response, body) {
11 | if((response && response.statusCode) !== 200 && website.dataValues.status === 'online') {
12 | models.Website.update(
13 | {status: 'offline'},
14 | { where: { id: website.id }, returning: true})
15 | .then(result => console.log('Status updated'))
16 | .catch(err => console.log(err));
17 | }
18 | })
19 | })
20 | })
21 | .catch(err => console.log(err));
22 | }
23 |
24 | let statusJob = new cron.CronJob({
25 | cronTime : '* * * * * *',
26 | onTick : changeStatus,
27 | start : false
28 | });
29 |
30 | return statusJob;
31 |
32 | }
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from '@storybook/react';
2 |
3 | function loadStories() {
4 | const req = require.context('../src', true, /.stories.js$/);
5 | req.keys().forEach((filename) => req(filename));
6 | }
7 |
8 | configure(loadStories, module);
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/README.md:
--------------------------------------------------------------------------------
1 | #### React Redux Starter Template
2 |
3 | This project contains a boilerplate template created from the create-react-app library and configured along with redux.
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-starter-template",
3 | "version": "0.1.0",
4 | "private": true,
5 | "proxy": "http://localhost:3001/",
6 | "dependencies": {
7 | "axios": "^0.18.0",
8 | "react": "^16.5.2",
9 | "react-dom": "^16.5.2",
10 | "react-redux": "^5.0.6",
11 | "react-router-dom": "^4.2.2",
12 | "react-scripts": "1.0.17",
13 | "redux": "^3.7.2",
14 | "redux-form": "^7.4.2",
15 | "redux-logger": "^3.0.6",
16 | "redux-promise": "^0.5.3"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test --env=jsdom",
22 | "eject": "react-scripts eject",
23 | "storybook": "start-storybook -p 9001 -c .storybook"
24 | },
25 | "devDependencies": {
26 | "@storybook/addon-storyshots": "^3.4.11",
27 | "@storybook/react": "^3.4.11",
28 | "react-test-renderer": "^16.5.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-10/code/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | form {
2 | border: solid 1px black;
3 | padding: 15px;
4 | margin: 15px;
5 | width: 30vw;
6 | text-align: center;
7 | }
8 |
9 | .websites {
10 | padding: 15px;
11 | margin: 15px;
12 | }
13 |
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export const LOGIN = 'LOGIN';
2 | export const WEBSITES = 'WEBSITES';
3 | export const LOGOUT = 'LOGOUT';
4 |
5 | export function loginUser() {
6 | return {
7 | type: LOGIN,
8 | payload: true
9 | }
10 | }
11 |
12 | export function getWebsites(websites) {
13 | return {
14 | type: WEBSITES,
15 | payload: websites
16 | }
17 | }
18 |
19 | export function logout() {
20 | return {
21 | type: LOGOUT,
22 | payload: false
23 | }
24 | }
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/src/components/Login.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Link } from "react-router-dom";
4 |
5 | let Login = props => {
6 | const { handleSubmit } = props
7 | return (
8 |
23 | )
24 | }
25 |
26 | Login = reduxForm({
27 | // a unique name for the form
28 | form: 'login'
29 | })(Login)
30 |
31 | export default Login
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/src/components/Signup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Link } from "react-router-dom";
4 |
5 | let Signup = props => {
6 | const { handleSubmit } = props
7 | return (
8 |
23 | )
24 | }
25 |
26 | Signup = reduxForm({
27 | // a unique name for the form
28 | form: 'signup'
29 | })(Signup)
30 |
31 | export default Signup
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/src/components/Website.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Website = ({ name, url, status }) => (
4 |
5 |
{name}
6 |
{url}
7 |
{status}
8 |
9 | )
10 |
11 | export default Website;
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/src/components/WebsiteForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Field, reduxForm } from 'redux-form';
4 | import { logout } from '../actions';
5 |
6 | let WebsiteForm = props => {
7 | const { handleSubmit } = props
8 |
9 | function logout () {
10 | sessionStorage.clear();
11 | props.logout();
12 | }
13 |
14 | return (
15 |
30 | )
31 | }
32 |
33 | const mapDispatchToProps = {
34 | logout
35 | }
36 |
37 | WebsiteForm = connect(
38 | null,
39 | mapDispatchToProps
40 | )(WebsiteForm);
41 |
42 | export default reduxForm({
43 | form: 'websiteForm'
44 | })(WebsiteForm);
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/src/components/WebsiteList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import axios from 'axios';
4 |
5 | import { getWebsites } from '../actions';
6 | import Website from './Website';
7 |
8 | class WebsiteList extends Component {
9 |
10 | componentDidMount(){
11 | let token = sessionStorage.getItem('token');
12 |
13 | axios.get('/websites/list', { headers: { 'Authorization': token } })
14 | .then(websites => {
15 | this.props.getWebsites(websites)
16 | })
17 | .catch(err => alert(err.response.data.msg));
18 | }
19 |
20 | render() {
21 | return (
22 |
23 |
Websites
24 | {
25 | this.props.user.websites.reverse().map(website =>
26 |
32 | )
33 | }
34 |
35 | );
36 | }
37 | }
38 |
39 | const mapStateToProps = state => {
40 | return {
41 | user: state.user
42 | }
43 | }
44 |
45 | const mapDispatchToProps = {
46 | getWebsites
47 | }
48 |
49 | export default connect(mapStateToProps, mapDispatchToProps)(WebsiteList);
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/src/configureStore.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, compose, createStore } from 'redux'
2 | import ReduxPromise from 'redux-promise';
3 |
4 | import rootReducer from './reducers';
5 |
6 | export default function configureStore(preloadedState) {
7 | const middlewareEnhancer = applyMiddleware(ReduxPromise)
8 |
9 | const enhancers = [middlewareEnhancer]
10 | const composedEnhancers = compose(...enhancers)
11 |
12 | const store = createStore(rootReducer, preloadedState, composedEnhancers)
13 |
14 | return store
15 | }
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | import { Provider } from 'react-redux';
8 |
9 | import configureStore from './configureStore'
10 |
11 | const store = configureStore()
12 |
13 | ReactDOM.render(
14 |
15 |
16 |
17 | , document.getElementById('root'));
18 | registerServiceWorker();
19 |
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import { reducer as formReducer } from 'redux-form';
4 | import userReducer from './user';
5 |
6 | const rootReducers = combineReducers({
7 | form: formReducer.plugin({
8 | websiteForm: (state, action) => {
9 | switch(action.type) {
10 | case 'ACCOUNT_SAVE_SUCCESS':
11 | return undefined;
12 | default:
13 | return state;
14 | }
15 | }
16 | }),
17 | user: userReducer
18 | });
19 |
20 | export default rootReducers;
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/src/reducers/user.js:
--------------------------------------------------------------------------------
1 | import { LOGIN, WEBSITES, LOGOUT } from '../actions';
2 |
3 | let initialState = {
4 | isLoggedIn: false,
5 | websites: []
6 | }
7 |
8 | const user = (state = initialState, action) => {
9 | switch(action.type) {
10 | case LOGIN:
11 | return state = {...state, isLoggedIn: true };
12 | case WEBSITES:
13 | return state = {...state, websites: action.payload.data };
14 | case LOGOUT:
15 | return state = {...state, isLoggedIn: action.payload };
16 | default:
17 | return state;
18 | }
19 | }
20 |
21 | export default user
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/src/test/stories/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { action } from '@storybook/addon-actions';
4 | import { Provider } from 'react-redux';
5 | import Login from '../../components/Login';
6 | import Signup from '../../components/Signup';
7 | import WebsiteForm from '../../components/WebsiteForm';
8 | import Website from '../../components/Website';
9 | import configureStore from '../../configureStore';
10 | import { MemoryRouter } from 'react-router';
11 |
12 | // We bring our redux Store
13 | const store = configureStore();
14 |
15 | storiesOf('Signup and Login', module)
16 | .addDecorator(story =>
17 | {/* This will prevent errors because of your Link component*/}
18 | {story()}
19 | )
20 | .add('Login', () => (
21 |
22 | ))
23 | .add('Signup', () => (
24 |
25 | ))
26 | .add('WebsiteForm', () => (
27 |
28 | ))
29 | .add('Website', () => (
30 |
31 | ))
--------------------------------------------------------------------------------
/tasks/task-10/code/frontend/src/test/storyshots.test.js:
--------------------------------------------------------------------------------
1 | import initStoryshots from '@storybook/addon-storyshots';
2 |
3 | initStoryshots({ /* configuration options */ });
--------------------------------------------------------------------------------
/tasks/task-10/images/first-worker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-10/images/first-worker.png
--------------------------------------------------------------------------------
/tasks/task-10/images/worker-test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-10/images/worker-test.png
--------------------------------------------------------------------------------
/tasks/task-11/README.md:
--------------------------------------------------------------------------------
1 | # Task 11 - Working with Third Party API's and libraries.
2 |
3 | ## Objectives
4 |
5 | - Learn to implement third party API and libraries with your application.
6 | - Learn to send SMS with Twilio.
7 |
8 | ## Learning Resources
9 |
10 | Here are the list of learning resources for this task.
11 |
12 | Topic | Resource
13 | ------------ | -------------
14 | Documentation | [Link to this resource](https://www.twilio.com/docs/)
15 |
16 |
17 | ## Tasks
18 |
19 | #### Step 1: Add a new column to user table.
20 |
21 | - Add a Phone Number column to user table. Collect that info on the signup form.
22 |
23 | #### Step 2: Send SMS when Website is down.
24 |
25 | - Create a new account on Twilio. They have you $10 free. That's a lot to test and integrate SMS functionality.
26 | - In the cron job you've built in Task 10. When the website goes down, send an SMS to the user on the Phone Number you collected during signup.
27 | - Make sure you dont send an SMS every minute, you only send when the status changes.
28 | - Send an SMS when the website is back online.
29 |
30 |
31 | #### Step 3: Test.
32 |
33 | - Unit test the SMS sending function that you have created.
34 |
35 | ## Deliverable
36 |
37 | - Push changes to Git, make sure the build passes.
38 |
39 |
40 |
--------------------------------------------------------------------------------
/tasks/task-11/code/backend/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | node_js:
4 | - "node"
5 | services:
6 | - postgresql
7 | install:
8 | - "npm install"
9 | script:
10 | - "npm test"
--------------------------------------------------------------------------------
/tasks/task-11/code/backend/db/index.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 |
3 | let dbUrl = null;
4 |
5 | if(process.env.NODE_ENV === 'test') dbUrl = 'postgres://utvdnzdb:hxLot3tUXkVkmG66z7uQlO05f-N1rjun@packy.db.elephantsql.com:5432/utvdnzdb';
6 | else {
7 | dbUrl = 'postgres://ivan:123456@localhost:5432/tutorial_web_monitor';
8 | }
9 |
10 | const sequelize = new Sequelize(dbUrl, {
11 | host: 'localhost',
12 | dialect: 'postgres',
13 | operatorsAliases: false,
14 | logging: false,
15 |
16 | pool: {
17 | max: 5,
18 | min: 0,
19 | acquire: 30000,
20 | idle: 10000
21 | }
22 | });
23 |
24 | module.exports = sequelize;
--------------------------------------------------------------------------------
/tasks/task-11/code/backend/index.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const express = require('express');
3 | const bodyParser = require('body-parser');
4 | const logger = require('morgan');
5 | const passport = require('passport');
6 | const cron = require('cron');
7 |
8 | const users = require('./routes/users');
9 | const websites = require('./routes/websites');
10 | const statusJob = require('./workers/uptime')(cron);
11 |
12 | const app = express();
13 |
14 | app.use(bodyParser.json());
15 |
16 | if(process.env.NODE_ENV !== 'test') {
17 | statusJob.start();
18 | app.use(logger('tiny'));
19 | };
20 |
21 | app.use(passport.initialize());
22 |
23 | require('./passport/')(passport);
24 |
25 | app.use('/users', users);
26 | app.use('/websites', websites);
27 |
28 | app.get('/', (req, res) => {
29 | res.json({ success: true })
30 | });
31 |
32 | app.listen(3001, () => console.log('Listening on Port 3001'));
33 |
34 | module.exports = app;
--------------------------------------------------------------------------------
/tasks/task-11/code/backend/models/User.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | const User = sequelize.define('User', {
5 | email: DataTypes.STRING,
6 | password: DataTypes.STRING
7 | });
8 |
9 | User.associate = function (models) {
10 | models.User.hasMany(models.Website);
11 | };
12 |
13 | User.beforeCreate((user, options) => {
14 | let salt = bcrypt.genSaltSync(10);
15 | let hash = bcrypt.hashSync(user.password, salt);
16 | return user.password = hash;
17 | })
18 |
19 | return User;
20 | };
--------------------------------------------------------------------------------
/tasks/task-11/code/backend/models/Website.js:
--------------------------------------------------------------------------------
1 | module.exports = (sequelize, DataTypes) => {
2 | const Website = sequelize.define('Website', {
3 | name: DataTypes.STRING,
4 | url: {
5 | type: DataTypes.STRING,
6 | validate: {
7 | isUrl: {
8 | msg: "URL is not valid"
9 | }
10 | }
11 | },
12 | status: {
13 | type: DataTypes.STRING,
14 | defaultValue: 'online'
15 | }
16 | });
17 |
18 | Website.associate = function (models) {
19 | models.Website.belongsTo(models.User, {
20 | foreignKey: { allowNull: false },
21 | onDelete: "CASCADE"
22 | });
23 | };
24 |
25 | return Website;
26 | };
--------------------------------------------------------------------------------
/tasks/task-11/code/backend/models/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const Sequelize = require('sequelize');
4 | const basename = path.basename(__filename);
5 | const sequelize = require('../db');
6 |
7 | let db = {};
8 |
9 | fs
10 | .readdirSync(__dirname)
11 | .filter(file => {
12 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
13 | })
14 | .forEach(file => {
15 | var model = sequelize['import'](path.join(__dirname, file));
16 | db[model.name] = model;
17 | });
18 |
19 | Object.keys(db).forEach(modelName => {
20 | if (db[modelName].associate) {
21 | db[modelName].associate(db);
22 | }
23 | });
24 |
25 | db.sequelize = sequelize;
26 | db.Sequelize = Sequelize;
27 |
28 | module.exports = db;
--------------------------------------------------------------------------------
/tasks/task-11/code/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "task-6",
3 | "version": "1.0.0",
4 | "description": "Tutorial for task 6",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha --timeout 10000 --exit",
8 | "start": "node index.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "bcryptjs": "^2.4.3",
14 | "body-parser": "^1.18.3",
15 | "cron": "^1.4.1",
16 | "dotenv": "^6.0.0",
17 | "express": "^4.16.3",
18 | "jsonwebtoken": "^8.3.0",
19 | "morgan": "^1.9.1",
20 | "passport": "^0.4.0",
21 | "passport-jwt": "^4.0.0",
22 | "pg": "^7.4.3",
23 | "pg-hstore": "^2.3.2",
24 | "request": "^2.88.0",
25 | "sequelize": "^4.38.1",
26 | "twilio": "^3.22.0"
27 | },
28 | "devDependencies": {
29 | "chai": "^4.1.2",
30 | "chai-http": "^4.2.0",
31 | "mocha": "^5.2.0",
32 | "sinon": "^6.3.5"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tasks/task-11/code/backend/passport/index.js:
--------------------------------------------------------------------------------
1 | const JwtStrategy = require('passport-jwt').Strategy;
2 | const ExtractJwt = require('passport-jwt').ExtractJwt;
3 | const models = require('../models')
4 |
5 | let opts = {};
6 |
7 | opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken()
8 | opts.secretOrKey = 'secret';
9 |
10 | module.exports = passport => {
11 | passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
12 | models.User.findById(jwt_payload.id)
13 | .then(user => {
14 | if (user) return done(null, user);
15 | return done(null, false)
16 | })
17 | .catch(err => console.log(err));
18 | }));
19 | }
--------------------------------------------------------------------------------
/tasks/task-11/code/backend/routes/users.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const jwt = require('jsonwebtoken');
3 | const bcrypt = require('bcryptjs');
4 |
5 | const models = require('../models');
6 | const router = express.Router();
7 |
8 | router.post('/signup', (req, res) => {
9 | let newUser = {
10 | email: req.body.email,
11 | password: req.body.password
12 | }
13 |
14 | models.User.findOne({ where: { email: newUser.email }})
15 | .then(user => {
16 | if(user) return res.status(401).json({ msg: 'Email already exists' })
17 | else {
18 | models.User.create(newUser)
19 | .then(user => {
20 | let payload = {
21 | email: user.email,
22 | id: user.id
23 | }
24 | jwt.sign(payload, 'secret', { expiresIn: '1h'}, (err, token) => {
25 | // Here we send the token to the client.
26 | res.json({session: token})
27 | })
28 | })
29 | }
30 | })
31 | .catch(err => res.status(401).json(err));
32 | });
33 |
34 | router.post('/login', (req, res) => {
35 | let newUser = {
36 | email: req.body.email,
37 | password: req.body.password
38 | }
39 |
40 | models.User.findOne({ where: { email: newUser.email }})
41 | .then(user => {
42 | if(user) {
43 | bcrypt.compare(newUser.password, user.password)
44 | .then(isMatch => {
45 | if(isMatch) {
46 | let payload = { email: user.email, id: user.id }
47 | jwt.sign(payload, 'secret', { expiresIn: '1h'}, (err, token) => {
48 | res.json({session: token})
49 | })
50 | }
51 | else {
52 | res.status(401).json({error: 'Invalid Password'})
53 | }
54 | })
55 | }
56 | else {
57 | res.status(404).json({error: 'User does not exist.'})
58 | }
59 | })
60 | .catch(err => res.status(401).json(err));
61 | })
62 |
63 | module.exports = router;
--------------------------------------------------------------------------------
/tasks/task-11/code/backend/routes/websites.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const models = require('../models/');
4 | const passport = require('passport');
5 | const jwt = require('jsonwebtoken');
6 |
7 | router.get('/list',
8 | passport.authenticate('jwt', { session: false }),
9 | (req, res) => {
10 | let pureToken = req.get('Authorization').slice(7)
11 | let currentUser = jwt.verify(pureToken, 'secret');
12 |
13 | models.Website.findAll({
14 | where: {
15 | UserId: currentUser.id
16 | }
17 | })
18 | .then(websites => res.json(websites))
19 | .catch(err => res.status(400).json({ msg: "Error" }))
20 | })
21 |
22 | router.post('/add',
23 | passport.authenticate('jwt', { session: false }),
24 | (req, res) => {
25 |
26 | let pureToken = req.get('Authorization').slice(7)
27 | let currentUser = jwt.verify(pureToken, 'secret');
28 |
29 | if(req.body.constructor === Object && Object.keys(req.body).length === 0) return res.status(400).json({ msg: 'Invalid data' })
30 |
31 | let newWebsite = {
32 | name: req.body.name,
33 | url: req.body.url
34 | }
35 |
36 | models.User.findOne({
37 | where: {
38 | id: currentUser.id
39 | }
40 | })
41 | .then(user => {
42 | if(!user) res.status(401).json({ msg:"User not found, please log in" })
43 | models.Website.find({
44 | where: {
45 | UserId: currentUser.id,
46 | url: newWebsite.url
47 | }
48 | })
49 | .then(website => {
50 | if(website) res.status(400).json({ msg: "Website already added" })
51 | else {
52 | models.Website.create(newWebsite)
53 | .then(website => {
54 | website.setUser(currentUser.id)
55 | res.json(website)
56 | })
57 | .catch(err => res.status(400).json(err))
58 | }
59 | })
60 | })
61 | .catch(err => res.status(401).send(err))
62 | })
63 |
64 | module.exports = router;
--------------------------------------------------------------------------------
/tasks/task-11/code/backend/workers/uptime.js:
--------------------------------------------------------------------------------
1 | const request = require('request');
2 | const models = require('../models');
3 | const accountSid = process.env.ACCOUNT_SID;
4 | const authToken = process.env.AUTH_TOKEN;
5 | const client = require('twilio')(accountSid, authToken);
6 |
7 | module.exports = function(cron){
8 |
9 | function changeStatus(){
10 | models.Website.findAll({})
11 | .then(websites => {
12 | websites.map(website => {
13 | request(`http://${website.dataValues.url}`, function (error, response, body) {
14 | if((response && response.statusCode) !== 200 && website.dataValues.status === 'online') {
15 | models.Website.update(
16 | {status: 'offline'},
17 | { where: { id: website.id }, returning: true})
18 | .then(result => {
19 | if(process.env.NODE_ENV !== 'test'){
20 | client.messages
21 | .create({
22 | body: `${website.name} is offline`,
23 | from: process.env.PHONE_NUMBER,
24 | to: process.env.MY_NUMBER
25 | })
26 | .then(message => console.log(message.sid))
27 | .done();
28 | }
29 | })
30 | .catch(err => console.log(err));
31 | }
32 | if((response && response.statusCode) === 200 && website.dataValues.status === 'offline') {
33 | models.Website.update(
34 | {status: 'online'},
35 | { where: { id: website.id }, returning: true})
36 | .then(result => {
37 | if(process.env.NODE_ENV !== 'test'){
38 | client.messages
39 | .create({
40 | body: `${website.name} is online`,
41 | from: process.env.PHONE_NUMBER,
42 | to: process.env.MY_NUMBER
43 | })
44 | .then(message => console.log(message.sid))
45 | .done();
46 | }
47 | })
48 | .catch(err => console.log(err));
49 | }
50 | })
51 | })
52 | })
53 | .catch(err => console.log(err));
54 | }
55 |
56 | let statusJob = new cron.CronJob({
57 | cronTime : '* * * * * *',
58 | onTick : changeStatus,
59 | start : false
60 | });
61 |
62 | return statusJob;
63 |
64 | }
--------------------------------------------------------------------------------
/tasks/task-11/images/test-sms.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-11/images/test-sms.png
--------------------------------------------------------------------------------
/tasks/task-12/README.md:
--------------------------------------------------------------------------------
1 | # Task 12 - Containerizing your projects with Docker.
2 |
3 | ## Objectives
4 |
5 | - Learn why containers are important.
6 | - Learn Docker.
7 |
8 | ## Learning Resources
9 |
10 | Here are the list of learning resources for this task.
11 |
12 | Topic | Resource
13 | ------------ | -------------
14 | Why Containers? | [Link to this resource](https://blog.cloudboost.io/how-cloudboost-uses-docker-kubernetes-and-azure-to-scale-60-000-apps-d54d7eaf02c9)
15 | Learn Docker | [Link to this resource](https://blog.cloudboost.io/get-started-with-docker-by-watching-these-5-videos-80d25d71c1a5)
16 | What is Docker? | [Link to this resource](https://www.youtube.com/watch?v=lcQfQRDAMpQ)
17 |
18 |
19 | ## Tasks
20 |
21 | #### Step 1: Dockerize your containers
22 |
23 | - Create a Docker file in your frontend and backend repos.
24 | - There are templates of Docker file for NodeJS and React projects. Google these templates out and copy them. Make sure you understand that it means.
25 | - Build your containers.
26 | - Run your conatiners locally to see if everything works and test your application (both frontend and backend) properly.
27 |
28 | #### Step 2: Push to Docker Hub
29 |
30 | - Create an account on Docker Hub. This is free to use for public and open source projects.
31 | - Create two repos on Docker Hub. One for the frontend and one for tha backend.
32 | - Push your images to Docker Hub.
33 |
34 |
35 | #### Step 3: Implement CI/CD with Containers
36 |
37 | - Add all of the script that you've worked in Step 1 and Step 2 to your Travis file.
38 | - We plan to use master-release workflow. You'll have two branches in your Git Repo. One would be master and the other would be release. You'll ideally push changes to master. When you want to release your software in production. You'll merge master branch to release.
39 | - Please make sure you build a Docker container with tag master- when you push changes to your master branch in your Git Repo and you build with tag release- and `latest` when you push changes to your release branch.
40 | - Ideally you'll push changes to staging server when changes are pushed in master and you'll deploy those changes i production when changes are pushed to release.
41 |
42 | ## Deliverable
43 |
44 | - Push changes to Git, with Dockerfile in each repo. Add links to your container images on Docker Hub in the README section of your repos.
45 |
46 |
47 |
--------------------------------------------------------------------------------
/tasks/task-12/code/backend/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/tasks/task-12/code/backend/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | node_js:
4 | - "node"
5 | services:
6 | - postgresql
7 | install:
8 | - "npm install"
9 | script:
10 | - "npm test"
--------------------------------------------------------------------------------
/tasks/task-12/code/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8
2 |
3 | WORKDIR /home/app
4 |
5 | COPY package*.json ./
6 |
7 | RUN npm install
8 |
9 | COPY . .
10 |
11 | EXPOSE 3001
12 |
13 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/tasks/task-12/code/backend/db/index.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 |
3 | let dbUrl = null;
4 |
5 | if(process.env.NODE_ENV === 'test') dbUrl = 'postgres://utvdnzdb:hxLot3tUXkVkmG66z7uQlO05f-N1rjun@packy.db.elephantsql.com:5432/utvdnzdb';
6 | else {
7 | dbUrl = 'postgres://student@postgres/web_monitor';
8 | }
9 |
10 | const sequelize = new Sequelize(dbUrl);
11 |
12 | module.exports = sequelize;
--------------------------------------------------------------------------------
/tasks/task-12/code/backend/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | app:
4 | container_name: docker-node
5 | restart: always
6 | build: .
7 | ports:
8 | - '3001:3001'
9 | links:
10 | - db
11 | depends_on:
12 | - db
13 | environment:
14 | DB_HOST: db
15 | db:
16 | container_name: postgres
17 | image: postgres
18 | ports:
19 | - '5432:5432'
20 | environment:
21 | POSTGRES_USER: student
22 | POSTGRES_DB: web_monitor
23 | healthcheck:
24 | test: ["CMD", "curl", "-f", "http://localhost:5432"]
25 | interval: 30s
26 | timeout: 15s
27 | retries: 5
28 | volumes:
29 | - ./init:/docker-entrypoint-initdb.d/
--------------------------------------------------------------------------------
/tasks/task-12/code/backend/index.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const express = require('express');
3 | const bodyParser = require('body-parser');
4 | const logger = require('morgan');
5 | const passport = require('passport');
6 | const cron = require('cron');
7 |
8 | const users = require('./routes/users');
9 | const websites = require('./routes/websites');
10 | const statusJob = require('./workers/uptime')(cron);
11 |
12 | const app = express();
13 |
14 | app.use(bodyParser.json());
15 |
16 | if(process.env.NODE_ENV !== 'test') {
17 | app.use(logger('tiny'));
18 | statusJob.start();
19 | };
20 |
21 | app.use(passport.initialize());
22 |
23 | require('./passport/')(passport);
24 |
25 | app.use('/users', users);
26 | app.use('/websites', websites);
27 |
28 | app.get('/', (req, res) => {
29 | res.json({ success: true })
30 | });
31 |
32 | app.listen(3001, () => console.log('Listening on Port 3001'));
33 |
34 | module.exports = app;
--------------------------------------------------------------------------------
/tasks/task-12/code/backend/init/init.sql:
--------------------------------------------------------------------------------
1 | CREATE USER docker;
2 | CREATE DATABASE docker;
3 | GRANT ALL PRIVILEGES ON DATABASE docker TO docker;
4 |
5 | CREATE SEQUENCE public.users_id_seq;
6 |
7 | ALTER SEQUENCE public.users_id_seq
8 | OWNER TO student;
9 |
10 | CREATE SEQUENCE public.websites_id_seq;
11 |
12 | ALTER SEQUENCE public.websites_id_seq
13 | OWNER TO student;
14 |
15 | CREATE TABLE public."Users"
16 | (
17 | id integer NOT NULL DEFAULT nextval('users_id_seq'::regclass),
18 | email character varying(255) COLLATE pg_catalog."default" NOT NULL,
19 | password character varying(255) COLLATE pg_catalog."default" NOT NULL,
20 | "createdAt" timestamp with time zone NOT NULL,
21 | "updatedAt" timestamp with time zone NOT NULL,
22 | CONSTRAINT users_pkey PRIMARY KEY (id),
23 | CONSTRAINT users_email_key UNIQUE (email)
24 |
25 | )
26 | WITH (
27 | OIDS = FALSE
28 | )
29 | TABLESPACE pg_default;
30 |
31 | ALTER TABLE public."Users"
32 | OWNER to student;
33 |
34 | CREATE TABLE public."Websites"
35 | (
36 | id integer NOT NULL DEFAULT nextval('websites_id_seq'::regclass),
37 | name character varying(255) COLLATE pg_catalog."default" NOT NULL,
38 | url character varying(255) COLLATE pg_catalog."default" NOT NULL,
39 | status character varying(255) COLLATE pg_catalog."default" NOT NULL DEFAULT 'online'::character varying,
40 | "createdAt" timestamp with time zone NOT NULL,
41 | "updatedAt" timestamp with time zone NOT NULL,
42 | "UserId" integer,
43 | CONSTRAINT websites_pkey PRIMARY KEY (id),
44 | CONSTRAINT "websites_userId_fkey" FOREIGN KEY ("UserId")
45 | REFERENCES public."Users" (id) MATCH SIMPLE
46 | ON UPDATE CASCADE
47 | ON DELETE SET NULL
48 | )
49 | WITH (
50 | OIDS = FALSE
51 | )
52 | TABLESPACE pg_default;
53 |
54 | ALTER TABLE public."Websites"
55 | OWNER to student;
--------------------------------------------------------------------------------
/tasks/task-12/code/backend/models/User.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | const User = sequelize.define('User', {
5 | email: DataTypes.STRING,
6 | password: DataTypes.STRING
7 | });
8 |
9 | User.associate = function (models) {
10 | models.User.hasMany(models.Website);
11 | };
12 |
13 | User.beforeCreate((user, options) => {
14 | let salt = bcrypt.genSaltSync(10);
15 | let hash = bcrypt.hashSync(user.password, salt);
16 | return user.password = hash;
17 | })
18 |
19 | return User;
20 | };
--------------------------------------------------------------------------------
/tasks/task-12/code/backend/models/Website.js:
--------------------------------------------------------------------------------
1 | module.exports = (sequelize, DataTypes) => {
2 | const Website = sequelize.define('Website', {
3 | name: DataTypes.STRING,
4 | url: {
5 | type: DataTypes.STRING,
6 | validate: {
7 | isUrl: {
8 | msg: "URL is not valid"
9 | }
10 | }
11 | },
12 | status: {
13 | type: DataTypes.STRING,
14 | defaultValue: 'online'
15 | }
16 | });
17 |
18 | Website.associate = function (models) {
19 | models.Website.belongsTo(models.User, {
20 | foreignKey: { allowNull: false },
21 | onDelete: "CASCADE"
22 | });
23 | };
24 |
25 | return Website;
26 | };
--------------------------------------------------------------------------------
/tasks/task-12/code/backend/models/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const Sequelize = require('sequelize');
4 | const basename = path.basename(__filename);
5 | const sequelize = require('../db');
6 |
7 | let db = {};
8 |
9 | fs
10 | .readdirSync(__dirname)
11 | .filter(file => {
12 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
13 | })
14 | .forEach(file => {
15 | var model = sequelize['import'](path.join(__dirname, file));
16 | db[model.name] = model;
17 | });
18 |
19 | Object.keys(db).forEach(modelName => {
20 | if (db[modelName].associate) {
21 | db[modelName].associate(db);
22 | }
23 | });
24 |
25 | db.sequelize = sequelize;
26 | db.Sequelize = Sequelize;
27 |
28 | module.exports = db;
--------------------------------------------------------------------------------
/tasks/task-12/code/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "task-6",
3 | "version": "1.0.0",
4 | "description": "Tutorial for task 6",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha --timeout 10000 --exit",
8 | "start": "node index.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "bcryptjs": "^2.4.3",
14 | "body-parser": "^1.18.3",
15 | "cron": "^1.4.1",
16 | "dotenv": "^6.0.0",
17 | "express": "^4.16.3",
18 | "jsonwebtoken": "^8.3.0",
19 | "morgan": "^1.9.1",
20 | "passport": "^0.4.0",
21 | "passport-jwt": "^4.0.0",
22 | "pg": "^7.4.3",
23 | "pg-hstore": "^2.3.2",
24 | "request": "^2.88.0",
25 | "sequelize": "^4.38.1",
26 | "twilio": "^3.22.0"
27 | },
28 | "devDependencies": {
29 | "chai": "^4.1.2",
30 | "chai-http": "^4.2.0",
31 | "mocha": "^5.2.0",
32 | "sinon": "^6.3.5"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tasks/task-12/code/backend/passport/index.js:
--------------------------------------------------------------------------------
1 | const JwtStrategy = require('passport-jwt').Strategy;
2 | const ExtractJwt = require('passport-jwt').ExtractJwt;
3 | const models = require('../models')
4 |
5 | let opts = {};
6 |
7 | opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken()
8 | opts.secretOrKey = 'secret';
9 |
10 | module.exports = passport => {
11 | passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
12 | models.User.findById(jwt_payload.id)
13 | .then(user => {
14 | if (user) return done(null, user);
15 | return done(null, false)
16 | })
17 | .catch(err => console.log(err));
18 | }));
19 | }
--------------------------------------------------------------------------------
/tasks/task-12/code/backend/routes/users.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const jwt = require('jsonwebtoken');
3 | const bcrypt = require('bcryptjs');
4 |
5 | const models = require('../models');
6 | const router = express.Router();
7 |
8 | router.post('/signup', (req, res) => {
9 | let newUser = {
10 | email: req.body.email,
11 | password: req.body.password
12 | }
13 |
14 | models.User.findOne({ where: { email: newUser.email }})
15 | .then(user => {
16 | if(user) return res.status(401).json({ msg: 'Email already exists' })
17 | else {
18 | models.User.create(newUser)
19 | .then(user => {
20 | let payload = {
21 | email: user.email,
22 | id: user.id
23 | }
24 | jwt.sign(payload, 'secret', { expiresIn: '1h'}, (err, token) => {
25 | // Here we send the token to the client.
26 | res.json({session: token})
27 | })
28 | })
29 | }
30 | })
31 | .catch(err => res.status(401).json(err));
32 | });
33 |
34 | router.post('/login', (req, res) => {
35 | let newUser = {
36 | email: req.body.email,
37 | password: req.body.password
38 | }
39 |
40 | models.User.findOne({ where: { email: newUser.email }})
41 | .then(user => {
42 | if(user) {
43 | bcrypt.compare(newUser.password, user.password)
44 | .then(isMatch => {
45 | if(isMatch) {
46 | let payload = { email: user.email, id: user.id }
47 | jwt.sign(payload, 'secret', { expiresIn: '1h'}, (err, token) => {
48 | res.json({session: token})
49 | })
50 | }
51 | else {
52 | res.status(401).json({error: 'Invalid Password'})
53 | }
54 | })
55 | }
56 | else {
57 | res.status(404).json({error: 'User does not exist.'})
58 | }
59 | })
60 | .catch(err => res.status(401).json(err));
61 | })
62 |
63 | module.exports = router;
--------------------------------------------------------------------------------
/tasks/task-12/code/backend/routes/websites.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const models = require('../models/');
4 | const passport = require('passport');
5 | const jwt = require('jsonwebtoken');
6 |
7 | router.get('/list',
8 | passport.authenticate('jwt', { session: false }),
9 | (req, res) => {
10 | let pureToken = req.get('Authorization').slice(7)
11 | let currentUser = jwt.verify(pureToken, 'secret');
12 |
13 | models.Website.findAll({
14 | where: {
15 | UserId: currentUser.id
16 | }
17 | })
18 | .then(websites => res.json(websites))
19 | .catch(err => res.status(400).json({ msg: "Error" }))
20 | })
21 |
22 | router.post('/add',
23 | passport.authenticate('jwt', { session: false }),
24 | (req, res) => {
25 |
26 | let pureToken = req.get('Authorization').slice(7)
27 | let currentUser = jwt.verify(pureToken, 'secret');
28 |
29 | if(req.body.constructor === Object && Object.keys(req.body).length === 0) return res.status(400).json({ msg: 'Invalid data' })
30 |
31 | let newWebsite = {
32 | name: req.body.name,
33 | url: req.body.url
34 | }
35 |
36 | models.User.findOne({
37 | where: {
38 | id: currentUser.id
39 | }
40 | })
41 | .then(user => {
42 | if(!user) res.status(401).json({ msg:"User not found, please log in" })
43 | models.Website.find({
44 | where: {
45 | UserId: currentUser.id,
46 | url: newWebsite.url
47 | }
48 | })
49 | .then(website => {
50 | if(website) res.status(400).json({ msg: "Website already added" })
51 | else {
52 | models.Website.create(newWebsite)
53 | .then(website => {
54 | website.setUser(currentUser.id)
55 | res.json(website)
56 | })
57 | .catch(err => res.status(400).json(err))
58 | }
59 | })
60 | })
61 | .catch(err => res.status(401).send(err))
62 | })
63 |
64 | module.exports = router;
--------------------------------------------------------------------------------
/tasks/task-12/code/backend/workers/uptime.js:
--------------------------------------------------------------------------------
1 | const request = require('request');
2 | const models = require('../models');
3 | const accountSid = process.env.ACCOUNT_SID;
4 | const authToken = process.env.AUTH_TOKEN;
5 | const client = require('twilio')(accountSid, authToken);
6 |
7 | module.exports = function(cron){
8 |
9 | function changeStatus(){
10 | models.Website.findAll({})
11 | .then(websites => {
12 | websites.map(website => {
13 | request(`http://${website.dataValues.url}`, function (error, response, body) {
14 | if((response && response.statusCode) !== 200 && website.dataValues.status === 'online') {
15 | models.Website.update(
16 | {status: 'offline'},
17 | { where: { id: website.id }, returning: true})
18 | .then(result => {
19 | if(process.env.NODE_ENV !== 'test'){
20 | client.messages
21 | .create({
22 | body: `${website.name} is offline`,
23 | from: process.env.PHONE_NUMBER,
24 | to: process.env.MY_NUMBER
25 | })
26 | .then(message => console.log(message.sid))
27 | .done();
28 | }
29 | })
30 | .catch(err => console.log(err));
31 | }
32 | if((response && response.statusCode) === 200 && website.dataValues.status === 'offline') {
33 | models.Website.update(
34 | {status: 'online'},
35 | { where: { id: website.id }, returning: true})
36 | .then(result => {
37 | if(process.env.NODE_ENV !== 'test'){
38 | client.messages
39 | .create({
40 | body: `${website.name} is online`,
41 | from: process.env.PHONE_NUMBER,
42 | to: process.env.MY_NUMBER
43 | })
44 | .then(message => console.log(message.sid))
45 | .done();
46 | }
47 | })
48 | .catch(err => console.log(err));
49 | }
50 | })
51 | })
52 | })
53 | .catch(err => console.log(err));
54 | }
55 |
56 | let statusJob = new cron.CronJob({
57 | cronTime : '* * * * * *',
58 | onTick : changeStatus,
59 | start : false
60 | });
61 |
62 | return statusJob;
63 |
64 | }
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from '@storybook/react';
2 |
3 | function loadStories() {
4 | const req = require.context('../src', true, /.stories.js$/);
5 | req.keys().forEach((filename) => req(filename));
6 | }
7 |
8 | configure(loadStories, module);
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8
2 |
3 | RUN mkdir /usr/src/app
4 | WORKDIR /usr/src/app
5 |
6 | ENV PATH /usr/src/app/node_modules/.bin:$PATH
7 |
8 | COPY package.json /usr/src/app/package.json
9 | RUN npm install --silent
10 | RUN npm install react-scripts@1.1.1 -g --silent
11 |
12 | # start app
13 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/README.md:
--------------------------------------------------------------------------------
1 | #### React Redux Starter Template
2 |
3 | This project contains a boilerplate template created from the create-react-app library and configured along with redux.
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.5'
2 |
3 | services:
4 |
5 | app:
6 | container_name: react-app
7 | build:
8 | context: .
9 | dockerfile: Dockerfile
10 | volumes:
11 | - '.:/usr/src/app'
12 | - '/usr/src/app/node_modules'
13 | ports:
14 | - '3000:3000'
15 | environment:
16 | - NODE_ENV=development
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-starter-template",
3 | "version": "0.1.0",
4 | "private": true,
5 | "proxy": "http://localhost:3001/",
6 | "dependencies": {
7 | "axios": "^0.18.0",
8 | "react": "^16.5.2",
9 | "react-dom": "^16.5.2",
10 | "react-redux": "^5.0.6",
11 | "react-router-dom": "^4.2.2",
12 | "react-scripts": "1.0.17",
13 | "redux": "^3.7.2",
14 | "redux-form": "^7.4.2",
15 | "redux-logger": "^3.0.6",
16 | "redux-promise": "^0.5.3"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test --env=jsdom",
22 | "eject": "react-scripts eject",
23 | "storybook": "start-storybook -p 9001 -c .storybook"
24 | },
25 | "devDependencies": {
26 | "@storybook/addon-storyshots": "^3.4.11",
27 | "@storybook/react": "^3.4.11",
28 | "react-test-renderer": "^16.5.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-12/code/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | form {
2 | border: solid 1px black;
3 | padding: 15px;
4 | margin: 15px;
5 | width: 30vw;
6 | text-align: center;
7 | }
8 |
9 | .websites {
10 | padding: 15px;
11 | margin: 15px;
12 | }
13 |
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export const LOGIN = 'LOGIN';
2 | export const WEBSITES = 'WEBSITES';
3 | export const LOGOUT = 'LOGOUT';
4 |
5 | export function loginUser() {
6 | return {
7 | type: LOGIN,
8 | payload: true
9 | }
10 | }
11 |
12 | export function getWebsites(websites) {
13 | return {
14 | type: WEBSITES,
15 | payload: websites
16 | }
17 | }
18 |
19 | export function logout() {
20 | return {
21 | type: LOGOUT,
22 | payload: false
23 | }
24 | }
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/src/components/Login.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Link } from "react-router-dom";
4 |
5 | let Login = props => {
6 | const { handleSubmit } = props
7 | return (
8 |
23 | )
24 | }
25 |
26 | Login = reduxForm({
27 | // a unique name for the form
28 | form: 'login'
29 | })(Login)
30 |
31 | export default Login
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/src/components/Signup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Link } from "react-router-dom";
4 |
5 | let Signup = props => {
6 | const { handleSubmit } = props
7 | return (
8 |
23 | )
24 | }
25 |
26 | Signup = reduxForm({
27 | // a unique name for the form
28 | form: 'signup'
29 | })(Signup)
30 |
31 | export default Signup
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/src/components/Website.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Website = ({ name, url, status }) => (
4 |
5 |
{name}
6 |
{url}
7 |
{status}
8 |
9 | )
10 |
11 | export default Website;
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/src/components/WebsiteForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Field, reduxForm } from 'redux-form';
4 | import { logout } from '../actions';
5 |
6 | let WebsiteForm = props => {
7 | const { handleSubmit } = props
8 |
9 | function logout () {
10 | sessionStorage.clear();
11 | props.logout();
12 | }
13 |
14 | return (
15 |
30 | )
31 | }
32 |
33 | const mapDispatchToProps = {
34 | logout
35 | }
36 |
37 | WebsiteForm = connect(
38 | null,
39 | mapDispatchToProps
40 | )(WebsiteForm);
41 |
42 | export default reduxForm({
43 | form: 'websiteForm'
44 | })(WebsiteForm);
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/src/components/WebsiteList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import axios from 'axios';
4 |
5 | import { getWebsites } from '../actions';
6 | import Website from './Website';
7 |
8 | class WebsiteList extends Component {
9 |
10 | componentDidMount(){
11 | let token = sessionStorage.getItem('token');
12 |
13 | axios.get('/websites/list', { headers: { 'Authorization': token } })
14 | .then(websites => {
15 | this.props.getWebsites(websites)
16 | })
17 | .catch(err => alert(err.response.data.msg));
18 | }
19 |
20 | render() {
21 | return (
22 |
23 |
Websites
24 | {
25 | this.props.user.websites.reverse().map(website =>
26 |
32 | )
33 | }
34 |
35 | );
36 | }
37 | }
38 |
39 | const mapStateToProps = state => {
40 | return {
41 | user: state.user
42 | }
43 | }
44 |
45 | const mapDispatchToProps = {
46 | getWebsites
47 | }
48 |
49 | export default connect(mapStateToProps, mapDispatchToProps)(WebsiteList);
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/src/configureStore.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, compose, createStore } from 'redux'
2 | import ReduxPromise from 'redux-promise';
3 |
4 | import rootReducer from './reducers';
5 |
6 | export default function configureStore(preloadedState) {
7 | const middlewareEnhancer = applyMiddleware(ReduxPromise)
8 |
9 | const enhancers = [middlewareEnhancer]
10 | const composedEnhancers = compose(...enhancers)
11 |
12 | const store = createStore(rootReducer, preloadedState, composedEnhancers)
13 |
14 | return store
15 | }
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | import { Provider } from 'react-redux';
8 |
9 | import configureStore from './configureStore'
10 |
11 | const store = configureStore()
12 |
13 | ReactDOM.render(
14 |
15 |
16 |
17 | , document.getElementById('root'));
18 | registerServiceWorker();
19 |
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import { reducer as formReducer } from 'redux-form';
4 | import userReducer from './user';
5 |
6 | const rootReducers = combineReducers({
7 | form: formReducer.plugin({
8 | websiteForm: (state, action) => {
9 | switch(action.type) {
10 | case 'ACCOUNT_SAVE_SUCCESS':
11 | return undefined;
12 | default:
13 | return state;
14 | }
15 | }
16 | }),
17 | user: userReducer
18 | });
19 |
20 | export default rootReducers;
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/src/reducers/user.js:
--------------------------------------------------------------------------------
1 | import { LOGIN, WEBSITES, LOGOUT } from '../actions';
2 |
3 | let initialState = {
4 | isLoggedIn: false,
5 | websites: []
6 | }
7 |
8 | const user = (state = initialState, action) => {
9 | switch(action.type) {
10 | case LOGIN:
11 | return state = {...state, isLoggedIn: true };
12 | case WEBSITES:
13 | return state = {...state, websites: action.payload.data };
14 | case LOGOUT:
15 | return state = {...state, isLoggedIn: action.payload };
16 | default:
17 | return state;
18 | }
19 | }
20 |
21 | export default user
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/src/test/stories/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { action } from '@storybook/addon-actions';
4 | import { Provider } from 'react-redux';
5 | import Login from '../../components/Login';
6 | import Signup from '../../components/Signup';
7 | import WebsiteForm from '../../components/WebsiteForm';
8 | import Website from '../../components/Website';
9 | import configureStore from '../../configureStore';
10 | import { MemoryRouter } from 'react-router';
11 |
12 | // We bring our redux Store
13 | const store = configureStore();
14 |
15 | storiesOf('Signup and Login', module)
16 | .addDecorator(story =>
17 | {/* This will prevent errors because of your Link component*/}
18 | {story()}
19 | )
20 | .add('Login', () => (
21 |
22 | ))
23 | .add('Signup', () => (
24 |
25 | ))
26 | .add('WebsiteForm', () => (
27 |
28 | ))
29 | .add('Website', () => (
30 |
31 | ))
--------------------------------------------------------------------------------
/tasks/task-12/code/frontend/src/test/storyshots.test.js:
--------------------------------------------------------------------------------
1 | import initStoryshots from '@storybook/addon-storyshots';
2 |
3 | initStoryshots({ /* configuration options */ });
--------------------------------------------------------------------------------
/tasks/task-12/images/docker-version.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-12/images/docker-version.png
--------------------------------------------------------------------------------
/tasks/task-13/README.md:
--------------------------------------------------------------------------------
1 | # Task 13 - Container Orchestration with Kubernetes - Part 1
2 |
3 | ## Objectives
4 |
5 | - Learn why container orchestration is important.
6 | - Learn Kubernetes.
7 |
8 | ## Learning Resources
9 |
10 | Here are the list of learning resources for this task.
11 |
12 | Topic | Resource
13 | ------------ | -------------
14 | What is Kubernetes | [Link to this resource](https://www.youtube.com/watch?v=F-p_7XaEC84)
15 | Learn Kubernetes | [Link to this resource](https://www.youtube.com/watch?v=R-3dfURb2hA&list=PLbG4OyfwIxjFE5Ban_n2JdGad4EDWmisR)
16 | Kubernetes Tutorial | [Link to this resource](https://www.youtube.com/watch?v=tqr581_bBM0&list=PLot-YkcC7wZ9xwMzkzR_EkOrPahSofe5Q)
17 |
18 | ## Tasks
19 |
20 | #### Step 1: Set up Repos.
21 |
22 | - You'll need a new repo to host all the kuberetes configuration files. Create a new repo called `kubernetes` on your GitHub.
23 |
24 | #### Step 2: Set up local cluster.
25 |
26 | - [Check this tutorial](https://kubernetes.io/docs/setup/minikube/) to run Kubernetes locally via minikube.
27 |
28 | #### Step 3: Deploy PostgreSQL
29 |
30 | - [Check this tutorial](https://kubernetes.io/blog/2017/02/postgresql-clusters-kubernetes-statefulsets/) and deploy PostgreSQL on your Kubernetes Cluster
31 |
32 | #### Step 4: Test.
33 |
34 | - Test if PostgreSQL is working properly.
35 |
36 | ## Deliverable
37 |
38 | - Push your kubernetes `yaml` or `json` files to your new GitHub repo.
39 |
40 |
41 |
--------------------------------------------------------------------------------
/tasks/task-14/README.md:
--------------------------------------------------------------------------------
1 | # Task 14 - Container Orchestration with Kubernetes - Part 2
2 |
3 | ## Objectives
4 |
5 | - Learn why container orchestration is important.
6 | - Learn Kubernetes.
7 |
8 | ## Learning Resources
9 |
10 | Here are the list of learning resources for this task.
11 |
12 | Topic | Resource
13 | ------------ | -------------
14 | What is Kubernetes | [Link to this resource](https://www.youtube.com/watch?v=F-p_7XaEC84)
15 | Learn Kubernetes | [Link to this resource](https://www.youtube.com/watch?v=R-3dfURb2hA&list=PLbG4OyfwIxjFE5Ban_n2JdGad4EDWmisR)
16 | Kubernetes Tutorial | [Link to this resource](https://www.youtube.com/watch?v=tqr581_bBM0&list=PLot-YkcC7wZ9xwMzkzR_EkOrPahSofe5Q)
17 |
18 | ## Tasks
19 |
20 | #### Step 1: Deploy Replication Controllers for Frontend and the Backend.
21 |
22 | - Create Kubernetes Replication Controller and Pods for Frontend and Backend. [Check this out to learn what replication controllers are](https://kubernetes.io/docs/concepts/workloads/controllers/replicationcontroller/).
23 |
24 | - Make sure the pod count of Frontend and Backend is 1 if you're running this locally. If you're running this in production, pod count should atleast be 3.
25 |
26 | - Make sure backend connects to PostgreSQL properly.
27 |
28 | #### Step 2: Deploy Services.
29 |
30 | - Create two services one for the frontend and one for the backend. Connect these services to Kubernetes RC's.
31 | - Services will expose a public IP address that you can then attach to your Domain's DNS. (If you have a domain you already own, try this out. If you don't have a domain. You can buy one or completely ignore this point.)
32 |
33 | #### Step 3: Deploy Autoscaling Rules
34 |
35 | - [Set up Autoscaler](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) for frontend and the backend.
36 |
37 | #### Step 4: Test
38 |
39 | - Test if the service is working properly.
40 |
41 | ## Deliverable
42 |
43 | - Push your kubernetes `yaml` or `json` files to your new GitHub repo.
44 |
45 |
46 |
--------------------------------------------------------------------------------
/tasks/task-15/README.md:
--------------------------------------------------------------------------------
1 | # Task 15 - Conclusion and What's next.
2 |
3 | ## Objectives
4 |
5 | - What's next?
6 |
7 | ## Conclusion
8 |
9 | Congratulations! You have built your first project on Postgre, React, Redux, NodeJS, Docker and Kubernetes. You''ve leant about builing API's on the server, working with a frontend library called React and how that makes your life easier. You've also learnt about working with Redux and state management on your frontend apps and have containerized your application and deployed it to Kubernetes Clusters.
10 |
11 | Kubernetes works on any cloud out there. Be it Google Cloud Platform, Azure, AWS or Digital Ocean. If you're curious - you can create an account there and deploy your service out to public. All of these services give you feww tier that should be sufficient for experimental use.
12 |
13 | There are two options for you going forward. On one hand you can continue to build your own projects, launch them, talk to potential customers and sell it. Talk to customers, get feedback, iterate and hopefully get some more people to join you in your journey and build a company. If you're a student, we highly recommend this approach. Keep working on projects, and you'll eventually learn. Work first, learn second. On the other hand - there are some of the best companies around that you can work for. We at HackerBay are always looking for people to join us and if you would like to interview with us. [Please check this out](https://github.com/hackerbay/interview).
14 |
15 | Thank you for taking this course. We hope you enjoyed working on these tasks as much as I enjoyed crafting and writing them. Please feel to give your feedback [here](https://docs.google.com/forms/u/3/d/e/1FAIpQLSdFSk86fsSIyfehHXN2vWfXq9ed3CcLjKs6B6r2OUCbDflCpQ/viewform). Feedback is important and this will help us improve the course for a lot more students around the world. Adios.
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/tasks/task-2/code/db/index.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 | const sequelize = new Sequelize('ivan', 'ivan', '123456', {
3 | host: 'localhost',
4 | dialect: 'postgres',
5 | operatorsAliases: false,
6 |
7 | pool: {
8 | max: 5,
9 | min: 0,
10 | acquire: 30000,
11 | idle: 10000
12 | }
13 | });
14 |
15 | module.exports = sequelize;
16 |
--------------------------------------------------------------------------------
/tasks/task-2/code/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 |
4 | const sequelize = require('./db');
5 | const user = require('./routes/users');
6 |
7 | const app = express();
8 | app.use(bodyParser.json());
9 |
10 | sequelize
11 | .authenticate()
12 | .then(() => {
13 | console.log('Connection has been established successfully.');
14 | })
15 | .catch(err => {
16 | console.error('Unable to connect to the database:', err);
17 | });
18 |
19 | // This tells our app to use this file for the /user route.
20 | app.use('/user', user);
21 |
22 | app.get('/', (req, res) => {
23 | res.json({ status: 'success' });
24 | });
25 |
26 | // variable should be outside of the API's scope.
27 | let data = null;
28 |
29 | app.post('/data', (req, res) => {
30 | // We assign the body of the request to the data variable.
31 | data = req.body;
32 | // We send the data variable in the response.
33 | res.json(data);
34 | });
35 |
36 | app.get('/data', (req, res) => {
37 | //data comes from the variable we declared and mutared earlier
38 | res.json(data);
39 | })
40 |
41 | app.listen(3000, () => console.log('Listening on port 3000'));
--------------------------------------------------------------------------------
/tasks/task-2/code/models/User.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 | const bcrypt = require('bcryptjs');
3 |
4 | const sequelize = require('../db');
5 |
6 | const User = sequelize.define('user', {
7 | email: {
8 | type: Sequelize.STRING
9 | },
10 | password: {
11 | type: Sequelize.STRING
12 | }
13 | });
14 |
15 | User.beforeCreate((user, options) => {
16 | let salt = bcrypt.genSaltSync(10);
17 | let hash = bcrypt.hashSync(user.password, salt);
18 | return user.password = hash;
19 | })
20 |
21 | module.exports = User;
--------------------------------------------------------------------------------
/tasks/task-2/code/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "first-api",
3 | "version": "1.0.0",
4 | "description": "Learn how server programming works and create your first get and post API",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "bcryptjs": "^2.4.3",
13 | "body-parser": "^1.18.3",
14 | "express": "^4.16.3",
15 | "jsonwebtoken": "^8.3.0",
16 | "pg": "^7.4.3",
17 | "pg-hstore": "^2.3.2",
18 | "sequelize": "^4.38.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tasks/task-2/images/postman-api-login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-2/images/postman-api-login.png
--------------------------------------------------------------------------------
/tasks/task-2/images/postman-api.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-2/images/postman-api.png
--------------------------------------------------------------------------------
/tasks/task-3/code/task-1/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 |
4 | const app = express();
5 | app.use(bodyParser.json());
6 |
7 | app.get('/', (req, res) => {
8 | res.json({ status: 'success' });
9 | });
10 |
11 | // variable should be outside of the API's scope.
12 | let data = null;
13 |
14 | app.post('/data', (req, res) => {
15 | // We assign the body of the request to the data variable.
16 | data = req.body;
17 | // We send the data variable in the response.
18 | res.json(data);
19 | });
20 |
21 | app.get('/data', (req, res) => {
22 | //data comes from the variable we declared and mutared earlier
23 | res.json(data);
24 | })
25 |
26 | app.listen(3000, () => console.log('Listening on port 3000'));
27 |
28 | module.exports = app;
--------------------------------------------------------------------------------
/tasks/task-3/code/task-1/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "first-api",
3 | "version": "1.0.0",
4 | "description": "Learn how server programming works and create your first get and post API",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "body-parser": "^1.18.3",
13 | "express": "^4.16.3"
14 | },
15 | "devDependencies": {
16 | "chai": "^4.1.2",
17 | "chai-http": "^4.2.0",
18 | "mocha": "^5.2.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tasks/task-3/code/task-1/test/test.js:
--------------------------------------------------------------------------------
1 | let chai = require('chai');
2 | let chaiHttp = require('chai-http');
3 | let should = chai.should();
4 | let expect = chai.expect;
5 | let server = require('../index.js');
6 |
7 | chai.use(chaiHttp);
8 |
9 | // Description of a group of tests
10 | describe('My tests', () => {
11 | // Description of an specific test
12 | describe('dumb test', () => {
13 | // The behaviour expected
14 | it('should return 4', done => {
15 | // what we are actually testing
16 | expect(2 + 2).to.equal(4);
17 | done();
18 | })
19 | })
20 | // test description
21 | describe('first test', () => {
22 | // expected behaviour
23 | it('should return { status: "success" }', done => {
24 | // connecting to the server
25 | chai.request(server)
26 | // method and route
27 | .get('/')
28 | // response from the server
29 | .end((err, res) => {
30 | // assertions
31 | expect(res.status).to.equal(200);
32 | expect(res.body).to.be.a('object');
33 | expect(res.body).to.deep.equal({ status: "success" })
34 | // done should be called to finish the test
35 | done();
36 | })
37 | })
38 | })
39 |
40 | describe('second test', () => {
41 | // expected behaviour
42 | it('should return { data: "Any String"}', done => {
43 | // connecting to the server
44 | chai.request(server)
45 | // method and route
46 | .post('/data')
47 | // our request body
48 | .send({ data: "Any String"})
49 | // response from the server
50 | .end((err, res) => {
51 | // assertions
52 | expect(res.status).to.equal(200);
53 | expect(res.body).to.be.a('object');
54 | expect(res.body).to.deep.equal({ data: "Any String"})
55 | // done should be called to finish the test
56 | done();
57 | })
58 | })
59 | })
60 |
61 | describe('third test', () => {
62 | // expected behaviour
63 | it('should return { data: "Any String"}', done => {
64 | // connecting to the server
65 | chai.request(server)
66 | // method and route
67 | .get('/data')
68 | // response from the server
69 | .end((err, res) => {
70 | // assertions
71 | expect(res.status).to.equal(200);
72 | expect(res.body).to.be.a('object');
73 | expect(res.body).to.deep.equal({ data: "Any String"})
74 | // done should be called to finish the test
75 | done();
76 | })
77 | })
78 | })
79 | })
--------------------------------------------------------------------------------
/tasks/task-3/code/task-2/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | node_js:
4 | - "node"
5 | services:
6 | - postgresql
7 | install:
8 | - "npm install"
9 | script:
10 | - "npm test"
--------------------------------------------------------------------------------
/tasks/task-3/code/task-2/db/index.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 |
3 | // We create a url variable
4 | let dbUrl = null;
5 |
6 | // If NODE_ENV equals 'test' we will use the cloud test database
7 | if(process.env.NODE_ENV === 'test') dbUrl = 'postgres://utvdnzdb:hxLot3tUXkVkmG66z7uQlO05f-N1rjun@packy.db.elephantsql.com:5432/utvdnzdb';
8 | else {
9 | // If not we will use our production database
10 | dbUrl = 'postgres://ivan:123456@localhost:5432/users';
11 | }
12 | // We initialize with our database url variable
13 | const sequelize = new Sequelize(dbUrl, {
14 | host: 'localhost',
15 | dialect: 'postgres',
16 | // I shut off logging because it's annoying
17 | logging: false,
18 | operatorsAliases: false,
19 |
20 | pool: {
21 | max: 5,
22 | min: 0,
23 | acquire: 30000,
24 | idle: 10000
25 | }
26 | });
27 |
28 | module.exports = sequelize;
29 |
--------------------------------------------------------------------------------
/tasks/task-3/code/task-2/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 |
4 | const sequelize = require('./db');
5 | const user = require('./routes/users');
6 |
7 | const app = express();
8 | app.use(bodyParser.json());
9 |
10 | sequelize
11 | .authenticate()
12 | .then(() => {
13 | console.log('Connection has been established successfully.');
14 | })
15 | .catch(err => {
16 | console.error('Unable to connect to the database:', err);
17 | });
18 |
19 | // This tells our app to use this file for the /user route.
20 | app.use('/user', user);
21 |
22 | app.get('/', (req, res) => {
23 | res.json({ status: 'success' });
24 | });
25 |
26 | // variable should be outside of the API's scope.
27 | let data = null;
28 |
29 | app.post('/data', (req, res) => {
30 | // We assign the body of the request to the data variable.
31 | data = req.body;
32 | // We send the data variable in the response.
33 | res.json(data);
34 | });
35 |
36 | app.get('/data', (req, res) => {
37 | //data comes from the variable we declared and mutared earlier
38 | res.json(data);
39 | })
40 |
41 | app.listen(3000, () => console.log('Listening on port 3000'));
42 |
43 | module.exports = app;
--------------------------------------------------------------------------------
/tasks/task-3/code/task-2/models/User.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 | const bcrypt = require('bcryptjs');
3 |
4 | const sequelize = require('../db');
5 |
6 | const User = sequelize.define('user', {
7 | email: {
8 | type: Sequelize.STRING
9 | // validate: {
10 | // isEmail: {
11 | // msg: 'Email address must be valid.'
12 | // }
13 | // }
14 | },
15 | password: {
16 | type: Sequelize.STRING
17 | }
18 | });
19 |
20 | User.beforeCreate((user, options) => {
21 | let salt = bcrypt.genSaltSync(10);
22 | let hash = bcrypt.hashSync(user.password, salt);
23 | return user.password = hash;
24 | })
25 |
26 | module.exports = User;
--------------------------------------------------------------------------------
/tasks/task-3/code/task-2/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "first-api",
3 | "version": "1.0.0",
4 | "description": "Learn how server programming works and create your first get and post API",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha --timeout 10000",
8 | "start": "node index.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "bcryptjs": "^2.4.3",
14 | "body-parser": "^1.18.3",
15 | "express": "^4.16.3",
16 | "jsonwebtoken": "^8.3.0",
17 | "pg": "^7.4.3",
18 | "pg-hstore": "^2.3.2",
19 | "sequelize": "^4.38.0"
20 | },
21 | "devDependencies": {
22 | "chai": "^4.1.2",
23 | "chai-http": "^4.2.0",
24 | "mocha": "^5.2.0"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tasks/task-3/code/task-2/test/test.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = 'test';
2 |
3 | let chai = require('chai');
4 | let chaiHttp = require('chai-http');
5 | let expect = chai.expect;
6 | let server = require('../index.js');
7 | let User = require('../models/User.js');
8 |
9 | chai.use(chaiHttp);
10 |
11 | describe('Tests for task 2', () => {
12 |
13 | before(done => {
14 | User.destroy({
15 | where: {},
16 | truncate: true
17 | })
18 | .then(() => {
19 | // After we empty our database we create one user for our login test
20 | User.create({
21 | email: 'test@email.com',
22 | password: '123456'
23 | })
24 | .then(() => done());
25 | });
26 | });
27 |
28 | describe('POST user/signup', () => {
29 | it('should sign a user', done => {
30 |
31 | chai.request(server)
32 | .post('/user/signup')
33 | .send({
34 | email: 'test@test.com',
35 | password: '123456'
36 | })
37 | .end((err, res) => {
38 | expect(res.status).to.equal(200);
39 | expect(res.body).to.be.a('object');
40 | expect(res.body).to.have.property('session');
41 | done();
42 | })
43 | })
44 | })
45 |
46 | describe('POST user/login', () => {
47 | it('should login a user', done => {
48 |
49 | chai.request(server)
50 | .post('/user/login')
51 | .send({
52 | email: 'test@email.com',
53 | password: '123456'
54 | })
55 | .end((err, res) => {
56 | expect(res.status).to.equal(200);
57 | expect(res.body).to.be.a('object');
58 | expect(res.body).to.have.property('session');
59 | done();
60 | })
61 | })
62 | })
63 | })
--------------------------------------------------------------------------------
/tasks/task-3/images/test-task-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-3/images/test-task-3.png
--------------------------------------------------------------------------------
/tasks/task-4/README.md:
--------------------------------------------------------------------------------
1 | # Task 4 - Getting started with React and Redux.
2 |
3 | ## Objectives
4 |
5 | - Learn to work with React and Redux.
6 | - Build login and sign up form with Redux Forms.
7 |
8 | ## Learning Resources
9 |
10 | Here are the list of learning resources for this task.
11 |
12 | Topic | Resource
13 | ------------ | -------------
14 | Learn ReactJS | [Link to this resource](https://www.youtube.com/playlist?list=PLoYCgNOIyGABj2GQSlDRjgvXtqfDxKm5b)
15 | Learn Redux | [Link to this resource](https://www.youtube.com/playlist?list=PLoYCgNOIyGADILc3iUJzygCqC8Tt3bRXt)
16 | ES6 | [Link to this resource](https://www.youtube.com/playlist?list=PLoYCgNOIyGACDQLaThEEKBAlgs4OIUGif)
17 | Redux Forms | [Link to this resource](https://www.youtube.com/watch?v=ey7H8h4ERHg)
18 | React, Redux Tutorial | [Link to this resource](https://medium.com/@notrab/getting-started-with-create-react-app-redux-react-router-redux-thunk-d6a19259f71f)
19 |
20 |
21 | ## Tasks
22 |
23 | #### Step 1: Git Clone the boiler plate template to get started with React and Redux.
24 |
25 | - Clone this repo to ge started `https://github.com/tarique93102/react-redux-starter-template`
26 | - Crate a new git repo on GitHub for your frontend. You should have two Git repos. One for your frontend and one for your backend.
27 |
28 | #### Step 2: Sign up form.
29 |
30 | - Create the UI of signup from with email and password as input boxes and a sign up button with ReduxForm.
31 | - Create actions and reducers for the signup form.
32 | - Connect actions to the backend with the sign up API's you've created in Task 2.
33 | - Make sure you have loaders on the form.
34 | - Make sure you validate emails and passwords
35 | - Make sure you show error messages from the backend on the form.
36 |
37 | #### Step 3: Login form.
38 |
39 | - Create the UI of login from with email and password as input boxes and a log in button with ReduxForm.
40 | - Create actions and reducers for the login form.
41 | - Connect actions to the backend with the login API's you've created in Task 2.
42 | - Make sure you have loaders on the form.
43 | - Make sure you validate emails and passwords
44 | - Make sure you show error messages from the backend on the form.
45 |
46 | ## Deliverable
47 |
48 | - Push changes to your frontend Git Repo with login and signup forms built and connected to the backend.
49 |
50 |
51 |
--------------------------------------------------------------------------------
/tasks/task-4/code/backend/db/index.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 |
3 | // We create a url variable
4 | let dbUrl = null;
5 |
6 | // If NODE_ENV equals 'test' we will use the cloud test database
7 | if(process.env.NODE_ENV === 'test') dbUrl = 'postgres://utvdnzdb:hxLot3tUXkVkmG66z7uQlO05f-N1rjun@packy.db.elephantsql.com:5432/utvdnzdb';
8 | else {
9 | // If not we will use our production database
10 | dbUrl = 'postgres://ivan:123456@localhost:5432/users';
11 | }
12 | // We initialize with our database url variable
13 | const sequelize = new Sequelize(dbUrl, {
14 | host: 'localhost',
15 | dialect: 'postgres',
16 | // I shut off logging because it's annoying
17 | logging: false,
18 | operatorsAliases: false,
19 |
20 | pool: {
21 | max: 5,
22 | min: 0,
23 | acquire: 30000,
24 | idle: 10000
25 | }
26 | });
27 |
28 | module.exports = sequelize;
29 |
--------------------------------------------------------------------------------
/tasks/task-4/code/backend/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 |
4 | const sequelize = require('./db');
5 | const user = require('./routes/users');
6 |
7 | const app = express();
8 | app.use(bodyParser.json());
9 |
10 | sequelize
11 | .authenticate()
12 | .then(() => {
13 | console.log('Connection has been established successfully.');
14 | })
15 | .catch(err => {
16 | console.error('Unable to connect to the database:', err);
17 | });
18 |
19 | // This tells our app to use this file for the /user route.
20 | app.use('/user', user);
21 |
22 | app.get('/', (req, res) => {
23 | res.json({ status: 'success' });
24 | });
25 |
26 | // variable should be outside of the API's scope.
27 | let data = null;
28 |
29 | app.post('/data', (req, res) => {
30 | // We assign the body of the request to the data variable.
31 | data = req.body;
32 | // We send the data variable in the response.
33 | res.json(data);
34 | });
35 |
36 | app.get('/data', (req, res) => {
37 | //data comes from the variable we declared and mutared earlier
38 | res.json(data);
39 | })
40 |
41 | app.listen(3001, () => console.log('Listening on port 3001'));
42 |
43 | module.exports = app;
--------------------------------------------------------------------------------
/tasks/task-4/code/backend/models/User.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 | const bcrypt = require('bcryptjs');
3 |
4 | const sequelize = require('../db');
5 |
6 | const User = sequelize.define('user', {
7 | email: {
8 | type: Sequelize.STRING
9 | // validate: {
10 | // isEmail: {
11 | // msg: 'Email address must be valid.'
12 | // }
13 | // }
14 | },
15 | password: {
16 | type: Sequelize.STRING
17 | }
18 | });
19 |
20 | User.beforeCreate((user, options) => {
21 | let salt = bcrypt.genSaltSync(10);
22 | let hash = bcrypt.hashSync(user.password, salt);
23 | return user.password = hash;
24 | })
25 |
26 | module.exports = User;
--------------------------------------------------------------------------------
/tasks/task-4/code/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "first-api",
3 | "version": "1.0.0",
4 | "description": "Learn how server programming works and create your first get and post API",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha --timeout 10000",
8 | "start": "node index.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "bcryptjs": "^2.4.3",
14 | "body-parser": "^1.18.3",
15 | "express": "^4.16.3",
16 | "jsonwebtoken": "^8.3.0",
17 | "pg": "^7.4.3",
18 | "pg-hstore": "^2.3.2",
19 | "sequelize": "^4.38.0"
20 | },
21 | "devDependencies": {
22 | "chai": "^4.1.2",
23 | "chai-http": "^4.2.0",
24 | "mocha": "^5.2.0"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tasks/task-4/code/backend/test/test.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = 'test';
2 |
3 | let chai = require('chai');
4 | let chaiHttp = require('chai-http');
5 | let expect = chai.expect;
6 | let server = require('../index.js');
7 | let User = require('../models/User.js');
8 |
9 | chai.use(chaiHttp);
10 |
11 | describe('Tests for task 2', () => {
12 |
13 | before(done => {
14 | User.destroy({
15 | where: {},
16 | truncate: true
17 | })
18 | .then(() => {
19 | // After we empty our database we create one user for our login test
20 | User.create({
21 | email: 'test@email.com',
22 | password: '123456'
23 | })
24 | .then(() => done());
25 | });
26 | });
27 |
28 | describe('POST user/signup', () => {
29 | it('should sign a user', done => {
30 |
31 | chai.request(server)
32 | .post('/user/signup')
33 | .send({
34 | email: 'test@test.com',
35 | password: '123456'
36 | })
37 | .end((err, res) => {
38 | expect(res.status).to.equal(200);
39 | expect(res.body).to.be.a('object');
40 | expect(res.body).to.have.property('session');
41 | done();
42 | })
43 | })
44 | })
45 |
46 | describe('POST user/login', () => {
47 | it('should login a user', done => {
48 |
49 | chai.request(server)
50 | .post('/user/login')
51 | .send({
52 | email: 'test@email.com',
53 | password: '123456'
54 | })
55 | .end((err, res) => {
56 | expect(res.status).to.equal(200);
57 | expect(res.body).to.be.a('object');
58 | expect(res.body).to.have.property('session');
59 | done();
60 | })
61 | })
62 | })
63 | })
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/README.md:
--------------------------------------------------------------------------------
1 | #### React Redux Starter Template
2 |
3 | This project contains a boilerplate template created from the create-react-app library and configured along with redux.
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-starter-template",
3 | "version": "0.1.0",
4 | "private": true,
5 | "proxy": "http://localhost:3001/",
6 | "dependencies": {
7 | "axios": "^0.18.0",
8 | "react": "^16.2.0",
9 | "react-dom": "^16.2.0",
10 | "react-redux": "^5.0.6",
11 | "react-router-dom": "^4.3.1",
12 | "react-scripts": "1.0.17",
13 | "redux": "^3.7.2",
14 | "redux-form": "^7.4.2",
15 | "redux-logger": "^3.0.6",
16 | "redux-promise": "^0.5.3"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test --env=jsdom",
22 | "eject": "react-scripts eject"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-4/code/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 80px;
8 | }
9 |
10 | .App-header {
11 | background-color: #222;
12 | height: 150px;
13 | padding: 20px;
14 | color: white;
15 | }
16 |
17 | .App-title {
18 | font-size: 1.5em;
19 | }
20 |
21 | .App-intro {
22 | font-size: large;
23 | }
24 |
25 | @keyframes App-logo-spin {
26 | from { transform: rotate(0deg); }
27 | to { transform: rotate(360deg); }
28 | }
29 |
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | // Importing react-router
3 | import { BrowserRouter as Router, Route } from "react-router-dom";
4 | import { connect } from 'react-redux';
5 | import axios from 'axios';
6 |
7 | import { defaultFunction } from './actions';
8 | import './App.css';
9 |
10 | // Import here!
11 | import Signup from './components/Signup';
12 | import Login from './components/Login';
13 |
14 | class App extends Component {
15 |
16 | // Our submit function
17 | signup (values) {
18 | axios.post('/user/signup', values)
19 | .then(res => console.log(res.data))
20 | .catch(err => console.log(err.response.data));
21 | }
22 |
23 | login (values) {
24 | axios.post('/user/login', values)
25 | .then(res => console.log(res.data))
26 | .catch(err => console.log(err.response.data));
27 | }
28 |
29 | render() {
30 | return (
31 |
32 |
33 | } />
34 | } />
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | // function to convert the global state obtained from redux to local props
42 | function mapStateToProps(state) {
43 | return {
44 | default: state.default
45 | };
46 | }
47 |
48 | export default connect(mapStateToProps, { defaultFunction })(App);
49 |
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | });
9 |
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export const FETCH_DATA = 'fetch_data';
2 |
3 | // default function to display redux action format
4 | export function defaultFunction() {
5 | let testVar = 'Hello';
6 |
7 | // action object format being return to a reducer
8 | return {
9 | type: FETCH_DATA,
10 | payload: testVar
11 | }
12 | }
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/src/components/Login.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Link } from "react-router-dom";
4 |
5 | let Login = props => {
6 | const { handleSubmit } = props
7 | return (
8 |
21 | )
22 | }
23 |
24 | Login = reduxForm({
25 | // a unique name for the form
26 | form: 'login'
27 | })(Login)
28 |
29 | export default Login
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/src/components/Signup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Link } from "react-router-dom";
4 |
5 | let Signup = props => {
6 | const { handleSubmit } = props
7 | return (
8 |
21 | )
22 | }
23 |
24 | Signup = reduxForm({
25 | // a unique name for the form
26 | form: 'signup'
27 | })(Signup)
28 |
29 | export default Signup
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/src/components/simple-component.js:
--------------------------------------------------------------------------------
1 | // basic react component starting template
2 | import React, { Component } from 'react';
3 |
4 | class SimpleComponent extends Component {
5 | render() {
6 | return (
7 | Simple Component
8 | );
9 | }
10 | }
11 |
12 | export default SimpleComponent;
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | import { createStore, applyMiddleware } from 'redux';
8 | import { Provider } from 'react-redux';
9 | import reducers from './reducers';
10 | import ReduxPromise from 'redux-promise';
11 |
12 | const store = createStore(
13 | reducers,
14 | // Goodbye logger
15 | applyMiddleware(ReduxPromise)
16 | );
17 |
18 | ReactDOM.render(
19 |
20 |
21 |
22 | , document.getElementById('root'));
23 | registerServiceWorker();
24 |
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/src/reducers/default-reducer.js:
--------------------------------------------------------------------------------
1 | // default reducer
2 | // Note: You can remove this reducer and create your own reducer
3 |
4 | import { FETCH_DATA } from '../actions';
5 |
6 | export default (state = {}, action) => {
7 | switch(action.type) {
8 | case FETCH_DATA:
9 | return action.payload;
10 | default:
11 | return state;
12 | }
13 | }
--------------------------------------------------------------------------------
/tasks/task-4/code/frontend/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { reducer as formReducer } from 'redux-form'
3 |
4 | // calling the default reducer to create a link
5 | import defaultReducer from './default-reducer';
6 |
7 | const rootReducers = combineReducers({
8 | // add reducer files references here
9 | default: defaultReducer,
10 | form: formReducer
11 | });
12 |
13 | export default rootReducers;
--------------------------------------------------------------------------------
/tasks/task-4/code/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-starter-template",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.2.0",
7 | "react-dom": "^16.2.0",
8 | "react-redux": "^5.0.6",
9 | "react-router-dom": "^4.3.1",
10 | "react-scripts": "1.0.17",
11 | "redux": "^3.7.2",
12 | "redux-form": "^7.4.2",
13 | "redux-logger": "^3.0.6",
14 | "redux-promise": "^0.5.3"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test --env=jsdom",
20 | "eject": "react-scripts eject"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tasks/task-4/images/signup-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-4/images/signup-1.png
--------------------------------------------------------------------------------
/tasks/task-4/images/signup-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-4/images/signup-2.png
--------------------------------------------------------------------------------
/tasks/task-4/images/signup-login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-4/images/signup-login.png
--------------------------------------------------------------------------------
/tasks/task-5/README.md:
--------------------------------------------------------------------------------
1 | # Task 5 - Testing Frontend.
2 |
3 | ## Objectives
4 |
5 | - Learn to test Frontend React and Redux applications.
6 | - Learn to use StorybookJS and Jest
7 |
8 | ## Learning Resources
9 |
10 | Here are the list of learning resources for this task.
11 |
12 | Topic | Resource
13 | ------------ | -------------
14 | Getting Started with React Storybook | [Link to this resource](https://www.youtube.com/watch?v=E2c183LS4lA)
15 | React Storybook: Design, Dev, Doc, Debug Components | [Link to this resource](https://www.youtube.com/watch?v=PF0Vi-iIyoo)
16 | Create Powerful Interactive Style Guides with Storybook | [Link to this resource](https://www.youtube.com/watch?v=cOI_k_5iOos)
17 | Jest Course | [Link to this resource](https://www.youtube.com/watch?v=4kNfeI37xu4&list=PLLnpHn493BHEB-YOl0APuQsrzlb3zbq3y)
18 | Jest Crash Course | [Link to this resource](https://www.youtube.com/watch?v=7r4xVDI2vho)
19 |
20 |
21 | ## Tasks
22 |
23 | #### Step 1: Set up tests.
24 |
25 | - Set up React tests with Storybook and Jest
26 | - Make sure tests run when you run `npm test`
27 |
28 | #### Step 2: Tests for signup from.
29 |
30 | - When you write test for signup form, make sure:
31 | - Check email validation. Check for an error and if there is an error - pass the test.
32 | - When you sign up with an email that has been signed up with already. Check for an error and if there is an error - pass the test.
33 | - Test the form by keeping fields blank.
34 | - Fill the form properly and test redirects to an authenticated page.
35 |
36 | #### Step 3: Test login form.
37 |
38 | - Just as you've tested signup form. Test login form too. Please make sure you cover all the edge test cases like incorrect login and password, keeping the fields blank, etc.
39 |
40 | #### Step 3: CI and CD
41 |
42 | - Integrate npm test in your Travis file and make sure the build passes.
43 |
44 | ## Deliverable
45 |
46 | - Push changes to your frontend Git Repo with tests.
47 | - Make sure the build passes on Travis.
--------------------------------------------------------------------------------
/tasks/task-5/code/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from '@storybook/react';
2 |
3 | function loadStories() {
4 | const req = require.context('../src', true, /.stories.js$/);
5 | req.keys().forEach((filename) => req(filename));
6 | }
7 |
8 | configure(loadStories, module);
--------------------------------------------------------------------------------
/tasks/task-5/code/README.md:
--------------------------------------------------------------------------------
1 | #### React Redux Starter Template
2 |
3 | This project contains a boilerplate template created from the create-react-app library and configured along with redux.
--------------------------------------------------------------------------------
/tasks/task-5/code/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-starter-template",
3 | "version": "0.1.0",
4 | "private": true,
5 | "proxy": "http://localhost:3001/",
6 | "dependencies": {
7 | "axios": "^0.18.0",
8 | "react": "^16.5.2",
9 | "react-dom": "^16.5.2",
10 | "react-redux": "^5.0.6",
11 | "react-router-dom": "^4.3.1",
12 | "react-scripts": "1.0.17",
13 | "redux": "^3.7.2",
14 | "redux-form": "^7.4.2",
15 | "redux-logger": "^3.0.6",
16 | "redux-promise": "^0.5.3"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test --env=jsdom",
22 | "eject": "react-scripts eject",
23 | "storybook": "start-storybook -p 9001 -c .storybook"
24 | },
25 | "devDependencies": {
26 | "@storybook/addon-storyshots": "^3.4.11",
27 | "@storybook/react": "^3.4.11",
28 | "babel-core": "^6.26.3",
29 | "react-test-renderer": "^16.5.2"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tasks/task-5/code/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-5/code/public/favicon.ico
--------------------------------------------------------------------------------
/tasks/task-5/code/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/tasks/task-5/code/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/tasks/task-5/code/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 80px;
8 | }
9 |
10 | .App-header {
11 | background-color: #222;
12 | height: 150px;
13 | padding: 20px;
14 | color: white;
15 | }
16 |
17 | .App-title {
18 | font-size: 1.5em;
19 | }
20 |
21 | .App-intro {
22 | font-size: large;
23 | }
24 |
25 | @keyframes App-logo-spin {
26 | from { transform: rotate(0deg); }
27 | to { transform: rotate(360deg); }
28 | }
29 |
--------------------------------------------------------------------------------
/tasks/task-5/code/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | // Importing react-router
3 | import { BrowserRouter as Router, Route } from "react-router-dom";
4 | import { connect } from 'react-redux';
5 | import axios from 'axios';
6 |
7 | import { defaultFunction } from './actions';
8 | import './App.css';
9 |
10 | // Import here!
11 | import Signup from './components/Signup';
12 | import Login from './components/Login';
13 |
14 | class App extends Component {
15 |
16 | componentDidMount() {
17 | // call default function to display redux operation
18 | this.props.defaultFunction();
19 | }
20 |
21 | // Our submit function
22 | signup (values) {
23 | axios.post('/user/signup', values)
24 | .then(res => console.log(res.data))
25 | .catch(err => console.log(err.response.data));
26 | }
27 |
28 | login (values) {
29 | axios.post('/user/login', values)
30 | .then(res => console.log(res.data))
31 | .catch(err => console.log(err.response.data));
32 | }
33 |
34 | render() {
35 | return (
36 |
37 |
38 | } />
39 | } />
40 |
41 |
42 | );
43 | }
44 | }
45 |
46 | // function to convert the global state obtained from redux to local props
47 | function mapStateToProps(state) {
48 | return {
49 | default: state.default
50 | };
51 | }
52 |
53 | export default connect(mapStateToProps, { defaultFunction })(App);
54 |
--------------------------------------------------------------------------------
/tasks/task-5/code/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export const FETCH_DATA = 'fetch_data';
2 |
3 | // default function to display redux action format
4 | export function defaultFunction() {
5 | let testVar = 'Hello';
6 |
7 | // action object format being return to a reducer
8 | return {
9 | type: FETCH_DATA,
10 | payload: testVar
11 | }
12 | }
--------------------------------------------------------------------------------
/tasks/task-5/code/src/components/Login.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Link } from "react-router-dom";
4 |
5 | let Login = props => {
6 | const { handleSubmit } = props
7 | return (
8 |
21 | )
22 | }
23 |
24 | Login = reduxForm({
25 | // a unique name for the form
26 | form: 'login'
27 | })(Login)
28 |
29 | export default Login
--------------------------------------------------------------------------------
/tasks/task-5/code/src/components/Signup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Link } from "react-router-dom";
4 |
5 | let Signup = props => {
6 | const { handleSubmit } = props
7 | return (
8 |
21 | )
22 | }
23 |
24 | Signup = reduxForm({
25 | // a unique name for the form
26 | form: 'signup'
27 | })(Signup)
28 |
29 | export default Signup
--------------------------------------------------------------------------------
/tasks/task-5/code/src/components/simple-component.js:
--------------------------------------------------------------------------------
1 | // basic react component starting template
2 | import React, { Component } from 'react';
3 |
4 | class SimpleComponent extends Component {
5 | render() {
6 | return (
7 | Simple Component
8 | );
9 | }
10 | }
11 |
12 | export default SimpleComponent;
--------------------------------------------------------------------------------
/tasks/task-5/code/src/configureStore.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, compose, createStore } from 'redux'
2 | import ReduxPromise from 'redux-promise';
3 |
4 | import rootReducer from './reducers';
5 |
6 | export default function configureStore(preloadedState) {
7 | const middlewareEnhancer = applyMiddleware(ReduxPromise)
8 |
9 | const enhancers = [middlewareEnhancer]
10 | const composedEnhancers = compose(...enhancers)
11 |
12 | const store = createStore(rootReducer, preloadedState, composedEnhancers)
13 |
14 | return store
15 | }
--------------------------------------------------------------------------------
/tasks/task-5/code/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/tasks/task-5/code/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | import { Provider } from 'react-redux';
8 | import configureStore from './configureStore';
9 |
10 | const store = configureStore()
11 |
12 | ReactDOM.render(
13 |
14 |
15 |
16 | , document.getElementById('root'));
17 | registerServiceWorker();
18 |
--------------------------------------------------------------------------------
/tasks/task-5/code/src/reducers/default-reducer.js:
--------------------------------------------------------------------------------
1 | // default reducer
2 | // Note: You can remove this reducer and create your own reducer
3 |
4 | import { FETCH_DATA } from '../actions';
5 |
6 | export default (state = {}, action) => {
7 | switch(action.type) {
8 | case FETCH_DATA:
9 | return action.payload;
10 | default:
11 | return state;
12 | }
13 | }
--------------------------------------------------------------------------------
/tasks/task-5/code/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { reducer as formReducer } from 'redux-form'
3 |
4 | // calling the default reducer to create a link
5 | import defaultReducer from './default-reducer';
6 |
7 | const rootReducers = combineReducers({
8 | // add reducer files references here
9 | default: defaultReducer,
10 | form: formReducer
11 | });
12 |
13 | export default rootReducers;
--------------------------------------------------------------------------------
/tasks/task-5/code/src/test/__snapshots__/storyshots.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Storyshots Signup and Login Login 1`] = `
4 |
61 | `;
62 |
63 | exports[`Storyshots Signup and Login Signup 1`] = `
64 |
121 | `;
122 |
--------------------------------------------------------------------------------
/tasks/task-5/code/src/test/stories/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { action } from '@storybook/addon-actions';
4 | import { Provider } from 'react-redux';
5 | import Login from '../../components/Login';
6 | import Signup from '../../components/Signup';
7 | import configureStore from '../../configureStore';
8 | import { MemoryRouter } from 'react-router';
9 |
10 | // We bring our redux Store
11 | const store = configureStore();
12 |
13 | storiesOf('Signup and Login', module)
14 | .addDecorator(story =>
15 | {/* This will prevent errors from the Link component*/}
16 | {story()}
17 | )
18 | .add('Login', () => (
19 |
20 | ))
21 | .add('Signup', () => (
22 |
23 | ));
--------------------------------------------------------------------------------
/tasks/task-5/code/src/test/storyshots.test.js:
--------------------------------------------------------------------------------
1 | import initStoryshots from '@storybook/addon-storyshots';
2 |
3 | initStoryshots({ /* configuration options */ });
--------------------------------------------------------------------------------
/tasks/task-5/images/snapshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-5/images/snapshot-1.png
--------------------------------------------------------------------------------
/tasks/task-5/images/storybook-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-5/images/storybook-1.png
--------------------------------------------------------------------------------
/tasks/task-6/code/db/index.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 | const sequelize = new Sequelize('tutorial_web_monitor', 'ivan', '123456', {
3 | host: 'localhost',
4 | dialect: 'postgres',
5 | operatorsAliases: false,
6 | logging: false,
7 |
8 | pool: {
9 | max: 5,
10 | min: 0,
11 | acquire: 30000,
12 | idle: 10000
13 | }
14 | });
15 |
16 | module.exports = sequelize;
--------------------------------------------------------------------------------
/tasks/task-6/code/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 | const logger = require('morgan');
4 | const passport = require('passport');
5 |
6 | const users = require('./routes/users');
7 | const websites = require('./routes/websites');
8 |
9 | const app = express();
10 |
11 | app.use(bodyParser.json());
12 | app.use(logger('tiny'));
13 |
14 | app.use(passport.initialize());
15 |
16 | require('./passport/')(passport);
17 |
18 | app.use('/users', users);
19 | app.use('/websites', websites);
20 |
21 | app.get('/', (req, res) => {
22 | res.json({ success: true })
23 | });
24 |
25 | app.listen(3001, () => console.log('Listening on Port 3001'));
--------------------------------------------------------------------------------
/tasks/task-6/code/models/User.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | const User = sequelize.define('User', {
5 | email: DataTypes.STRING,
6 | password: DataTypes.STRING
7 | });
8 |
9 | User.associate = function (models) {
10 | models.User.hasMany(models.Website);
11 | };
12 |
13 | User.beforeCreate((user, options) => {
14 | let salt = bcrypt.genSaltSync(10);
15 | let hash = bcrypt.hashSync(user.password, salt);
16 | return user.password = hash;
17 | })
18 |
19 | return User;
20 | };
--------------------------------------------------------------------------------
/tasks/task-6/code/models/Website.js:
--------------------------------------------------------------------------------
1 | module.exports = (sequelize, DataTypes) => {
2 | const Website = sequelize.define('Website', {
3 | name: DataTypes.STRING,
4 | url: DataTypes.STRING,
5 | status: {
6 | type: DataTypes.STRING,
7 | defaultValue: 'online'
8 | }
9 | });
10 |
11 | Website.associate = function (models) {
12 | models.Website.belongsTo(models.User, {
13 | foreignKey: { allowNull: false },
14 | onDelete: "CASCADE"
15 | });
16 | };
17 |
18 | return Website;
19 | };
--------------------------------------------------------------------------------
/tasks/task-6/code/models/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const Sequelize = require('sequelize');
4 | const basename = path.basename(__filename);
5 | const sequelize = require('../db');
6 |
7 | let db = {};
8 |
9 | fs
10 | .readdirSync(__dirname)
11 | .filter(file => {
12 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
13 | })
14 | .forEach(file => {
15 | var model = sequelize['import'](path.join(__dirname, file));
16 | db[model.name] = model;
17 | });
18 |
19 | Object.keys(db).forEach(modelName => {
20 | if (db[modelName].associate) {
21 | db[modelName].associate(db);
22 | }
23 | });
24 |
25 | db.sequelize = sequelize;
26 | db.Sequelize = Sequelize;
27 |
28 | module.exports = db;
--------------------------------------------------------------------------------
/tasks/task-6/code/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "task-6",
3 | "version": "1.0.0",
4 | "description": "Tutorial for task 6",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "bcryptjs": "^2.4.3",
13 | "body-parser": "^1.18.3",
14 | "express": "^4.16.3",
15 | "jsonwebtoken": "^8.3.0",
16 | "morgan": "^1.9.1",
17 | "passport": "^0.4.0",
18 | "passport-jwt": "^4.0.0",
19 | "pg": "^7.4.3",
20 | "pg-hstore": "^2.3.2",
21 | "sequelize": "^4.38.1"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tasks/task-6/code/passport/index.js:
--------------------------------------------------------------------------------
1 | const JwtStrategy = require('passport-jwt').Strategy;
2 | const ExtractJwt = require('passport-jwt').ExtractJwt;
3 | const models = require('../models')
4 |
5 | let opts = {};
6 |
7 | opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken()
8 | opts.secretOrKey = 'secret';
9 |
10 | module.exports = passport => {
11 | passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
12 | models.User.findById(jwt_payload.id)
13 | .then(user => {
14 | if (user) return done(null, user);
15 | return done(null, false)
16 | })
17 | .catch(err => console.log(err));
18 | }));
19 | }
--------------------------------------------------------------------------------
/tasks/task-6/code/routes/users.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const jwt = require('jsonwebtoken');
3 | const bcrypt = require('bcryptjs');
4 |
5 | const models = require('../models');
6 | const router = express.Router();
7 |
8 | router.post('/signup', (req, res) => {
9 | let newUser = {
10 | email: req.body.email,
11 | password: req.body.password
12 | }
13 |
14 | models.User.findOne({ where: { email: newUser.email }})
15 | .then(user => {
16 | if(user) return res.status(401).json({ msg: 'Email already exists' })
17 | else {
18 | models.User.create(newUser)
19 | .then(user => {
20 | let payload = {
21 | email: user.email,
22 | id: user.id
23 | }
24 | jwt.sign(payload, 'secret', { expiresIn: '1h'}, (err, token) => {
25 | // Here we send the token to the client.
26 | res.json({session: token})
27 | })
28 | })
29 | }
30 | })
31 | .catch(err => res.status(401).json(err));
32 | });
33 |
34 | router.post('/login', (req, res) => {
35 | let newUser = {
36 | email: req.body.email,
37 | password: req.body.password
38 | }
39 |
40 | models.User.findOne({ where: { email: newUser.email }})
41 | .then(user => {
42 | if(user) {
43 | bcrypt.compare(newUser.password, user.password)
44 | .then(isMatch => {
45 | if(isMatch) {
46 | let payload = { email: user.email, id: user.id }
47 | jwt.sign(payload, 'secret', { expiresIn: '1h'}, (err, token) => {
48 | res.json({session: token})
49 | })
50 | }
51 | else {
52 | res.status(401).json({error: 'Invalid Password'})
53 | }
54 | })
55 | }
56 | else {
57 | res.status(404).json({error: 'User does not exist.'})
58 | }
59 | })
60 | .catch(err => res.status(401).json(err));
61 | })
62 |
63 | module.exports = router;
--------------------------------------------------------------------------------
/tasks/task-6/code/routes/websites.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const models = require('../models/');
4 | const passport = require('passport');
5 | const jwt = require('jsonwebtoken');
6 |
7 | router.get('/list',
8 | passport.authenticate('jwt', { session: false }),
9 | (req, res) => {
10 | let pureToken = req.get('Authorization').slice(7)
11 | let currentUser = jwt.verify(pureToken, 'secret');
12 |
13 | // Check if body is null
14 | if(req.body.constructor === Object && Object.keys(req.body).length === 0) return res.status(400).json({ msg: 'Invalid data' })
15 |
16 | models.Website.findAll({
17 | where: {
18 | UserId: currentUser.id
19 | }
20 | })
21 | .then(websites => res.json(websites))
22 | .catch(err => res.status(400).json({ msg: "Error" }))
23 | })
24 |
25 | router.post('/add',
26 | passport.authenticate('jwt', { session: false }),
27 | (req, res) => {
28 |
29 | let pureToken = req.get('Authorization').slice(7)
30 | let currentUser = jwt.verify(pureToken, 'secret');
31 |
32 | let newWebsite = {
33 | name: req.body.name,
34 | url: req.body.url
35 | }
36 |
37 | models.User.findOne({
38 | where: {
39 | id: currentUser.id
40 | }
41 | })
42 | .then(user => {
43 | if(!user) res.status(401).json({ msg:"User not found, please log in" })
44 | models.Website.find({
45 | where: {
46 | UserId: currentUser.id,
47 | url: newWebsite.url
48 | }
49 | })
50 | .then(website => {
51 | if(website) res.status(400).json({ msg: "Website already added" })
52 | else {
53 | models.Website.create(newWebsite)
54 | .then(website => {
55 | website.setUser(currentUser.id)
56 | res.json(website)
57 | })
58 | .catch(err => res.status(400).json(err))
59 | }
60 | })
61 | })
62 | .catch(err => res.status(401).send(err))
63 | })
64 |
65 | module.exports = router;
--------------------------------------------------------------------------------
/tasks/task-7/README.md:
--------------------------------------------------------------------------------
1 | # Task 7 - Relationships - Part 1 (Tests)
2 |
3 | ## Objectives
4 |
5 | - Write unit tests for your backend and test relationships.
6 | - Use a Code Coverage Tool
7 |
8 | ## Learning Resources
9 |
10 | Here are the list of learning resources for this task.
11 |
12 | Topic | Resource
13 | ------------ | -------------
14 | Why unit tests? | [Link to this resource](https://www.youtube.com/watch?v=Eu35xM76kKY)
15 | Your first tests | [Link to this resource](https://www.youtube.com/watch?v=XsFQEUP1MxI)
16 | Intro to JavaScript Unit Tests with Mocha and Chai | [Link to this resource](https://www.youtube.com/watch?v=MLTRHc5dk6s)
17 | Unit Testing with Mocha and Chai | [Link to this resource](https://www.youtube.com/playlist?list=PLXSs3HKyWAE5k-l3edQLn8uai4-WHHudB)
18 | Using Travis with GitHub for CI & CD | [Link to this resource](https://www.youtube.com/watch?v=Uft5KBimzyk)
19 |
20 |
21 | ## Tasks
22 |
23 | #### Step 1: Testing your API from Task 6.
24 |
25 | - You've created two API's on Task 6.
26 | - You've now got to write tests for all of these API's that you've created.
27 | - For `POST /website/add`
28 | - Make sure it returns 200 after the post with request string.
29 | - Make sure it returns 400 when data is null
30 | - Make sure it returns 400 when URL is invalid.
31 | - Make sure it returns 400 when you have the same name or url.
32 | - Make sure it return 401 when user is not authenticated.
33 | - For `GET /website/list`
34 | - Make sure it returns 200 with empty array when no data is inserted.
35 | - Make sure it returns 200 with list of items when they are inserted.
36 | - Make sure it return 401 when user is not authenticated.
37 |
38 | #### Step 2: Use CodeCov
39 |
40 | - Make sure CodeCov is > 80%
41 |
42 |
43 | ## Deliverable
44 |
45 | - GitHub repo of your project with Travis file in it which shows Code Cov > 80%.
46 |
47 |
48 |
--------------------------------------------------------------------------------
/tasks/task-7/code/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | node_js:
4 | - "node"
5 | services:
6 | - postgresql
7 | install:
8 | - "npm install"
9 | script:
10 | - "npm test"
--------------------------------------------------------------------------------
/tasks/task-7/code/db/index.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 |
3 | let dbUrl = null;
4 |
5 | if(process.env.NODE_ENV === 'test') dbUrl = 'postgres://utvdnzdb:hxLot3tUXkVkmG66z7uQlO05f-N1rjun@packy.db.elephantsql.com:5432/utvdnzdb';
6 | else {
7 | dbUrl = 'postgres://ivan:123456@localhost:5432/tutorial_web_monitor';
8 | }
9 |
10 | const sequelize = new Sequelize(dbUrl, {
11 | host: 'localhost',
12 | dialect: 'postgres',
13 | operatorsAliases: false,
14 | logging: false,
15 |
16 | pool: {
17 | max: 5,
18 | min: 0,
19 | acquire: 30000,
20 | idle: 10000
21 | }
22 | });
23 |
24 | module.exports = sequelize;
--------------------------------------------------------------------------------
/tasks/task-7/code/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 | const logger = require('morgan');
4 | const passport = require('passport');
5 |
6 | const users = require('./routes/users');
7 | const websites = require('./routes/websites');
8 |
9 | const app = express();
10 |
11 | app.use(bodyParser.json());
12 |
13 | if(process.env.NODE_ENV !== 'test') app.use(logger('tiny'));
14 |
15 | app.use(passport.initialize());
16 |
17 | require('./passport/')(passport);
18 |
19 | app.use('/users', users);
20 | app.use('/websites', websites);
21 |
22 | app.get('/', (req, res) => {
23 | res.json({ success: true })
24 | });
25 |
26 | app.listen(3001, () => console.log('Listening on Port 3001'));
27 |
28 | module.exports = app;
--------------------------------------------------------------------------------
/tasks/task-7/code/models/User.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | const User = sequelize.define('User', {
5 | email: DataTypes.STRING,
6 | password: DataTypes.STRING
7 | });
8 |
9 | User.associate = function (models) {
10 | models.User.hasMany(models.Website);
11 | };
12 |
13 | User.beforeCreate((user, options) => {
14 | let salt = bcrypt.genSaltSync(10);
15 | let hash = bcrypt.hashSync(user.password, salt);
16 | return user.password = hash;
17 | })
18 |
19 | return User;
20 | };
--------------------------------------------------------------------------------
/tasks/task-7/code/models/Website.js:
--------------------------------------------------------------------------------
1 | module.exports = (sequelize, DataTypes) => {
2 | const Website = sequelize.define('Website', {
3 | name: DataTypes.STRING,
4 | url: {
5 | type: DataTypes.STRING,
6 | validate: {
7 | isUrl: {
8 | msg: "URL is not valid"
9 | }
10 | }
11 | },
12 | status: {
13 | type: DataTypes.STRING,
14 | defaultValue: 'online'
15 | }
16 | });
17 |
18 | Website.associate = function (models) {
19 | models.Website.belongsTo(models.User, {
20 | foreignKey: { allowNull: false },
21 | onDelete: "CASCADE"
22 | });
23 | };
24 |
25 | return Website;
26 | };
--------------------------------------------------------------------------------
/tasks/task-7/code/models/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const Sequelize = require('sequelize');
4 | const basename = path.basename(__filename);
5 | const sequelize = require('../db');
6 |
7 | let db = {};
8 |
9 | fs
10 | .readdirSync(__dirname)
11 | .filter(file => {
12 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
13 | })
14 | .forEach(file => {
15 | var model = sequelize['import'](path.join(__dirname, file));
16 | db[model.name] = model;
17 | });
18 |
19 | Object.keys(db).forEach(modelName => {
20 | if (db[modelName].associate) {
21 | db[modelName].associate(db);
22 | }
23 | });
24 |
25 | db.sequelize = sequelize;
26 | db.Sequelize = Sequelize;
27 |
28 | module.exports = db;
--------------------------------------------------------------------------------
/tasks/task-7/code/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "task-6",
3 | "version": "1.0.0",
4 | "description": "Tutorial for task 6",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha --timeout 10000 --exit",
8 | "start": "node index.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "bcryptjs": "^2.4.3",
14 | "body-parser": "^1.18.3",
15 | "express": "^4.16.3",
16 | "jsonwebtoken": "^8.3.0",
17 | "morgan": "^1.9.1",
18 | "passport": "^0.4.0",
19 | "passport-jwt": "^4.0.0",
20 | "pg": "^7.4.3",
21 | "pg-hstore": "^2.3.2",
22 | "sequelize": "^4.38.1"
23 | },
24 | "devDependencies": {
25 | "chai": "^4.1.2",
26 | "chai-http": "^4.2.0",
27 | "mocha": "^5.2.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tasks/task-7/code/passport/index.js:
--------------------------------------------------------------------------------
1 | const JwtStrategy = require('passport-jwt').Strategy;
2 | const ExtractJwt = require('passport-jwt').ExtractJwt;
3 | const models = require('../models')
4 |
5 | let opts = {};
6 |
7 | opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken()
8 | opts.secretOrKey = 'secret';
9 |
10 | module.exports = passport => {
11 | passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
12 | models.User.findById(jwt_payload.id)
13 | .then(user => {
14 | if (user) return done(null, user);
15 | return done(null, false)
16 | })
17 | .catch(err => console.log(err));
18 | }));
19 | }
--------------------------------------------------------------------------------
/tasks/task-7/code/routes/users.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const jwt = require('jsonwebtoken');
3 | const bcrypt = require('bcryptjs');
4 |
5 | const models = require('../models');
6 | const router = express.Router();
7 |
8 | router.post('/signup', (req, res) => {
9 | let newUser = {
10 | email: req.body.email,
11 | password: req.body.password
12 | }
13 |
14 | models.User.findOne({ where: { email: newUser.email }})
15 | .then(user => {
16 | if(user) return res.status(401).json({ msg: 'Email already exists' })
17 | else {
18 | models.User.create(newUser)
19 | .then(user => {
20 | let payload = {
21 | email: user.email,
22 | id: user.id
23 | }
24 | jwt.sign(payload, 'secret', { expiresIn: '1h'}, (err, token) => {
25 | // Here we send the token to the client.
26 | res.json({session: token})
27 | })
28 | })
29 | }
30 | })
31 | .catch(err => res.status(401).json(err));
32 | });
33 |
34 | router.post('/login', (req, res) => {
35 | let newUser = {
36 | email: req.body.email,
37 | password: req.body.password
38 | }
39 |
40 | models.User.findOne({ where: { email: newUser.email }})
41 | .then(user => {
42 | if(user) {
43 | bcrypt.compare(newUser.password, user.password)
44 | .then(isMatch => {
45 | if(isMatch) {
46 | let payload = { email: user.email, id: user.id }
47 | jwt.sign(payload, 'secret', { expiresIn: '1h'}, (err, token) => {
48 | res.json({session: token})
49 | })
50 | }
51 | else {
52 | res.status(401).json({error: 'Invalid Password'})
53 | }
54 | })
55 | }
56 | else {
57 | res.status(404).json({error: 'User does not exist.'})
58 | }
59 | })
60 | .catch(err => res.status(401).json(err));
61 | })
62 |
63 | module.exports = router;
--------------------------------------------------------------------------------
/tasks/task-7/code/routes/websites.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const models = require('../models/');
4 | const passport = require('passport');
5 | const jwt = require('jsonwebtoken');
6 |
7 | router.get('/list',
8 | passport.authenticate('jwt', { session: false }),
9 | (req, res) => {
10 | let pureToken = req.get('Authorization').slice(7)
11 | let currentUser = jwt.verify(pureToken, 'secret');
12 |
13 | models.Website.findAll({
14 | where: {
15 | UserId: currentUser.id
16 | }
17 | })
18 | .then(websites => res.json(websites))
19 | .catch(err => res.status(400).json({ msg: "Error" }))
20 | })
21 |
22 | router.post('/add',
23 | passport.authenticate('jwt', { session: false }),
24 | (req, res) => {
25 |
26 | let pureToken = req.get('Authorization').slice(7)
27 | let currentUser = jwt.verify(pureToken, 'secret');
28 |
29 | if(req.body.constructor === Object && Object.keys(req.body).length === 0) return res.status(400).json({ msg: 'Invalid data' })
30 |
31 | let newWebsite = {
32 | name: req.body.name,
33 | url: req.body.url
34 | }
35 |
36 | models.User.findOne({
37 | where: {
38 | id: currentUser.id
39 | }
40 | })
41 | .then(user => {
42 | if(!user) res.status(401).json({ msg:"User not found, please log in" })
43 | models.Website.find({
44 | where: {
45 | UserId: currentUser.id,
46 | url: newWebsite.url
47 | }
48 | })
49 | .then(website => {
50 | if(website) res.status(400).json({ msg: "Website already added" })
51 | else {
52 | models.Website.create(newWebsite)
53 | .then(website => {
54 | website.setUser(currentUser.id)
55 | res.json(website)
56 | })
57 | .catch(err => res.status(400).json(err))
58 | }
59 | })
60 | })
61 | .catch(err => res.status(401).send(err))
62 | })
63 |
64 | module.exports = router;
--------------------------------------------------------------------------------
/tasks/task-7/images/test-result-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-7/images/test-result-1.png
--------------------------------------------------------------------------------
/tasks/task-7/images/test-result-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-7/images/test-result-2.png
--------------------------------------------------------------------------------
/tasks/task-7/images/test-result-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-7/images/test-result-3.png
--------------------------------------------------------------------------------
/tasks/task-7/images/test-result-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-7/images/test-result-4.png
--------------------------------------------------------------------------------
/tasks/task-8/Screen Shot 2018-08-12 at 11.57.25 AM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-8/Screen Shot 2018-08-12 at 11.57.25 AM.png
--------------------------------------------------------------------------------
/tasks/task-8/Screen Shot 2018-08-12 at 12.01.27 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-8/Screen Shot 2018-08-12 at 12.01.27 PM.png
--------------------------------------------------------------------------------
/tasks/task-8/Screen Shot 2018-08-12 at 12.01.31 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-8/Screen Shot 2018-08-12 at 12.01.31 PM.png
--------------------------------------------------------------------------------
/tasks/task-8/Screen Shot 2018-08-12 at 12.01.37 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-8/Screen Shot 2018-08-12 at 12.01.37 PM.png
--------------------------------------------------------------------------------
/tasks/task-8/code/backend/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | node_js:
4 | - "node"
5 | services:
6 | - postgresql
7 | install:
8 | - "npm install"
9 | script:
10 | - "npm test"
--------------------------------------------------------------------------------
/tasks/task-8/code/backend/db/index.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 |
3 | let dbUrl = null;
4 |
5 | if(process.env.NODE_ENV === 'test') dbUrl = 'postgres://utvdnzdb:hxLot3tUXkVkmG66z7uQlO05f-N1rjun@packy.db.elephantsql.com:5432/utvdnzdb';
6 | else {
7 | dbUrl = 'postgres://ivan:123456@localhost:5432/tutorial_web_monitor';
8 | }
9 |
10 | const sequelize = new Sequelize(dbUrl, {
11 | host: 'localhost',
12 | dialect: 'postgres',
13 | operatorsAliases: false,
14 | logging: false,
15 |
16 | pool: {
17 | max: 5,
18 | min: 0,
19 | acquire: 30000,
20 | idle: 10000
21 | }
22 | });
23 |
24 | module.exports = sequelize;
--------------------------------------------------------------------------------
/tasks/task-8/code/backend/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 | const logger = require('morgan');
4 | const passport = require('passport');
5 |
6 | const users = require('./routes/users');
7 | const websites = require('./routes/websites');
8 |
9 | const app = express();
10 |
11 | app.use(bodyParser.json());
12 |
13 | if(process.env.NODE_ENV !== 'test') app.use(logger('tiny'));
14 |
15 | app.use(passport.initialize());
16 |
17 | require('./passport/')(passport);
18 |
19 | app.use('/users', users);
20 | app.use('/websites', websites);
21 |
22 | app.get('/', (req, res) => {
23 | res.json({ success: true })
24 | });
25 |
26 | app.listen(3001, () => console.log('Listening on Port 3001'));
27 |
28 | module.exports = app;
--------------------------------------------------------------------------------
/tasks/task-8/code/backend/models/User.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | const User = sequelize.define('User', {
5 | email: DataTypes.STRING,
6 | password: DataTypes.STRING
7 | });
8 |
9 | User.associate = function (models) {
10 | models.User.hasMany(models.Website);
11 | };
12 |
13 | User.beforeCreate((user, options) => {
14 | let salt = bcrypt.genSaltSync(10);
15 | let hash = bcrypt.hashSync(user.password, salt);
16 | return user.password = hash;
17 | })
18 |
19 | return User;
20 | };
--------------------------------------------------------------------------------
/tasks/task-8/code/backend/models/Website.js:
--------------------------------------------------------------------------------
1 | module.exports = (sequelize, DataTypes) => {
2 | const Website = sequelize.define('Website', {
3 | name: DataTypes.STRING,
4 | url: {
5 | type: DataTypes.STRING,
6 | validate: {
7 | isUrl: {
8 | msg: "URL is not valid"
9 | }
10 | }
11 | },
12 | status: {
13 | type: DataTypes.STRING,
14 | defaultValue: 'online'
15 | }
16 | });
17 |
18 | Website.associate = function (models) {
19 | models.Website.belongsTo(models.User, {
20 | foreignKey: { allowNull: false },
21 | onDelete: "CASCADE"
22 | });
23 | };
24 |
25 | return Website;
26 | };
--------------------------------------------------------------------------------
/tasks/task-8/code/backend/models/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const Sequelize = require('sequelize');
4 | const basename = path.basename(__filename);
5 | const sequelize = require('../db');
6 |
7 | let db = {};
8 |
9 | fs
10 | .readdirSync(__dirname)
11 | .filter(file => {
12 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
13 | })
14 | .forEach(file => {
15 | var model = sequelize['import'](path.join(__dirname, file));
16 | db[model.name] = model;
17 | });
18 |
19 | Object.keys(db).forEach(modelName => {
20 | if (db[modelName].associate) {
21 | db[modelName].associate(db);
22 | }
23 | });
24 |
25 | db.sequelize = sequelize;
26 | db.Sequelize = Sequelize;
27 |
28 | module.exports = db;
--------------------------------------------------------------------------------
/tasks/task-8/code/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "task-6",
3 | "version": "1.0.0",
4 | "description": "Tutorial for task 6",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha --timeout 10000 --exit",
8 | "start": "node index.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "bcryptjs": "^2.4.3",
14 | "body-parser": "^1.18.3",
15 | "express": "^4.16.3",
16 | "jsonwebtoken": "^8.3.0",
17 | "morgan": "^1.9.1",
18 | "passport": "^0.4.0",
19 | "passport-jwt": "^4.0.0",
20 | "pg": "^7.4.3",
21 | "pg-hstore": "^2.3.2",
22 | "sequelize": "^4.38.1"
23 | },
24 | "devDependencies": {
25 | "chai": "^4.1.2",
26 | "chai-http": "^4.2.0",
27 | "mocha": "^5.2.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tasks/task-8/code/backend/passport/index.js:
--------------------------------------------------------------------------------
1 | const JwtStrategy = require('passport-jwt').Strategy;
2 | const ExtractJwt = require('passport-jwt').ExtractJwt;
3 | const models = require('../models')
4 |
5 | let opts = {};
6 |
7 | opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken()
8 | opts.secretOrKey = 'secret';
9 |
10 | module.exports = passport => {
11 | passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
12 | models.User.findById(jwt_payload.id)
13 | .then(user => {
14 | if (user) return done(null, user);
15 | return done(null, false)
16 | })
17 | .catch(err => console.log(err));
18 | }));
19 | }
--------------------------------------------------------------------------------
/tasks/task-8/code/backend/routes/users.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const jwt = require('jsonwebtoken');
3 | const bcrypt = require('bcryptjs');
4 |
5 | const models = require('../models');
6 | const router = express.Router();
7 |
8 | router.post('/signup', (req, res) => {
9 | let newUser = {
10 | email: req.body.email,
11 | password: req.body.password
12 | }
13 |
14 | models.User.findOne({ where: { email: newUser.email }})
15 | .then(user => {
16 | if(user) return res.status(401).json({ msg: 'Email already exists' })
17 | else {
18 | models.User.create(newUser)
19 | .then(user => {
20 | let payload = {
21 | email: user.email,
22 | id: user.id
23 | }
24 | jwt.sign(payload, 'secret', { expiresIn: '1h'}, (err, token) => {
25 | // Here we send the token to the client.
26 | res.json({session: token})
27 | })
28 | })
29 | }
30 | })
31 | .catch(err => res.status(401).json(err));
32 | });
33 |
34 | router.post('/login', (req, res) => {
35 | let newUser = {
36 | email: req.body.email,
37 | password: req.body.password
38 | }
39 |
40 | models.User.findOne({ where: { email: newUser.email }})
41 | .then(user => {
42 | if(user) {
43 | bcrypt.compare(newUser.password, user.password)
44 | .then(isMatch => {
45 | if(isMatch) {
46 | let payload = { email: user.email, id: user.id }
47 | jwt.sign(payload, 'secret', { expiresIn: '1h'}, (err, token) => {
48 | res.json({session: token})
49 | })
50 | }
51 | else {
52 | res.status(401).json({error: 'Invalid Password'})
53 | }
54 | })
55 | }
56 | else {
57 | res.status(404).json({error: 'User does not exist.'})
58 | }
59 | })
60 | .catch(err => res.status(401).json(err));
61 | })
62 |
63 | module.exports = router;
--------------------------------------------------------------------------------
/tasks/task-8/code/backend/routes/websites.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const models = require('../models/');
4 | const passport = require('passport');
5 | const jwt = require('jsonwebtoken');
6 |
7 | router.get('/list',
8 | passport.authenticate('jwt', { session: false }),
9 | (req, res) => {
10 | let pureToken = req.get('Authorization').slice(7)
11 | let currentUser = jwt.verify(pureToken, 'secret');
12 |
13 | models.Website.findAll({
14 | where: {
15 | UserId: currentUser.id
16 | }
17 | })
18 | .then(websites => res.json(websites))
19 | .catch(err => res.status(400).json({ msg: "Error" }))
20 | })
21 |
22 | router.post('/add',
23 | passport.authenticate('jwt', { session: false }),
24 | (req, res) => {
25 |
26 | let pureToken = req.get('Authorization').slice(7)
27 | let currentUser = jwt.verify(pureToken, 'secret');
28 |
29 | if(req.body.constructor === Object && Object.keys(req.body).length === 0) return res.status(400).json({ msg: 'Invalid data' })
30 |
31 | let newWebsite = {
32 | name: req.body.name,
33 | url: req.body.url
34 | }
35 |
36 | models.User.findOne({
37 | where: {
38 | id: currentUser.id
39 | }
40 | })
41 | .then(user => {
42 | if(!user) res.status(401).json({ msg:"User not found, please log in" })
43 | models.Website.find({
44 | where: {
45 | UserId: currentUser.id,
46 | url: newWebsite.url
47 | }
48 | })
49 | .then(website => {
50 | if(website) res.status(400).json({ msg: "Website already added" })
51 | else {
52 | models.Website.create(newWebsite)
53 | .then(website => {
54 | website.setUser(currentUser.id)
55 | res.json(website)
56 | })
57 | .catch(err => res.status(400).json(err))
58 | }
59 | })
60 | })
61 | .catch(err => res.status(401).send(err))
62 | })
63 |
64 | module.exports = router;
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/README.md:
--------------------------------------------------------------------------------
1 | #### React Redux Starter Template
2 |
3 | This project contains a boilerplate template created from the create-react-app library and configured along with redux.
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-starter-template",
3 | "version": "0.1.0",
4 | "private": true,
5 | "proxy": "http://localhost:3001/",
6 | "dependencies": {
7 | "axios": "^0.18.0",
8 | "react": "^16.2.0",
9 | "react-dom": "^16.2.0",
10 | "react-redux": "^5.0.6",
11 | "react-router-dom": "^4.2.2",
12 | "react-scripts": "1.0.17",
13 | "redux": "^3.7.2",
14 | "redux-form": "^7.4.2",
15 | "redux-logger": "^3.0.6",
16 | "redux-promise": "^0.5.3"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test --env=jsdom",
22 | "eject": "react-scripts eject"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-8/code/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | form {
2 | border: solid 1px black;
3 | padding: 15px;
4 | margin: 15px;
5 | width: 30vw;
6 | text-align: center;
7 | }
8 |
9 | .websites {
10 | padding: 15px;
11 | margin: 15px;
12 | }
13 |
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | });
9 |
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export const LOGIN = 'LOGIN';
2 | export const WEBSITES = 'WEBSITES';
3 | export const LOGOUT = 'LOGOUT';
4 |
5 | export function loginUser() {
6 | return {
7 | type: LOGIN,
8 | payload: true
9 | }
10 | }
11 |
12 | export function getWebsites(websites) {
13 | return {
14 | type: WEBSITES,
15 | payload: websites
16 | }
17 | }
18 |
19 | export function logout() {
20 | return {
21 | type: LOGOUT,
22 | payload: false
23 | }
24 | }
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/src/components/Login.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Link } from "react-router-dom";
4 |
5 | let Login = props => {
6 | const { handleSubmit } = props
7 | return (
8 |
23 | )
24 | }
25 |
26 | Login = reduxForm({
27 | // a unique name for the form
28 | form: 'login'
29 | })(Login)
30 |
31 | export default Login
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/src/components/Signup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Link } from "react-router-dom";
4 |
5 | let Signup = props => {
6 | const { handleSubmit } = props
7 | return (
8 |
23 | )
24 | }
25 |
26 | Signup = reduxForm({
27 | // a unique name for the form
28 | form: 'signup'
29 | })(Signup)
30 |
31 | export default Signup
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/src/components/Website.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Website = ({ name, url, status }) => (
4 |
5 |
{name}
6 |
{url}
7 |
{status}
8 |
9 | )
10 |
11 | export default Website;
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/src/components/WebsiteForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Field, reduxForm } from 'redux-form';
4 | import { logout } from '../actions';
5 |
6 | let WebsiteForm = props => {
7 | const { handleSubmit } = props
8 |
9 | function logout () {
10 | sessionStorage.clear();
11 | props.logout();
12 | }
13 |
14 | return (
15 |
30 | )
31 | }
32 |
33 | const mapDispatchToProps = {
34 | logout
35 | }
36 |
37 | WebsiteForm = connect(
38 | null,
39 | mapDispatchToProps
40 | )(WebsiteForm);
41 |
42 | export default reduxForm({
43 | form: 'websiteForm'
44 | })(WebsiteForm);
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/src/components/WebsiteList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import axios from 'axios';
4 |
5 | import { getWebsites } from '../actions';
6 | import Website from './Website';
7 |
8 | class WebsiteList extends Component {
9 |
10 | componentDidMount(){
11 | let token = sessionStorage.getItem('token');
12 |
13 | axios.get('/websites/list', { headers: { 'Authorization': token } })
14 | .then(websites => {
15 | this.props.getWebsites(websites)
16 | })
17 | .catch(err => alert(err.response.data.msg));
18 | }
19 |
20 | render() {
21 | return (
22 |
23 |
Websites
24 | {
25 | this.props.user.websites.reverse().map(website =>
26 |
32 | )
33 | }
34 |
35 | );
36 | }
37 | }
38 |
39 | const mapStateToProps = state => {
40 | return {
41 | user: state.user
42 | }
43 | }
44 |
45 | const mapDispatchToProps = {
46 | getWebsites
47 | }
48 |
49 | export default connect(mapStateToProps, mapDispatchToProps)(WebsiteList);
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | import { createStore, applyMiddleware } from 'redux';
8 | import { Provider } from 'react-redux';
9 | import reducers from './reducers';
10 | import ReduxPromise from 'redux-promise';
11 |
12 | const store = createStore(
13 | reducers,
14 | applyMiddleware(ReduxPromise)
15 | );
16 |
17 | ReactDOM.render(
18 |
19 |
20 |
21 | , document.getElementById('root'));
22 | registerServiceWorker();
23 |
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import { reducer as formReducer } from 'redux-form';
4 | import userReducer from './user';
5 |
6 | const rootReducers = combineReducers({
7 | form: formReducer.plugin({
8 | websiteForm: (state, action) => {
9 | switch(action.type) {
10 | case 'ACCOUNT_SAVE_SUCCESS':
11 | return undefined;
12 | default:
13 | return state;
14 | }
15 | }
16 | }),
17 | user: userReducer
18 | });
19 |
20 | export default rootReducers;
--------------------------------------------------------------------------------
/tasks/task-8/code/frontend/src/reducers/user.js:
--------------------------------------------------------------------------------
1 | import { LOGIN, WEBSITES, LOGOUT } from '../actions';
2 |
3 | let initialState = {
4 | isLoggedIn: false,
5 | websites: []
6 | }
7 |
8 | const user = (state = initialState, action) => {
9 | switch(action.type) {
10 | case LOGIN:
11 | return state = {...state, isLoggedIn: true };
12 | case WEBSITES:
13 | return state = {...state, websites: action.payload.data };
14 | case LOGOUT:
15 | return state = {...state, isLoggedIn: action.payload };
16 | default:
17 | return state;
18 | }
19 | }
20 |
21 | export default user
--------------------------------------------------------------------------------
/tasks/task-8/images/folders.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-8/images/folders.png
--------------------------------------------------------------------------------
/tasks/task-8/images/form-and-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-8/images/form-and-list.png
--------------------------------------------------------------------------------
/tasks/task-8/images/signup-form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-8/images/signup-form.png
--------------------------------------------------------------------------------
/tasks/task-8/images/website-form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-8/images/website-form.png
--------------------------------------------------------------------------------
/tasks/task-9/README.md:
--------------------------------------------------------------------------------
1 | # Task 9 - Testing Dashboard.
2 |
3 | ## Objectives
4 |
5 | - Learn to test Frontend React and Redux applications.
6 | - Learn to use StorybookJS and Jest
7 |
8 | ## Learning Resources
9 |
10 | Here are the list of learning resources for this task.
11 |
12 | Topic | Resource
13 | ------------ | -------------
14 | Getting Started with React Storybook | [Link to this resource](https://www.youtube.com/watch?v=E2c183LS4lA)
15 | React Storybook: Design, Dev, Doc, Debug Components | [Link to this resource](https://www.youtube.com/watch?v=PF0Vi-iIyoo)
16 | Create Powerful Interactive Style Guides with Storybook | [Link to this resource](https://www.youtube.com/watch?v=cOI_k_5iOos)
17 | Jest Course | [Link to this resource](https://www.youtube.com/watch?v=4kNfeI37xu4&list=PLLnpHn493BHEB-YOl0APuQsrzlb3zbq3y)
18 | Jest Crash Course | [Link to this resource](https://www.youtube.com/watch?v=7r4xVDI2vho)
19 |
20 |
21 | ## Tasks
22 |
23 | #### Step 1: Tests for Add Website form.
24 |
25 | - When you write test for add website form, make sure:
26 | - You show loaders properly, when the form is being submitted.
27 | - Check URL validation.
28 | - Test the form by keeping fields blank.
29 | - Fill the form properly and test if the new website is added to the list.
30 |
31 | #### Step 2: Test website list
32 |
33 | - When you write test for list website component, make sure:
34 | - You show loaders properly, when the list is being loaded.
35 | - When there are no websites in the list. You show the info message.
36 | - You list all the websites properly.
37 | - When there is error in loading, an error message is shown on the page. You can mock that error in Storybook + Jest.
38 |
39 | #### Step 3: Test log out
40 |
41 | - Make sure when you click on log out, you're redirected to sign in page and you have cleared all local storage and sessions. Try navigating back to dashboard (without logging in( and make sure you're automatically navigated back to sign in.
42 |
43 | ## Deliverable
44 |
45 | - Push changes to your frontend Git Repo with tests.
46 | - Make sure the build passes on Travis.
47 |
48 |
49 |
--------------------------------------------------------------------------------
/tasks/task-9/Solution.md:
--------------------------------------------------------------------------------
1 | # Task 9 Tutorial
2 | *For this task just follow the instructions for **task 5** with only one minor change. All of this inside of your frontend folder.*
3 |
4 | * Add the following code to `scr/test/stories/index.stories.js`.
5 | ```javacript
6 | // Other code
7 |
8 | storiesOf('Signup and Login', module)
9 | .addDecorator(story =>
10 | {/* This will prevent errors because of your Link component*/}
11 | {story()}
12 | )
13 | // Other code
14 | .add('WebsiteForm', () => (
15 |
16 | ))
17 | .add('Website', () => (
18 |
19 | ))
20 | ```
21 |
22 | *Remember to follow the instructions in **task 5** first, see you in the next task!*
--------------------------------------------------------------------------------
/tasks/task-9/code/backend/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | node_js:
4 | - "node"
5 | services:
6 | - postgresql
7 | install:
8 | - "npm install"
9 | script:
10 | - "npm test"
--------------------------------------------------------------------------------
/tasks/task-9/code/backend/db/index.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 |
3 | let dbUrl = null;
4 |
5 | if(process.env.NODE_ENV === 'test') dbUrl = 'postgres://utvdnzdb:hxLot3tUXkVkmG66z7uQlO05f-N1rjun@packy.db.elephantsql.com:5432/utvdnzdb';
6 | else {
7 | dbUrl = 'postgres://ivan:123456@localhost:5432/tutorial_web_monitor';
8 | }
9 |
10 | const sequelize = new Sequelize(dbUrl, {
11 | host: 'localhost',
12 | dialect: 'postgres',
13 | operatorsAliases: false,
14 | logging: false,
15 |
16 | pool: {
17 | max: 5,
18 | min: 0,
19 | acquire: 30000,
20 | idle: 10000
21 | }
22 | });
23 |
24 | module.exports = sequelize;
--------------------------------------------------------------------------------
/tasks/task-9/code/backend/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 | const logger = require('morgan');
4 | const passport = require('passport');
5 |
6 | const users = require('./routes/users');
7 | const websites = require('./routes/websites');
8 |
9 | const app = express();
10 |
11 | app.use(bodyParser.json());
12 |
13 | if(process.env.NODE_ENV !== 'test') app.use(logger('tiny'));
14 |
15 | app.use(passport.initialize());
16 |
17 | require('./passport/')(passport);
18 |
19 | app.use('/users', users);
20 | app.use('/websites', websites);
21 |
22 | app.get('/', (req, res) => {
23 | res.json({ success: true })
24 | });
25 |
26 | app.listen(3001, () => console.log('Listening on Port 3001'));
27 |
28 | module.exports = app;
--------------------------------------------------------------------------------
/tasks/task-9/code/backend/models/User.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | const User = sequelize.define('User', {
5 | email: DataTypes.STRING,
6 | password: DataTypes.STRING
7 | });
8 |
9 | User.associate = function (models) {
10 | models.User.hasMany(models.Website);
11 | };
12 |
13 | User.beforeCreate((user, options) => {
14 | let salt = bcrypt.genSaltSync(10);
15 | let hash = bcrypt.hashSync(user.password, salt);
16 | return user.password = hash;
17 | })
18 |
19 | return User;
20 | };
--------------------------------------------------------------------------------
/tasks/task-9/code/backend/models/Website.js:
--------------------------------------------------------------------------------
1 | module.exports = (sequelize, DataTypes) => {
2 | const Website = sequelize.define('Website', {
3 | name: DataTypes.STRING,
4 | url: {
5 | type: DataTypes.STRING,
6 | validate: {
7 | isUrl: {
8 | msg: "URL is not valid"
9 | }
10 | }
11 | },
12 | status: {
13 | type: DataTypes.STRING,
14 | defaultValue: 'online'
15 | }
16 | });
17 |
18 | Website.associate = function (models) {
19 | models.Website.belongsTo(models.User, {
20 | foreignKey: { allowNull: false },
21 | onDelete: "CASCADE"
22 | });
23 | };
24 |
25 | return Website;
26 | };
--------------------------------------------------------------------------------
/tasks/task-9/code/backend/models/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const Sequelize = require('sequelize');
4 | const basename = path.basename(__filename);
5 | const sequelize = require('../db');
6 |
7 | let db = {};
8 |
9 | fs
10 | .readdirSync(__dirname)
11 | .filter(file => {
12 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
13 | })
14 | .forEach(file => {
15 | var model = sequelize['import'](path.join(__dirname, file));
16 | db[model.name] = model;
17 | });
18 |
19 | Object.keys(db).forEach(modelName => {
20 | if (db[modelName].associate) {
21 | db[modelName].associate(db);
22 | }
23 | });
24 |
25 | db.sequelize = sequelize;
26 | db.Sequelize = Sequelize;
27 |
28 | module.exports = db;
--------------------------------------------------------------------------------
/tasks/task-9/code/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "task-6",
3 | "version": "1.0.0",
4 | "description": "Tutorial for task 6",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha --timeout 10000 --exit",
8 | "start": "node index.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "bcryptjs": "^2.4.3",
14 | "body-parser": "^1.18.3",
15 | "express": "^4.16.3",
16 | "jsonwebtoken": "^8.3.0",
17 | "morgan": "^1.9.1",
18 | "passport": "^0.4.0",
19 | "passport-jwt": "^4.0.0",
20 | "pg": "^7.4.3",
21 | "pg-hstore": "^2.3.2",
22 | "sequelize": "^4.38.1"
23 | },
24 | "devDependencies": {
25 | "chai": "^4.1.2",
26 | "chai-http": "^4.2.0",
27 | "mocha": "^5.2.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tasks/task-9/code/backend/passport/index.js:
--------------------------------------------------------------------------------
1 | const JwtStrategy = require('passport-jwt').Strategy;
2 | const ExtractJwt = require('passport-jwt').ExtractJwt;
3 | const models = require('../models')
4 |
5 | let opts = {};
6 |
7 | opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken()
8 | opts.secretOrKey = 'secret';
9 |
10 | module.exports = passport => {
11 | passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
12 | models.User.findById(jwt_payload.id)
13 | .then(user => {
14 | if (user) return done(null, user);
15 | return done(null, false)
16 | })
17 | .catch(err => console.log(err));
18 | }));
19 | }
--------------------------------------------------------------------------------
/tasks/task-9/code/backend/routes/users.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const jwt = require('jsonwebtoken');
3 | const bcrypt = require('bcryptjs');
4 |
5 | const models = require('../models');
6 | const router = express.Router();
7 |
8 | router.post('/signup', (req, res) => {
9 | let newUser = {
10 | email: req.body.email,
11 | password: req.body.password
12 | }
13 |
14 | models.User.findOne({ where: { email: newUser.email }})
15 | .then(user => {
16 | if(user) return res.status(401).json({ msg: 'Email already exists' })
17 | else {
18 | models.User.create(newUser)
19 | .then(user => {
20 | let payload = {
21 | email: user.email,
22 | id: user.id
23 | }
24 | jwt.sign(payload, 'secret', { expiresIn: '1h'}, (err, token) => {
25 | // Here we send the token to the client.
26 | res.json({session: token})
27 | })
28 | })
29 | }
30 | })
31 | .catch(err => res.status(401).json(err));
32 | });
33 |
34 | router.post('/login', (req, res) => {
35 | let newUser = {
36 | email: req.body.email,
37 | password: req.body.password
38 | }
39 |
40 | models.User.findOne({ where: { email: newUser.email }})
41 | .then(user => {
42 | if(user) {
43 | bcrypt.compare(newUser.password, user.password)
44 | .then(isMatch => {
45 | if(isMatch) {
46 | let payload = { email: user.email, id: user.id }
47 | jwt.sign(payload, 'secret', { expiresIn: '1h'}, (err, token) => {
48 | res.json({session: token})
49 | })
50 | }
51 | else {
52 | res.status(401).json({error: 'Invalid Password'})
53 | }
54 | })
55 | }
56 | else {
57 | res.status(404).json({error: 'User does not exist.'})
58 | }
59 | })
60 | .catch(err => res.status(401).json(err));
61 | })
62 |
63 | module.exports = router;
--------------------------------------------------------------------------------
/tasks/task-9/code/backend/routes/websites.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const models = require('../models/');
4 | const passport = require('passport');
5 | const jwt = require('jsonwebtoken');
6 |
7 | router.get('/list',
8 | passport.authenticate('jwt', { session: false }),
9 | (req, res) => {
10 | let pureToken = req.get('Authorization').slice(7)
11 | let currentUser = jwt.verify(pureToken, 'secret');
12 |
13 | models.Website.findAll({
14 | where: {
15 | UserId: currentUser.id
16 | }
17 | })
18 | .then(websites => res.json(websites))
19 | .catch(err => res.status(400).json({ msg: "Error" }))
20 | })
21 |
22 | router.post('/add',
23 | passport.authenticate('jwt', { session: false }),
24 | (req, res) => {
25 |
26 | let pureToken = req.get('Authorization').slice(7)
27 | let currentUser = jwt.verify(pureToken, 'secret');
28 |
29 | if(req.body.constructor === Object && Object.keys(req.body).length === 0) return res.status(400).json({ msg: 'Invalid data' })
30 |
31 | let newWebsite = {
32 | name: req.body.name,
33 | url: req.body.url
34 | }
35 |
36 | models.User.findOne({
37 | where: {
38 | id: currentUser.id
39 | }
40 | })
41 | .then(user => {
42 | if(!user) res.status(401).json({ msg:"User not found, please log in" })
43 | models.Website.find({
44 | where: {
45 | UserId: currentUser.id,
46 | url: newWebsite.url
47 | }
48 | })
49 | .then(website => {
50 | if(website) res.status(400).json({ msg: "Website already added" })
51 | else {
52 | models.Website.create(newWebsite)
53 | .then(website => {
54 | website.setUser(currentUser.id)
55 | res.json(website)
56 | })
57 | .catch(err => res.status(400).json(err))
58 | }
59 | })
60 | })
61 | .catch(err => res.status(401).send(err))
62 | })
63 |
64 | module.exports = router;
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from '@storybook/react';
2 |
3 | function loadStories() {
4 | const req = require.context('../src', true, /.stories.js$/);
5 | req.keys().forEach((filename) => req(filename));
6 | }
7 |
8 | configure(loadStories, module);
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/README.md:
--------------------------------------------------------------------------------
1 | #### React Redux Starter Template
2 |
3 | This project contains a boilerplate template created from the create-react-app library and configured along with redux.
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-starter-template",
3 | "version": "0.1.0",
4 | "private": true,
5 | "proxy": "http://localhost:3001/",
6 | "dependencies": {
7 | "axios": "^0.18.0",
8 | "react": "^16.5.2",
9 | "react-dom": "^16.5.2",
10 | "react-redux": "^5.0.6",
11 | "react-router-dom": "^4.2.2",
12 | "react-scripts": "1.0.17",
13 | "redux": "^3.7.2",
14 | "redux-form": "^7.4.2",
15 | "redux-logger": "^3.0.6",
16 | "redux-promise": "^0.5.3"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test --env=jsdom",
22 | "eject": "react-scripts eject",
23 | "storybook": "start-storybook -p 9001 -c .storybook"
24 | },
25 | "devDependencies": {
26 | "@storybook/addon-storyshots": "^3.4.11",
27 | "@storybook/react": "^3.4.11",
28 | "react-test-renderer": "^16.5.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackerbayuniversity/Course/3f1c6b6c6cb6b8c354ec79b27d2da5458d3d307f/tasks/task-9/code/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | form {
2 | border: solid 1px black;
3 | padding: 15px;
4 | margin: 15px;
5 | width: 30vw;
6 | text-align: center;
7 | }
8 |
9 | .websites {
10 | padding: 15px;
11 | margin: 15px;
12 | }
13 |
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export const LOGIN = 'LOGIN';
2 | export const WEBSITES = 'WEBSITES';
3 | export const LOGOUT = 'LOGOUT';
4 |
5 | export function loginUser() {
6 | return {
7 | type: LOGIN,
8 | payload: true
9 | }
10 | }
11 |
12 | export function getWebsites(websites) {
13 | return {
14 | type: WEBSITES,
15 | payload: websites
16 | }
17 | }
18 |
19 | export function logout() {
20 | return {
21 | type: LOGOUT,
22 | payload: false
23 | }
24 | }
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/src/components/Login.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Link } from "react-router-dom";
4 |
5 | let Login = props => {
6 | const { handleSubmit } = props
7 | return (
8 |
23 | )
24 | }
25 |
26 | Login = reduxForm({
27 | // a unique name for the form
28 | form: 'login'
29 | })(Login)
30 |
31 | export default Login
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/src/components/Signup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { Link } from "react-router-dom";
4 |
5 | let Signup = props => {
6 | const { handleSubmit } = props
7 | return (
8 |
23 | )
24 | }
25 |
26 | Signup = reduxForm({
27 | // a unique name for the form
28 | form: 'signup'
29 | })(Signup)
30 |
31 | export default Signup
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/src/components/Website.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Website = ({ name, url, status }) => (
4 |
5 |
{name}
6 |
{url}
7 |
{status}
8 |
9 | )
10 |
11 | export default Website;
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/src/components/WebsiteForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Field, reduxForm } from 'redux-form';
4 | import { logout } from '../actions';
5 |
6 | let WebsiteForm = props => {
7 | const { handleSubmit } = props
8 |
9 | function logout () {
10 | sessionStorage.clear();
11 | props.logout();
12 | }
13 |
14 | return (
15 |
30 | )
31 | }
32 |
33 | const mapDispatchToProps = {
34 | logout
35 | }
36 |
37 | WebsiteForm = connect(
38 | null,
39 | mapDispatchToProps
40 | )(WebsiteForm);
41 |
42 | export default reduxForm({
43 | form: 'websiteForm'
44 | })(WebsiteForm);
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/src/components/WebsiteList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import axios from 'axios';
4 |
5 | import { getWebsites } from '../actions';
6 | import Website from './Website';
7 |
8 | class WebsiteList extends Component {
9 |
10 | componentDidMount(){
11 | let token = sessionStorage.getItem('token');
12 |
13 | axios.get('/websites/list', { headers: { 'Authorization': token } })
14 | .then(websites => {
15 | this.props.getWebsites(websites)
16 | })
17 | .catch(err => alert(err.response.data.msg));
18 | }
19 |
20 | render() {
21 | return (
22 |
23 |
Websites
24 | {
25 | this.props.user.websites.reverse().map(website =>
26 |
32 | )
33 | }
34 |
35 | );
36 | }
37 | }
38 |
39 | const mapStateToProps = state => {
40 | return {
41 | user: state.user
42 | }
43 | }
44 |
45 | const mapDispatchToProps = {
46 | getWebsites
47 | }
48 |
49 | export default connect(mapStateToProps, mapDispatchToProps)(WebsiteList);
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/src/configureStore.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, compose, createStore } from 'redux'
2 | import ReduxPromise from 'redux-promise';
3 |
4 | import rootReducer from './reducers';
5 |
6 | export default function configureStore(preloadedState) {
7 | const middlewareEnhancer = applyMiddleware(ReduxPromise)
8 |
9 | const enhancers = [middlewareEnhancer]
10 | const composedEnhancers = compose(...enhancers)
11 |
12 | const store = createStore(rootReducer, preloadedState, composedEnhancers)
13 |
14 | return store
15 | }
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | import { Provider } from 'react-redux';
8 |
9 | import configureStore from './configureStore'
10 |
11 | const store = configureStore()
12 |
13 | ReactDOM.render(
14 |
15 |
16 |
17 | , document.getElementById('root'));
18 | registerServiceWorker();
19 |
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import { reducer as formReducer } from 'redux-form';
4 | import userReducer from './user';
5 |
6 | const rootReducers = combineReducers({
7 | form: formReducer.plugin({
8 | websiteForm: (state, action) => {
9 | switch(action.type) {
10 | case 'ACCOUNT_SAVE_SUCCESS':
11 | return undefined;
12 | default:
13 | return state;
14 | }
15 | }
16 | }),
17 | user: userReducer
18 | });
19 |
20 | export default rootReducers;
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/src/reducers/user.js:
--------------------------------------------------------------------------------
1 | import { LOGIN, WEBSITES, LOGOUT } from '../actions';
2 |
3 | let initialState = {
4 | isLoggedIn: false,
5 | websites: []
6 | }
7 |
8 | const user = (state = initialState, action) => {
9 | switch(action.type) {
10 | case LOGIN:
11 | return state = {...state, isLoggedIn: true };
12 | case WEBSITES:
13 | return state = {...state, websites: action.payload.data };
14 | case LOGOUT:
15 | return state = {...state, isLoggedIn: action.payload };
16 | default:
17 | return state;
18 | }
19 | }
20 |
21 | export default user
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/src/test/stories/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { action } from '@storybook/addon-actions';
4 | import { Provider } from 'react-redux';
5 | import Login from '../../components/Login';
6 | import Signup from '../../components/Signup';
7 | import WebsiteForm from '../../components/WebsiteForm';
8 | import Website from '../../components/Website';
9 | import configureStore from '../../configureStore';
10 | import { MemoryRouter } from 'react-router';
11 |
12 | // We bring our redux Store
13 | const store = configureStore();
14 |
15 | storiesOf('Signup and Login', module)
16 | .addDecorator(story =>
17 | {/* This will prevent errors because of your Link component*/}
18 | {story()}
19 | )
20 | .add('Login', () => (
21 |
22 | ))
23 | .add('Signup', () => (
24 |
25 | ))
26 | .add('WebsiteForm', () => (
27 |
28 | ))
29 | .add('Website', () => (
30 |
31 | ))
--------------------------------------------------------------------------------
/tasks/task-9/code/frontend/src/test/storyshots.test.js:
--------------------------------------------------------------------------------
1 | import initStoryshots from '@storybook/addon-storyshots';
2 |
3 | initStoryshots({ /* configuration options */ });
--------------------------------------------------------------------------------