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