From 34f59319a170af5dd3301b1540d525347478d63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CZiyedB=E2=80=9D?= Date: Sun, 14 Feb 2021 20:47:41 +0000 Subject: [PATCH 01/20] feat(login): add hook for login + interceptor --- .eslintrc.js | 3 + package-lock.json | 8 ++ package.json | 1 + src/App.tsx | 49 +++++--- .../layouts/BaseLayout/BaseLayout.tsx | 2 +- .../layouts/NavigationBar/NavigationBar.tsx | 26 +++-- src/components/layouts/NavigationBar/index.ts | 1 + .../layouts/NavigationBar/index.tsx | 3 - src/config/api.ts | 5 + src/config/index.ts | 2 + src/config/storage-names.ts | 3 + src/hooks/UserContext.ts | 11 ++ src/hooks/index.ts | 1 + src/pages/Login/Login.scss | 72 ++++++++++++ src/pages/Login/Login.tsx | 108 ++++++++++++++++++ src/pages/Login/index.tsx | 1 + src/services/interceptor/auth.ts | 32 ++++++ src/services/interceptor/index.ts | 3 + src/shared/ProtectedRoute/ProtectedRoute.tsx | 28 +++++ src/shared/ProtectedRoute/index.tsx | 1 + 20 files changed, 335 insertions(+), 25 deletions(-) create mode 100644 src/components/layouts/NavigationBar/index.ts delete mode 100644 src/components/layouts/NavigationBar/index.tsx create mode 100644 src/config/api.ts create mode 100644 src/config/index.ts create mode 100644 src/config/storage-names.ts create mode 100644 src/hooks/UserContext.ts create mode 100644 src/hooks/index.ts create mode 100644 src/pages/Login/Login.scss create mode 100644 src/pages/Login/Login.tsx create mode 100644 src/pages/Login/index.tsx create mode 100644 src/services/interceptor/auth.ts create mode 100644 src/services/interceptor/index.ts create mode 100644 src/shared/ProtectedRoute/ProtectedRoute.tsx create mode 100644 src/shared/ProtectedRoute/index.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 64dee9b..a8e8325 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,6 @@ module.exports = { extends: ['airbnb-typescript-prettier'], + ignorePatterns: ['.eslintrc.js'], // Rules can be here to override the preset of eslint from airbnb, if they are too strict. rules: { '@typescript-eslint/ban-ts-comment': 'off', @@ -22,6 +23,8 @@ module.exports = { 'no-console': 'off', 'react/destructuring-assignment': 0, + 'react/jsx-props-no-spreading': 'off', + 'import/prefer-default-export': 0, // 'react/prop-types': 0, -> this is an example }, overrides: [ diff --git a/package-lock.json b/package-lock.json index fe60949..678ddc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2872,6 +2872,14 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.1.tgz", "integrity": "sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ==" }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", diff --git a/package.json b/package.json index 4180365..7fe32ab 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", + "axios": "^0.21.1", "brace": "^0.11.1", "jsoneditor": "^9.1.9", "jsoneditor-react": "^3.1.0", diff --git a/src/App.tsx b/src/App.tsx index ed84c33..1a81948 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,21 +1,44 @@ -import React from 'react'; -import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; +import React, { useState } from 'react'; +import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; +import './App.scss'; import BaseLayout from './components/layouts/BaseLayout'; -import IpfsList from './pages/ipfs/IpfsList'; +import { storageNames } from './config'; +import UserContext from './hooks/UserContext'; import Ipfs from './pages/ipfs/Ipfs'; - -import './App.scss'; +import IpfsList from './pages/ipfs/IpfsList'; +import Login from './pages/Login/Login'; +import { ProtectedRoute } from './shared/ProtectedRoute'; function App(): JSX.Element { + // Handle user state + // Ask if we should have this instead directly in a utils instead + const [isLoggedIn, setLoggedIn] = useState(false); + + // Handle user login by setting the storage and state + const login = (token: string): void => { + setLoggedIn(true); + localStorage.setItem(storageNames.user, token); + }; + + // Handle user logout + const logout = (): void => { + setLoggedIn(false); + localStorage.removeItem(storageNames.user); + }; + + // TODO: still need to handle the first time getting to site ( if token still valid, present etc. ) return ( - - - - - - - - + + + + + + + + + + + ); } diff --git a/src/components/layouts/BaseLayout/BaseLayout.tsx b/src/components/layouts/BaseLayout/BaseLayout.tsx index 9d43d7c..2cdf024 100644 --- a/src/components/layouts/BaseLayout/BaseLayout.tsx +++ b/src/components/layouts/BaseLayout/BaseLayout.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Container } from 'semantic-ui-react'; -import NavigationBar from '../NavigationBar'; +import { NavigationBar } from '../NavigationBar'; interface BaseLayoutProps { children: any; diff --git a/src/components/layouts/NavigationBar/NavigationBar.tsx b/src/components/layouts/NavigationBar/NavigationBar.tsx index 8083009..5145996 100644 --- a/src/components/layouts/NavigationBar/NavigationBar.tsx +++ b/src/components/layouts/NavigationBar/NavigationBar.tsx @@ -1,10 +1,16 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { FaUserCircle } from 'react-icons/fa'; import { Link } from 'react-router-dom'; - +import UserContext from '../../../hooks/UserContext'; import './NavigationBar.scss'; const NavigationBar = (): JSX.Element => { + const userContext = useContext(UserContext); + + const logout = (): void => { + userContext.logout(); + }; + return (
-
- {/* TODO: Need to be a user page */} - - - -
+ {userContext.isLoggedIn ? ( +
+ {/* TODO: Need to have this as a small menu showing up */} + + + +
+ ) : ( + '' + )} ); diff --git a/src/components/layouts/NavigationBar/index.ts b/src/components/layouts/NavigationBar/index.ts new file mode 100644 index 0000000..d4fd1e8 --- /dev/null +++ b/src/components/layouts/NavigationBar/index.ts @@ -0,0 +1 @@ +export { default as NavigationBar } from './NavigationBar'; diff --git a/src/components/layouts/NavigationBar/index.tsx b/src/components/layouts/NavigationBar/index.tsx deleted file mode 100644 index 513a881..0000000 --- a/src/components/layouts/NavigationBar/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import NavigationBar from './NavigationBar'; - -export default NavigationBar; diff --git a/src/config/api.ts b/src/config/api.ts new file mode 100644 index 0000000..9df0189 --- /dev/null +++ b/src/config/api.ts @@ -0,0 +1,5 @@ +export const apis = { + // TODO: Replace this by the BE api urls we would be using. Probably storing this in a config file + url: 'https://jsonplaceholder.typicode.com', + timeout: 2 * 60 * 1000, // 2 minutes timeout +}; diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..f8cb76b --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,2 @@ +export { apis } from './api'; +export { storageNames } from './storage-names'; diff --git a/src/config/storage-names.ts b/src/config/storage-names.ts new file mode 100644 index 0000000..b36ebf7 --- /dev/null +++ b/src/config/storage-names.ts @@ -0,0 +1,3 @@ +export const storageNames = { + user: 'pl11', +}; diff --git a/src/hooks/UserContext.ts b/src/hooks/UserContext.ts new file mode 100644 index 0000000..f33236e --- /dev/null +++ b/src/hooks/UserContext.ts @@ -0,0 +1,11 @@ +import { createContext } from 'react'; + +interface UserContextState { + isLoggedIn: boolean; + login: (token: string) => void; + logout: () => void; +} + +const UserContext = createContext({} as UserContextState); + +export default UserContext; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..4780e53 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1 @@ +export { default as UserContext } from './UserContext'; diff --git a/src/pages/Login/Login.scss b/src/pages/Login/Login.scss new file mode 100644 index 0000000..3eeb606 --- /dev/null +++ b/src/pages/Login/Login.scss @@ -0,0 +1,72 @@ +$elevation-card: 0 0 1px 0 rgba(8, 11, 14, 0.6), 0 16px 16px -1px rgba(8, 11, 14, 0.1); +$color-secondary: #e1e4e8; + +.login { + &__card { + max-width: 400px; + margin: auto; + box-shadow: $elevation-card; + padding: 30px; + background: white; + } + + &__title { + font-size: 22px; + line-height: 32px; + font-weight: 500; + text-align: center; + margin: 8px 0 20px; + font-weight: bold; + } + + &__input-container { + position: relative; + margin-top: 20px; + } + + &__input { + border-radius: 100px; + border: 1px solid $color-secondary; + font-size: 14px; + line-height: 24px; + font-weight: 500; + height: 40px; + width: 100%; + outline: none; + padding: 0 15px; + } + + &__input-icon { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + background: transparent; + border: none; + outline: none; + display: inline-block; + padding: 10px; + color: $color-secondary; + } + + &__password-icon { + color: black; + cursor: pointer; + } + + &__button { + border-radius: 100px; + font-size: 14px; + line-height: 24px; + background: black; + color: white; + padding: 0 10px; + height: 40px; + width: 100%; + border: none; + margin-top: 40px; + cursor: pointer; + padding: 0 20px; + outline: none; + } +} diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx new file mode 100644 index 0000000..078c822 --- /dev/null +++ b/src/pages/Login/Login.tsx @@ -0,0 +1,108 @@ +import React, { useContext } from 'react'; +import { FaEnvelope, FaEye, FaEyeSlash } from 'react-icons/fa'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; +import UserContext from '../../hooks/UserContext'; +import paralinkApi from '../../services/interceptor'; +import './Login.scss'; + +interface LoginProps { + from: string; +} + +const Login = (props: RouteComponentProps<{}, {}, LoginProps>): JSX.Element => { + const userContext = useContext(UserContext); + + const [state, setState] = React.useState({ + email: '', + password: '', + seePassword: false, + }); + + const { from } = props.location.state || { from: { pathname: '/' } }; + + const handleChange = (e: React.ChangeEvent): void => { + const { id, value } = e.target; + setState((prevState) => ({ + ...prevState, + [id]: value, + })); + }; + + const togglePassword = (): void => { + setState((prevState) => ({ + ...prevState, + seePassword: !prevState.seePassword, + })); + }; + + const login = async (): Promise => { + // TODO: put like a loading icon on the login button + + // TODO: do like a safe check on the email & password here + + // Just to check + await paralinkApi + // For now this is going to be the placeholder + .get('https://jsonplaceholder.typicode.com/todos') + .then(() => { + // We should get the login token here from the BE + const token = 'faketokenhere'; + userContext.login(token); + }) + .catch((err: any) => { + // TODO: Need to display the error in the UI ( user not found, not correct password, not correct email) + console.error(err); + }) + .finally(() => { + // TODO: login loading should be set to false here + }); + }; + + if (userContext.isLoggedIn) { + return ; + } + + // TODO: handle pressing enter to submit ( maybe having it as a form instead and using submit ) + return ( +
+
Login into your account
+
+ +
+ +
+
+
+ + + +
+ +
+ ); +}; + +export default Login; diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx new file mode 100644 index 0000000..bf79bae --- /dev/null +++ b/src/pages/Login/index.tsx @@ -0,0 +1 @@ +export { default as Login } from './Login'; diff --git a/src/services/interceptor/auth.ts b/src/services/interceptor/auth.ts new file mode 100644 index 0000000..eaf339a --- /dev/null +++ b/src/services/interceptor/auth.ts @@ -0,0 +1,32 @@ +import axios from 'axios'; +import { apis, storageNames } from '../../config'; + +const paralinkApi = axios.create({ + baseURL: apis.url, + timeout: apis.timeout, +}); + +paralinkApi.interceptors.request.use( + (config: any) => { + // Check for user token + const accessToken = localStorage.getItem(storageNames.user); + + // If user not logged we just pass it through, the backend should not accept it + // This lets us do api requests like for logging in for example. + // Depending on the API we could potentially reject here before doing the call and add an exception for login path + if (!accessToken) { + return config; + } + + // Otherwise set the token in the request + const request = config; + request.headers.authorization = `Bearer ${accessToken}`; + return request; + }, + (error) => { + // User might have been rejected + return Promise.reject(error); + }, +); + +export default paralinkApi; diff --git a/src/services/interceptor/index.ts b/src/services/interceptor/index.ts new file mode 100644 index 0000000..46571d5 --- /dev/null +++ b/src/services/interceptor/index.ts @@ -0,0 +1,3 @@ +import paralinkApi from './auth'; + +export default paralinkApi; diff --git a/src/shared/ProtectedRoute/ProtectedRoute.tsx b/src/shared/ProtectedRoute/ProtectedRoute.tsx new file mode 100644 index 0000000..b7d01f5 --- /dev/null +++ b/src/shared/ProtectedRoute/ProtectedRoute.tsx @@ -0,0 +1,28 @@ +import React, { useContext } from 'react'; +import { Redirect, Route } from 'react-router-dom'; +import UserContext from '../../hooks/UserContext'; + +// Disable the eslint for next any +// eslint-disable-next-line +const ProtectedRoute = ({ component: Component, ...rest }: any): JSX.Element => { + const userContext = useContext(UserContext); + /** Verify that user is logged in otherwise redirect to login page */ + return ( + + userContext.isLoggedIn ? ( + + ) : ( + + ) + } + /> + ); +}; +export default ProtectedRoute; diff --git a/src/shared/ProtectedRoute/index.tsx b/src/shared/ProtectedRoute/index.tsx new file mode 100644 index 0000000..4de35d0 --- /dev/null +++ b/src/shared/ProtectedRoute/index.tsx @@ -0,0 +1 @@ +export { default as ProtectedRoute } from './ProtectedRoute'; From e116fc7a03215724c43add8860f0268a1d6b396b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CZiyedB=E2=80=9D?= Date: Tue, 16 Feb 2021 20:37:04 +0000 Subject: [PATCH 02/20] add check on inputs --- package-lock.json | 44 +++++++ package.json | 5 +- src/App.tsx | 5 +- .../layouts/NavigationBar/NavigationBar.tsx | 2 +- src/pages/Login/Login.scss | 11 ++ src/pages/Login/Login.tsx | 109 +++++++++++------- src/services/interceptor/auth.ts | 2 +- 7 files changed, 129 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index 678ddc4..5f4b849 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1273,6 +1273,11 @@ "@hapi/hoek": "^8.3.0" } }, + "@hookform/resolvers": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-1.3.4.tgz", + "integrity": "sha512-K56VLSInXNIT/r14pkzRn1FJclqzGOWqpe3Bf0kz2Hf98ZOmRRFh4fhB7F3ofqCQ03CEQQkV44CTg7ql6nEvEg==" + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2026,6 +2031,11 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" }, + "@types/lodash": { + "version": "4.14.168", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", + "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==" + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -10039,6 +10049,11 @@ "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" }, + "nanoclone": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" + }, "nanoid": { "version": "3.1.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", @@ -12297,6 +12312,11 @@ "react-is": "^16.8.1" } }, + "property-expr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz", + "integrity": "sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==" + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -12631,6 +12651,11 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, + "react-hook-form": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.1.tgz", + "integrity": "sha512-bL0LQuQ3OlM3JYfbacKtBPLOHhmgYz8Lj6ivMrvu2M6e1wnt4sbGRtPEPYCc/8z3WDbjrMwfAfLX92OsB65pFA==" + }, "react-icons": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.2.0.tgz", @@ -15153,6 +15178,11 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=" + }, "tough-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", @@ -17183,6 +17213,20 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "yup": { + "version": "0.32.8", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.8.tgz", + "integrity": "sha512-SZulv5FIZ9d5H99EN5tRCRPXL0eyoYxWIP1AacCrjC9d4DfP13J1dROdKGfpfRHT3eQB6/ikBl5jG21smAfCkA==", + "requires": { + "@babel/runtime": "^7.10.5", + "@types/lodash": "^4.14.165", + "lodash": "^4.17.20", + "lodash-es": "^4.17.11", + "nanoclone": "^0.2.1", + "property-expr": "^2.0.4", + "toposort": "^2.0.2" + } } } } diff --git a/package.json b/package.json index 7fe32ab..d0bd862 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@hookform/resolvers": "^1.3.4", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", @@ -13,13 +14,15 @@ "node-sass": "^4.14.1", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-hook-form": "^6.15.1", "react-icons": "^4.2.0", "react-router-dom": "^5.2.0", "react-scripts": "4.0.1", "semantic-ui-css": "^2.4.1", "semantic-ui-react": "^2.0.3", "typescript": "^4.1.3", - "web-vitals": "^0.2.4" + "web-vitals": "^0.2.4", + "yup": "^0.32.8" }, "scripts": { "start": "react-scripts start", diff --git a/src/App.tsx b/src/App.tsx index 1a81948..89ad187 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,8 +11,8 @@ import { ProtectedRoute } from './shared/ProtectedRoute'; function App(): JSX.Element { // Handle user state - // Ask if we should have this instead directly in a utils instead - const [isLoggedIn, setLoggedIn] = useState(false); + const accessToken = localStorage.getItem(storageNames.user) || ''; + const [isLoggedIn, setLoggedIn] = useState(!!accessToken); // Handle user login by setting the storage and state const login = (token: string): void => { @@ -26,7 +26,6 @@ function App(): JSX.Element { localStorage.removeItem(storageNames.user); }; - // TODO: still need to handle the first time getting to site ( if token still valid, present etc. ) return ( diff --git a/src/components/layouts/NavigationBar/NavigationBar.tsx b/src/components/layouts/NavigationBar/NavigationBar.tsx index 5145996..daf082b 100644 --- a/src/components/layouts/NavigationBar/NavigationBar.tsx +++ b/src/components/layouts/NavigationBar/NavigationBar.tsx @@ -32,7 +32,7 @@ const NavigationBar = (): JSX.Element => { {userContext.isLoggedIn ? (
{/* TODO: Need to have this as a small menu showing up */} - +
diff --git a/src/pages/Login/Login.scss b/src/pages/Login/Login.scss index 3eeb606..d12c6ab 100644 --- a/src/pages/Login/Login.scss +++ b/src/pages/Login/Login.scss @@ -1,5 +1,6 @@ $elevation-card: 0 0 1px 0 rgba(8, 11, 14, 0.6), 0 16px 16px -1px rgba(8, 11, 14, 0.1); $color-secondary: #e1e4e8; +$error-color: red; .login { &__card { @@ -34,6 +35,16 @@ $color-secondary: #e1e4e8; width: 100%; outline: none; padding: 0 15px; + + &--error { + border-color: $error-color; + } + } + + &__input-error-msg { + font-size: 12px; + margin-left: 15px; + color: $error-color; } &__input-icon { diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index 078c822..b1f8368 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -1,6 +1,9 @@ +import { yupResolver } from '@hookform/resolvers/yup'; import React, { useContext } from 'react'; +import { useForm } from 'react-hook-form'; import { FaEnvelope, FaEye, FaEyeSlash } from 'react-icons/fa'; import { Redirect, RouteComponentProps } from 'react-router-dom'; +import * as yup from 'yup'; import UserContext from '../../hooks/UserContext'; import paralinkApi from '../../services/interceptor'; import './Login.scss'; @@ -9,24 +12,36 @@ interface LoginProps { from: string; } +type LoginFormData = { + email: string; + password: string; +}; + +const loginSchema = yup.object().shape({ + email: yup.string().email().required(), + password: yup.string().required(), // We could define rules here for password if there are +}); + const Login = (props: RouteComponentProps<{}, {}, LoginProps>): JSX.Element => { const userContext = useContext(UserContext); + const { register, handleSubmit, errors } = useForm({ resolver: yupResolver(loginSchema) }); const [state, setState] = React.useState({ - email: '', - password: '', + // email: '', + // password: '', seePassword: false, }); const { from } = props.location.state || { from: { pathname: '/' } }; - const handleChange = (e: React.ChangeEvent): void => { - const { id, value } = e.target; - setState((prevState) => ({ - ...prevState, - [id]: value, - })); - }; + // const handleChange = (e: React.ChangeEvent): void => { + // const { id, value } = e.target; + // setState((prevState) => ({ + // ...prevState, + // [id]: value, + // })); + // console.log('errors', errors); + // }; const togglePassword = (): void => { setState((prevState) => ({ @@ -35,11 +50,14 @@ const Login = (props: RouteComponentProps<{}, {}, LoginProps>): JSX.Element => { })); }; - const login = async (): Promise => { + const login = async (data: LoginFormData): Promise => { + // Avoid the page to refresh from submitting the form + // event.preventDefault(); // TODO: put like a loading icon on the login button // TODO: do like a safe check on the email & password here + console.log('data', data); // Just to check await paralinkApi // For now this is going to be the placeholder @@ -66,41 +84,46 @@ const Login = (props: RouteComponentProps<{}, {}, LoginProps>): JSX.Element => { return (
Login into your account
-
- -
- +
+
+ +
+ +
+
+ {errors.email?.message ? {errors.email?.message} : ''} + +
+ + +
-
-
- - - -
- +
); }; diff --git a/src/services/interceptor/auth.ts b/src/services/interceptor/auth.ts index eaf339a..0cc1482 100644 --- a/src/services/interceptor/auth.ts +++ b/src/services/interceptor/auth.ts @@ -8,7 +8,7 @@ const paralinkApi = axios.create({ paralinkApi.interceptors.request.use( (config: any) => { - // Check for user token + // Check for user token, we can't use hooks from here const accessToken = localStorage.getItem(storageNames.user); // If user not logged we just pass it through, the backend should not accept it From 23c46a57d58dd0f9c5f0df84a6dce6aaa5b718b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Z=CC=8Ciga=20Franko?= Date: Thu, 25 Feb 2021 18:14:47 +0100 Subject: [PATCH 03/20] packages update --- package-lock.json | 34 +++++++++++++++++----------------- package.json | 4 ++-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5f4b849..e020b20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7656,9 +7656,9 @@ "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" }, "immer": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.9.tgz", - "integrity": "sha512-Vs/gxoM4DqNAYR7pugIxi0Xc8XAun/uy7AQu4fLLqaTBHxjOP9pJ266Q9MWA/ly4z6rAFZbvViOtihxUZ7O28A==" + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", + "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==" }, "import-cwd": { "version": "2.1.0", @@ -10608,9 +10608,9 @@ } }, "open": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.0.tgz", - "integrity": "sha512-PGoBCX/lclIWlpS/R2PQuIR4NJoXh6X5AwVzE7WXnWRGvHg7+4TBCgsujUgiPpm0K1y4qvQeWnCWVTpTKZBtvA==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", "requires": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" @@ -12501,9 +12501,9 @@ } }, "react-dev-utils": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.1.tgz", - "integrity": "sha512-rlgpCupaW6qQqvu0hvv2FDv40QG427fjghV56XyPcP5aKtOAPzNAhQ7bHqk1YdS2vpW1W7aSV3JobedxuPlBAA==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.3.tgz", + "integrity": "sha512-4lEA5gF4OHrcJLMUV1t+4XbNDiJbsAWCH5Z2uqlTqW6dD7Cf5nEASkeXrCI/Mz83sI2o527oBIFKVMXtRf1Vtg==", "requires": { "@babel/code-frame": "7.10.4", "address": "1.1.2", @@ -12518,13 +12518,13 @@ "global-modules": "2.0.0", "globby": "11.0.1", "gzip-size": "5.1.1", - "immer": "7.0.9", + "immer": "8.0.1", "is-root": "2.1.0", "loader-utils": "2.0.0", "open": "^7.0.2", "pkg-up": "3.1.0", "prompts": "2.4.0", - "react-error-overlay": "^6.0.8", + "react-error-overlay": "^6.0.9", "recursive-readdir": "2.2.2", "shell-quote": "1.7.2", "strip-ansi": "6.0.0", @@ -12642,9 +12642,9 @@ } }, "react-error-overlay": { - "version": "6.0.8", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.8.tgz", - "integrity": "sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw==" + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", + "integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==" }, "react-fast-compare": { "version": "3.2.0", @@ -15355,9 +15355,9 @@ } }, "typescript": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", - "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==" + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.2.tgz", + "integrity": "sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ==" }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", diff --git a/package.json b/package.json index d0bd862..2d1e634 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "react-scripts": "4.0.1", "semantic-ui-css": "^2.4.1", "semantic-ui-react": "^2.0.3", - "typescript": "^4.1.3", + "typescript": "^4.2.2", "web-vitals": "^0.2.4", "yup": "^0.32.8" }, @@ -75,7 +75,7 @@ ] }, "lint-staged": { - "*.{js,tsx}": "eslint --fix" + "*.{js,tsx}": "eslint_d --fix --catch" }, "husky": { "hooks": { From d706210ade4e240746da60be4a7e8991565468a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Z=CC=8Ciga=20Franko?= Date: Thu, 25 Feb 2021 18:16:47 +0100 Subject: [PATCH 04/20] JWT token authentication prepared. - Defined access and refresh tokens - Prepared interceptor for obtaining new access token or redirecting to the login page when refresh token is expired --- src/api/api.ts | 64 ++++++++++++++++++++++++++++++++++++++++++++ src/api/auth/auth.ts | 24 +++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 src/api/api.ts create mode 100644 src/api/auth/auth.ts diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 0000000..379f888 --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,64 @@ +import axios from 'axios'; + +const OBTAIN_TOKEN = '/token/obtain/'; +// const REFRESH_TOKEN_URL = '/token/refresh/'; + +export const ACCESS_TOKEN = 'access_token'; +export const REFRESH_TOKEN = 'refresh_token'; + +interface JWTTokenResponse { + refresh: string; + access: string; +} + +export const axiosInstance = axios.create({ + baseURL: 'http://127.0.0.1:7424/api/', + timeout: 5000, + headers: { + // 'Authorization': localStorage.getItem('access_token') ? 'JWT ' + localStorage.getItem('access_token') : null, + 'Content-Type': 'application/json', + accept: 'application/json', + }, +}); + +export const saveTokens = (jwtToken: JWTTokenResponse): JWTTokenResponse => { + localStorage.setItem(ACCESS_TOKEN, jwtToken.access); + localStorage.setItem(REFRESH_TOKEN, jwtToken.refresh); + return jwtToken; +}; + +export const getAccessToken = (): string => localStorage.getItem(ACCESS_TOKEN)!; +export const getRefreshToken = (): string => localStorage.getItem(REFRESH_TOKEN)!; + +axiosInstance.interceptors.response.use( + (res) => res, + (err) => { + // const originalRequest = err.config; + // If refresh tokens is expired redirect to login page + // if (err.response.status === 401 && originalRequest.url === REFRESH_TOKEN_URL) { + // window.location.href = '/login/'; + // return Promise.reject(err); + // } + + // // If access token is expired update it + // if (err.response.status === 401 && err.response.statusText === 'Unauthorized') { + // return axiosInstance + // .post(REFRESH_TOKEN_URL, {refresh: localStorage.getItem(REFRESH_TOKEN)}) + // .then(res => res.data) + // .then(saveTokens) + // .then(res => { + // axiosInstance.defaults.headers['Authorization'] = 'JWT ' + res.access; + // originalRequest.headers['Authorization'] = 'JWT ' + res.access; + + // return axiosInstance(originalRequest); + // }) + // } + + return Promise.reject(err); + }, +); + +export const obtainTokenApi = async (email: string, password: string): Promise => + axiosInstance + .post(OBTAIN_TOKEN, { email, password }) + .then((res) => res.data); diff --git a/src/api/auth/auth.ts b/src/api/auth/auth.ts new file mode 100644 index 0000000..7c4cd48 --- /dev/null +++ b/src/api/auth/auth.ts @@ -0,0 +1,24 @@ +// import { axiosInstance, obtainTokenApi, saveTokens } from "../api" + +// const LOGIN_URL = ''; + +export interface UserAuthenticationData { + email: string; + password: string; +} + +const loginUser = (data: UserAuthenticationData): Promise => { + const { email, password } = data; + + if (email !== 'test@test.com') throw new Error('Email does not exist!'); + if (password !== 'geslo123') throw new Error('Password is incorrect!'); + // axiosInstance.post<{}>(LOGIN_URL, data) + // .then((res) => res.data); + return Promise.resolve(); +}; + +export const authenticateUser = async (data: UserAuthenticationData): Promise => { + await loginUser(data); + // const jwtTokens = await obtainTokenApi(data.email, data.password); + // saveTokens(jwtTokens); +}; From 37dfdec2e8c0e2a3c46d14649a9ff8fa968f528e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Z=CC=8Ciga=20Franko?= Date: Thu, 25 Feb 2021 18:19:14 +0100 Subject: [PATCH 05/20] urls.ts is meant to contain all the urs that the app will have. Each page has defined custom variable URL --- src/components/urls.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/components/urls.ts diff --git a/src/components/urls.ts b/src/components/urls.ts new file mode 100644 index 0000000..50ef1b5 --- /dev/null +++ b/src/components/urls.ts @@ -0,0 +1,14 @@ +export const HOME_PAGE = '/'; + +// Authentication pages +export const LOGIN_PAGE = '/login'; +export const REGISTER_PAGE = '/register'; + +// IPFS pages +export const IPFS_BASE_PAGE = '/ipfs'; +export const IPFS_PAGE = `${IPFS_BASE_PAGE}/:hash`; + +// Error pages +export const ERROR_404_PAGE = '/error404'; + +export const USER_PAGES = [HOME_PAGE, IPFS_BASE_PAGE, IPFS_PAGE]; From e7be84ca180cbcbd988ae69538df67038b0b69e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Z=CC=8Ciga=20Franko?= Date: Thu, 25 Feb 2021 18:20:32 +0100 Subject: [PATCH 06/20] IPFS and IPFSList components redefined --- src/components/pages/ipfs/Error.tsx | 20 ++++ src/components/pages/ipfs/Ipfs.tsx | 94 +++++++++++++++++ src/components/pages/ipfs/IpfsList.tsx | 61 +++++++++++ src/components/pages/ipfs/pql.ts | 35 +++++++ src/pages/ipfs/Ipfs.tsx | 134 ------------------------- src/pages/ipfs/IpfsList.tsx | 71 ------------- 6 files changed, 210 insertions(+), 205 deletions(-) create mode 100644 src/components/pages/ipfs/Error.tsx create mode 100644 src/components/pages/ipfs/Ipfs.tsx create mode 100644 src/components/pages/ipfs/IpfsList.tsx create mode 100644 src/components/pages/ipfs/pql.ts delete mode 100644 src/pages/ipfs/Ipfs.tsx delete mode 100644 src/pages/ipfs/IpfsList.tsx diff --git a/src/components/pages/ipfs/Error.tsx b/src/components/pages/ipfs/Error.tsx new file mode 100644 index 0000000..eb27723 --- /dev/null +++ b/src/components/pages/ipfs/Error.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Header, Segment } from 'semantic-ui-react'; + +interface Error { + message: string; + hash: string; +} + +const Error: React.FC = ({ message, hash }) => ( +
+
+ Error: {message} +
+ + An error occurred retrieving IPFS hash {hash} . The selected IPFS hash was not a valid PQL definition file. + +
+); + +export default Error; diff --git a/src/components/pages/ipfs/Ipfs.tsx b/src/components/pages/ipfs/Ipfs.tsx new file mode 100644 index 0000000..029091a --- /dev/null +++ b/src/components/pages/ipfs/Ipfs.tsx @@ -0,0 +1,94 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Link, useParams } from 'react-router-dom'; +import { Button, Grid, Message } from 'semantic-ui-react'; + +import ace from 'brace'; +import 'brace/mode/json'; +import 'brace/theme/github'; + +// No typings for jsoneditor-react for now just using @ts-ignore for getting around the issue +// Later on with advanced design we might not used that library. If we do we can as well define typings for it +// @ts-ignore +import { JsonEditor as Editor } from 'jsoneditor-react'; +import 'jsoneditor-react/es/editor.min.css'; +import { LoadPQLHash } from './pql'; +import Error from './Error'; +import { axiosInstance } from '../../../api/api'; + +interface PqlAction { + success: string; +} + +const pqlAction = (endpoint: string, text: LoadPQLHash, setResult: (value: string) => void): void => { + Promise.resolve() + .then(() => axiosInstance.post(`${endpoint}`, { text })) + .then((res) => res.data.success) + .then(setResult) + .catch((err) => setResult(err.message)); +}; + +interface UrlParams { + hash: string; +} + +const Ipfs: React.FC<{}> = () => { + const { hash } = useParams(); + const editor = useRef(); + + const [error, setError] = useState(''); + const [resultContent, setResultContent] = useState(''); + + const getContent = (): LoadPQLHash => editor.current.jsonEditor.getText(); + const testAction = (): void => pqlAction('ipfs/test', getContent(), setResultContent); + const saveAction = (): void => pqlAction('ipfs/save_pql', getContent(), setResultContent); + + useEffect(() => { + Promise.resolve() + .then(() => axiosInstance.get(`/ipfs/${hash}`)) + .then((res) => res.data) + .then((res) => { + editor.current.jsonEditor.set(res.pql); + setResultContent(res.hash); + }) + .catch((err) => setError(err.message)); + }, [hash]); + + return error.length > 0 ? ( + + ) : ( + + +
+ IPFS + +
{hash}
+
+
+ +
+ +
+
+ + + + + + Result: +

{resultContent}

+
+
+
+ ); +}; + +export default Ipfs; diff --git a/src/components/pages/ipfs/IpfsList.tsx b/src/components/pages/ipfs/IpfsList.tsx new file mode 100644 index 0000000..573c1a9 --- /dev/null +++ b/src/components/pages/ipfs/IpfsList.tsx @@ -0,0 +1,61 @@ +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { Button, Header, Table } from 'semantic-ui-react'; +import { axiosInstance } from '../../../api/api'; + +interface ApiCall { + hashes: string[]; +} + +const IpfsList = (): JSX.Element => { + const [, setIsLoading] = useState(true); + const [ipfsHashes, setIpfsHashes] = useState([]); + + useEffect(() => { + // Async/Await pattern + const update = async (): Promise => { + try { + setIsLoading(true); + const res = await axiosInstance.get('ipfs'); + setIpfsHashes(res.data.hashes); + setIsLoading(false); + } catch (e) { + console.log(e); + } + }; + update(); + }, []); + + const ipfsItems = ipfsHashes.map((hash) => ( + + + {hash} + + + )); + + return ( +
+ + + + +
List of local IPFS hashes
+ + + + + IPFS hash + + + + {ipfsItems} +
+
+ ); +}; + +export default IpfsList; diff --git a/src/components/pages/ipfs/pql.ts b/src/components/pages/ipfs/pql.ts new file mode 100644 index 0000000..b0623d2 --- /dev/null +++ b/src/components/pages/ipfs/pql.ts @@ -0,0 +1,35 @@ +export interface SourceItem { + step: string; + method: string; + url: string; +} + +export interface Source { + name: string; + pipeline: SourceItem[]; +} + +export interface Aggregate { + method: string; +} + +export interface PQL { + name: string; + psql_version: string; // TODO changed from snake cased to camel cased! + aggregate: Aggregate; + sources: Source[]; +} + +export interface LoadPQLHash { + pql: PQL; + hash: string; +} + +export const emptyPql: PQL = { + name: '', + psql_version: '', + aggregate: { + method: '', + }, + sources: [], +}; diff --git a/src/pages/ipfs/Ipfs.tsx b/src/pages/ipfs/Ipfs.tsx deleted file mode 100644 index b87e0b8..0000000 --- a/src/pages/ipfs/Ipfs.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React, { ReactNode } from 'react'; -import { Link } from 'react-router-dom'; -import { Button, Grid, Header, Message, Segment } from 'semantic-ui-react'; - -import ace from 'brace'; -import 'brace/mode/json'; -import 'brace/theme/github'; - -// No typings for jsoneditor-react for now just using @ts-ignore for getting around the issue -// Later on with advanced design we might not used that library. If we do we can as well define typings for it -// @ts-ignore -import { JsonEditor as Editor } from 'jsoneditor-react'; -import 'jsoneditor-react/es/editor.min.css'; - -interface IpfsProps { - match: { params: { hash: string } }; -} - -interface IpfsState { - hash: string; - result: string; - error: any; -} - -class Ipfs extends React.Component { - API_PATH = 'http://127.0.0.1:7424/api'; - - editor: any; - - constructor(props: IpfsProps) { - super(props); - this.state = { - hash: '', - result: 'Click "Test" button to get the result of the above PQL definition.', - error: null, - }; - this.editor = React.createRef(); - - this.pqlAction = this.pqlAction.bind(this); - } - - componentDidMount(): void { - const { hash } = this.props.match.params; - - fetch(`${this.API_PATH}/ipfs/${hash}`) - .then((res) => { - if (!res.ok) { - return res.json().then((json) => { - throw json; - }); - } - return res.json(); - }) - .then((res) => { - this.setState({ - hash: res.hash, - error: null, - }); - this.editor.current.jsonEditor.set(res.pql); - }) - .catch((res) => { - this.setState({ - error: res.error, - }); - }); - } - - pqlAction(endpoint: string): void { - const text = this.editor.current.jsonEditor.getText(); - - const requestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: text, - }; - - fetch(`${this.API_PATH}/${endpoint}`, requestOptions) - .then((res) => res.json()) - .then((res) => this.setState({ result: JSON.stringify(res) })); - } - - render(): ReactNode { - const { hash } = this.props.match.params; - if (this.state.error) { - return ( -
-
- Error: {this.state.error} -
- - An error occurred retrieving IPFS hash {hash} . The selected IPFS hash was not a valid PQL definition - file. - -
- ); - } - return ( - - -
- IPFS - -
{this.state.hash}
-
-
- -
- -
-
- - - - - - Result: -

{this.state.result}

-
-
-
- ); - } -} - -export default Ipfs; diff --git a/src/pages/ipfs/IpfsList.tsx b/src/pages/ipfs/IpfsList.tsx deleted file mode 100644 index a12d3bf..0000000 --- a/src/pages/ipfs/IpfsList.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { ReactNode } from 'react'; -import { Link } from 'react-router-dom'; -import { Button, Header, Table } from 'semantic-ui-react'; - -interface IpfsListState { - ipfsHashes: any[]; -} - -class IpfsList extends React.Component<{}, IpfsListState> { - API_PATH = 'http://127.0.0.1:7424/api'; - - constructor(props: {}) { - super(props); - this.state = { - ipfsHashes: [], - }; - } - - componentDidMount(): void { - fetch(`${this.API_PATH}/ipfs`) - .then((res) => res.json()) - .then( - (res) => { - this.setState({ - ipfsHashes: res.hashes, - }); - }, - (error) => { - this.setState({ - ipfsHashes: [], - }); - console.log(error); - }, - ); - } - - render(): ReactNode { - return ( -
- - - - -
List of local IPFS hashes
- - - - - IPFS hash - - - - - {this.state.ipfsHashes.map((hash) => ( - - - {hash} - - - ))} - -
-
- ); - } -} - -export default IpfsList; From 525fca7e614dcf657792619bf83e923059d21de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Z=CC=8Ciga=20Franko?= Date: Thu, 25 Feb 2021 18:26:14 +0100 Subject: [PATCH 07/20] BaseLayout modified --- package.json | 2 +- .../layouts/BaseLayout/BaseLayout.tsx | 22 ++++++++----------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 2d1e634..363e163 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ ] }, "lint-staged": { - "*.{js,tsx}": "eslint_d --fix --catch" + "*.{js,tsx}": "eslint --fix" }, "husky": { "hooks": { diff --git a/src/components/layouts/BaseLayout/BaseLayout.tsx b/src/components/layouts/BaseLayout/BaseLayout.tsx index 2cdf024..cb64c07 100644 --- a/src/components/layouts/BaseLayout/BaseLayout.tsx +++ b/src/components/layouts/BaseLayout/BaseLayout.tsx @@ -2,18 +2,14 @@ import React from 'react'; import { Container } from 'semantic-ui-react'; import { NavigationBar } from '../NavigationBar'; -interface BaseLayoutProps { - children: any; -} +const BaseLayout: React.FC<{}> = ({ children }) => ( +
+ -export default function BaseLayout(props: BaseLayoutProps): JSX.Element { - return ( -
- + + {children} + +
+); - - {props.children} - -
- ); -} +export default BaseLayout; From 8db789edc3208b5ca96089f15fa5a4b6340ebdf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Z=CC=8Ciga=20Franko?= Date: Thu, 25 Feb 2021 18:27:05 +0100 Subject: [PATCH 08/20] user context state logic moved into state/user.ts with some minor modifications --- src/hooks/UserContext.ts | 11 ----------- src/hooks/index.ts | 1 - src/state/user.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 12 deletions(-) delete mode 100644 src/hooks/UserContext.ts delete mode 100644 src/hooks/index.ts create mode 100644 src/state/user.ts diff --git a/src/hooks/UserContext.ts b/src/hooks/UserContext.ts deleted file mode 100644 index f33236e..0000000 --- a/src/hooks/UserContext.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createContext } from 'react'; - -interface UserContextState { - isLoggedIn: boolean; - login: (token: string) => void; - logout: () => void; -} - -const UserContext = createContext({} as UserContextState); - -export default UserContext; diff --git a/src/hooks/index.ts b/src/hooks/index.ts deleted file mode 100644 index 4780e53..0000000 --- a/src/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as UserContext } from './UserContext'; diff --git a/src/state/user.ts b/src/state/user.ts new file mode 100644 index 0000000..d218a9e --- /dev/null +++ b/src/state/user.ts @@ -0,0 +1,27 @@ +import { createContext } from 'react'; + +interface User { + email: string; + isLoggedIn: boolean; +} + +interface UserContextState { + user: User; + login: (email: string) => void; + logout: () => void; +} + +export const emptyUser: User = { + isLoggedIn: false, + email: '', +}; + +export const emptyUserState: UserContextState = { + user: { ...emptyUser }, + login: () => {}, + logout: () => {}, +}; + +const UserContext = createContext({ ...emptyUserState }); + +export default UserContext; From 824309bd6a1ea7feb77f3773738725a778c061d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Z=CC=8Ciga=20Franko?= Date: Thu, 25 Feb 2021 19:12:36 +0100 Subject: [PATCH 09/20] Login component updated, it uses some basic logic to login. Available email: test@test.com, password: geslo123 --- .eslintrc.js | 2 + package.json | 2 +- .../pages/auth}/Login.scss | 0 .../Login => components/pages/auth}/Login.tsx | 81 +++++-------------- src/pages/Login/index.tsx | 1 - 5 files changed, 21 insertions(+), 65 deletions(-) rename src/{pages/Login => components/pages/auth}/Login.scss (100%) rename src/{pages/Login => components/pages/auth}/Login.tsx (52%) delete mode 100644 src/pages/Login/index.tsx diff --git a/.eslintrc.js b/.eslintrc.js index a8e8325..6bad519 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,6 +3,8 @@ module.exports = { ignorePatterns: ['.eslintrc.js'], // Rules can be here to override the preset of eslint from airbnb, if they are too strict. rules: { + camelcase: 'warn', + '@typescript-eslint/no-empty-function': 'warn', '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/explicit-function-return-type': 'off', diff --git a/package.json b/package.json index 363e163..09b83f1 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ ] }, "lint-staged": { - "*.{js,tsx}": "eslint --fix" + "*.{js,ts,tsx}": "eslint --fix" }, "husky": { "hooks": { diff --git a/src/pages/Login/Login.scss b/src/components/pages/auth/Login.scss similarity index 100% rename from src/pages/Login/Login.scss rename to src/components/pages/auth/Login.scss diff --git a/src/pages/Login/Login.tsx b/src/components/pages/auth/Login.tsx similarity index 52% rename from src/pages/Login/Login.tsx rename to src/components/pages/auth/Login.tsx index b1f8368..ee3df83 100644 --- a/src/pages/Login/Login.tsx +++ b/src/components/pages/auth/Login.tsx @@ -1,85 +1,44 @@ import { yupResolver } from '@hookform/resolvers/yup'; -import React, { useContext } from 'react'; +import React, { useContext, useState } from 'react'; import { useForm } from 'react-hook-form'; import { FaEnvelope, FaEye, FaEyeSlash } from 'react-icons/fa'; -import { Redirect, RouteComponentProps } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import * as yup from 'yup'; -import UserContext from '../../hooks/UserContext'; -import paralinkApi from '../../services/interceptor'; import './Login.scss'; - -interface LoginProps { - from: string; -} - -type LoginFormData = { - email: string; - password: string; -}; +import { authenticateUser, UserAuthenticationData } from '../../../api/auth/auth'; +import { HOME_PAGE } from '../../urls'; +import UserContext from '../../../state/user'; const loginSchema = yup.object().shape({ email: yup.string().email().required(), password: yup.string().required(), // We could define rules here for password if there are }); -const Login = (props: RouteComponentProps<{}, {}, LoginProps>): JSX.Element => { +const Login = (): JSX.Element => { + const history = useHistory(); const userContext = useContext(UserContext); - const { register, handleSubmit, errors } = useForm({ resolver: yupResolver(loginSchema) }); - - const [state, setState] = React.useState({ - // email: '', - // password: '', - seePassword: false, - }); - - const { from } = props.location.state || { from: { pathname: '/' } }; + const [seePassword, setSeePasword] = useState(false); + const { register, handleSubmit, errors } = useForm({ resolver: yupResolver(loginSchema) }); - // const handleChange = (e: React.ChangeEvent): void => { - // const { id, value } = e.target; - // setState((prevState) => ({ - // ...prevState, - // [id]: value, - // })); - // console.log('errors', errors); - // }; + const togglePassword = (): void => setSeePasword(!seePassword); - const togglePassword = (): void => { - setState((prevState) => ({ - ...prevState, - seePassword: !prevState.seePassword, - })); - }; - - const login = async (data: LoginFormData): Promise => { + const login = (data: UserAuthenticationData): void => { // Avoid the page to refresh from submitting the form // event.preventDefault(); // TODO: put like a loading icon on the login button // TODO: do like a safe check on the email & password here - console.log('data', data); - // Just to check - await paralinkApi - // For now this is going to be the placeholder - .get('https://jsonplaceholder.typicode.com/todos') + authenticateUser(data) .then(() => { - // We should get the login token here from the BE - const token = 'faketokenhere'; - userContext.login(token); + userContext.login(data.email); + history.push(HOME_PAGE); }) - .catch((err: any) => { - // TODO: Need to display the error in the UI ( user not found, not correct password, not correct email) - console.error(err); - }) - .finally(() => { - // TODO: login loading should be set to false here + .catch((error) => { + console.error(error); }); }; - if (userContext.isLoggedIn) { - return ; - } - // TODO: handle pressing enter to submit ( maybe having it as a form instead and using submit ) return (
@@ -103,7 +62,7 @@ const Login = (props: RouteComponentProps<{}, {}, LoginProps>): JSX.Element => {
): JSX.Element => { />
{errors.password?.message ? {errors.password?.message} : ''} diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx deleted file mode 100644 index bf79bae..0000000 --- a/src/pages/Login/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default as Login } from './Login'; From ea39b072eca2c55e4bc3ef752d0f8bdad19d2761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Z=CC=8Ciga=20Franko?= Date: Thu, 25 Feb 2021 19:13:43 +0100 Subject: [PATCH 10/20] urls applied in NavigationBar --- .../layouts/NavigationBar/NavigationBar.tsx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/components/layouts/NavigationBar/NavigationBar.tsx b/src/components/layouts/NavigationBar/NavigationBar.tsx index daf082b..f967549 100644 --- a/src/components/layouts/NavigationBar/NavigationBar.tsx +++ b/src/components/layouts/NavigationBar/NavigationBar.tsx @@ -1,35 +1,32 @@ import React, { useContext } from 'react'; import { FaUserCircle } from 'react-icons/fa'; import { Link } from 'react-router-dom'; -import UserContext from '../../../hooks/UserContext'; +import UserContext from '../../../state/user'; +import { HOME_PAGE, IPFS_BASE_PAGE } from '../../urls'; import './NavigationBar.scss'; const NavigationBar = (): JSX.Element => { - const userContext = useContext(UserContext); - - const logout = (): void => { - userContext.logout(); - }; + const { user, logout } = useContext(UserContext); return (