├── .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 |
9 |

Login!

10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |

Already have and account?
Click here

22 |
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 |
9 |

Sign up!

10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |

Already have and account?
Click here

22 |
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 |
16 |

Add a website to monitor

17 | 18 |
19 | 20 |
21 | 22 |
23 |
24 | 25 |
26 | 27 |
28 | 29 |
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 |
9 |

Login!

10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |

Already have and account?
Click here

22 |
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 |
9 |

Sign up!

10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |

Already have and account?
Click here

22 |
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 |
16 |

Add a website to monitor

17 | 18 |
19 | 20 |
21 | 22 |
23 |
24 | 25 |
26 | 27 |
28 | 29 |
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 |
9 |
10 |

Login!

11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 | 19 |

Already have and account?
Click here

20 |
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 |
9 |

Sign up!

10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 | 19 |

Already have and account?
Click here

20 |
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 |
9 |
10 |

Login!

11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 | 19 |

Already have and account?
Click here

20 |
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 |
9 |

Sign up!

10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 | 19 |

Already have and account?
Click here

20 |
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 |
7 |
8 |

9 | Login! 10 |

11 | 16 | 26 |
27 |
28 | 33 | 43 |
44 | 49 |

50 | Already have and account? 51 |
52 | Click 53 | 57 | here 58 | 59 |

60 |
61 | `; 62 | 63 | exports[`Storyshots Signup and Login Signup 1`] = ` 64 |
67 |

68 | Sign up! 69 |

70 |
71 | 76 | 86 |
87 |
88 | 93 | 103 |
104 | 109 |

110 | Already have and account? 111 |
112 | Click 113 | 117 | here 118 | 119 |

120 |
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 |
9 |

Login!

10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |

Already have and account?
Click here

22 |
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 |
9 |

Sign up!

10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |

Already have and account?
Click here

22 |
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 |
16 |

Add a website to monitor

17 | 18 |
19 | 20 |
21 | 22 |
23 |
24 | 25 |
26 | 27 |
28 | 29 |
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 |
9 |

Login!

10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |

Already have and account?
Click here

22 |
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 |
9 |

Sign up!

10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |

Already have and account?
Click here

22 |
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 |
16 |

Add a website to monitor

17 | 18 |
19 | 20 |
21 | 22 |
23 |
24 | 25 |
26 | 27 |
28 | 29 |
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 */ }); --------------------------------------------------------------------------------