From 449d3c91ce4e7be3915fea68bfff7638b7ceab2f Mon Sep 17 00:00:00 2001 From: rjwats Date: Tue, 9 Jun 2020 21:57:44 +0100 Subject: [PATCH] Allow features to be disabled at build time (#143) * Add framework for built-time feature selection * Allow MQTT, NTP, OTA features to be disabled at build time * Allow Project screens to be disabled at build time * Allow security features to be disabled at build time * Switch to std::function for StatefulService function aliases for greater flexibility * Bump various UI lib versions * Update docs --- README.md | 38 +++- features.ini | 7 + interface/.env.development | 4 +- interface/package-lock.json | 208 +++++++++++++++--- interface/package.json | 10 +- interface/src/App.tsx | 11 +- interface/src/AppRouting.tsx | 33 ++- interface/src/api/Endpoints.ts | 1 + .../src/authentication/Authentication.ts | 7 +- .../authentication/AuthenticationWrapper.tsx | 36 +-- .../authentication/UnauthenticatedRoute.tsx | 12 +- interface/src/components/ApplicationError.tsx | 57 +++++ .../src/components/FullScreenLoading.tsx | 32 +++ interface/src/components/MenuAppBar.tsx | 122 +++++----- interface/src/components/RestController.tsx | 2 +- interface/src/features/ApplicationContext.tsx | 23 ++ interface/src/features/FeaturesContext.tsx | 27 +++ interface/src/features/FeaturesWrapper.tsx | 61 +++++ interface/src/features/types.ts | 7 + interface/src/system/System.tsx | 15 +- lib/framework/APSettingsService.h | 2 +- lib/framework/AuthenticationService.cpp | 4 + lib/framework/AuthenticationService.h | 6 +- lib/framework/ESP8266React.cpp | 35 ++- lib/framework/ESP8266React.h | 32 ++- lib/framework/Features.h | 31 +++ lib/framework/FeaturesService.cpp | 37 ++++ lib/framework/FeaturesService.h | 29 +++ lib/framework/RestartService.h | 2 +- lib/framework/SecurityManager.h | 16 +- lib/framework/SecuritySettingsService.cpp | 39 +++- lib/framework/SecuritySettingsService.h | 20 +- lib/framework/StatefulService.h | 8 +- lib/framework/SystemStatus.h | 2 +- lib/framework/WiFiScanner.cpp | 2 +- platformio.ini | 5 +- 36 files changed, 795 insertions(+), 188 deletions(-) create mode 100644 features.ini create mode 100644 interface/src/components/ApplicationError.tsx create mode 100644 interface/src/components/FullScreenLoading.tsx create mode 100644 interface/src/features/ApplicationContext.tsx create mode 100644 interface/src/features/FeaturesContext.tsx create mode 100644 interface/src/features/FeaturesWrapper.tsx create mode 100644 interface/src/features/types.ts create mode 100644 lib/framework/Features.h create mode 100644 lib/framework/FeaturesService.cpp create mode 100644 lib/framework/FeaturesService.h diff --git a/README.md b/README.md index 075108d..d780ccc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://travis-ci.org/rjwats/esp8266-react.svg?branch=master)](https://travis-ci.org/rjwats/esp8266-react) -A simple, secure and extensible framework for IoT projects built on ESP8266/ESP32 platforms with responsive React front-end. +A simple, secure and extensible framework for IoT projects built on ESP8266/ESP32 platforms with responsive [React](https://reactjs.org/) front-end built with [Material-UI](https://material-ui.com/). Designed to work with the PlatformIO IDE with [limited setup](#getting-started). Please read below for setup, build and upload instructions. @@ -19,9 +19,7 @@ Provides many of the features required for IoT projects: * Remote Firmware Updates - Enable secured OTA updates * Security - Protected RESTful endpoints and a secured user interface -The back end is provided by a set of RESTful endpoints and the responsive React based front end is built using [Material-UI](https://material-ui.com/). - -The front end has the prerequisite manifest file and icon, so it can be added to the home screen of a mobile device if required. +Features may be [enabled or disabled](#selecting-features) as required at compile time. ## Getting Started @@ -139,7 +137,7 @@ REACT_APP_HTTP_ROOT=http://192.168.0.99 REACT_APP_WEB_SOCKET_ROOT=ws://192.168.0.99 ``` -The `REACT_APP_HTTP_ROOT` and `REACT_APP_WEB_SOCKET_ROOT` properties can be modified to point a ESP device running the back end firmware. +The `REACT_APP_HTTP_ROOT` and `REACT_APP_WEB_SOCKET_ROOT` properties can be modified to point a ESP device running the back end. > **Tip**: You must restart the development server for changes to the environment file to come into effect. @@ -152,9 +150,31 @@ You can enable CORS on the back end by uncommenting the -D ENABLE_CORS build fla -D CORS_ORIGIN=\"http://localhost:3000\" ``` +## Selecting features + +Many of the framework's built in features may be enabled or disabled as required at compile time. This can help save sketch space and memory if your project does not require the full suite of features. The access point and WiFi management features are "core features" and are always enabled. Feature selection may be controlled with the build flags defined in [features.ini](features.ini). + +Customize the settings as you see fit. A value of 0 will disable the specified feature: + +```ini + -D FT_PROJECT=1 + -D FT_SECURITY=1 + -D FT_MQTT=1 + -D FT_NTP=1 + -D FT_OTA=1 +``` + +Flag | Description +------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- +FT_PROJECT | Controls whether the "project" section of the UI is enabled. Disable this if you don't intend to have your own screens in the UI. +FT_SECURITY | Controls whether the [security features](#security-features) are enabled. Disabling this means you won't need to authenticate to access the device and all authentication predicates will be bypassed. +FT_MQTT | Controls whether the MQTT features are enabled. Disable this if your project does not require MQTT support. +FT_NTP | Controls whether network time protocol synchronization features are enabled. Disable this if your project does not require accurate time. +FT_OTA | Controls whether OTA update support is enabled. Disable this if you won't be using the remote update feature. + ## Factory settings -The firmware has built-in factory settings which act as default values for the various configurable services where settings are not saved on the file system. These settings can be overridden using the build flags defined in [factory_settings.ini](factory_settings.ini). +The framework has built-in factory settings which act as default values for the various configurable services where settings are not saved on the file system. These settings can be overridden using the build flags defined in [factory_settings.ini](factory_settings.ini). Customize the settings as you see fit, for example you might configure your home WiFi network as the factory default: @@ -193,7 +213,7 @@ Changing factory time zone setting is a common requirement. This requires a litt ### Device ID factory defaults -If not overridden with a build flag, the firmware will use the device ID to generate factory defaults for settings such as the JWT secret and MQTT client ID. +If not overridden with a build flag, the framework will use the device ID to generate factory defaults for settings such as the JWT secret and MQTT client ID. > **Tip**: Random values are generally better defaults for these settings, so it is recommended you leave these flags undefined. @@ -481,7 +501,7 @@ class LightStateService : public StatefulService { }; ``` -Endpoint security is provided by authentication predicates which are [documented below](#security-features). The SecurityManager and authentication predicate may be provided if a secure endpoint is required. The demo project shows how endpoints can be secured. +Endpoint security is provided by authentication predicates which are [documented below](#security-features). The SecurityManager and authentication predicate may be provided if a secure endpoint is required. The placeholder project shows how endpoints can be secured. #### Persistence @@ -519,7 +539,7 @@ class LightStateService : public StatefulService { }; ``` -WebSocket security is provided by authentication predicates which are [documented below](#security-features). The SecurityManager and authentication predicate may be provided if a secure WebSocket is required. The demo project shows how WebSockets can be secured. +WebSocket security is provided by authentication predicates which are [documented below](#security-features). The SecurityManager and authentication predicate may be provided if a secure WebSocket is required. The placeholder project shows how WebSockets can be secured. #### MQTT diff --git a/features.ini b/features.ini new file mode 100644 index 0000000..e5a5078 --- /dev/null +++ b/features.ini @@ -0,0 +1,7 @@ +[features] +build_flags = + -D FT_PROJECT=1 + -D FT_SECURITY=1 + -D FT_MQTT=1 + -D FT_NTP=1 + -D FT_OTA=1 diff --git a/interface/.env.development b/interface/.env.development index 7aaf530..b12cfd0 100644 --- a/interface/.env.development +++ b/interface/.env.development @@ -1,4 +1,4 @@ # Change the IP address to that of your ESP device to enable local development of the UI. # Remember to also enable CORS in platformio.ini before uploading the code to the device. -REACT_APP_HTTP_ROOT=http://192.168.0.99 -REACT_APP_WEB_SOCKET_ROOT=ws://192.168.0.99 +REACT_APP_HTTP_ROOT=http://192.168.0.88 +REACT_APP_WEB_SOCKET_ROOT=ws://192.168.0.88 diff --git a/interface/package-lock.json b/interface/package-lock.json index 01ebe7c..fb4c79a 100644 --- a/interface/package-lock.json +++ b/interface/package-lock.json @@ -1403,6 +1403,21 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" }, + "@npmcli/move-file": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz", + "integrity": "sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==", + "requires": { + "mkdirp": "^1.0.4" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + } + } + }, "@svgr/babel-plugin-add-jsx-attribute": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz", @@ -3324,11 +3339,6 @@ } } }, - "classnames": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" - }, "clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", @@ -3538,16 +3548,122 @@ } }, "compression-webpack-plugin": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-3.1.0.tgz", - "integrity": "sha512-iqTHj3rADN4yHwXMBrQa/xrncex/uEQy8QHlaTKxGchT/hC0SdlJlmL/5eRqffmWq2ep0/Romw6Ld39JjTR/ug==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-4.0.0.tgz", + "integrity": "sha512-DRoFQNTkQ8gadlk117Y2wxANU+MDY56b1FIZj/yJXucBOTViTHXjthM7G9ocnitksk4kLzt1N2RLF0gDjxI+hg==", "requires": { - "cacache": "^13.0.1", - "find-cache-dir": "^3.0.0", - "neo-async": "^2.5.0", - "schema-utils": "^2.6.1", - "serialize-javascript": "^2.1.2", - "webpack-sources": "^1.0.1" + "cacache": "^15.0.3", + "find-cache-dir": "^3.3.1", + "schema-utils": "^2.6.6", + "serialize-javascript": "^3.0.0", + "webpack-sources": "^1.4.3" + }, + "dependencies": { + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "cacache": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.4.tgz", + "integrity": "sha512-YlnKQqTbD/6iyoJvEY3KJftjrdBYroCbxxYXzhOzsFLWlp6KX4BOlEf4mTx0cMUfVaTS3ENL2QtDWeRYoGLkkw==", + "requires": { + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.0", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "requires": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + } + }, + "serialize-javascript": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", + "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "ssri": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", + "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", + "requires": { + "minipass": "^3.1.1" + } + } } }, "concat-map": { @@ -8122,6 +8238,15 @@ "minipass": "^3.0.0" } }, + "minizlib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.0.tgz", + "integrity": "sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, "mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -8183,9 +8308,9 @@ } }, "moment": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz", + "integrity": "sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==" }, "move-concurrently": { "version": "1.0.1", @@ -8415,14 +8540,12 @@ } }, "notistack": { - "version": "0.9.7", - "resolved": "https://registry.npmjs.org/notistack/-/notistack-0.9.7.tgz", - "integrity": "sha512-OztbtaIiCMR7QdcDGXTcYu96Uuvu26k41d7cnMGdf4NaKkAX06fsLPAlodGPj4HrMjMBUl8nvUQ3LmQOS6kHWQ==", + "version": "0.9.16", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-0.9.16.tgz", + "integrity": "sha512-+q1KKj2XkU+mKnbp9PbVkRLSLfVYnPJGi+MHT+N9Pm3nZUMVtbjDFodwdv/RoEldvkXKCROnecayUFMwLOiIQA==", "requires": { - "classnames": "^2.2.6", - "hoist-non-react-statics": "^3.3.0", - "prop-types": "^15.7.2", - "react-is": "^16.8.6" + "clsx": "^1.1.0", + "hoist-non-react-statics": "^3.3.0" } }, "npm-run-path": { @@ -10227,9 +10350,9 @@ } }, "react-app-rewired": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/react-app-rewired/-/react-app-rewired-2.1.5.tgz", - "integrity": "sha512-Gr8KfCeL9/PTQs8Vvxc7v8wQ9vCFMnYPhcAkrMlzkLiMFXS+BgSwm11MoERjZm7dpA2WjTi+Pvbu/w7rujAV+A==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/react-app-rewired/-/react-app-rewired-2.1.6.tgz", + "integrity": "sha512-06flj0kK5tf/RN4naRv/sn6j3sQd7rsURoRLKLpffXDzJeNiAaTNic+0I8Basojy5WDwREkTqrMLewSAjcb13w==", "dev": true, "requires": { "semver": "^5.6.0" @@ -12099,6 +12222,31 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" }, + "tar": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.2.tgz", + "integrity": "sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.0", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + } + } + }, "terser": { "version": "4.6.7", "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.7.tgz", @@ -12354,9 +12502,9 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "typescript": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", - "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==" + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.5.tgz", + "integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==" }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", diff --git a/interface/package.json b/interface/package.json index f6e42f6..49e381c 100644 --- a/interface/package.json +++ b/interface/package.json @@ -13,12 +13,12 @@ "@types/react-material-ui-form-validator": "^2.0.5", "@types/react-router": "^5.1.3", "@types/react-router-dom": "^5.1.3", - "compression-webpack-plugin": "^3.0.1", + "compression-webpack-plugin": "^4.0.0", "jwt-decode": "^2.2.0", "lodash": "^4.17.15", "mime-types": "^2.1.25", - "moment": "^2.24.0", - "notistack": "^0.9.7", + "moment": "^2.26.0", + "notistack": "^0.9.16", "react": "^16.13.1", "react-dom": "^16.13.1", "react-form-validator-core": "^0.6.4", @@ -27,7 +27,7 @@ "react-router-dom": "^5.1.2", "react-scripts": "3.4.1", "sockette": "^2.0.6", - "typescript": "^3.7.5", + "typescript": "^3.9.5", "zlib": "^1.0.5" }, "scripts": { @@ -51,6 +51,6 @@ ] }, "devDependencies": { - "react-app-rewired": "^2.1.5" + "react-app-rewired": "^2.1.6" } } diff --git a/interface/src/App.tsx b/interface/src/App.tsx index 97004b1..532050e 100644 --- a/interface/src/App.tsx +++ b/interface/src/App.tsx @@ -8,6 +8,7 @@ import CloseIcon from '@material-ui/icons/Close'; import AppRouting from './AppRouting'; import CustomMuiTheme from './CustomMuiTheme'; import { PROJECT_NAME } from './api'; +import FeaturesWrapper from './features/FeaturesWrapper'; // this redirect forces a call to authenticationContext.refresh() which invalidates the JWT if it is invalid. const unauthorizedRedirect = () => ; @@ -34,10 +35,12 @@ class App extends Component { )}> - - - - + + + + + + ); diff --git a/interface/src/AppRouting.tsx b/interface/src/AppRouting.tsx index 48eac82..f17d5b5 100644 --- a/interface/src/AppRouting.tsx +++ b/interface/src/AppRouting.tsx @@ -16,30 +16,45 @@ import System from './system/System'; import { PROJECT_PATH } from './api'; import Mqtt from './mqtt/Mqtt'; +import { withFeatures, WithFeaturesProps } from './features/FeaturesContext'; +import { Features } from './features/types'; -class AppRouting extends Component { +export const getDefaultRoute = (features: Features) => features.project ? `/${PROJECT_PATH}/` : "/wifi/"; + +class AppRouting extends Component { componentDidMount() { Authentication.clearLoginRedirect(); } render() { + const { features } = this.props; return ( - - - + {features.security && ( + + )} + {features.project && ( + + )} + + {features.ntp && ( - - - - + )} + {features.mqtt && ( + + )} + {features.security && ( + + )} + + ) } } -export default AppRouting; +export default withFeatures(AppRouting); diff --git a/interface/src/api/Endpoints.ts b/interface/src/api/Endpoints.ts index b16d564..c81ccc8 100644 --- a/interface/src/api/Endpoints.ts +++ b/interface/src/api/Endpoints.ts @@ -1,5 +1,6 @@ import { ENDPOINT_ROOT } from './Env'; +export const FEATURES_ENDPOINT = ENDPOINT_ROOT + "features"; export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus"; export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings"; export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "apSettings"; diff --git a/interface/src/authentication/Authentication.ts b/interface/src/authentication/Authentication.ts index 8b3eaf3..51b3306 100644 --- a/interface/src/authentication/Authentication.ts +++ b/interface/src/authentication/Authentication.ts @@ -1,7 +1,8 @@ import * as H from 'history'; import history from '../history'; -import { PROJECT_PATH } from '../api'; +import { Features } from '../features/types'; +import { getDefaultRoute } from '../AppRouting'; export const ACCESS_TOKEN = 'access_token'; export const LOGIN_PATHNAME = 'loginPathname'; @@ -26,12 +27,12 @@ export function clearLoginRedirect() { getStorage().removeItem(LOGIN_SEARCH); } -export function fetchLoginRedirect(): H.LocationDescriptorObject { +export function fetchLoginRedirect(features: Features): H.LocationDescriptorObject { const loginPathname = getStorage().getItem(LOGIN_PATHNAME); const loginSearch = getStorage().getItem(LOGIN_SEARCH); clearLoginRedirect(); return { - pathname: loginPathname || `/${PROJECT_PATH}/`, + pathname: loginPathname || getDefaultRoute(features), search: (loginPathname && loginSearch) || undefined }; } diff --git a/interface/src/authentication/AuthenticationWrapper.tsx b/interface/src/authentication/AuthenticationWrapper.tsx index 21584a4..266368a 100644 --- a/interface/src/authentication/AuthenticationWrapper.tsx +++ b/interface/src/authentication/AuthenticationWrapper.tsx @@ -2,37 +2,21 @@ import * as React from 'react'; import { withSnackbar, WithSnackbarProps } from 'notistack'; import jwtDecode from 'jwt-decode'; -import CircularProgress from '@material-ui/core/CircularProgress'; -import Typography from '@material-ui/core/Typography'; -import { withStyles, Theme, createStyles, WithStyles } from '@material-ui/core/styles'; - import history from '../history' import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api'; import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication'; import { AuthenticationContext, Me } from './AuthenticationContext'; +import FullScreenLoading from '../components/FullScreenLoading'; +import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext'; export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken); -const styles = (theme: Theme) => createStyles({ - loadingPanel: { - padding: theme.spacing(2), - display: "flex", - alignItems: "center", - justifyContent: "center", - height: "100vh", - flexDirection: "column" - }, - progress: { - margin: theme.spacing(4), - } -}); - interface AuthenticationWrapperState { context: AuthenticationContext; initialized: boolean; } -type AuthenticationWrapperProps = WithSnackbarProps & WithStyles; +type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps; class AuthenticationWrapper extends React.Component { @@ -69,18 +53,16 @@ class AuthenticationWrapper extends React.Component - - - Loading... - - + ); } refresh = () => { + if (!this.props.features.security) { + this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } }); + return; + } const accessToken = getStorage().getItem(ACCESS_TOKEN) if (accessToken) { authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT) @@ -124,4 +106,4 @@ class AuthenticationWrapper extends React.Component> | React.ComponentType; } type RenderComponent = (props: RouteComponentProps) => React.ReactNode; -class UnauthenticatedRoute extends Route { +class UnauthenticatedRoute extends Route { + public render() { - const { authenticationContext, component:Component, ...rest } = this.props; + const { authenticationContext, component: Component, features, ...rest } = this.props; const renderComponent: RenderComponent = (props) => { if (authenticationContext.me) { - return (); + return (); } return (); } @@ -25,4 +27,4 @@ class UnauthenticatedRoute extends Route = ({ error }) => { + const classes = styles(); + return ( +
+ + + + + +  Application error + + + + Failed to configure the application, please refresh to try again. + + {error && + ( + + Error: {error} + + ) + } + +
+ ); +} + +export default ApplicationError; diff --git a/interface/src/components/FullScreenLoading.tsx b/interface/src/components/FullScreenLoading.tsx new file mode 100644 index 0000000..43a34fe --- /dev/null +++ b/interface/src/components/FullScreenLoading.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import { Typography, Theme } from '@material-ui/core'; +import { makeStyles, createStyles } from '@material-ui/styles'; + +const useStyles = makeStyles((theme: Theme) => createStyles({ + fullScreenLoading: { + padding: theme.spacing(2), + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "100vh", + flexDirection: "column" + }, + progress: { + margin: theme.spacing(4), + } +})); + +const FullScreenLoading = () => { + const classes = useStyles(); + return ( +
+ + + Loading … + +
+ ) +} + +export default FullScreenLoading; diff --git a/interface/src/components/MenuAppBar.tsx b/interface/src/components/MenuAppBar.tsx index daeafc7..dfe5f22 100644 --- a/interface/src/components/MenuAppBar.tsx +++ b/interface/src/components/MenuAppBar.tsx @@ -1,4 +1,4 @@ -import React, { RefObject } from 'react'; +import React, { RefObject, Fragment } from 'react'; import { Link, withRouter, RouteComponentProps } from 'react-router-dom'; import { Drawer, AppBar, Toolbar, Avatar, Divider, Button, Box, IconButton } from '@material-ui/core'; @@ -20,6 +20,7 @@ import MenuIcon from '@material-ui/icons/Menu'; import ProjectMenu from '../project/ProjectMenu'; import { PROJECT_NAME } from '../api'; import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication'; +import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext'; const drawerWidth = 290; @@ -82,7 +83,7 @@ interface MenuAppBarState { authMenuOpen: boolean; } -interface MenuAppBarProps extends AuthenticatedContextProps, WithTheme, WithStyles, RouteComponentProps { +interface MenuAppBarProps extends WithFeaturesProps, AuthenticatedContextProps, WithTheme, WithStyles, RouteComponentProps { sectionTitle: string; } @@ -114,7 +115,7 @@ class MenuAppBar extends React.Component { }; render() { - const { classes, theme, children, sectionTitle, authenticatedContext } = this.props; + const { classes, theme, children, sectionTitle, authenticatedContext, features } = this.props; const { mobileOpen, authMenuOpen } = this.state; const path = this.props.match.url; const drawer = ( @@ -128,9 +129,12 @@ class MenuAppBar extends React.Component { - - - + {features.project && ( + + + + + )} @@ -144,24 +148,30 @@ class MenuAppBar extends React.Component { + {features.ntp && ( - - - - - - - - - - - - + )} + {features.mqtt && ( + + + + + + + )} + {features.security && ( + + + + + + + )} @@ -172,6 +182,42 @@ class MenuAppBar extends React.Component { ); + const userMenu = ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); + return (
@@ -188,39 +234,7 @@ class MenuAppBar extends React.Component { {sectionTitle} -
- - - - - - - - - - - - - - - - - - - - - - - - - -
+ {features.security && userMenu}