finally got authentication working

This commit is contained in:
hippoz 2020-12-29 22:55:53 +02:00
parent d3af43bc7c
commit fa97a8f126
Signed by: hippoz
GPG key ID: 7C52899193467641
12 changed files with 297 additions and 43 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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 {

View file

@ -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;

View file

@ -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);

View file

@ -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>
);
} }

View file

@ -0,0 +1,7 @@
export default function Notification({ text }) {
return (
<div className="notification">
<p>{ text }</p>
</div>
);
}

View 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>
);
}
}

View file

@ -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
View 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 }

View file

@ -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>
<Provider store={ store }>
<App /> <App />
</Provider>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById('root')
); );

20
bfrontend/src/store.js Normal file
View 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;