Browse Source

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
master
rjwats 4 years ago
committed by GitHub
parent
commit
449d3c91ce
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 38
      README.md
  2. 7
      features.ini
  3. 4
      interface/.env.development
  4. 208
      interface/package-lock.json
  5. 10
      interface/package.json
  6. 11
      interface/src/App.tsx
  7. 33
      interface/src/AppRouting.tsx
  8. 1
      interface/src/api/Endpoints.ts
  9. 7
      interface/src/authentication/Authentication.ts
  10. 36
      interface/src/authentication/AuthenticationWrapper.tsx
  11. 12
      interface/src/authentication/UnauthenticatedRoute.tsx
  12. 57
      interface/src/components/ApplicationError.tsx
  13. 32
      interface/src/components/FullScreenLoading.tsx
  14. 122
      interface/src/components/MenuAppBar.tsx
  15. 2
      interface/src/components/RestController.tsx
  16. 23
      interface/src/features/ApplicationContext.tsx
  17. 27
      interface/src/features/FeaturesContext.tsx
  18. 61
      interface/src/features/FeaturesWrapper.tsx
  19. 7
      interface/src/features/types.ts
  20. 15
      interface/src/system/System.tsx
  21. 2
      lib/framework/APSettingsService.h
  22. 4
      lib/framework/AuthenticationService.cpp
  23. 6
      lib/framework/AuthenticationService.h
  24. 35
      lib/framework/ESP8266React.cpp
  25. 32
      lib/framework/ESP8266React.h
  26. 31
      lib/framework/Features.h
  27. 37
      lib/framework/FeaturesService.cpp
  28. 29
      lib/framework/FeaturesService.h
  29. 2
      lib/framework/RestartService.h
  30. 16
      lib/framework/SecurityManager.h
  31. 39
      lib/framework/SecuritySettingsService.cpp
  32. 20
      lib/framework/SecuritySettingsService.h
  33. 8
      lib/framework/StatefulService.h
  34. 2
      lib/framework/SystemStatus.h
  35. 2
      lib/framework/WiFiScanner.cpp
  36. 5
      platformio.ini

38
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<LightState> {
};
```
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<LightState> {
};
```
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

7
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

4
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

208
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",

10
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"
}
}

11
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 = () => <Redirect to="/" />;
@ -34,10 +35,12 @@ class App extends Component {
<CloseIcon />
</IconButton>
)}>
<Switch>
<Route exact path="/unauthorized" component={unauthorizedRedirect} />
<Route component={AppRouting} />
</Switch>
<FeaturesWrapper>
<Switch>
<Route exact path="/unauthorized" component={unauthorizedRedirect} />
<Route component={AppRouting} />
</Switch>
</FeaturesWrapper>
</SnackbarProvider>
</CustomMuiTheme>
);

33
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<WithFeaturesProps> {
componentDidMount() {
Authentication.clearLoginRedirect();
}
render() {
const { features } = this.props;
return (
<AuthenticationWrapper>
<Switch>
<UnauthenticatedRoute exact path="/" component={SignIn} />
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
<AuthenticatedRoute exact path="/wifi/*" component={WiFiConnection} />
{features.security && (
<UnauthenticatedRoute exact path="/" component={SignIn} />
)}
{features.project && (
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
)}
<AuthenticatedRoute exact path="/wifi/*" component={WiFiConnection} />
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
{features.ntp && (
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
<AuthenticatedRoute exact path="/mqtt/*" component={Mqtt} />
<AuthenticatedRoute exact path="/security/*" component={Security} />
<AuthenticatedRoute exact path="/system/*" component={System} />
<Redirect to="/" />
)}
{features.mqtt && (
<AuthenticatedRoute exact path="/mqtt/*" component={Mqtt} />
)}
{features.security && (
<AuthenticatedRoute exact path="/security/*" component={Security} />
)}
<AuthenticatedRoute exact path="/system/*" component={System} />
<Redirect to={getDefaultRoute(features)} />
</Switch>
</AuthenticationWrapper>
)
}
}
export default AppRouting;
export default withFeatures(AppRouting);

1
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";

7
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
};
}

36
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<typeof styles>;
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, AuthenticationWrapperState> {
@ -69,18 +53,16 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
}
renderContentLoading() {
const { classes } = this.props;
return (
<div className={classes.loadingPanel}>
<CircularProgress className={classes.progress} size={100} />
<Typography variant="h4" >
Loading...
</Typography>
</div>
<FullScreenLoading />
);
}
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<AuthenticationWrapperProps,
}
export default withStyles(styles)(withSnackbar(AuthenticationWrapper))
export default withFeatures(withSnackbar(AuthenticationWrapper))

12
interface/src/authentication/UnauthenticatedRoute.tsx

@ -3,19 +3,21 @@ import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-d
import { withAuthenticationContext, AuthenticationContextProps } from './AuthenticationContext';
import * as Authentication from './Authentication';
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
interface UnauthenticatedRouteProps extends RouteProps {
interface UnauthenticatedRouteProps extends RouteProps, AuthenticationContextProps, WithFeaturesProps {
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
}
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps & AuthenticationContextProps> {
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
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 (<Redirect to={Authentication.fetchLoginRedirect()} />);
return (<Redirect to={Authentication.fetchLoginRedirect(features)} />);
}
return (<Component {...props} />);
}
@ -25,4 +27,4 @@ class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps & Authenticat
}
}
export default withAuthenticationContext(UnauthenticatedRoute);
export default withFeatures(withAuthenticationContext(UnauthenticatedRoute));

57
interface/src/components/ApplicationError.tsx

@ -0,0 +1,57 @@
import React, { FC } from 'react';
import { makeStyles } from '@material-ui/styles';
import { Paper, Typography, Box, CssBaseline } from "@material-ui/core";
import WarningIcon from "@material-ui/icons/Warning"
const styles = makeStyles(
{
siteErrorPage: {
display: "flex",
height: "100vh",
justifyContent: "center",
flexDirection: "column"
},
siteErrorPagePanel: {
textAlign: "center",
padding: "280px 0 40px 0",
backgroundImage: 'url("/app/icon.png")',
backgroundRepeat: "no-repeat",
backgroundPosition: "50% 40px",
backgroundSize: "200px auto",
width: "100%",
}
}
);
interface ApplicationErrorProps {
error?: string;
}
const ApplicationError: FC<ApplicationErrorProps> = ({ error }) => {
const classes = styles();
return (
<div className={classes.siteErrorPage}>
<CssBaseline />
<Paper className={classes.siteErrorPagePanel} elevation={10}>
<Box display="flex" flexDirection="row" justifyContent="center">
<WarningIcon fontSize="large" color="error" />
<Typography variant="h4" gutterBottom>
&nbsp;Application error
</Typography>
</Box>
<Typography variant="subtitle1" gutterBottom>
Failed to configure the application, please refresh to try again.
</Typography>
{error &&
(
<Typography variant="subtitle2" gutterBottom>
Error: {error}
</Typography>
)
}
</Paper>
</div>
);
}
export default ApplicationError;

32
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 (
<div className={classes.fullScreenLoading}>
<CircularProgress className={classes.progress} size={100} />
<Typography variant="h4">
Loading &hellip;
</Typography>
</div>
)
}
export default FullScreenLoading;

122
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<typeof styles>, RouteComponentProps {
interface MenuAppBarProps extends WithFeaturesProps, AuthenticatedContextProps, WithTheme, WithStyles<typeof styles>, RouteComponentProps {
sectionTitle: string;
}
@ -114,7 +115,7 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
};
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<MenuAppBarProps, MenuAppBarState> {
</Typography>
<Divider absolute />
</Toolbar>
<Divider />
<ProjectMenu />
<Divider />
{features.project && (
<Fragment>
<ProjectMenu />
<Divider />
</Fragment>
)}
<List>
<ListItem to='/wifi/' selected={path.startsWith('/wifi/')} button component={Link}>
<ListItemIcon>
@ -144,24 +148,30 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
</ListItemIcon>
<ListItemText primary="Access Point" />
</ListItem>
{features.ntp && (
<ListItem to='/ntp/' selected={path.startsWith('/ntp/')} button component={Link}>
<ListItemIcon>
<AccessTimeIcon />
</ListItemIcon>
<ListItemText primary="Network Time" />
</ListItem>
<ListItem to='/mqtt/' selected={path.startsWith('/mqtt/')} button component={Link}>
<ListItemIcon>
<DeviceHubIcon />
</ListItemIcon>
<ListItemText primary="MQTT" />
</ListItem>
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticatedContext.me.admin}>
<ListItemIcon>
<LockIcon />
</ListItemIcon>
<ListItemText primary="Security" />
</ListItem>
)}
{features.mqtt && (
<ListItem to='/mqtt/' selected={path.startsWith('/mqtt/')} button component={Link}>
<ListItemIcon>
<DeviceHubIcon />
</ListItemIcon>
<ListItemText primary="MQTT" />
</ListItem>
)}
{features.security && (
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticatedContext.me.admin}>
<ListItemIcon>
<LockIcon />
</ListItemIcon>
<ListItemText primary="Security" />
</ListItem>
)}
<ListItem to='/system/' selected={path.startsWith('/system/')} button component={Link} >
<ListItemIcon>
<SettingsIcon />
@ -172,6 +182,42 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
</div>
);
const userMenu = (
<div>
<IconButton
ref={this.anchorRef}
aria-owns={authMenuOpen ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={this.handleToggle}
color="inherit"
>
<AccountCircleIcon />
</IconButton>
<Popper open={authMenuOpen} anchorEl={this.anchorRef.current} transition className={classes.authMenu}>
<ClickAwayListener onClickAway={this.handleClose}>
<Card id="menu-list-grow">
<CardContent>
<List disablePadding>
<ListItem disableGutters>
<ListItemAvatar>
<Avatar>
<AccountCircleIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={"Signed in as: " + authenticatedContext.me.username} secondary={authenticatedContext.me.admin ? "Admin User" : undefined} />
</ListItem>
</List>
</CardContent>
<Divider />
<CardActions className={classes.authMenuActions}>
<Button variant="contained" fullWidth color="primary" onClick={authenticatedContext.signOut}>Sign Out</Button>
</CardActions>
</Card>
</ClickAwayListener>
</Popper>
</div>
);
return (
<div className={classes.root}>
<AppBar position="fixed" className={classes.appBar} elevation={0}>
@ -188,39 +234,7 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
<Typography variant="h6" color="inherit" noWrap className={classes.title}>
{sectionTitle}
</Typography>
<div>
<IconButton
ref={this.anchorRef}
aria-owns={authMenuOpen ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={this.handleToggle}
color="inherit"
>
<AccountCircleIcon />
</IconButton>
<Popper open={authMenuOpen} anchorEl={this.anchorRef.current} transition className={classes.authMenu}>
<ClickAwayListener onClickAway={this.handleClose}>
<Card id="menu-list-grow">
<CardContent>
<List disablePadding>
<ListItem disableGutters>
<ListItemAvatar>
<Avatar>
<AccountCircleIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={"Signed in as: " + authenticatedContext.me.username} secondary={authenticatedContext.me.admin ? "Admin User" : undefined} />
</ListItem>
</List>
</CardContent>
<Divider />
<CardActions className={classes.authMenuActions}>
<Button variant="contained" fullWidth color="primary" onClick={authenticatedContext.signOut}>Sign Out</Button>
</CardActions>
</Card>
</ClickAwayListener>
</Popper>
</div>
{features.security && userMenu}
</Toolbar>
</AppBar>
<nav className={classes.drawer}>
@ -263,8 +277,10 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
export default withRouter(
withTheme(
withAuthenticatedContext(
withStyles(styles)(MenuAppBar)
withFeatures(
withAuthenticatedContext(
withStyles(styles)(MenuAppBar)
)
)
)
);

2
interface/src/components/RestController.tsx

@ -100,8 +100,8 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
render() {
return <RestController
{...this.props as P}
{...this.state}
{...this.props as P}
handleValueChange={this.handleValueChange}
setData={this.setData}
saveData={this.saveData}

23
interface/src/features/ApplicationContext.tsx

@ -0,0 +1,23 @@
import React from 'react';
import { Features } from './types';
export interface ApplicationContext {
features: Features;
}
const ApplicationContextDefaultValue = {} as ApplicationContext
export const ApplicationContext = React.createContext(
ApplicationContextDefaultValue
);
export function withAuthenticatedContexApplicationContext<T extends ApplicationContext>(Component: React.ComponentType<T>) {
return class extends React.Component<Omit<T, keyof ApplicationContext>> {
render() {
return (
<ApplicationContext.Consumer>
{authenticatedContext => <Component {...this.props as T} features={authenticatedContext} />}
</ApplicationContext.Consumer>
);
}
};
}

27
interface/src/features/FeaturesContext.tsx

@ -0,0 +1,27 @@
import React from 'react';
import { Features } from './types';
export interface FeaturesContext {
features: Features;
}
const FeaturesContextDefaultValue = {} as FeaturesContext
export const FeaturesContext = React.createContext(
FeaturesContextDefaultValue
);
export interface WithFeaturesProps {
features: Features;
}
export function withFeatures<T extends WithFeaturesProps>(Component: React.ComponentType<T>) {
return class extends React.Component<Omit<T, keyof WithFeaturesProps>> {
render() {
return (
<FeaturesContext.Consumer>
{featuresContext => <Component {...this.props as T} features={featuresContext.features} />}
</FeaturesContext.Consumer>
);
}
};
}

61
interface/src/features/FeaturesWrapper.tsx

@ -0,0 +1,61 @@
import React, { Component } from 'react';
import { Features } from './types';
import { FeaturesContext } from './FeaturesContext';
import FullScreenLoading from '../components/FullScreenLoading';
import ApplicationError from '../components/ApplicationError';
import { FEATURES_ENDPOINT } from '../api';
interface FeaturesWrapperState {
features?: Features;
error?: string;
};
class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
state: FeaturesWrapperState = {};
componentDidMount() {
this.fetchFeaturesDetails();
}
fetchFeaturesDetails = () => {
fetch(FEATURES_ENDPOINT)
.then(response => {
if (response.status === 200) {
return response.json();
} else {
throw Error("Unexpected status code: " + response.status);
}
}).then(features => {
this.setState({ features });
})
.catch(error => {
this.setState({ error: error.message });
});
}
render() {
const { features, error } = this.state;
if (features) {
return (
<FeaturesContext.Provider value={{
features
}}>
{this.props.children}
</FeaturesContext.Provider>
);
}
if (error) {
return (
<ApplicationError error={error} />
);
}
return (
<FullScreenLoading />
);
}
}
export default FeaturesWrapper;

7
interface/src/features/types.ts

@ -0,0 +1,7 @@
export interface Features {
project: boolean;
security: boolean;
mqtt: boolean;
ntp: boolean;
ota: boolean;
}

15
interface/src/system/System.tsx

@ -8,8 +8,9 @@ import { MenuAppBar } from '../components';
import SystemStatusController from './SystemStatusController';
import OTASettingsController from './OTASettingsController';
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
type SystemProps = AuthenticatedContextProps & RouteComponentProps;
type SystemProps = AuthenticatedContextProps & RouteComponentProps & WithFeaturesProps;
class System extends Component<SystemProps> {
@ -18,16 +19,20 @@ class System extends Component<SystemProps> {
};
render() {
const { authenticatedContext } = this.props;
const { authenticatedContext, features } = this.props;
return (
<MenuAppBar sectionTitle="System">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tab value="/system/status" label="System Status" />
<Tab value="/system/ota" label="OTA Settings" disabled={!authenticatedContext.me.admin} />
{features.ota && (
<Tab value="/system/ota" label="OTA Settings" disabled={!authenticatedContext.me.admin} />
)}
</Tabs>
<Switch>
<AuthenticatedRoute exact path="/system/status" component={SystemStatusController} />
<AuthenticatedRoute exact path="/system/ota" component={OTASettingsController} />
{features.ota && (
<AuthenticatedRoute exact path="/system/ota" component={OTASettingsController} />
)}
<Redirect to="/system/status" />
</Switch>
</MenuAppBar>
@ -35,4 +40,4 @@ class System extends Component<SystemProps> {
}
}
export default withAuthenticatedContext(System);
export default withFeatures(withAuthenticatedContext(System));

2
lib/framework/APSettingsService.h

@ -96,4 +96,4 @@ class APSettingsService : public StatefulService<APSettings> {
void handleDNS();
};
#endif // end APSettingsConfig_h
#endif // end APSettingsConfig_h

4
lib/framework/AuthenticationService.cpp

@ -1,5 +1,7 @@
#include <AuthenticationService.h>
#if FT_ENABLED(FT_SECURITY)
AuthenticationService::AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager) :
_securityManager(securityManager),
_signInHandler(SIGN_IN_PATH,
@ -42,3 +44,5 @@ void AuthenticationService::signIn(AsyncWebServerRequest* request, JsonVariant&
AsyncWebServerResponse* response = request->beginResponse(401);
request->send(response);
}
#endif // end FT_ENABLED(FT_SECURITY)

6
lib/framework/AuthenticationService.h

@ -1,6 +1,7 @@
#ifndef AuthenticationService_H_
#define AuthenticationService_H_
#include <Features.h>
#include <AsyncJson.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
@ -10,6 +11,8 @@
#define MAX_AUTHENTICATION_SIZE 256
#if FT_ENABLED(FT_SECURITY)
class AuthenticationService {
public:
AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager);
@ -23,4 +26,5 @@ class AuthenticationService {
void verifyAuthorization(AsyncWebServerRequest* request);
};
#endif // end SecurityManager_h
#endif // end FT_ENABLED(FT_SECURITY)
#endif // end SecurityManager_h

35
lib/framework/ESP8266React.cpp

@ -1,20 +1,29 @@
#include <ESP8266React.h>
ESP8266React::ESP8266React(AsyncWebServer* server, FS* fs) :
_featureService(server),
_securitySettingsService(server, fs),
_wifiSettingsService(server, fs, &_securitySettingsService),
_wifiScanner(server, &_securitySettingsService),
_wifiStatus(server, &_securitySettingsService),
_apSettingsService(server, fs, &_securitySettingsService),
_apStatus(server, &_securitySettingsService, &_apSettingsService),
#if FT_ENABLED(FT_NTP)
_ntpSettingsService(server, fs, &_securitySettingsService),
_ntpStatus(server, &_securitySettingsService),
#endif
#if FT_ENABLED(FT_OTA)
_otaSettingsService(server, fs, &_securitySettingsService),
#endif
#if FT_ENABLED(FT_MQTT)
_mqttSettingsService(server, fs, &_securitySettingsService),
_mqttStatus(server, &_mqttSettingsService, &_securitySettingsService),
#endif
#if FT_ENABLED(FT_SECURITY)
_authenticationService(server, &_securitySettingsService),
#endif
_restartService(server, &_securitySettingsService),
_factoryResetService(server, fs, &_securitySettingsService),
_authenticationService(server, &_securitySettingsService),
_wifiScanner(server, &_securitySettingsService),
_wifiStatus(server, &_securitySettingsService),
_ntpStatus(server, &_securitySettingsService),
_apStatus(server, &_securitySettingsService, &_apSettingsService),
_mqttStatus(server, &_mqttSettingsService, &_securitySettingsService),
_systemStatus(server, &_securitySettingsService) {
#ifdef PROGMEM_WWW
// Serve static resources from PROGMEM
@ -69,17 +78,29 @@ ESP8266React::ESP8266React(AsyncWebServer* server, FS* fs) :
}
void ESP8266React::begin() {
_securitySettingsService.begin();
_wifiSettingsService.begin();
_apSettingsService.begin();
#if FT_ENABLED(FT_NTP)
_ntpSettingsService.begin();
#endif
#if FT_ENABLED(FT_OTA)
_otaSettingsService.begin();
#endif
#if FT_ENABLED(FT_MQTT)
_mqttSettingsService.begin();
#endif
#if FT_ENABLED(FT_SECURITY)
_securitySettingsService.begin();
#endif
}
void ESP8266React::loop() {
_wifiSettingsService.loop();
_apSettingsService.loop();
#if FT_ENABLED(FT_OTA)
_otaSettingsService.loop();
#endif
#if FT_ENABLED(FT_MQTT)
_mqttSettingsService.loop();
#endif
}

32
lib/framework/ESP8266React.h

@ -11,6 +11,7 @@
#include <ESPAsyncTCP.h>
#endif
#include <FeaturesService.h>
#include <APSettingsService.h>
#include <APStatus.h>
#include <AuthenticationService.h>
@ -42,9 +43,11 @@ class ESP8266React {
return &_securitySettingsService;
}
#if FT_ENABLED(FT_SECURITY)
StatefulService<SecuritySettings>* getSecuritySettingsService() {
return &_securitySettingsService;
}
#endif
StatefulService<WiFiSettings>* getWiFiSettingsService() {
return &_wifiSettingsService;
@ -54,14 +57,19 @@ class ESP8266React {
return &_apSettingsService;
}
#if FT_ENABLED(FT_NTP)
StatefulService<NTPSettings>* getNTPSettingsService() {
return &_ntpSettingsService;
}
#endif
#if FT_ENABLED(FT_OTA)
StatefulService<OTASettings>* getOTASettingsService() {
return &_otaSettingsService;
}
#endif
#if FT_ENABLED(FT_MQTT)
StatefulService<MqttSettings>* getMqttSettingsService() {
return &_mqttSettingsService;
}
@ -69,28 +77,36 @@ class ESP8266React {
AsyncMqttClient* getMqttClient() {
return _mqttSettingsService.getMqttClient();
}
#endif
void factoryReset() {
_factoryResetService.factoryReset();
}
private:
FeaturesService _featureService;
SecuritySettingsService _securitySettingsService;
WiFiSettingsService _wifiSettingsService;
WiFiScanner _wifiScanner;
WiFiStatus _wifiStatus;
APSettingsService _apSettingsService;
APStatus _apStatus;
#if FT_ENABLED(FT_NTP)
NTPSettingsService _ntpSettingsService;
NTPStatus _ntpStatus;
#endif
#if FT_ENABLED(FT_OTA)
OTASettingsService _otaSettingsService;
#endif
#if FT_ENABLED(FT_MQTT)
MqttSettingsService _mqttSettingsService;
MqttStatus _mqttStatus;
#endif
#if FT_ENABLED(FT_SECURITY)
AuthenticationService _authenticationService;
#endif
RestartService _restartService;
FactoryResetService _factoryResetService;
AuthenticationService _authenticationService;
WiFiScanner _wifiScanner;
WiFiStatus _wifiStatus;
NTPStatus _ntpStatus;
APStatus _apStatus;
MqttStatus _mqttStatus;
SystemStatus _systemStatus;
};

31
lib/framework/Features.h

@ -0,0 +1,31 @@
#ifndef Features_h
#define Features_h
#define FT_ENABLED(feature) feature
// project feature off by default
#ifndef FT_PROJECT
#define FT_PROJECT 0
#endif
// security feature on by default
#ifndef FT_SECURITY
#define FT_SECURITY 1
#endif
// mqtt feature on by default
#ifndef FT_MQTT
#define FT_MQTT 1
#endif
// ntp feature on by default
#ifndef FT_NTP
#define FT_NTP 1
#endif
// mqtt feature on by default
#ifndef FT_OTA
#define FT_OTA 1
#endif
#endif

37
lib/framework/FeaturesService.cpp

@ -0,0 +1,37 @@
#include <FeaturesService.h>
FeaturesService::FeaturesService(AsyncWebServer* server) {
server->on(FEATURES_SERVICE_PATH, HTTP_GET, std::bind(&FeaturesService::features, this, std::placeholders::_1));
}
void FeaturesService::features(AsyncWebServerRequest* request) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_FEATURES_SIZE);
JsonObject root = response->getRoot();
#if FT_ENABLED(FT_PROJECT)
root["project"] = true;
#else
root["project"] = false;
#endif
#if FT_ENABLED(FT_SECURITY)
root["security"] = true;
#else
root["security"] = false;
#endif
#if FT_ENABLED(FT_MQTT)
root["mqtt"] = true;
#else
root["mqtt"] = false;
#endif
#if FT_ENABLED(FT_NTP)
root["ntp"] = true;
#else
root["ntp"] = false;
#endif
#if FT_ENABLED(FT_OTA)
root["ota"] = true;
#else
root["ota"] = false;
#endif
response->setLength();
request->send(response);
}

29
lib/framework/FeaturesService.h

@ -0,0 +1,29 @@
#ifndef FeaturesService_h
#define FeaturesService_h
#include <Features.h>
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ArduinoJson.h>
#include <AsyncJson.h>
#include <ESPAsyncWebServer.h>
#define MAX_FEATURES_SIZE 256
#define FEATURES_SERVICE_PATH "/rest/features"
class FeaturesService {
public:
FeaturesService(AsyncWebServer* server);
private:
void features(AsyncWebServerRequest* request);
};
#endif

2
lib/framework/RestartService.h

@ -22,4 +22,4 @@ class RestartService {
void restart(AsyncWebServerRequest* request);
};
#endif // end RestartService_h
#endif // end RestartService_h

16
lib/framework/SecurityManager.h

@ -1,6 +1,7 @@
#ifndef SecurityManager_h
#define SecurityManager_h
#include <Features.h>
#include <ArduinoJsonJWT.h>
#include <ESPAsyncWebServer.h>
#include <ESPUtils.h>
@ -62,20 +63,23 @@ class AuthenticationPredicates {
class SecurityManager {
public:
#if FT_ENABLED(FT_SECURITY)
/*
* Authenticate, returning the user if found
*/
virtual Authentication authenticate(const String& username, const String& password) = 0;
/*
* Check the request header for the Authorization token
* Generate a JWT for the user provided
*/
virtual Authentication authenticateRequest(AsyncWebServerRequest* request) = 0;
virtual String generateJWT(User* user) = 0;
#endif
/*
* Generate a JWT for the user provided
* Check the request header for the Authorization token
*/
virtual String generateJWT(User* user) = 0;
virtual Authentication authenticateRequest(AsyncWebServerRequest* request) = 0;
/**
* Filter a request with the provided predicate, only returning true if the predicate matches.
@ -91,8 +95,8 @@ class SecurityManager {
/**
* Wrap the provided json request callback to provide validation against an AuthenticationPredicate.
*/
virtual ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction callback,
virtual ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction onRequest,
AuthenticationPredicate predicate) = 0;
};
#endif // end SecurityManager_h
#endif // end SecurityManager_h

39
lib/framework/SecuritySettingsService.cpp

@ -1,5 +1,7 @@
#include <SecuritySettingsService.h>
#if FT_ENABLED(FT_SECURITY)
SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs) :
_httpEndpoint(SecuritySettings::read, SecuritySettings::update, this, server, SECURITY_SETTINGS_PATH, this),
_fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE),
@ -94,14 +96,45 @@ ArRequestHandlerFunction SecuritySettingsService::wrapRequest(ArRequestHandlerFu
};
}
ArJsonRequestHandlerFunction SecuritySettingsService::wrapCallback(ArJsonRequestHandlerFunction callback,
ArJsonRequestHandlerFunction SecuritySettingsService::wrapCallback(ArJsonRequestHandlerFunction onRequest,
AuthenticationPredicate predicate) {
return [this, callback, predicate](AsyncWebServerRequest* request, JsonVariant& json) {
return [this, onRequest, predicate](AsyncWebServerRequest* request, JsonVariant& json) {
Authentication authentication = authenticateRequest(request);
if (!predicate(authentication)) {
request->send(401);
return;
}
callback(request, json);
onRequest(request, json);
};
}
#else
User ADMIN_USER = User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true);
SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs) : SecurityManager() {
}
SecuritySettingsService::~SecuritySettingsService() {
}
ArRequestFilterFunction SecuritySettingsService::filterRequest(AuthenticationPredicate predicate) {
return [this, predicate](AsyncWebServerRequest* request) { return true; };
}
// Return the admin user on all request - disabling security features
Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerRequest* request) {
return Authentication(ADMIN_USER);
}
// Return the function unwrapped
ArRequestHandlerFunction SecuritySettingsService::wrapRequest(ArRequestHandlerFunction onRequest,
AuthenticationPredicate predicate) {
return onRequest;
}
ArJsonRequestHandlerFunction SecuritySettingsService::wrapCallback(ArJsonRequestHandlerFunction onRequest,
AuthenticationPredicate predicate) {
return onRequest;
}
#endif

20
lib/framework/SecuritySettingsService.h

@ -1,6 +1,7 @@
#ifndef SecuritySettingsService_h
#define SecuritySettingsService_h
#include <Features.h>
#include <SecurityManager.h>
#include <HttpEndpoint.h>
#include <FSPersistence.h>
@ -24,6 +25,8 @@
#define SECURITY_SETTINGS_FILE "/config/securitySettings.json"
#define SECURITY_SETTINGS_PATH "/rest/securitySettings"
#if FT_ENABLED(FT_SECURITY)
class SecuritySettings {
public:
String jwtSecret;
@ -93,4 +96,19 @@ class SecuritySettingsService : public StatefulService<SecuritySettings>, public
boolean validatePayload(JsonObject& parsedPayload, User* user);
};
#endif // end SecuritySettingsService_h
#else
class SecuritySettingsService : public SecurityManager {
public:
SecuritySettingsService(AsyncWebServer* server, FS* fs);
~SecuritySettingsService();
// minimal set of functions to support framework with security settings disabled
Authentication authenticateRequest(AsyncWebServerRequest* request);
ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate);
ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate);
ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction onRequest, AuthenticationPredicate predicate);
};
#endif // end FT_ENABLED(FT_SECURITY)
#endif // end SecuritySettingsService_h

8
lib/framework/StatefulService.h

@ -21,11 +21,11 @@ enum class StateUpdateResult {
ERROR // There was a problem updating the state, propagation should not take place
};
template <class T>
using JsonStateUpdater = StateUpdateResult (*)(JsonObject& root, T& settings);
template <typename T>
using JsonStateUpdater = std::function<StateUpdateResult(JsonObject& root, T& settings)>;
template <class T>
using JsonStateReader = void (*)(T& settings, JsonObject& root);
template <typename T>
using JsonStateReader = std::function<void(T& settings, JsonObject& root)>;
typedef size_t update_handler_id_t;
typedef std::function<void(const String& originId)> StateUpdateCallback;

2
lib/framework/SystemStatus.h

@ -27,4 +27,4 @@ class SystemStatus {
void systemStatus(AsyncWebServerRequest* request);
};
#endif // end SystemStatus_h
#endif // end SystemStatus_h

2
lib/framework/WiFiScanner.cpp

@ -67,4 +67,4 @@ uint8_t WiFiScanner::convertEncryptionType(uint8_t encryptionType) {
}
return -1;
}
#endif
#endif

5
platformio.ini

@ -1,11 +1,14 @@
[platformio]
extra_configs = factory_settings.ini
extra_configs =
factory_settings.ini
features.ini
default_envs = esp12e
;default_envs = node32s
[env]
build_flags=
${factory_settings.build_flags}
${features.build_flags}
-D NO_GLOBAL_ARDUINOOTA
; Uncomment ENABLE_CORS to enable Cross-Origin Resource Sharing (required for local React development)
;-D ENABLE_CORS

Loading…
Cancel
Save