From 298705c9c7a7a453d4a295a1d330120c5734a0f4 Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Thu, 24 Mar 2022 00:38:30 +0000 Subject: [PATCH] implement login --- .eslintrc.json | 5 + package-lock.json | 106 +++++++++++++++--- package.json | 1 + src/App.js | 11 +- src/api/auth.js | 44 ++++++++ src/api/devices.js | 7 ++ src/api/instance.js | 56 +++++++++ src/api/request.js | 31 +++++ src/components/views/{home.jsx => Home.jsx} | 0 src/components/views/{login.jsx => Login.jsx} | 15 ++- .../views/{useradmin.jsx => UserAdmin.jsx} | 0 src/context/devices/index.js | 6 +- 12 files changed, 248 insertions(+), 34 deletions(-) create mode 100644 src/api/auth.js create mode 100644 src/api/devices.js create mode 100644 src/api/instance.js create mode 100644 src/api/request.js rename src/components/views/{home.jsx => Home.jsx} (100%) rename src/components/views/{login.jsx => Login.jsx} (88%) rename src/components/views/{useradmin.jsx => UserAdmin.jsx} (100%) diff --git a/.eslintrc.json b/.eslintrc.json index dbd2865..eeeed3c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -36,6 +36,11 @@ "ignoreTemplateLiterals": true }], + // disallow else after a return in an if + // https://eslint.org/docs/rules/no-else-return + // retropilot: allow else-if... + "no-else-return": ["error", { "allowElseIf": true }], + // restrict file extensions that may contain JSX // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-filename-extension.md // retropilot: we don't care about this diff --git a/package-lock.json b/package-lock.json index b0877ab..fc9eaee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "axios": "^0.24.0", "google-map-react": "^2.1.10", "prop-types": "^15.8.1", + "query-string": "^7.1.1", "rc-scrollbars": "^1.1.3", "react": "^17.0.2", "react-custom-scrollbars": "^4.2.1", @@ -9704,6 +9705,14 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -14632,6 +14641,26 @@ "node": ">=4" } }, + "node_modules/normalize-url/node_modules/query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dependencies": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url/node_modules/strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -16865,15 +16894,20 @@ } }, "node_modules/query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", + "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==", "dependencies": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" + "decode-uri-component": "^0.2.0", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/querystring": { @@ -19016,6 +19050,14 @@ "node": ">= 6" } }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "engines": { + "node": ">=6" + } + }, "node_modules/split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -19139,11 +19181,11 @@ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, "node_modules/strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=", "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, "node_modules/string_decoder": { @@ -29520,6 +29562,11 @@ "to-regex-range": "^5.0.1" } }, + "filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=" + }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -33263,6 +33310,22 @@ "prepend-http": "^1.0.0", "query-string": "^4.1.0", "sort-keys": "^1.0.0" + }, + "dependencies": { + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + } } }, "npm-run-path": { @@ -35052,12 +35115,14 @@ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", + "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==", "requires": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" + "decode-uri-component": "^0.2.0", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" } }, "querystring": { @@ -36751,6 +36816,11 @@ } } }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -36857,9 +36927,9 @@ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" }, "string_decoder": { "version": "1.1.1", diff --git a/package.json b/package.json index ea98178..819f4fe 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "axios": "^0.24.0", "google-map-react": "^2.1.10", "prop-types": "^15.8.1", + "query-string": "^7.1.1", "rc-scrollbars": "^1.1.3", "react": "^17.0.2", "react-custom-scrollbars": "^4.2.1", diff --git a/src/App.js b/src/App.js index 7d4e4e8..c31c295 100644 --- a/src/App.js +++ b/src/App.js @@ -2,21 +2,22 @@ import React, { useState } from 'react'; import CssBaseline from '@mui/material/CssBaseline'; import { createTheme, ThemeProvider } from '@mui/material/styles'; -import Login from './components/views/login'; -import UserAdmin from './components/views/useradmin'; +import { getSession } from './api/auth'; + +import Login from './components/views/Login'; +import UserAdmin from './components/views/UserAdmin'; import GlobalSnack from './components/widgets/globalSnack'; import DevicesProvider from './context/devices'; import ToastProvider from './context/toast'; import UserProvider from './context/users'; -import * as authenticationController from './controllers/authentication'; function App() { const [isAuthenticated, setAuthenticated] = useState(false); - authenticationController.getSession().then((session) => { + getSession().then((session) => { const { authenticated } = session.data; setAuthenticated(authenticated); - }); + }).catch(console.error); const theme = React.useMemo( () => createTheme({ diff --git a/src/api/auth.js b/src/api/auth.js new file mode 100644 index 0000000..a012f02 --- /dev/null +++ b/src/api/auth.js @@ -0,0 +1,44 @@ +import * as request from './request'; + +export async function login(email, password) { + const response = await request.postForm('auth/login', { + email, + password, + }); + + const { success, data } = response; + if (!success) { + throw new Error(`Could not login: ${JSON.stringify(response)}`); + } + + const { jwt, user } = data; + console.debug('Logged in as', user); + request.setAccessToken(jwt); + return jwt; +} + +export async function getSession() { + const response = await request.get('auth/session'); + const { data } = response.data; + return data.user; +} + +export async function refreshAccessToken(code, provider) { + const resp = await request.postForm('session', { + code, + provider, + }); + + const { access_token: accessToken } = resp; + if (accessToken) { + request.setAccessToken(accessToken); + return accessToken; + } else if (resp.response) { + throw new Error(`Could not exchange oauth code for access token: response ${resp.response}`); + } else if (resp.error) { + throw new Error(`Could not exchange oauth code for access token: error ${resp.error}`); + } else { + console.warn('refreshAccessToken: unexpected response', resp); + throw new Error('Could not exchange oauth code for access token'); + } +} diff --git a/src/api/devices.js b/src/api/devices.js new file mode 100644 index 0000000..d9d5ea6 --- /dev/null +++ b/src/api/devices.js @@ -0,0 +1,7 @@ +import * as request from './request'; + +export async function listDevices() { + return request.get('devices'); +} + +export default null; diff --git a/src/api/instance.js b/src/api/instance.js new file mode 100644 index 0000000..a7d6ca3 --- /dev/null +++ b/src/api/instance.js @@ -0,0 +1,56 @@ +import qs from 'query-string'; + +export default class RequestConfig { + constructor(baseUrl = process.env.REACT_APP_API_URL) { + this.baseUrl = `${baseUrl}${!baseUrl.endsWith('/') ? '/' : ''}api/`; + this.defaultHeaders = { + 'Content-Type': 'application/json', + }; + } + + setAccessToken(accessToken) { + if (accessToken) { + this.defaultHeaders.Authorization = `JWT ${accessToken}`; + } + } + + async request(method, path, params, dataJson = true, responseJson = true) { + const headers = { ...this.defaultHeaders }; + if (!dataJson) { + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + + let url = this.baseUrl + path; + let body; + if (params && Object.keys(params).length > 0) { + if (method === 'GET' || method === 'HEAD') { + url += `?${qs.stringify(params)}`; + } else if (dataJson) { + body = JSON.stringify(params); + } else { + body = qs.stringify(params); + } + } + + console.debug(`fetch ${method} ${url}`); + const response = await fetch(url, { + method, + headers, + body, + }); + if (!response.ok) { + const error = await response.text(); + throw new Error(`${response.status} ${response.statusText}: ${error}`); + } else if (!responseJson) { + return response; + } + return response.json(); + } +} + +['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'].forEach((method) => { + const methodName = method.toLowerCase(); + RequestConfig.prototype[methodName] = async function (path, params, dataJson, responseJson) { + return this.request(method, path, params, dataJson, responseJson); + }; +}); diff --git a/src/api/request.js b/src/api/request.js new file mode 100644 index 0000000..2f1498d --- /dev/null +++ b/src/api/request.js @@ -0,0 +1,31 @@ +import RequestConfig from './instance'; + +const request = new RequestConfig(); + +export function setAccessToken(accessToken) { + request.setAccessToken(accessToken); +} + +export async function get(endpoint, data) { + return request.get(endpoint, data); +} + +export async function post(endpoint, data) { + return request.post(endpoint, data); +} + +export async function postForm(endpoint, data) { + return request.post(endpoint, data, false); +} + +export async function patch(endpoint, data) { + return request.patch(endpoint, data); +} + +export async function put(endpoint, data) { + return request.put(endpoint, data); +} + +export async function del(endpoint, data) { + return request.delete(endpoint, data); +} diff --git a/src/components/views/home.jsx b/src/components/views/Home.jsx similarity index 100% rename from src/components/views/home.jsx rename to src/components/views/Home.jsx diff --git a/src/components/views/login.jsx b/src/components/views/Login.jsx similarity index 88% rename from src/components/views/login.jsx rename to src/components/views/Login.jsx index 5ee1317..367d1e7 100644 --- a/src/components/views/login.jsx +++ b/src/components/views/Login.jsx @@ -9,22 +9,21 @@ import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import { UserContext } from '../../context/users'; +import { login } from '../../api/auth'; -export default function SignIn() { +export default function Login() { const [loading, setLoading] = useState(false); const [state, dispatch] = useContext(UserContext); console.log('component', state); - const handleSubmit = (event) => { + const handleSubmit = async (event) => { dispatch({ type: 'toggle_button' }); event.preventDefault(); const data = new FormData(event.currentTarget); - // eslint-disable-next-line no-console - console.log({ - email: data.get('email'), - password: data.get('password'), - }); + login(data.get('email'), data.get('password')) + .then((result) => console.log('result', result)) + .catch(console.error); setLoading(true); }; @@ -80,7 +79,7 @@ export default function SignIn() { Sign In - + New Here or Forgotten password? diff --git a/src/components/views/useradmin.jsx b/src/components/views/UserAdmin.jsx similarity index 100% rename from src/components/views/useradmin.jsx rename to src/components/views/UserAdmin.jsx diff --git a/src/context/devices/index.js b/src/context/devices/index.js index d492472..fae1cc1 100644 --- a/src/context/devices/index.js +++ b/src/context/devices/index.js @@ -6,7 +6,7 @@ import React, { } from 'react'; import PropTypes from 'prop-types'; -import * as deviceController from '../../controllers/devices'; +import { listDevices } from '../../api/devices'; import ACTIONS from './actions'; import reducer from './reducer'; @@ -32,11 +32,11 @@ function DevicesProvider({ children }) { } }; - deviceController.getAllDevices().then((devices) => { + listDevices().then((devices) => { console.log('devices store', devices); dispatch({ type: ACTIONS.FETCH_ALL_DONGLES, data: devices }); - }); + }).catch(console.error); return () => { // Clean up the websocket