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