Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 3 user authentication #34

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,052 changes: 1,018 additions & 34 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@
"main": "src/server/server.js",
"scripts": {
"build": "webpack --mode production",
"start": "npm run build && node src/server/server.js",
"start": "export NODE_ENV=production && npm run build && node src/server/server.js",
"client": "webpack-dev-server --mode development --devtool inline-source-map --hot",
"server": "nodemon src/server/server.js",
"dev": "concurrently \"npm run server\" \"npm run client\""
"dev": "export NODE_ENV=dev && concurrently \"npm run server\" \"npm run client\""
},
"author": "RespiraWorks",
"license": "Apache2",
"dependencies": {
"axios": "^0.26.1",
"body-parser": "^1.20.0",
"bootstrap": "^5.1.3",
"connect-ensure-login": "^0.1.1",
"cookie-parser": "^1.4.6",
"core-js": "^3.22.0",
"cors": "^2.8.5",
Expand All @@ -23,10 +25,16 @@
"dotenv": "^16.0.0",
"express": "^4.17.3",
"express-fileupload": "^1.3.1",
"express-session": "^1.17.2",
"file-saver": "^2.0.5",
"jsonwebtoken": "^8.5.1",
"mongodb": "^4.5.0",
"mongoose": "^6.3.2",
"morgan": "^1.10.0",
"passport": "^0.5.2",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"passport-local-mongoose": "^7.0.0",
"react": "^18.0.0",
"react-bootstrap": "^2.3.0",
"react-dom": "^18.0.0",
Expand Down
6 changes: 5 additions & 1 deletion src/client/App.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import {
BrowserRouter, Routes, Route
} from 'react-router-dom';
import React from 'react';
import MyNavbar from './pages/navbar';
import DataFileTable from './pages/data-file-table';
import UploadFile from './pages/upload-file';
import DataSet from './pages/dataset';
import NotFound from './pages/notfound';
import Authorize from './pages/authorize';

export default function App() {
return (
Expand All @@ -15,6 +18,7 @@ export default function App() {
<Route path="/" element={<DataFileTable />} />
<Route path="/upload-file" element={<UploadFile />} />
<Route path="/dataset" element={<DataSet />} />
<Route path="/login" element={<Authorize />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
Expand Down
17 changes: 17 additions & 0 deletions src/client/context/UserContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { useState } from 'react';

const UserContext = React.createContext([{}, p => {}]);

let initialState = {};

function UserProvider(props) {
const [state, setState] = useState(initialState);

return (
<UserContext.Provider value={[state, setState]}>
{ props.children }
</UserContext.Provider>
);
}

export { UserContext, UserProvider };
5 changes: 4 additions & 1 deletion src/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import './App.scss';
import App from './App';
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import { UserProvider } from './context/UserContext';

const container = document.getElementById('root');
const root = createRoot(container);

root.render(
<React.StrictMode>
<App />
<UserProvider>
<App />
</UserProvider>
</React.StrictMode>
);
55 changes: 55 additions & 0 deletions src/client/pages/authorize.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, {
useCallback, useContext, useEffect, useState
} from 'react';
import {
Container, Spinner, Tab, Tabs
} from 'react-bootstrap';
import { UserContext } from '../context/UserContext';
import Login from './login';
import Register from './register';
import Welcome from './welcome';

function Authorize() {
const [userContext, setUserContext] = useContext(UserContext);

const verifyUser = useCallback(() => {
fetch('/users/refreshToken', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
}).then(async (response) => {
if (response.ok) {
const data = await response.json();
setUserContext((oldValues) => ({ ...oldValues, token: data.token }));
} else {
setUserContext((oldValues) => ({ ...oldValues, token: null }));
}
// TODO: pick better refresh time - 5 min?
// call refreshToken every 1 minute to renew the authentication token.
setTimeout(verifyUser, 1 * 60 * 1000);
});
}, [setUserContext]);

useEffect(() => {
verifyUser();
}, [verifyUser]);

return userContext.token === null ? (
<Container>
<Tabs defaultActiveKey="login" id="uncontrolled-tab-example" className="mb-3">
<Tab eventKey="login" title="Login">
<Login />
</Tab>
<Tab eventKey="register" title="Register">
<Register />
</Tab>
</Tabs>
</Container>
) : userContext.token ? (
<Welcome />
) : (
<Spinner animation="border" variant="primary" size="xl" />
);
}

export default Authorize;
2 changes: 1 addition & 1 deletion src/client/pages/dataset.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import {
Button, Col, Container, Row, Spinner, Stack, Table,
} from 'react-bootstrap'
} from 'react-bootstrap';
import dateFormat from 'dateformat';
import { saveAs } from 'file-saver';
import { downloadFile } from '../api';
Expand Down
117 changes: 117 additions & 0 deletions src/client/pages/login.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import {
Alert, Button, Container, Form,
} from 'react-bootstrap';
import React, { useContext, useState } from 'react';
// import axios from 'axios';
import { UserContext } from '../context/UserContext';

function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const [userContext, setUserContext] = useContext(UserContext);

function validateForm() {
return email.length > 0 && password.length > 0;
}

function handleSubmit(event) {
event.preventDefault();
setIsSubmitting(true);
setError('');

const route = '/users/login';
const data = { username: email, password };
const genericErrorMessage = 'Something went wrong! Please try again later.';

// axios.post(route, data, {
// headers: {
// Accept: { 'Content-Type': 'application/json' },
// },
// withCredentials: true
// }).then((response) => {
// setIsSubmitting(false);
// const responseOK = response && response.status === 200 && response.statusText === 'OK';
// if (!responseOK) {
// if (response.status === 400) {
// setError('Please fill all the fields correctly!');
// } else if (response.status === 401) {
// setError('Invalid email and password combination.');
// } else {
// setError(genericErrorMessage);
// }
// } else {
// const responseData = response.data;
// setUserContext((oldValues) => ({ ...oldValues, token: responseData.token }));
// }
// }).catch((caughtError) => {
// setIsSubmitting(false);
// console.log('Axios error caught');
// console.log(caughtError.toJSON());
// setError(caughtError);
// });

fetch(route, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
.then(async (response) => {
setIsSubmitting(false);
if (!response.ok) {
if (response.status === 400) {
setError('Please fill all the fields correctly!');
} else if (response.status === 401) {
setError('Invalid email and password combination.');
} else {
setError(genericErrorMessage);
}
} else {
const responseData = await response.json();
setUserContext((oldValues) => ({ ...oldValues, token: responseData.token }));
}
})
.catch((caughtError) => {
setIsSubmitting(false);
setError(caughtError);
});
}

return (
<Container>
{error
&& (
<Alert variant="danger">
<Alert.Heading>Oh snap! You got an error!</Alert.Heading>
<p>{error}</p>
</Alert>
)}
<Form onSubmit={handleSubmit}>
<Form.Group size="lg" controlId="email">
<Form.Label>Email</Form.Label>
<Form.Control
autoFocus
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</Form.Group>
<Form.Group size="lg" controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</Form.Group>
<Button block size="lg" type="submit" disabled={!validateForm() || isSubmitting}>
{isSubmitting ? 'Signing in…' : 'Sign In'}
</Button>
</Form>
</Container>
);
}

export default Login;
3 changes: 3 additions & 0 deletions src/client/pages/navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ function MyNavbar() {
<Nav.Link href="/upload-file">
<p className="fs-4 mx-lg-3 pt-lg-2">UPLOAD</p>
</Nav.Link>
<Nav.Link href="/login">
<p className="fs-4 mx-lg-3 pt-lg-2">USER</p>
</Nav.Link>
</Nav>
</Navbar.Collapse>
</Navbar>
Expand Down
Loading