diff --git a/bfrontend/package-lock.json b/bfrontend/package-lock.json index 5f1ae37..d75fd2d 100644 --- a/bfrontend/package-lock.json +++ b/bfrontend/package-lock.json @@ -12306,6 +12306,18 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "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": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -12538,6 +12550,15 @@ "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": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -14234,6 +14255,11 @@ "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": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/bfrontend/package.json b/bfrontend/package.json index f930e7d..8bb80ce 100644 --- a/bfrontend/package.json +++ b/bfrontend/package.json @@ -8,8 +8,10 @@ "@testing-library/user-event": "^12.6.0", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-redux": "^7.2.2", "react-router-dom": "^5.2.0", "react-scripts": "4.0.1", + "redux": "^4.0.5", "web-vitals": "^0.2.4" }, "scripts": { @@ -18,6 +20,7 @@ "test": "react-scripts test", "eject": "react-scripts eject" }, + "proxy": "http://localhost:3002", "eslintConfig": { "extends": [ "react-app", diff --git a/bfrontend/src/APIRequest.js b/bfrontend/src/APIRequest.js index e4c2eda..4916ada 100644 --- a/bfrontend/src/APIRequest.js +++ b/bfrontend/src/APIRequest.js @@ -33,9 +33,9 @@ APIRequest.authenticated = async function(endpoint, options) { if (!options) options = {}; if (!options.headers) options.headers = {}; - options.headers = { + options = { credentials: 'include', - ...options.headers + ...options }; try { diff --git a/bfrontend/src/Authenticator.js b/bfrontend/src/Authenticator.js index e0dca0b..ce0fcf2 100644 --- a/bfrontend/src/Authenticator.js +++ b/bfrontend/src/Authenticator.js @@ -1,30 +1,13 @@ import APIRequest from './APIRequest'; -class Authenticator { - constructor(method='cookie') { - this.method = method; - this.__token = undefined; - this.user = undefined; +const Authenticator = { + getLoggedInUserFromCookie: async function() { + const { json, isOK, err } = await APIRequest.authenticated('/api/v1/users/current/info'); + if (!isOK && err) throw new Error(err); + 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) { - return this.__token; -}; - -const authenticator = new Authenticator('cookie'); -export default authenticator; \ No newline at end of file +export default Authenticator; \ No newline at end of file diff --git a/bfrontend/src/Components/App.js b/bfrontend/src/Components/App.js index d87c486..586a2f8 100644 --- a/bfrontend/src/Components/App.js +++ b/bfrontend/src/Components/App.js @@ -1,23 +1,39 @@ -import { useEffect, useState } from 'react'; -import authenticator from './../Authenticator'; +import Login from './Auth/Login'; +import Root from './Root'; +import Authenticator from './../Authenticator'; -export default function App() { - const [isLoggedIn, setIsLoggedIn] = useState([]); +import { useEffect } from 'react'; +import { useDispatch, connect } from 'react-redux' +import { BrowserRouter, Switch, Route } from 'react-router-dom'; + +function App({ user }) { + const dispatch = useDispatch(); useEffect(() => { - authenticator.fetchLoggedInUserInfo() + Authenticator.getLoggedInUserFromCookie() .then((res) => { - setIsLoggedIn(res); + dispatch({ type: 'authenticator/updatelocaluserobject', user: res }) }); - }, [authenticator.user]); + }, [dispatch]); return ( - + - + + + + - + ); -} \ No newline at end of file +} + +const stateToProps = (state) => { + return { + user: state?.user || undefined + }; +}; + +export default connect(stateToProps)(App); \ No newline at end of file diff --git a/bfrontend/src/Components/Auth/Login.js b/bfrontend/src/Components/Auth/Login.js index 108ebb8..88dae2a 100644 --- a/bfrontend/src/Components/Auth/Login.js +++ b/bfrontend/src/Components/Auth/Login.js @@ -1,3 +1,63 @@ +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 ( +
+ setUsernameInput(target.value) } /> +
+ setPasswordInput(target.value) } /> +
+ + +
+ ); } \ No newline at end of file diff --git a/bfrontend/src/Components/Notification.js b/bfrontend/src/Components/Notification.js new file mode 100644 index 0000000..2b8de06 --- /dev/null +++ b/bfrontend/src/Components/Notification.js @@ -0,0 +1,7 @@ +export default function Notification({ text }) { + return ( +
+

{ text }

+
+ ); +} \ No newline at end of file diff --git a/bfrontend/src/Components/Root.js b/bfrontend/src/Components/Root.js new file mode 100644 index 0000000..dd1f267 --- /dev/null +++ b/bfrontend/src/Components/Root.js @@ -0,0 +1,21 @@ +import { useHistory } from 'react-router-dom'; + +export default function Root(props) { + const history = useHistory(); + + if (props.user) { + return ( +
+

Welcome, { props.user.username }

+
+ ); + } else { + return ( +
+

Welcome, - nevermind, you aren't logged in

+ +
+ ); + } + +} \ No newline at end of file diff --git a/bfrontend/src/Config.js b/bfrontend/src/Config.js index 44d7617..1c373a8 100644 --- a/bfrontend/src/Config.js +++ b/bfrontend/src/Config.js @@ -1,5 +1,5 @@ const config = { - apiUrl: 'http://localhost:3002' + apiUrl: 'http://localhost:3000' }; export default config; \ No newline at end of file diff --git a/bfrontend/src/Errors.js b/bfrontend/src/Errors.js new file mode 100644 index 0000000..c6c81a2 --- /dev/null +++ b/bfrontend/src/Errors.js @@ -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 } \ No newline at end of file diff --git a/bfrontend/src/index.js b/bfrontend/src/index.js index bb73391..5711482 100644 --- a/bfrontend/src/index.js +++ b/bfrontend/src/index.js @@ -1,11 +1,16 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; +import store from './store'; import './index.css'; import App from './Components/App'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; + ReactDOM.render( - + + + , document.getElementById('root') ); \ No newline at end of file diff --git a/bfrontend/src/store.js b/bfrontend/src/store.js new file mode 100644 index 0000000..885e684 --- /dev/null +++ b/bfrontend/src/store.js @@ -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; \ No newline at end of file