finally got authentication working
This commit is contained in:
parent
d3af43bc7c
commit
fa97a8f126
12 changed files with 297 additions and 43 deletions
26
bfrontend/package-lock.json
generated
26
bfrontend/package-lock.json
generated
|
@ -12306,6 +12306,18 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"version": "7.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz",
|
||||||
|
"integrity": "sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.12.1",
|
||||||
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-refresh": {
|
"react-refresh": {
|
||||||
"version": "0.8.3",
|
"version": "0.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
|
||||||
|
@ -12538,6 +12550,15 @@
|
||||||
"strip-indent": "^3.0.0"
|
"strip-indent": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"redux": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
|
||||||
|
"requires": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"symbol-observable": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"regenerate": {
|
"regenerate": {
|
||||||
"version": "1.4.2",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
||||||
|
@ -14234,6 +14255,11 @@
|
||||||
"util.promisify": "~1.0.0"
|
"util.promisify": "~1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"symbol-observable": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
|
||||||
|
},
|
||||||
"symbol-tree": {
|
"symbol-tree": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
|
|
|
@ -8,8 +8,10 @@
|
||||||
"@testing-library/user-event": "^12.6.0",
|
"@testing-library/user-event": "^12.6.0",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
|
"react-redux": "^7.2.2",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.1",
|
"react-scripts": "4.0.1",
|
||||||
|
"redux": "^4.0.5",
|
||||||
"web-vitals": "^0.2.4"
|
"web-vitals": "^0.2.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -18,6 +20,7 @@
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
},
|
},
|
||||||
|
"proxy": "http://localhost:3002",
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
"react-app",
|
"react-app",
|
||||||
|
|
|
@ -33,9 +33,9 @@ APIRequest.authenticated = async function(endpoint, options) {
|
||||||
if (!options) options = {};
|
if (!options) options = {};
|
||||||
if (!options.headers) options.headers = {};
|
if (!options.headers) options.headers = {};
|
||||||
|
|
||||||
options.headers = {
|
options = {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
...options.headers
|
...options
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,30 +1,13 @@
|
||||||
import APIRequest from './APIRequest';
|
import APIRequest from './APIRequest';
|
||||||
|
|
||||||
class Authenticator {
|
const Authenticator = {
|
||||||
constructor(method='cookie') {
|
getLoggedInUserFromCookie: async function() {
|
||||||
this.method = method;
|
const { json, isOK, err } = await APIRequest.authenticated('/api/v1/users/current/info');
|
||||||
this.__token = undefined;
|
if (!isOK && err) throw new Error(err);
|
||||||
this.user = undefined;
|
if (!isOK && !err) return undefined;
|
||||||
|
if (!json || !json.user) return undefined;
|
||||||
|
return json.user;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Authenticator.prototype.fetchLoggedInUserInfo = async function(token) {
|
|
||||||
const { json, isOK } = await APIRequest.authenticated();
|
|
||||||
if (!isOK) return false;
|
|
||||||
if (!json.user) return false;
|
|
||||||
|
|
||||||
this.user = json.user;
|
|
||||||
this.__token = json.user.token;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Authenticator.prototype.setToken = function(token) {
|
|
||||||
this.__token = token;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Authenticator.prototype.getToken = function(token) {
|
export default Authenticator;
|
||||||
return this.__token;
|
|
||||||
};
|
|
||||||
|
|
||||||
const authenticator = new Authenticator('cookie');
|
|
||||||
export default authenticator;
|
|
|
@ -1,23 +1,39 @@
|
||||||
import { useEffect, useState } from 'react';
|
import Login from './Auth/Login';
|
||||||
import authenticator from './../Authenticator';
|
import Root from './Root';
|
||||||
|
import Authenticator from './../Authenticator';
|
||||||
|
|
||||||
export default function App() {
|
import { useEffect } from 'react';
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState([]);
|
import { useDispatch, connect } from 'react-redux'
|
||||||
|
import { BrowserRouter, Switch, Route } from 'react-router-dom';
|
||||||
|
|
||||||
|
function App({ user }) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
authenticator.fetchLoggedInUserInfo()
|
Authenticator.getLoggedInUserFromCookie()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setIsLoggedIn(res);
|
dispatch({ type: 'authenticator/updatelocaluserobject', user: res })
|
||||||
});
|
});
|
||||||
}, [authenticator.user]);
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<BrowserRouter>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/login">
|
<Route path="/login">
|
||||||
|
<Login />
|
||||||
|
</Route>
|
||||||
|
<Route path="/">
|
||||||
|
<Root user={user} />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Router>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stateToProps = (state) => {
|
||||||
|
return {
|
||||||
|
user: state?.user || undefined
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(stateToProps)(App);
|
|
@ -1,3 +1,63 @@
|
||||||
export default function Login() {
|
import { useState } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import Notification from '../Notification';
|
||||||
|
import APIRequest from '../../APIRequest';
|
||||||
|
import Authenticator from './../../Authenticator';
|
||||||
|
import { getLoginMessageFromError } from '../../Errors'
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const history = useHistory();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [ usernameInput, setUsernameInput ] = useState();
|
||||||
|
const [ passwordInput, setPasswordInput ] = useState();
|
||||||
|
|
||||||
|
const [ info, setInfo ] = useState();
|
||||||
|
|
||||||
|
const handleLoginContinueButton = async () => {
|
||||||
|
if (!usernameInput || !passwordInput) {
|
||||||
|
setInfo('One of more fields is not filled in.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { json, isOK } = await APIRequest('/api/v1/users/token/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: usernameInput,
|
||||||
|
password: passwordInput,
|
||||||
|
alsoSetCookie: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isOK && json) {
|
||||||
|
setInfo(getLoginMessageFromError(json));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isOK) {
|
||||||
|
setInfo('Something went wrong');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await Authenticator.getLoggedInUserFromCookie();
|
||||||
|
dispatch({ type: 'authenticator/updatelocaluserobject', user: res });
|
||||||
|
|
||||||
|
history.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="login-container">
|
||||||
|
<input type="text" name="username" onChange={ ({ target }) => setUsernameInput(target.value) } />
|
||||||
|
<br />
|
||||||
|
<input type="password" name="password" onChange={ ({ target }) => setPasswordInput(target.value) } />
|
||||||
|
<br />
|
||||||
|
<button id="login-submit" onClick={ handleLoginContinueButton }>Continue</button>
|
||||||
|
<Notification text={ info } />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
7
bfrontend/src/Components/Notification.js
Normal file
7
bfrontend/src/Components/Notification.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export default function Notification({ text }) {
|
||||||
|
return (
|
||||||
|
<div className="notification">
|
||||||
|
<p>{ text }</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
21
bfrontend/src/Components/Root.js
Normal file
21
bfrontend/src/Components/Root.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function Root(props) {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
if (props.user) {
|
||||||
|
return (
|
||||||
|
<div id="root-container">
|
||||||
|
<h1>Welcome, { props.user.username }</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div id="root-container">
|
||||||
|
<h1>Welcome, - nevermind, you aren't logged in</h1>
|
||||||
|
<button onClick={ () => { history.push('/login') } }>Log in</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
const config = {
|
const config = {
|
||||||
apiUrl: 'http://localhost:3002'
|
apiUrl: 'http://localhost:3000'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
113
bfrontend/src/Errors.js
Normal file
113
bfrontend/src/Errors.js
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
const getLoginMessageFromError = (json) => {
|
||||||
|
switch (json.message) {
|
||||||
|
case 'ERROR_REQUEST_LOGIN_INVALID': {
|
||||||
|
return 'Invalid username or password.';
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ERROR_ACCESS_DENIED': {
|
||||||
|
return 'You are not allowed to perform this action.'
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return 'Unknown error. Something went wrong.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSignupMessageFromError = (json) => {
|
||||||
|
switch (json.message) {
|
||||||
|
case 'ERROR_REQUEST_INVALID_DATA': {
|
||||||
|
|
||||||
|
switch (json.errors[0].param) {
|
||||||
|
case 'username': {
|
||||||
|
return 'Invalid username. Username must be between 3 and 32 characters long, and be alphanumeric.';
|
||||||
|
}
|
||||||
|
case 'password': {
|
||||||
|
return 'Invalid password. Password must be at least 8 characters long and at most 128 characters.';
|
||||||
|
}
|
||||||
|
case 'email': {
|
||||||
|
return 'Invalid email.';
|
||||||
|
}
|
||||||
|
case 'specialCode': {
|
||||||
|
return 'Invalid special code.';
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return 'Invalid value sent to server. Something went wrong.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ERROR_ACCESS_DENIED': {
|
||||||
|
return 'You are not allowed to perform this action.'
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ERROR_REQUEST_USERNAME_EXISTS': {
|
||||||
|
return 'That username is taken.';
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return 'Unknown error. Something went wrong.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCreatePostError = (json) => {
|
||||||
|
switch (json.message) {
|
||||||
|
case 'ERROR_REQUEST_INVALID_DATA': {
|
||||||
|
switch (json.errors[0].param) {
|
||||||
|
case 'title': {
|
||||||
|
return 'Invalid title. Must be between 3 and 32 characters.';
|
||||||
|
}
|
||||||
|
case 'body': {
|
||||||
|
return 'Invalid content. Must be between 3 and 1000 characters';
|
||||||
|
}
|
||||||
|
case 'category': {
|
||||||
|
return 'Invalid category. Something went wrong.';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return 'Invalid value sent to server. Something went wrong.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ERROR_CATEGORY_NOT_FOUND': {
|
||||||
|
return 'The category you tried to post to no longer exists.';
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ERROR_ACCESS_DENIED': {
|
||||||
|
return 'You are not allowed to perform this action.'
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return 'Unknown error. Something went wrong.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCreateCategoryError = (json) => {
|
||||||
|
switch (json.message) {
|
||||||
|
case 'ERROR_REQUEST_INVALID_DATA': {
|
||||||
|
switch (json.errors[0].param) {
|
||||||
|
case 'title': {
|
||||||
|
return 'Invalid title. Title must be between 3 and 32 characters.';
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return 'Invalid value sent to server. Something went wrong.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ERROR_ACCESS_DENIED': {
|
||||||
|
return 'You are not allowed to perform this action.'
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return 'Unknown error. Something went wrong.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { getLoginMessageFromError, getSignupMessageFromError, getCreatePostError, getCreateCategoryError }
|
|
@ -1,11 +1,16 @@
|
||||||
import React from 'react';
|
import store from './store';
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './Components/App';
|
import App from './Components/App';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<Provider store={ store }>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
20
bfrontend/src/store.js
Normal file
20
bfrontend/src/store.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { createStore } from 'redux';
|
||||||
|
|
||||||
|
const reducer = (state, payload) => {
|
||||||
|
switch (payload.type) {
|
||||||
|
case 'authenticator/updatelocaluserobject': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
user: payload.user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
|
||||||
|
|
||||||
|
export default store;
|
Loading…
Reference in a new issue