├── .gitignore ├── .DS_Store ├── jest-teardown.js ├── client ├── src │ ├── .DS_Store │ └── assets │ │ └── angle-circle-arrow-up.png ├── scss │ ├── _variables.scss │ ├── application.scss │ ├── globals.scss │ ├── LoginStyle.scss │ └── mainPage.scss ├── reducers │ ├── index.js │ ├── outputReducer.js │ └── formReducer.js ├── constants │ └── actionTypes.js ├── store.js ├── index.jsx ├── index.html ├── containers │ ├── MainContainer.jsx │ └── OutputBoxContainer.jsx ├── App.jsx ├── components │ ├── outputBox.jsx │ ├── InputBox.jsx │ ├── Login.jsx │ └── Signup.jsx └── actions │ └── action.js ├── scratchProjectDiagram.pdf ├── jest-setup.js ├── .eslintrc.js ├── server ├── db │ └── databaseIndex.js ├── router │ ├── authrouter.js │ └── mainrouter.js ├── server.js └── controller │ ├── authcontroller.js │ └── maincontroller.js ├── .babelrc ├── webpack.config.js ├── package.json ├── README.md └── __tests__ ├── enzyme.jsx └── supertest.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | package-lock.json -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HSDCS/Uptime-API-Monitor/HEAD/.DS_Store -------------------------------------------------------------------------------- /jest-teardown.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | global.testServer.close(); 3 | }; 4 | -------------------------------------------------------------------------------- /client/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HSDCS/Uptime-API-Monitor/HEAD/client/src/.DS_Store -------------------------------------------------------------------------------- /scratchProjectDiagram.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HSDCS/Uptime-API-Monitor/HEAD/scratchProjectDiagram.pdf -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | global.testServer = await require('./server/server'); 3 | }; 4 | -------------------------------------------------------------------------------- /client/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #ffd918; 2 | $secondary-color: #6eb2da; 3 | $background-color: #10152a; 4 | -------------------------------------------------------------------------------- /client/scss/application.scss: -------------------------------------------------------------------------------- 1 | @import './LoginStyle.scss'; 2 | @import './mainPage.scss'; 3 | @import './globals.scss'; 4 | -------------------------------------------------------------------------------- /client/src/assets/angle-circle-arrow-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HSDCS/Uptime-API-Monitor/HEAD/client/src/assets/angle-circle-arrow-up.png -------------------------------------------------------------------------------- /client/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import outputReducer from './outputReducer'; 3 | import formReducer from './formReducer'; 4 | 5 | // combining reducers 6 | const reducers = combineReducers({ 7 | outputs: outputReducer, 8 | form: formReducer, 9 | }); 10 | 11 | export default reducers; 12 | -------------------------------------------------------------------------------- /client/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_URL = 'ADD CARD'; 2 | export const CHECK_NOW = 'CHECK_NOW'; 3 | export const SIGNUP_FORM_INPUT = 'SIGNUP_FORM_INPUT'; 4 | export const SIGNUP_FORM_SUBMIT = 'SIGNUP_FORM_SUBMIT'; 5 | export const LOGIN_FORM_INPUT = 'LOGIN_FORM_INPUT'; 6 | export const LOGIN_FAILURE = 'LOGIN_FAILURE'; 7 | export const LOGIN_START = 'LOGIN_START'; 8 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; 9 | -------------------------------------------------------------------------------- /client/scss/globals.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | box-sizing: border-box; 4 | margin: 0px; 5 | padding: 0px; 6 | font-family: 'Segoe UI', Arial, Helvetica, sans-serif; 7 | } 8 | 9 | *, 10 | *::before, 11 | *::after { 12 | box-sizing: inherit; 13 | font-family: 'Segoe UI', Arial, Helvetica, sans-serif; 14 | } 15 | 16 | button { 17 | font-family: 'Roboto Mono', monospace; 18 | font-weight: 400; 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | jest: true, 6 | node: true, 7 | }, 8 | extends: ['plugin:react/recommended', 'airbnb'], 9 | parserOptions: { 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | ecmaVersion: 12, 14 | sourceType: 'module', 15 | }, 16 | plugins: ['react'], 17 | rules: { 18 | 'react/prop-types': 'off', 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /client/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { composeWithDevTools } from 'redux-devtools-extension'; 3 | import thunk from 'redux-thunk'; 4 | import reducers from './reducers/index'; 5 | 6 | // we are adding composeWIthDevTools here to get easy access to the Redux dev tools 7 | const store = createStore( 8 | reducers, 9 | composeWithDevTools(applyMiddleware(thunk)) 10 | ); 11 | 12 | export default store; 13 | -------------------------------------------------------------------------------- /client/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import App from './App'; 6 | import store from './store'; 7 | import './scss/application.scss'; 8 | 9 | render( 10 | 11 | 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); 17 | -------------------------------------------------------------------------------- /server/db/databaseIndex.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require("pg"); 2 | const PG_URI = 3 | "postgres://zfumxfwb:0PNXwsB496tVxPq3HDpv_At_HmY4clq3@lallah.db.elephantsql.com:5432/zfumxfwb"; 4 | const pool = new Pool({ 5 | connectionString: PG_URI, 6 | }); 7 | 8 | /* 9 | https://node-postgres.com/guides/project-structure 10 | https://sp.postgresqltutorial.com/wp-content/uploads/2018/03/PostgreSQL-Cheat-Sheet.pdf 11 | */ 12 | 13 | module.exports = { 14 | query: (text, params, callback) => { 15 | return pool.query(text, params, callback); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 13 | 14 | Uptime 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | 4 | "plugins": [["@babel/plugin-proposal-class-properties", { "loose": true }]], 5 | "env": { 6 | "test": { 7 | "plugins": [ 8 | "@babel/plugin-transform-runtime", 9 | "@babel/transform-async-to-generator" 10 | ] 11 | } 12 | } 13 | } 14 | 15 | // { 16 | // "plugins": [ 17 | // "@babel/plugin-proposal-decorators", 18 | // "@babel/plugin-proposal-class-properties" 19 | // ] 20 | // } 21 | 22 | // { 23 | // "presets": [ 24 | // [ 25 | // "next/babel", 26 | // { 27 | // "preset-env": { "targets": { "node": true } } 28 | // } 29 | // ] 30 | // ], 31 | // "plugins": [] 32 | // } 33 | -------------------------------------------------------------------------------- /client/containers/MainContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import OutputBoxContainer from './OutputBoxContainer'; 4 | import InputBox from '../components/InputBox'; 5 | import * as actions from '../actions/action'; 6 | import Signup from '../components/Signup'; 7 | import Login from '../components/Login'; 8 | 9 | const mapStateToProps = (state) => ({ 10 | currentUser: state.outputs.currentUser, 11 | }); 12 | 13 | const mapDispatchToProps = (dispatch) => ({ 14 | addURL: (urlObj) => dispatch(actions.addURL(urlObj)), 15 | }); 16 | 17 | const MainContainer = ({ addURL, currentUser }) => ( 18 |
19 | 20 |
21 | 22 |
23 |
24 | ); 25 | 26 | export default connect(mapStateToProps, mapDispatchToProps)(MainContainer); 27 | -------------------------------------------------------------------------------- /server/router/authrouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const authcontroller = require('../controller/authcontroller'); 3 | const router = express.Router(); 4 | 5 | /* Iteration Option: We have not touched this, only laid the framework for authentication. */ 6 | 7 | /*1) will pull username and password from input box 8 | url= /login, info will come in req.body 9 | response is 200 status/error status 10 | error status= direct back to sign up page*/ 11 | 12 | router.post( 13 | '/login', 14 | authcontroller.verify, 15 | authcontroller.checkPw, 16 | (req, res) => { 17 | if (!res.locals.exists) res.send('username or password does not match'); 18 | else 19 | res.send({ user_id: res.locals.user_id, username: res.locals.username }); 20 | } 21 | ); 22 | 23 | router.post( 24 | '/signup', 25 | authcontroller.hashPassword, 26 | authcontroller.saveUser, 27 | (req, res) => { 28 | res.redirect('/'); 29 | } 30 | ); 31 | 32 | module.exports = router; 33 | -------------------------------------------------------------------------------- /client/containers/OutputBoxContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../actions/action'; 4 | import OutputBox from '../components/outputBox'; 5 | 6 | const mapStateToProps = (state) => ({ 7 | urlList: state.outputs.urlList, 8 | url_id: state.outputs.urlList[0].url_id, 9 | url: state.outputs.urlList[0].url, 10 | }); 11 | 12 | const mapDispatchToProps = (dispatch) => ({ 13 | checkStatus: (statusObj) => dispatch(actions.checkNow(statusObj)), 14 | }); 15 | 16 | const OutputBoxContainer = ({ urlList, checkStatus }) => ( 17 |
18 | {urlList.map((index) => ( 19 | 26 | ))} 27 |
28 | ); 29 | 30 | export default connect(mapStateToProps, mapDispatchToProps)(OutputBoxContainer); 31 | -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect, Switch } from 'react-router'; 3 | import { connect } from 'react-redux'; 4 | 5 | // import Login, Signup, and create new container for inputBox/outputBox 6 | import Login from './components/Login.jsx'; 7 | import Signup from './components/Signup.jsx'; 8 | import MainContainer from './containers/MainContainer'; 9 | 10 | // mapping state to props, so props will get state values 11 | const mapStateToProps = (state) => ({ 12 | isLoggedIn: state.form.isLoggedIn, 13 | }); 14 | 15 | // mapping dispatch to props, each key is a method which dispatches an action creator 16 | const mapDispatchToProps = (dispatch) => ({}); 17 | 18 | const App = ({ isLoggedIn }) => ( 19 |
20 | 21 | 22 | 23 | 24 | {isLoggedIn ? : } 25 | 26 | 27 |
28 | ); 29 | 30 | // connect state props & dispatch props to the App props 31 | export default connect(mapStateToProps, mapDispatchToProps)(App); 32 | -------------------------------------------------------------------------------- /client/components/outputBox.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import axios from 'axios'; 3 | 4 | class OutputBox extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.checkNow = this.checkNow.bind(this); 8 | } 9 | 10 | checkNow() { 11 | return axios 12 | .post('http://localhost:3000/main/checkNow', { 13 | url_id: this.props.url_id, 14 | url: this.props.url, 15 | }) 16 | .then((status) => 17 | this.props.dispatchCheckStatus({ 18 | status: status.data.status, 19 | url_id: this.props.url_id, 20 | }) 21 | ) 22 | .catch((err) => { 23 | console.error(err.messsage); 24 | }); 25 | } 26 | 27 | render() { 28 | return ( 29 |
30 |
31 |

{this.props.url.toUpperCase()}

32 |

{this.props.status}

33 |
34 | 35 | 36 |
37 |
38 |
39 | ); 40 | } 41 | } 42 | 43 | export default OutputBox; 44 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: './client/index.jsx', 6 | output: { 7 | filename: 'bundle.js', 8 | path: path.resolve(__dirname, 'build'), 9 | publicPath: '/', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?/, 15 | use: { 16 | loader: 'babel-loader', 17 | options: { 18 | presets: ['@babel/preset-env', '@babel/preset-react'], 19 | plugins: [ 20 | '@babel/plugin-transform-runtime', 21 | '@babel/transform-async-to-generator', 22 | ], 23 | }, 24 | }, 25 | exclude: /node_modules/, 26 | }, 27 | { 28 | test: /\.s?css$/, 29 | use: ['style-loader', 'css-loader', 'sass-loader'], 30 | }, 31 | ], 32 | }, 33 | resolve: { 34 | extensions: ['.js', '.jsx'], 35 | }, 36 | devServer: { 37 | historyApiFallback: true, 38 | contentBase: path.resolve(__dirname, './client'), 39 | port: 8080, 40 | proxy: { 41 | '/api': 'http://localhost:3000', 42 | '/auth': 'http://localhost:3000', 43 | '/main': 'http://localhost:3000', 44 | }, 45 | publicPath: '/build/', 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /client/components/InputBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import { connect } from 'react-redux'; 4 | import { AddURL } from '../actions/action.js'; 5 | import * as actions from '../actions/action.js'; 6 | 7 | class InputBox extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.onSubForm = this.onSubForm.bind(this); 11 | } 12 | 13 | onSubForm(e) { 14 | e.preventDefault(); 15 | const input = document.getElementById('addUrlForm'); 16 | const url = input.value; 17 | return axios 18 | .post('http://localhost:3000/main/addURL', url) 19 | .then((result) => { 20 | this.props.dispatchAddUrl({ 21 | username: this.props.currentUser, 22 | url_id: result.data.url_id, 23 | status: result.data.status, 24 | url: url, 25 | }); 26 | }) 27 | .catch((err) => console.log('err onsubform', err)); 28 | } 29 | 30 | render() { 31 | return ( 32 |
33 | 38 |

UPTIME

39 | 40 | 43 |
44 | ); 45 | } 46 | } 47 | 48 | export default InputBox; 49 | -------------------------------------------------------------------------------- /client/scss/LoginStyle.scss: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .flex-container { 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | height: 100%; 12 | } 13 | 14 | .login-container { 15 | display: flex; 16 | flex-direction: column; 17 | #uptime-logo { 18 | width: 100px; 19 | margin: 0 auto 10px auto; 20 | } 21 | h1 { 22 | font-family: 'Bungee Hairline', cursive; 23 | color: $primary-color; 24 | font-size: 32px; 25 | } 26 | } 27 | 28 | .input { 29 | display: flex; 30 | flex-direction: column; 31 | margin: 15px 0 0 0; 32 | .input-fields { 33 | display: flex; 34 | } 35 | input { 36 | width: 100%; 37 | text-align: center; 38 | padding: 5px 0; 39 | margin: 3px 0; 40 | } 41 | button { 42 | padding: 5px 0; 43 | margin: 3px 0 0 0; 44 | background-color: $primary-color; 45 | border-radius: 2px; 46 | border: none; 47 | } 48 | } 49 | 50 | .registerbutton { 51 | display: flex; 52 | flex-direction: column; 53 | margin: 10px 0 0 0; 54 | a { 55 | color: $secondary-color; 56 | text-decoration: none; 57 | margin: 0 auto; 58 | font-family: 'Roboto Mono', monospace; 59 | font-size: 14px; 60 | } 61 | } 62 | 63 | .login-signupcontainer { 64 | display: flex; 65 | flex-direction: column; 66 | justify-content: center; 67 | align-content: center; 68 | height: 100%; 69 | #uptime-logo { 70 | width: 100px; 71 | margin: 0 auto 10px auto; 72 | } 73 | h1 { 74 | font-family: 'Bungee Hairline', cursive; 75 | color: $primary-color; 76 | font-size: 32px; 77 | margin: 0 auto; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /client/reducers/outputReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | 3 | const initialState = { 4 | /* Dummy Data this would be for pulling from DB */ 5 | urlList: [ 6 | { 7 | // username: 'Lucy', 8 | url: 'www.yahoo.com', 9 | status: 400, 10 | url_id: 80, 11 | }, 12 | { 13 | // username: 'Chris', 14 | url: 'www.coinbase.com', 15 | status: 400, 16 | url_id: 81, 17 | }, 18 | { 19 | // username: 'Joon', 20 | url: 'www.facebook.com', 21 | status: 400, 22 | url_id: 90, 23 | }, 24 | ], 25 | newEndpoint: '', 26 | status: '', 27 | currentUser: '', 28 | // graphData(maybe time/) 29 | }; 30 | 31 | const outputReducer = (state = initialState, action) => { 32 | switch (action.type) { 33 | case types.ADD_URL: { 34 | const newURLobj = action.payload; 35 | const copyUrlList = state.urlList.slice(); 36 | copyUrlList.push(newURLobj); 37 | 38 | const newStatus = action.payload.status; 39 | 40 | return { 41 | ...state, 42 | urlList: copyUrlList, 43 | status: newStatus, 44 | }; 45 | } 46 | case types.CHECK_NOW: { 47 | const newStatusObj = action.payload; 48 | console.log(newStatusObj); 49 | const copyUrlList = state.urlList.slice(); 50 | 51 | copyUrlList.forEach((item) => { 52 | if (item.url_id === newStatusObj.url_id) { 53 | item.status = newStatusObj.status; 54 | } 55 | }); 56 | console.log(copyUrlList); 57 | return { 58 | ...state, 59 | urlList: copyUrlList, 60 | }; 61 | } 62 | default: 63 | return state; 64 | } 65 | }; 66 | 67 | export default outputReducer; 68 | -------------------------------------------------------------------------------- /client/actions/action.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | import axios from 'axios'; 3 | 4 | export const addURL = (urlObj) => ({ 5 | type: types.ADD_URL, 6 | payload: urlObj, 7 | }); 8 | 9 | export const checkNow = (statusObj) => ({ 10 | type: types.CHECK_NOW, 11 | payload: statusObj, 12 | }); 13 | 14 | export const signupInput = (input) => ({ 15 | type: types.SIGNUP_FORM_INPUT, 16 | payload: input, 17 | }); 18 | 19 | export const signupSubmit = (input) => ({ 20 | type: types.SIGNUP_FORM_SUBMIT, 21 | payload: input, 22 | }); 23 | 24 | export const loginInput = (input) => ({ 25 | type: types.LOGIN_FORM_INPUT, 26 | payload: input, 27 | }); 28 | 29 | export const loginFailed = (loginErr) => ({ 30 | type: types.LOGIN_FAILURE, 31 | payload: { ...loginErr }, 32 | }); 33 | 34 | export const loginStarted = () => ({ 35 | type: types.LOGIN_START, 36 | }); 37 | 38 | export const loginSuccess = (loginResponse) => ({ 39 | type: types.LOGIN_SUCCESS, 40 | payload: loginResponse, 41 | }); 42 | 43 | export const loginRequest = (input) => { 44 | return (dispatch) => { 45 | // let store know a request has started 46 | dispatch(loginStarted()); 47 | // post request to server, asios automatically parses response 48 | axios 49 | .post('/auth/login', { 50 | username: input.username, 51 | password: input.password, 52 | }) 53 | // expect user info object from the request 54 | .then((response) => { 55 | if (typeof response.data === 'string') 56 | dispatch(loginFailed(err.message)); 57 | // console.log('loginRequest response.data: ', response.data); 58 | else dispatch(loginSuccess(response.data)); 59 | }) 60 | .catch((err) => dispatch(loginFailed(err.message))); 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const app = express(); 4 | const PORT = 3000; 5 | const path = require('path'); 6 | 7 | /*required routers*/ 8 | const authrouter = require('./router/authrouter'); 9 | const mainrouter = require('./router/mainrouter'); 10 | 11 | /*CORS middleware to prevent CORS policy during POST*/ 12 | app.use(cors()); 13 | 14 | /** 15 | * Automatically parse urlencoded body content from incoming requests and place it 16 | * in req.body 17 | * https://www.npmjs.com/package/body-parser 18 | */ 19 | app.use(express.urlencoded({ extended: true })); 20 | app.use(express.json()); 21 | 22 | app.use( 23 | cors({ 24 | origin: ['http://localhost:8080', 'http://localhost:3000'], 25 | }) 26 | ); 27 | 28 | // handle authentication requests 29 | // server recieves request to /auth/login or /auth/register, then direct to /authrouter 30 | app.use('/auth', authrouter); 31 | 32 | // handle all other requests 33 | // receive request for /main/historicaldata, /main/addURL, /main/interval, /main/checknow, then direct to /mainrouter 34 | app.use('/main', mainrouter); 35 | 36 | // request to '/', redirect to /authrouter (same as request to /register) 37 | app.use('/', authrouter); 38 | 39 | app.get('*', (req, res) => { 40 | res.render(path.join(__dirname, '../client/index.html')); 41 | }); 42 | 43 | // handle unknown path 44 | app.use((req, res) => { 45 | res.status(404).send('Not Found'); 46 | }); 47 | 48 | // error handler 49 | app.use((err, req, res, next) => { 50 | const defaultErr = { 51 | log: 'Express error handler caught unknown error', 52 | status: 400, 53 | message: { err: 'an error occured' }, 54 | }; 55 | 56 | const errorObj = Object.assign(defaultErr, err); 57 | console.log('error', errorObj.log); 58 | res.status(errorObj.status || 500).send(errorObj.message); 59 | }); 60 | 61 | module.exports = app.listen(PORT, () => { 62 | console.log(`Listening on port ${PORT}...`); 63 | }); 64 | -------------------------------------------------------------------------------- /client/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Redirect, Link } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import * as actions from '../actions/action'; 5 | 6 | const mapStateToProps = (state) => ({ 7 | username: state.form.login.username, 8 | password: state.form.login.password, 9 | }); 10 | 11 | const mapDispatchToProps = (dispatch) => ({ 12 | handleFormInputLogin: (newState) => dispatch(actions.loginInput(newState)), 13 | loginRequest: (newState) => dispatch(actions.loginRequest(newState)), 14 | }); 15 | 16 | /* exporting Login component here without connecting it to store 17 | * for testing purposes only 18 | */ 19 | export const Login = ({ 20 | username, 21 | password, 22 | handleFormInputLogin, 23 | loginRequest, 24 | }) => ( 25 |
26 |
27 |
28 | {/*

Up ⏰

*/} 29 | 34 |

UPTIME

35 |
{ 37 | e.preventDefault(); 38 | loginRequest({ 39 | username, 40 | password, 41 | }); 42 | }} 43 | > 44 |
45 |
46 | handleFormInputLogin(e.target)} 51 | /> 52 |
53 |
54 | handleFormInputLogin(e.target)} 59 | /> 60 |
61 | 62 |
63 | 64 | Create Account 65 | 66 |
67 |
68 |
69 |
70 |
71 |
72 | ); 73 | 74 | export default connect(mapStateToProps, mapDispatchToProps)(Login); 75 | -------------------------------------------------------------------------------- /client/scss/mainPage.scss: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | 3 | body { 4 | background: $background-color; 5 | } 6 | 7 | #main-container { 8 | display: flex; 9 | flex-direction: column; 10 | align-content: center; 11 | justify-content: center; 12 | #uptime-logo { 13 | width: 100px; 14 | margin: 100px auto 10px auto; 15 | } 16 | h1 { 17 | font-family: 'Bungee Hairline', cursive; 18 | color: $primary-color; 19 | font-size: 32px; 20 | margin: 0 auto 30px auto; 21 | } 22 | } 23 | 24 | #outputBoxContainer { 25 | display: flex; 26 | justify-content: center; 27 | #outputBox { 28 | display: flex; 29 | flex-wrap: wrap; 30 | flex-direction: row; 31 | justify-content: center; 32 | margin: 0 0 100px 0; 33 | } 34 | } 35 | 36 | #inputBox { 37 | display: flex; 38 | flex-direction: column; 39 | justify-content: center; 40 | margin: 0 auto; 41 | padding-bottom: 60px; 42 | width: 199px; 43 | input { 44 | width: 100%; 45 | text-align: center; 46 | padding: 5px 0; 47 | margin: 3px 0; 48 | } 49 | button { 50 | padding: 5px 0; 51 | margin: 3px 0 0 0; 52 | background-color: $primary-color; 53 | border-radius: 2px; 54 | border: none; 55 | } 56 | } 57 | 58 | #boxes { 59 | display: flex; 60 | flex-direction: column; 61 | align-items: center; 62 | padding: 20px; 63 | border: 1px solid $secondary-color; 64 | border-radius: 3%; 65 | width: 240px; 66 | height: 200px; 67 | margin: 20px; 68 | color: #ffffff; 69 | 70 | button { 71 | padding: 5px 8px; 72 | margin: 3px 5px 0 5px; 73 | background-color: $primary-color; 74 | border-radius: 2px; 75 | border: none; 76 | } 77 | .api-cards-text { 78 | display: flex; 79 | flex-direction: column; 80 | margin: auto 0; 81 | } 82 | .status-text { 83 | display: flex; 84 | margin: 0 auto; 85 | font-size: 50px; 86 | font-family: 'Roboto Mono', monospace; 87 | font-weight: 700; 88 | color: #ffffff; 89 | } 90 | .url-text { 91 | display: flex; 92 | margin: 0 auto 10px auto; 93 | font-family: 'Bungee Hairline', cursive; 94 | max-width: 175px; 95 | overflow-x: auto; 96 | } 97 | .url-text::-webkit-scrollbar { 98 | width: 0 !important; 99 | display: none; 100 | } 101 | .output-buttons { 102 | display: flex; 103 | margin: 10px auto 0 auto; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /client/components/Signup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import * as actions from '../actions/action'; 5 | 6 | const mapStateToProps = (state) => ({ 7 | username: state.form.signup.username, 8 | password: state.form.signup.password, 9 | phoneNumber: state.form.signup.phoneNumber, 10 | }); 11 | 12 | const mapDispatchToProps = (dispatch) => ({ 13 | handleFormInput: (newState) => dispatch(actions.signupInput(newState)), 14 | handleFormSubmit: (newState) => dispatch(actions.signupSubmit(newState)), 15 | }); 16 | 17 | /* exporting Signup component here without connecting it to store 18 | * for testing purposes only 19 | */ 20 | export const Signup = ({ 21 | username, 22 | password, 23 | phoneNumber, 24 | handleFormInput, 25 | handleFormSubmit, 26 | }) => ( 27 |
28 |
29 |
30 | 35 |

UPTIME

36 |
{ 38 | e.preventDefault(); 39 | handleFormSubmit({ 40 | username, 41 | password, 42 | phoneNumber, 43 | }); 44 | }} 45 | > 46 |
47 |
48 | handleFormInput(e.target)} 53 | /> 54 |
55 |
56 | handleFormInput(e.target)} 61 | /> 62 |
63 |
64 | handleFormInput(e.target)} 69 | /> 70 |
71 | 72 |
73 |
74 |
75 |
76 |
77 | ); 78 | 79 | export default connect(mapStateToProps, mapDispatchToProps)(Signup); 80 | -------------------------------------------------------------------------------- /client/reducers/formReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | import axios from 'axios'; 3 | 4 | const initialState = { 5 | signup: { 6 | username: '', 7 | password: '', 8 | phoneNumber: '', 9 | }, 10 | login: { 11 | username: '', 12 | password: '', 13 | }, 14 | isLoggingIn: false, 15 | isLoggedIn: false, 16 | currentUserId: '', 17 | currentUser: '', 18 | }; 19 | 20 | const formReducer = (state = initialState, action) => { 21 | switch (action.type) { 22 | // signup reducers 23 | case types.SIGNUP_FORM_INPUT: { 24 | const { name, value } = action.payload; 25 | console.log('signup state: ', state); 26 | return { 27 | ...state, 28 | signup: { 29 | ...state.signup, 30 | [name]: value, 31 | }, 32 | }; 33 | } 34 | case types.SIGNUP_FORM_SUBMIT: { 35 | axios 36 | .post('/auth/signup', action.payload) 37 | .then((response) => { 38 | console.log(response); 39 | }) 40 | .catch((err) => { 41 | console.log(err); 42 | }); 43 | return { 44 | ...state, 45 | signup: { 46 | ...state.signup, 47 | ...action.payload, 48 | }, 49 | }; 50 | } 51 | // login reducers handling asyncs 52 | case types.LOGIN_FORM_INPUT: { 53 | const { name, value } = action.payload; 54 | return { 55 | ...state, 56 | login: { 57 | ...state.login, 58 | [name]: value, 59 | }, 60 | }; 61 | } 62 | 63 | case types.LOGIN_FAILURE: { 64 | // console.log('login_failure action.payload', action.playload); 65 | alert('Incorrect login information!'); 66 | return { 67 | ...state, 68 | isLoggingIn: false, 69 | }; 70 | } 71 | 72 | case types.LOGIN_START: { 73 | // console.log('login_start state before return state: ', state); 74 | return { 75 | ...state, 76 | isLoggingIn: true, 77 | }; 78 | } 79 | 80 | case types.LOGIN_SUCCESS: { 81 | const { username, user_id } = action.payload; 82 | // console.log('login_success action.payload', action.payload); 83 | // console.log('login_success state before return state: ', state); 84 | return { 85 | ...state, 86 | isLoggingIn: false, 87 | isLoggedIn: true, 88 | currentUser: username, 89 | currentUserId: user_id, 90 | }; 91 | } 92 | 93 | default: 94 | return state; 95 | } 96 | }; 97 | 98 | export default formReducer; 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Uptime", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./client/index.jsx", 6 | "scripts": { 7 | "start": "nodemon server/server.js", 8 | "test": "jest", 9 | "build": "cross-env NODE_ENV=production webpack", 10 | "frontend": "NODE_ENV=development webpack-dev-server --open", 11 | "dev": "concurrently \"cross-env NODE_ENV=development webpack-dev-server --open\" \"nodemon ./server/server.js\"" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/JoonyoungKim025/Scratch-Project.git" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/JoonyoungKim025/Scratch-Project/issues" 22 | }, 23 | "homepage": "https://github.com/JoonyoungKim025/Scratch-Project#readme", 24 | "jest": { 25 | "verbose": true, 26 | "globalSetup": "./jest-setup.js", 27 | "globalTeardown": "./jest-teardown.js" 28 | }, 29 | "dependencies": { 30 | "@babel/plugin-transform-async-to-generator": "^7.10.4", 31 | "@babel/runtime": "^7.11.2", 32 | "axios": "^0.20.0", 33 | "bcrypt": "^5.0.0", 34 | "body-parser": "^1.19.0", 35 | "bootstrap": "^4.5.2", 36 | "cors": "^2.8.5", 37 | "express": "^4.17.1", 38 | "node-cron": "^2.0.3", 39 | "node-fetch": "^2.6.1", 40 | "pg": "^8.3.3", 41 | "prop-types": "^15.7.2", 42 | "react": "^16.13.1", 43 | "react-bootstrap": "^1.3.0", 44 | "react-dom": "^16.13.1", 45 | "react-redux": "^7.2.1", 46 | "react-router": "^5.2.0", 47 | "react-router-dom": "^5.2.0", 48 | "redux": "^4.0.5", 49 | "redux-thunk": "^2.3.0" 50 | }, 51 | "devDependencies": { 52 | "@babel/core": "^7.11.6", 53 | "@babel/plugin-proposal-class-properties": "^7.10.4", 54 | "@babel/plugin-transform-runtime": "^7.11.5", 55 | "@babel/preset-env": "^7.11.5", 56 | "@babel/preset-react": "^7.10.4", 57 | "babel-core": "^6.26.3", 58 | "babel-jest": "^26.3.0", 59 | "babel-loader": "^8.1.0", 60 | "babel-polyfill": "^6.26.0", 61 | "babel-preset-es2015": "^6.24.1", 62 | "babel-preset-stage-0": "^6.24.1", 63 | "concurrently": "^5.3.0", 64 | "cross-env": "^7.0.2", 65 | "css-loader": "^4.3.0", 66 | "enzyme": "^3.11.0", 67 | "enzyme-adapter-react-16": "^1.15.4", 68 | "eslint": "^7.9.0", 69 | "eslint-config-airbnb": "^18.2.0", 70 | "eslint-plugin-import": "^2.22.0", 71 | "eslint-plugin-jsx-a11y": "^6.3.1", 72 | "eslint-plugin-react": "^7.20.6", 73 | "eslint-plugin-react-hooks": "^4.1.2", 74 | "isomorphic-fetch": "^2.2.1", 75 | "jest": "^26.4.2", 76 | "nodemon": "^2.0.4", 77 | "redux-devtools-extension": "^2.13.8", 78 | "sass": "^1.26.10", 79 | "sass-loader": "^10.0.2", 80 | "style-loader": "^1.2.1", 81 | "supertest": "^4.0.2", 82 | "webpack": "^4.44.1", 83 | "webpack-cli": "^3.3.12", 84 | "webpack-dev-server": "^3.11.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scratch-Project 2 | 3 | google.account for Elephant SQL (OAth with google account) 4 | username: pinkfairyarmadillo38 5 | password: codesmithLA 6 | birthday: Jan. 1, 2000 7 | 8 | node-cron : to schedule tasks 9 | 10 | https://www.npmjs.com/package/node-cron 11 | 12 | https://www.digitalocean.com/community/tutorials/nodejs-cron-jobs-by-examples 13 | 14 | https://scotch.io/tutorials/nodejs-cron-jobs-by-examples 15 | 16 |
17 | 1. Login form and signup form notes. (partially done, stretch) 18 | will pull username and password from input box. 19 | 20 | url= /auth/login, info will come in req.body. 21 | 22 | response is 200 status with and userId (primary key from database) /error status . 23 | 24 | 200= go to dashboard/front page. 25 | 26 | userID will go into state. 27 | 28 | error status= direct back to sign up page. 29 | 30 | when user logs in, pull array of URLlinks that match user id and only render those in outputbox container. 31 |
32 | 2. Register (stretch) 33 | api= /auth/register, 34 | 35 | req.body = username, password, phoneNumber, 36 | 37 | middleware will validate whether username or phone number is already taken, 38 | 39 | store username, etc in database, 40 | 41 | send to frontend- res.status 200 or error , 42 | 43 | frontend will direct to dashboard, 44 |
45 | 3. add URL with default interval. 46 | BACKEND DONE 47 | 48 | user adds in URL that they want to track. 49 | 50 | api= /main/addURL. 51 | 52 | req.body = will hold URL. 53 | 54 | res.status of 200 or error , and .send"success", send in res.locals url_Id (URL primary key). 55 | 56 | FRONTEND- wait for success message before populating URL into state and creating URL container. 57 | 58 | Url- primary key will go into state, 59 | 60 | default interval every hour . 61 | 62 | backend timer: [https://nodejs.org/en/docs/guides/timers-in-node/](https://nodejs.org/en/docs/guides/timers-in-node/) 63 | 64 | twillio API for text messages. 65 |
66 | 4. STRETCH FEATURE-C - change interval time. 67 | 68 | api= /main/interval. 69 | 70 | time will be req.body. 71 | 72 | (default time is set when URL is inputted). 73 | 74 | use this timer: [https://nodejs.org/en/docs/guides/timers-in-node/](https://nodejs.org/en/docs/guides/timers-in-node/) 75 | 76 | this will change #3. 77 |
78 | 5. check endpoint/API now. 79 | 80 | BACKEND DONE- 81 | 82 | based on user clicking on button in front end, will check current status code . 83 | 84 | api= /main/checkNow - will be invoked on an interval or also based on a button click. 85 | 86 | req.body = will hold the URL . 87 | 88 | res.locals = will hold the "URL status". 89 |
90 | 6- STRETCH FEATURE- A -modal for historical data. 91 | [https://mdbootstrap.com/docs/react/advanced/charts/](https://mdbootstrap.com/docs/react/advanced/charts/) 92 | 93 | get historical data from database , will be default time (we will test to determine later). 94 | 95 | api =/main /historicalData. 96 | 97 | req.body = will hold URL. 98 | 99 | res.locals = will send back 2 arrays, 100 | 101 | A)all the times URL was pinged. 102 | 103 | B)all the status codes. -------------------------------------------------------------------------------- /server/router/mainrouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const maincontroller = require('../controller/maincontroller'); 3 | 4 | /* Iterate Option: We didn't get to this, but these are the controllers if we had authentication */ 5 | const authcontroller = require('../controller/authcontroller'); 6 | const router = express.Router(); 7 | 8 | /* Default timer pings all urls in the db every hour */ 9 | // 1) query all urls from the db queryAll 10 | // 2) ping all of them pingAll 11 | // 3) save status to db saveStatus 12 | const cron = require('node-cron'); 13 | //cron.schedule('* * * * * *', maincontroller.startTasks); 14 | 15 | 16 | /* Iterate Option: We didn't get to this, we would like incorporate Twilio API when the endpoints goes down */ 17 | 18 | /* Twilio express sms docs 19 | https://www.twilio.com/docs/sms/tutorials/how-to-send-sms-messages-node-js 20 | */ 21 | 22 | 23 | 24 | //This is the router for the ADD Endpoint Button 25 | // post request 26 | // saveUrl - store URL in database 27 | // pingUrlInterval - A- retrieve URL and interval from database, B-set timer to ping URL, C-send message to twilio if status is not 200, D- save status code and time in database 28 | // send to client success message so client can render URL component 29 | router.post('/addURL', 30 | maincontroller.saveUrl, 31 | maincontroller.pingUrl, 32 | maincontroller.addStatus, 33 | (req, res) => { 34 | res.status(200).json({ 35 | status: res.locals.status, 36 | url_id: res.locals.url_id}); 37 | }); 38 | 39 | /* Once a URL is added, this route handles the functionality of clicking checkNow to check status at any time */ 40 | router.post('/checkNow', maincontroller.pingUrl, maincontroller.addStatus, (req, res) => { 41 | res.status(200).json({ status: res.locals.status }); 42 | }); 43 | 44 | /* STRETCH */ 45 | 46 | /* Provide more visual context for each endpoint, user clicks and historical graphs are shown*/ 47 | /* 6) - data pull[https://mdbootstrap.com/docs/react/advanced/charts/](https://mdbootstrap.com/docs/react/advanced/charts/) 48 | get historical data from database , will be default time (we will test to determine later) 49 | api = /historicaldata 50 | req.body = will hold URL 51 | res.locals = will send back 2 arrays 52 | A)all the times URL was pinged 53 | B)all the status codes */ 54 | // getData 5 -query the database for times and status code for url given in req.body, then save to res.locals and send back a res contiaing res.locals 55 | 56 | // outer.get('/historicalData', maincontroller.getData, (req, res) => { 57 | // res.status(200).send('test'); 58 | // }); 59 | 60 | 61 | /* 4) api= /interval 62 | time will be req.body */ 63 | // put request (to update interval) 64 | // updateInterval - update interval in database 65 | // pingURLInterval - A- retrieve URL and interval from database, B-set timer to ping URL, C-send message to twilio if status is not 200, D- save status code and time in database 66 | // router.put('/interval', maincontroller.updateInterval, maincontroller.pingUrlInterval, (req, res) => { 67 | // res.status(200).send('Interval successfully changed'); 68 | // }); 69 | 70 | 71 | // router.get('/historicalData', maincontroller.getData, (req, res) => { 72 | // res.status(200).send('test'); 73 | // }); 74 | 75 | module.exports = router; 76 | -------------------------------------------------------------------------------- /__tests__/enzyme.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import React from 'react'; 3 | import { configure, shallow, mount } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | 6 | // import components for testing 7 | import { Signup } from '../client/components/Signup'; 8 | import { Login } from '../client/components/Login'; 9 | import MainContainer from '../client/containers/MainContainer'; 10 | import InputBox from '../client/components/InputBox'; 11 | import OutputBox from '../client/components/outputBox'; 12 | 13 | // Configure Enzyme to work with newer versions of React 14 | configure({ adapter: new Adapter() }); 15 | 16 | describe('Enzyme unit tests', () => { 17 | describe('', () => { 18 | let wrapper; 19 | const props = { 20 | username: 'Rico', 21 | password: 'stinko123', 22 | phoneNumber: '911', 23 | handleFormInput: jest.fn(), 24 | handleFormSubmit: jest.fn(), 25 | }; 26 | const mockEvent = { target: {} }; 27 | 28 | beforeAll(() => { 29 | wrapper = shallow(); 30 | }); 31 | 32 | it('Should render a div with 3 input fields', () => { 33 | expect(wrapper.type()).toEqual('div'); 34 | expect(wrapper.find('input').length).toEqual(3); 35 | }); 36 | 37 | it('Should invoke handleFormInput onChange', () => { 38 | wrapper.find('input[name="username"]').simulate('change', mockEvent); 39 | expect(props.handleFormInput).toHaveBeenCalled(); 40 | wrapper.find('input[name="password"]').simulate('change', mockEvent); 41 | expect(props.handleFormInput).toHaveBeenCalled(); 42 | wrapper.find('input[name="phoneNumber"]').simulate('change', mockEvent); 43 | expect(props.handleFormInput).toHaveBeenCalled(); 44 | }); 45 | 46 | it('Should invoke handleFormSubmit onSubmit', () => { 47 | wrapper.find('form').simulate('submit', { 48 | preventDefault: () => {}, 49 | }); 50 | expect(props.handleFormSubmit).toHaveBeenCalled(); 51 | }); 52 | }); 53 | 54 | describe('', () => { 55 | let wrapper; 56 | 57 | beforeAll(() => { 58 | wrapper = shallow(); 59 | }); 60 | 61 | it('Should render a div with 2 input fields and 1 button', () => { 62 | expect(wrapper.type()).toEqual('div'); 63 | expect(wrapper.find('input').length).toEqual(2); 64 | expect(wrapper.find('button').length).toEqual(1); 65 | }); 66 | }); 67 | 68 | describe('', () => { 69 | let wrapper; 70 | 71 | beforeAll(() => { 72 | wrapper = shallow(); 73 | }); 74 | 75 | it('Should render a div with 1 input field and 1 button', () => { 76 | expect(wrapper.type()).toEqual('div'); 77 | expect(wrapper.find('input').length).toEqual(1); 78 | expect(wrapper.find('button').length).toEqual(1); 79 | }); 80 | }); 81 | 82 | describe('', () => { 83 | let wrapper; 84 | const props = { 85 | url: 'test.com', 86 | url_id: '', 87 | status: 200, 88 | checkNow: jest.fn(), 89 | }; 90 | 91 | beforeAll(() => { 92 | wrapper = shallow(); 93 | }); 94 | 95 | it('Should render a div', () => { 96 | expect(wrapper.type()).toEqual('div'); 97 | }); 98 | 99 | it('Should display url and status from its props', () => { 100 | expect(wrapper.text()).toBe( 101 | `url: ${props.url}status: ${props.status}check nowuptime` 102 | ); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /server/controller/authcontroller.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const bcrypt = require('bcrypt'); 4 | const db = require('../db/databaseIndex.js'); 5 | 6 | const authcontroller = {}; 7 | 8 | // middleware 9 | // verify if user exists with the db.query to check in Postgres 10 | // 0= go to dashboard/ front page 11 | authcontroller.verify = (req, res, next) => { 12 | console.log('req.body.username: ', req.body.username); 13 | res.locals.username = req.body.username; 14 | res.locals.password = req.body.password; 15 | 16 | // save query to a const= verifyUser , which is set to 'SELECT username FROM users where username = $1 17 | // create values vartiable which contains an array with ${username} as the first param 18 | const queryUser = 'SELECT username, user_id FROM users WHERE username = $1'; 19 | const values = [res.locals.username]; 20 | // call query on db passing 21 | 22 | db.query(queryUser, values).then((verified) => { 23 | // if username input is not found in db, prompt user to log in again or register 24 | if (verified.rows.length === 0) { 25 | res.locals.exists = false; 26 | console.log("Username doesn't exist in database"); 27 | // should return error 28 | return next(); 29 | } 30 | // if username input is found in db, call the next middleware to check password 31 | // res.locals.exists = true; 32 | res.locals.user_id = verified.rows[0].user_id; 33 | console.log('res.locals.user_id', res.locals.user_id); 34 | return next(); 35 | }); 36 | 37 | // then if verify.rows.length === 0 , that means user doesn't exist 38 | // res.locals.exist = false 39 | // !== 0 then the user exist 40 | // call middleware that checks passswords 41 | // return next 42 | }; 43 | 44 | authcontroller.checkPw = (req, res, next) => { 45 | const passedInName = res.locals.username; 46 | const passedInPass = res.locals.password; 47 | 48 | const queryUser = 'SELECT password FROM users WHERE username = $1'; 49 | 50 | db.query(queryUser, [passedInName]).then((dbHashPw) => { 51 | bcrypt.compare(passedInPass, dbHashPw.rows[0].password, (err, result) => { 52 | console.log('dbHashPw: ', dbHashPw); 53 | if (result === true) { 54 | // return to front end obj with username / userid 55 | // { user_id: res.locals.user_id, username: res.locals.username } 56 | res.locals.exists = true; 57 | res.locals.user_id = dbHashPw.rows[0].user_id; 58 | return next(); 59 | } else { 60 | res.locals.exists = false; 61 | return next(); 62 | } 63 | }); 64 | }); 65 | }; 66 | 67 | authcontroller.hashPassword = (req, res, next) => { 68 | const { password } = req.body; 69 | 70 | bcrypt.hash(password, 10, (err, hashed) => { 71 | if (err) { 72 | console.log(err); 73 | return next(err); 74 | } 75 | console.log('hashed', hashed); 76 | res.locals.hash = hashed; 77 | return next(); 78 | }); 79 | }; 80 | 81 | authcontroller.saveUser = (req, res, next) => { 82 | if (res.locals.exist) return next(); 83 | 84 | const { username, phoneNumber } = req.body; 85 | 86 | const saveQuery = 87 | 'INSERT INTO users (username, password, phone_number) VALUES ($1, $2, $3);'; 88 | 89 | db.query(saveQuery, [username, res.locals.hash, phoneNumber]) 90 | .then((saved) => { 91 | console.log('saved: ', saved); 92 | // if (saved) return next() 93 | return next(); 94 | }) 95 | .catch((error) => 96 | next({ 97 | log: 98 | 'Express error handler caught error in maincontroller.storeUrl in db query selectUrlQuery', 99 | status: 400, 100 | message: { err: error }, 101 | }) 102 | ); 103 | }; 104 | 105 | module.exports = authcontroller; 106 | -------------------------------------------------------------------------------- /__tests__/supertest.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | const server = 'http://localhost:3000'; 4 | 5 | /** 6 | * TESTING ROOT ROUTE HANDLER FUNCTIONALITY 7 | */ 8 | describe('root router integration', () => { 9 | describe('/', () => { 10 | /** 11 | * NEED TO CONFIRM WHAT KIND OF REQUEST WE ARE MAKING TO '/' 12 | * IN SERVER.JS WE HAVE THE ROOT END ROUTE BEING SENT TO 13 | * AUTHROUTER 14 | */ 15 | }); 16 | }); 17 | /** 18 | * TESTING MAIN ROUTER FUNCTIONALITY 19 | */ 20 | describe('main router integration', () => { 21 | describe('/main/addURL', () => { 22 | describe('POST', () => { 23 | const demoApi = 'https://dog.ceo/api/breeds/image/random'; 24 | it('responds with 200 status and application/json content type', (done) => { 25 | return request(server) 26 | .post('/main/addURL') 27 | .send(demoApi) 28 | .expect(200) 29 | .expect('Content-Type', 'application/json; charset=utf-8', done); 30 | }); 31 | const badApi = 'thisisnotavalidapi'; 32 | it('responds with 400 status and err with an invalid url', (done) => { 33 | return request(server) 34 | .post('/main/addURL') 35 | .send(badApi) 36 | .expect(400) 37 | .then(({ body }) => { 38 | expect(body).toHaveProperty('err'); 39 | done(); 40 | }); 41 | }); 42 | }); 43 | }); 44 | describe('/main/checkNow', () => { 45 | describe('POST', () => { 46 | const demoApi = 'https://dog.ceo/api/breeds/image/random'; 47 | it('responds with 200 status and application/json content type', (done) => { 48 | return request(server) 49 | .post('/main/checkNow') 50 | .send(demoApi) 51 | .expect('Content-Type', 'application/json; charset=utf-8', done); 52 | }); 53 | }); 54 | }); 55 | }); 56 | 57 | /** 58 | * TESTING AUTH ROUTER FUNCTIONALITY 59 | */ 60 | describe('auth router integration', () => { 61 | /** 62 | * TESTING LOGIN ROUTE FUNCTIONALITY 63 | */ 64 | describe('/auth/login', () => { 65 | describe('POST', () => { 66 | // --> 'POST' used as first parameter below to reflect what was used in authrouter.js 67 | /** 68 | * NEED TO CONFIRM IF WE ARE USING REACT-ROUTER FOR REDIRECT 69 | */ 70 | }); 71 | }); 72 | /** 73 | * TESTING SIGNUP ROUTE FUNCTIONALITY 74 | */ 75 | describe('/auth/register', () => { 76 | describe('POST', () => { 77 | const data = { 78 | username: 'username', 79 | password: 'password', 80 | phoneNumber: 8882222222, 81 | }; 82 | /** 83 | * TESTING SUCCESSFUL CREATION OF USER 84 | */ 85 | it('respond with 201 created', (done) => { 86 | request(server) 87 | .post('/auth/register') 88 | .send(data) 89 | .set('Accept', 'application/json') 90 | .expect('Content-Type', /json/) 91 | .end((err) => { 92 | if (err) return done(err); 93 | done(); 94 | }); 95 | }); 96 | /** 97 | * TESTING UNSUCCESSFUL CREATION OF USER 98 | */ 99 | const incorrectData = { 100 | username: 'username', 101 | password: 'password', 102 | phoneNumber: '888-222-2222', 103 | }; 104 | it('respond with 400 status and err with invalid data type entered', (done) => { 105 | request(server) 106 | .post('/auth/register') 107 | .send(incorrectData) 108 | .expect(400) 109 | .then(({ body }) => { 110 | expect(body).toHaveProperty('err'); 111 | done(); 112 | }); 113 | }); 114 | }); 115 | }); 116 | }); 117 | 118 | /** 119 | * TESTING UNKNOWN ROUTE HANDLER FUNCTIONALITY 120 | */ 121 | describe('unknown routes integration', () => { 122 | it("reponds with 404 status and 'Not Found'", (done) => 123 | request(server).get('/random').expect(404).expect('Not Found', done)); 124 | }); 125 | 126 | // NOT TOO SURE ON HOW TO APPROACH? LOOKED UP RESOURCES & NADA 127 | describe('global error handler integration', () => {}); 128 | -------------------------------------------------------------------------------- /server/controller/maincontroller.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-destructuring */ 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const maincontroller = {}; 5 | const db = require('../db/databaseIndex.js'); 6 | 7 | /* Need to import node library if we want to use fetch in the backend */ 8 | const fetch = require('node-fetch'); 9 | 10 | /* REQUEST/RESPONSE MIDDLEWARE */ 11 | 12 | 13 | maincontroller.saveUrl = (req, res, next) => { 14 | const urlBody = req.body; 15 | console.log('req.body', req.body); // --> 16 | const urlArray = Object.keys(urlBody); 17 | const url = urlArray[0]; 18 | res.locals.url = url; 19 | 20 | const userId = 42; /* ITERATION OPTION: this should pull from state that's updated from DB */ 21 | 22 | const updateUrlTable = 'INSERT INTO url (user_id, url) VALUES ($1, $2) RETURNING url_id'; 23 | db.query(updateUrlTable, [userId, `${url}`]) 24 | .then((saved) => { 25 | res.locals.db_url_id = saved.rows[0].url_id; 26 | return next(); 27 | })// MAKE SURE url IS LOWERCASE ON FRONTEND REQUEST OBJECT 28 | .catch((error) => next({ 29 | log: 30 | 'Express error handler caught error in maincontroller.saveURL', 31 | status: 400, 32 | message: { err: error }, 33 | })); 34 | }; 35 | 36 | /*Checks to see the status code of the URL we added depending on the response we get back */ 37 | maincontroller.pingUrl = (req, res, next) => { 38 | let check; 39 | if (!res.locals.url) check = req.body.url; 40 | else check = res.locals.url; 41 | fetch(check)// recieved from state 42 | .then((data) => data.json()) 43 | .then((response) => { 44 | console.log(response); 45 | if (typeof response === 'object') { 46 | res.locals.url_id = req.body.url_id; 47 | res.locals.status = '200'; //We assumed that it is status 200 if we receive an object, this could be more specific 48 | return next(); 49 | } 50 | res.locals.status = '400'; 51 | return next(); 52 | }) 53 | .catch((error) => next({ 54 | log: 55 | 'Express error handler caught error in maincontroller.pingUrl', 56 | status: 400, 57 | message: { err: error }, 58 | })); 59 | }; 60 | 61 | /* Adds URL attributes to Postgres, but also sends back status to the client so that we can keep track in state */ 62 | maincontroller.addStatus = (req, res, next) => { 63 | if (res.locals.db_url_id) res.locals.url_id = res.locals.db_url_id; 64 | const time = Date.now(); 65 | const urlId = res.locals.url_id; 66 | const status = res.locals.status; 67 | const updateStatusTable = 'INSERT INTO status (url_id,status,time) VALUES ($1, $2, $3)'; 68 | 69 | db.query(updateStatusTable, [urlId, status, time]) 70 | .then(() => next())// MAKE SURE url IS LOWERCASE ON FRONTEND REQUEST OBJECT 71 | .catch((error) => next({ 72 | log: 73 | 'Express error handler caught error in maincontroller.addStatus', 74 | status: 400, 75 | message: { err: error }, 76 | })); 77 | }; 78 | 79 | /* ITERATION OPTION: TASK SCHEDULER MIDDLEWARE */ 80 | 81 | maincontroller.startTasks = () => { 82 | return maincontroller.pingAll('test'); 83 | const allUrls = 'SELECT url_id,url FROM url'; 84 | 85 | db.query(allUrls) 86 | .then((urlCollection) => { 87 | this.pingAll(urlCollection.rows); 88 | })// MAKE SURE url IS LOWERCASE ON FRONTEND REQUEST OBJECT 89 | .catch((error) => console.log('Error in Task Schduler query: ', error)); 90 | }; 91 | 92 | maincontroller.pingAll = (urlArr) => { 93 | return maincontroller.saveStatus(urlArr); 94 | for (let i = 0; i < UrlArr.length; i++) { 95 | fetch(urlArr[i.url]) 96 | .then((data) => data.json()) 97 | .then((response) => { 98 | urlArr[i.status] = response.status; 99 | this.saveStatus(urlArr); 100 | }) 101 | .catch((error) => console.log('Error in Task Schduler query: ', error)); 102 | } 103 | }; 104 | 105 | maincontroller.saveStatus = (updatedUrlArr) => { 106 | return console.log(updatedUrlArr); 107 | for (let i = 0; i < updatedUrlArr.length; i++) { 108 | const time = Date.now(); 109 | const urlId = updatedUrlArr[i.url_id]; 110 | const status = updatedUrlArr[i.status]; 111 | const updateStatusTable = 'INSERT INTO status (url_id,status,time) VALUES ($1, $2, $3) RETURNING'; 112 | db.query(updateStatusTable, [urlId, status, time]) 113 | .then(() => { 114 | console.log('Ping task completed: ', time); 115 | }) 116 | .catch((error) => console.log('Error in Task Schduler query: ', error)); 117 | } 118 | }; 119 | 120 | /*Readme/Resources */ 121 | 122 | /* Timestamp for psql 123 | https://www.sqlservertutorial.net/sql-server-date-functions/sql-server-current_time-function/ 124 | */ 125 | 126 | /* getstatus - to be used inside middleware and node-cron 127 | input is URL or array of URLs to ping 128 | pings URL, saves to database 129 | output is received status code 130 | saveToDb - include our query to save to database, parameter will be status returned from api ping 131 | https://www.restapitutorial.com/httpstatuscodes.html 132 | */ 133 | 134 | /* 3) user adds in URL that they want to track 135 | api= /addURL 136 | req.body = will hold URL 137 | res.status of 200 or error 138 | default interval every hour 139 | backend timer: [https://nodejs.org/en/docs/guides/timers-in-node/](https://nodejs.org/en/docs/guides/timers-in-node/) 140 | twillio API for text messages */ 141 | 142 | // Login 143 | // cam hello -> 7yxf, bcrypt adds salt register 144 | // updateInterval - update interval in database 145 | maincontroller.updateInterval = (req, res, next) => { 146 | next(); 147 | }; 148 | 149 | /* 6) - data pull[https://mdbootstrap.com/docs/react/advanced/charts/](https://mdbootstrap.com/docs/react/advanced/charts/) 150 | get historical data from database , will be default time (we will test to determine later) 151 | api = /historicaldata 152 | req.body = will hold URL 153 | res.locals = will send back 2 arrays 154 | A)all the times URL was pinged 155 | B)all the status codes */ 156 | // getData 5 -query the database for times and status code for url given in req.body, then save to res.locals and send back a res contiaing res.locals 157 | maincontroller.getData = (req, res, next) => { 158 | next(); 159 | }; 160 | 161 | module.exports = maincontroller; 162 | --------------------------------------------------------------------------------