├── .clang-format ├── .github └── workflows │ └── verify_build.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── factory_settings.ini ├── features.ini ├── interface ├── .env ├── .env.production ├── config-overrides.js ├── package-lock.json ├── package.json ├── progmem-generator.js ├── public │ ├── app │ │ ├── icon.png │ │ └── manifest.json │ ├── css │ │ └── roboto.css │ ├── favicon.ico │ ├── fonts │ │ ├── md.woff2 │ │ └── re.woff2 │ └── index.html ├── src │ ├── App.tsx │ ├── AppRouting.tsx │ ├── AuthenticatedRouting.tsx │ ├── CustomTheme.tsx │ ├── SignIn.tsx │ ├── api │ │ ├── ap.ts │ │ ├── authentication.ts │ │ ├── endpoints.ts │ │ ├── env.ts │ │ ├── features.ts │ │ ├── mqtt.ts │ │ ├── ntp.ts │ │ ├── security.ts │ │ ├── system.ts │ │ └── wifi.ts │ ├── components │ │ ├── ButtonRow.tsx │ │ ├── MessageBox.tsx │ │ ├── SectionContent.tsx │ │ ├── index.ts │ │ ├── inputs │ │ │ ├── BlockFormControlLabel.tsx │ │ │ ├── ValidatedPasswordField.tsx │ │ │ ├── ValidatedTextField.tsx │ │ │ └── index.ts │ │ ├── layout │ │ │ ├── Layout.tsx │ │ │ ├── LayoutAppBar.tsx │ │ │ ├── LayoutAuthMenu.tsx │ │ │ ├── LayoutDrawer.tsx │ │ │ ├── LayoutMenu.tsx │ │ │ ├── LayoutMenuItem.tsx │ │ │ ├── context.ts │ │ │ └── index.ts │ │ ├── loading │ │ │ ├── ApplicationError.tsx │ │ │ ├── FormLoader.tsx │ │ │ ├── LoadingSpinner.tsx │ │ │ └── index.ts │ │ ├── routing │ │ │ ├── RequireAdmin.tsx │ │ │ ├── RequireAuthenticated.tsx │ │ │ ├── RequireUnauthenticated.tsx │ │ │ ├── RouterTabs.tsx │ │ │ ├── index.ts │ │ │ └── useRouterTab.ts │ │ └── upload │ │ │ ├── SingleUpload.tsx │ │ │ ├── index.ts │ │ │ └── useFileUpload.ts │ ├── contexts │ │ ├── authentication │ │ │ ├── Authentication.tsx │ │ │ ├── context.ts │ │ │ └── index.ts │ │ └── features │ │ │ ├── FeaturesLoader.tsx │ │ │ ├── context.ts │ │ │ └── index.ts │ ├── framework │ │ ├── ap │ │ │ ├── APSettingsForm.tsx │ │ │ ├── APStatusForm.tsx │ │ │ └── AccessPoint.tsx │ │ ├── mqtt │ │ │ ├── Mqtt.tsx │ │ │ ├── MqttSettingsForm.tsx │ │ │ └── MqttStatusForm.tsx │ │ ├── ntp │ │ │ ├── NTPSettingsForm.tsx │ │ │ ├── NTPStatusForm.tsx │ │ │ ├── NetworkTime.tsx │ │ │ └── TZ.tsx │ │ ├── security │ │ │ ├── ManageUsersForm.tsx │ │ │ ├── Security.tsx │ │ │ ├── SecuritySettingsForm.tsx │ │ │ └── UserForm.tsx │ │ ├── system │ │ │ ├── FirmwareFileUpload.tsx │ │ │ ├── FirmwareRestartMonitor.tsx │ │ │ ├── OTASettingsForm.tsx │ │ │ ├── System.tsx │ │ │ ├── SystemStatusForm.tsx │ │ │ └── UploadFirmwareForm.tsx │ │ └── wifi │ │ │ ├── WiFiConnection.tsx │ │ │ ├── WiFiConnectionContext.tsx │ │ │ ├── WiFiNetworkScanner.tsx │ │ │ ├── WiFiNetworkSelector.tsx │ │ │ ├── WiFiSettingsForm.tsx │ │ │ └── WiFiStatusForm.tsx │ ├── index.tsx │ ├── project │ │ ├── DemoInformation.tsx │ │ ├── DemoProject.tsx │ │ ├── LightMqttSettingsForm.tsx │ │ ├── LightStateRestForm.tsx │ │ ├── LightStateWebSocketForm.tsx │ │ ├── ProjectMenu.tsx │ │ ├── ProjectRouting.tsx │ │ ├── api.ts │ │ ├── types.ts │ │ └── validators.ts │ ├── react-app-env.d.ts │ ├── setupProxy.js │ ├── types │ │ ├── ap.ts │ │ ├── features.ts │ │ ├── index.ts │ │ ├── me.ts │ │ ├── mqtt.ts │ │ ├── ntp.ts │ │ ├── security.ts │ │ ├── signin.ts │ │ ├── system.ts │ │ └── wifi.ts │ ├── utils │ │ ├── binding.ts │ │ ├── endpoints.ts │ │ ├── index.ts │ │ ├── props.ts │ │ ├── route.ts │ │ ├── submit.ts │ │ ├── time.ts │ │ ├── useRest.ts │ │ └── useWs.ts │ └── validators │ │ ├── ap.ts │ │ ├── authentication.ts │ │ ├── index.ts │ │ ├── mqtt.ts │ │ ├── ntp.ts │ │ ├── security.ts │ │ ├── shared.ts │ │ ├── system.ts │ │ └── wifi.ts └── tsconfig.json ├── lib ├── framework │ ├── APSettingsService.cpp │ ├── APSettingsService.h │ ├── APStatus.cpp │ ├── APStatus.h │ ├── ArduinoJsonJWT.cpp │ ├── ArduinoJsonJWT.h │ ├── AuthenticationService.cpp │ ├── AuthenticationService.h │ ├── ESP8266React.cpp │ ├── ESP8266React.h │ ├── ESPFS.h │ ├── FSPersistence.h │ ├── FactoryResetService.cpp │ ├── FactoryResetService.h │ ├── Features.h │ ├── FeaturesService.cpp │ ├── FeaturesService.h │ ├── HttpEndpoint.h │ ├── IPUtils.h │ ├── JsonUtils.h │ ├── MqttPubSub.h │ ├── MqttSettingsService.cpp │ ├── MqttSettingsService.h │ ├── MqttStatus.cpp │ ├── MqttStatus.h │ ├── NTPSettingsService.cpp │ ├── NTPSettingsService.h │ ├── NTPStatus.cpp │ ├── NTPStatus.h │ ├── OTASettingsService.cpp │ ├── OTASettingsService.h │ ├── RestartService.cpp │ ├── RestartService.h │ ├── SecurityManager.h │ ├── SecuritySettingsService.cpp │ ├── SecuritySettingsService.h │ ├── SettingValue.cpp │ ├── SettingValue.h │ ├── StatefulService.cpp │ ├── StatefulService.h │ ├── SystemStatus.cpp │ ├── SystemStatus.h │ ├── UploadFirmwareService.cpp │ ├── UploadFirmwareService.h │ ├── WebSocketTxRx.h │ ├── WiFiScanner.cpp │ ├── WiFiScanner.h │ ├── WiFiSettingsService.cpp │ ├── WiFiSettingsService.h │ ├── WiFiStatus.cpp │ └── WiFiStatus.h └── readme.txt ├── media ├── build.png ├── dark.png ├── devserver.png ├── esp12e.jpg ├── esp32.jpg ├── framework.png ├── screenshots.png ├── uploadfs.png └── uploadfw.png ├── platformio.ini ├── scripts └── build_interface.py └── src ├── LightMqttSettingsService.cpp ├── LightMqttSettingsService.h ├── LightStateService.cpp ├── LightStateService.h └── main.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | Language: Cpp 2 | BasedOnStyle: Google 3 | ColumnLimit: 120 4 | AllowShortBlocksOnASingleLine: false 5 | AllowShortFunctionsOnASingleLine: false 6 | AllowShortIfStatementsOnASingleLine: false 7 | AllowShortLoopsOnASingleLine: false 8 | BinPackArguments: false 9 | BinPackParameters: false 10 | BreakConstructorInitializers: AfterColon 11 | AllowAllParametersOfDeclarationOnNextLine: false 12 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 13 | ExperimentalAutoDetectBinPacking: false 14 | KeepEmptyLinesAtTheStartOfBlocks: false 15 | DerivePointerAlignment: false 16 | SortIncludes: false 17 | -------------------------------------------------------------------------------- /.github/workflows/verify_build.yml: -------------------------------------------------------------------------------- 1 | name: Build Framework 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Cache pip 19 | uses: actions/cache@v2 20 | with: 21 | path: ~/.cache/pip 22 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 23 | restore-keys: | 24 | ${{ runner.os }}-pip- 25 | - name: Cache PlatformIO 26 | uses: actions/cache@v2 27 | with: 28 | path: ~/.platformio 29 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 30 | - name: Set up Python 31 | uses: actions/setup-python@v2 32 | - name: Install PlatformIO 33 | run: | 34 | python -m pip install --upgrade pip 35 | pip install --upgrade platformio 36 | - name: Run PlatformIO 37 | run: pio run -e esp12e -e node32s 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .clang_complete 3 | .gcc-flags.json 4 | *Thumbs.db 5 | /data/www 6 | /lib/framework/WWWData.h 7 | /interface/build 8 | /interface/node_modules 9 | /interface/.eslintcache 10 | .vscode 11 | -------------------------------------------------------------------------------- /factory_settings.ini: -------------------------------------------------------------------------------- 1 | ; The indicated settings support placeholder substitution as follows: 2 | ; 3 | ; #{platform} - The microcontroller platform, e.g. "esp32" or "esp8266" 4 | ; #{unique_id} - A unique identifier derived from the MAC address, e.g. "0b0a859d6816" 5 | ; #{random} - A random number encoded as a hex string, e.g. "55722f94" 6 | 7 | [factory_settings] 8 | build_flags = 9 | ; WiFi settings 10 | -D FACTORY_WIFI_SSID=\"\" 11 | -D FACTORY_WIFI_PASSWORD=\"\" 12 | -D FACTORY_WIFI_HOSTNAME=\"#{platform}-#{unique_id}\" ; supports placeholders 13 | 14 | ; Access point settings 15 | -D FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED 16 | -D FACTORY_AP_SSID=\"ESP8266-React-#{unique_id}\" ; 1-64 characters, supports placeholders 17 | -D FACTORY_AP_PASSWORD=\"esp-react\" ; 8-64 characters 18 | -D FACTORY_AP_CHANNEL=1 19 | -D FACTORY_AP_SSID_HIDDEN=false 20 | -D FACTORY_AP_MAX_CLIENTS=4 21 | -D FACTORY_AP_LOCAL_IP=\"192.168.4.1\" 22 | -D FACTORY_AP_GATEWAY_IP=\"192.168.4.1\" 23 | -D FACTORY_AP_SUBNET_MASK=\"255.255.255.0\" 24 | 25 | ; User credentials for admin and guest user 26 | -D FACTORY_ADMIN_USERNAME=\"admin\" 27 | -D FACTORY_ADMIN_PASSWORD=\"admin\" 28 | -D FACTORY_GUEST_USERNAME=\"guest\" 29 | -D FACTORY_GUEST_PASSWORD=\"guest\" 30 | 31 | ; NTP settings 32 | -D FACTORY_NTP_ENABLED=true 33 | -D FACTORY_NTP_TIME_ZONE_LABEL=\"Europe/London\" 34 | -D FACTORY_NTP_TIME_ZONE_FORMAT=\"GMT0BST,M3.5.0/1,M10.5.0\" 35 | -D FACTORY_NTP_SERVER=\"time.google.com\" 36 | 37 | ; OTA settings 38 | -D FACTORY_OTA_PORT=8266 39 | -D FACTORY_OTA_PASSWORD=\"esp-react\" 40 | -D FACTORY_OTA_ENABLED=true 41 | 42 | ; MQTT settings 43 | -D FACTORY_MQTT_ENABLED=false 44 | -D FACTORY_MQTT_HOST=\"test.mosquitto.org\" 45 | -D FACTORY_MQTT_PORT=1883 46 | -D FACTORY_MQTT_USERNAME=\"\" ; supports placeholders 47 | -D FACTORY_MQTT_PASSWORD=\"\" 48 | -D FACTORY_MQTT_CLIENT_ID=\"#{platform}-#{unique_id}\" ; supports placeholders 49 | -D FACTORY_MQTT_KEEP_ALIVE=60 50 | -D FACTORY_MQTT_CLEAN_SESSION=true 51 | -D FACTORY_MQTT_MAX_TOPIC_LENGTH=128 52 | 53 | ; JWT Secret 54 | -D FACTORY_JWT_SECRET=\"#{random}-#{random}\" ; supports placeholders 55 | -------------------------------------------------------------------------------- /features.ini: -------------------------------------------------------------------------------- 1 | [features] 2 | build_flags = 3 | -D FT_PROJECT=1 4 | -D FT_SECURITY=1 5 | -D FT_MQTT=1 6 | -D FT_NTP=1 7 | -D FT_OTA=1 8 | -D FT_UPLOAD_FIRMWARE=1 9 | -------------------------------------------------------------------------------- /interface/.env: -------------------------------------------------------------------------------- 1 | # This enables lint extensions 2 | EXTEND_ESLINT=true 3 | 4 | # This is the name of your project. It appears on the sign-in page and in the menu bar. 5 | REACT_APP_PROJECT_NAME=ESP8266 React 6 | 7 | # This is the url path your project will be exposed under. 8 | REACT_APP_PROJECT_PATH=project 9 | -------------------------------------------------------------------------------- /interface/.env.production: -------------------------------------------------------------------------------- 1 | # Disable the generation of the sourcemap on the production build to reduce the artefact size 2 | GENERATE_SOURCEMAP=false 3 | -------------------------------------------------------------------------------- /interface/config-overrides.js: -------------------------------------------------------------------------------- 1 | const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const ProgmemGenerator = require('./progmem-generator.js'); 4 | const TerserPlugin = require('terser-webpack-plugin'); 5 | 6 | module.exports = function override(config, env) { 7 | if (env === "production") { 8 | // rename the ouput file, we need it's path to be short, for embedded FS 9 | config.output.filename = 'js/[id].[chunkhash:4].js'; 10 | config.output.chunkFilename = 'js/[id].[chunkhash:4].js'; 11 | 12 | // take out the manifest plugin 13 | config.plugins = config.plugins.filter((plugin) => !(plugin instanceof WebpackManifestPlugin)); 14 | 15 | // shorten css filenames 16 | const miniCssExtractPlugin = config.plugins.find((plugin) => plugin instanceof MiniCssExtractPlugin); 17 | miniCssExtractPlugin.options.filename = "css/[id].[contenthash:4].css"; 18 | miniCssExtractPlugin.options.chunkFilename = "css/[id].[contenthash:4].c.css"; 19 | 20 | // don't emit license file 21 | const terserPlugin = config.optimization.minimizer.find((plugin) => plugin instanceof TerserPlugin); 22 | terserPlugin.options.extractComments = false; 23 | 24 | // build progmem data files 25 | config.plugins.push(new ProgmemGenerator({ outputPath: "../lib/framework/WWWData.h", bytesPerLine: 20 })); 26 | } 27 | return config; 28 | }; 29 | -------------------------------------------------------------------------------- /interface/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esp8266-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://192.168.0.23", 6 | "dependencies": { 7 | "@emotion/react": "^11.9.0", 8 | "@emotion/styled": "^11.8.1", 9 | "@mui/icons-material": "^5.8.0", 10 | "@mui/material": "^5.8.0", 11 | "@types/lodash": "^4.14.176", 12 | "@types/node": "^16.11.14", 13 | "@types/react": "^18.0.9", 14 | "@types/react-dom": "^18.0.4", 15 | "async-validator": "^4.1.1", 16 | "axios": "^0.27.2", 17 | "http-proxy-middleware": "^2.0.1", 18 | "jwt-decode": "^3.1.2", 19 | "lodash": "^4.17.21", 20 | "notistack": "^2.0.5", 21 | "parse-ms": "^3.0.0", 22 | "react": "^18.1.0", 23 | "react-app-rewired": "^2.1.8", 24 | "react-dom": "^18.1.0", 25 | "react-dropzone": "^11.4.2", 26 | "react-router-dom": "^6.3.0", 27 | "react-scripts": "5.0.1", 28 | "sockette": "^2.0.6", 29 | "typescript": "^4.6.4" 30 | }, 31 | "scripts": { 32 | "start": "react-app-rewired start", 33 | "build": "react-app-rewired build", 34 | "test": "react-app-rewired test", 35 | "eject": "react-scripts eject" 36 | }, 37 | "eslintConfig": { 38 | "extends": [ 39 | "react-app", 40 | "react-app/jest" 41 | ], 42 | "rules": { 43 | "eol-last": 1, 44 | "react/jsx-closing-bracket-location": 1, 45 | "react/jsx-closing-tag-location": 1, 46 | "react/jsx-wrap-multilines": 1, 47 | "react/jsx-curly-newline": 1, 48 | "no-multiple-empty-lines": [ 49 | 1, 50 | { 51 | "max": 1 52 | } 53 | ], 54 | "no-trailing-spaces": 1, 55 | "semi": 1, 56 | "no-extra-semi": 1, 57 | "react/jsx-max-props-per-line": [ 58 | 1, 59 | { 60 | "when": "multiline" 61 | } 62 | ], 63 | "react/jsx-first-prop-new-line": [ 64 | 1, 65 | "multiline" 66 | ], 67 | "@typescript-eslint/no-shadow": 1, 68 | "max-len": [ 69 | 1, 70 | { 71 | "code": 140 72 | } 73 | ], 74 | "arrow-parens": 1 75 | } 76 | }, 77 | "browserslist": { 78 | "production": [ 79 | ">0.2%", 80 | "not dead", 81 | "not op_mini all" 82 | ], 83 | "development": [ 84 | "last 1 chrome version", 85 | "last 1 firefox version", 86 | "last 1 safari version" 87 | ] 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /interface/public/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjwats/esp8266-react/b9b9c1b2149b3d4472125c5684045f434df66d42/interface/public/app/icon.png -------------------------------------------------------------------------------- /interface/public/app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"ESP8266 React", 3 | "icons":[ 4 | { 5 | "src":"/app/icon.png", 6 | "sizes":"48x48 72x72 96x96 128x128 256x256" 7 | } 8 | ], 9 | "start_url":"/", 10 | "display":"fullscreen", 11 | "orientation":"any" 12 | } 13 | -------------------------------------------------------------------------------- /interface/public/css/roboto.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Just supporting latin due to size constrains on the esp chip 3 | * 4 | * The framework only makes use of 400 (regular) + 500 (medium) weight fonts. 5 | * 6 | * If using light or strong typography variants you will need to add additional fonts. 7 | */ 8 | @font-face { 9 | font-family: 'Roboto'; 10 | font-style: normal; 11 | font-weight: 400; 12 | src: local('Roboto'), local('Roboto-Regular'), url(../fonts/re.woff2) format('woff2'); 13 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 14 | } 15 | 16 | @font-face { 17 | font-family: 'Roboto'; 18 | font-style: normal; 19 | font-weight: 500; 20 | src: local('Roboto Medium'), local('Roboto-Medium'), url(../fonts/md.woff2) format('woff2'); 21 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 22 | } 23 | -------------------------------------------------------------------------------- /interface/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjwats/esp8266-react/b9b9c1b2149b3d4472125c5684045f434df66d42/interface/public/favicon.ico -------------------------------------------------------------------------------- /interface/public/fonts/md.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjwats/esp8266-react/b9b9c1b2149b3d4472125c5684045f434df66d42/interface/public/fonts/md.woff2 -------------------------------------------------------------------------------- /interface/public/fonts/re.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjwats/esp8266-react/b9b9c1b2149b3d4472125c5684045f434df66d42/interface/public/fonts/re.woff2 -------------------------------------------------------------------------------- /interface/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ESP8266 React 9 | 10 | 11 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /interface/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, RefObject } from 'react'; 2 | import { SnackbarProvider } from 'notistack'; 3 | 4 | import { IconButton } from '@mui/material'; 5 | import CloseIcon from '@mui/icons-material/Close'; 6 | 7 | import { FeaturesLoader } from './contexts/features'; 8 | 9 | import CustomTheme from './CustomTheme'; 10 | import AppRouting from './AppRouting'; 11 | 12 | const App: FC = () => { 13 | const notistackRef: RefObject = React.createRef(); 14 | 15 | const onClickDismiss = (key: string | number | undefined) => () => { 16 | notistackRef.current.closeSnackbar(key); 17 | }; 18 | 19 | return ( 20 | 21 | ( 26 | 27 | 28 | 29 | )} 30 | > 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /interface/src/AppRouting.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useContext, useEffect } from 'react'; 2 | import { Navigate, Routes, Route, useLocation } from 'react-router-dom'; 3 | import { useSnackbar, VariantType } from 'notistack'; 4 | 5 | import { Authentication, AuthenticationContext } from './contexts/authentication'; 6 | import { FeaturesContext } from './contexts/features'; 7 | import { RequireAuthenticated, RequireUnauthenticated } from './components'; 8 | 9 | import SignIn from './SignIn'; 10 | import AuthenticatedRouting from './AuthenticatedRouting'; 11 | 12 | interface SecurityRedirectProps { 13 | message: string; 14 | variant?: VariantType; 15 | signOut?: boolean; 16 | } 17 | 18 | const RootRedirect: FC = ({ message, variant, signOut }) => { 19 | const authenticationContext = useContext(AuthenticationContext); 20 | const { enqueueSnackbar } = useSnackbar(); 21 | useEffect(() => { 22 | signOut && authenticationContext.signOut(false); 23 | enqueueSnackbar(message, { variant }); 24 | }, [message, variant, signOut, authenticationContext, enqueueSnackbar]); 25 | return (); 26 | }; 27 | 28 | export const RemoveTrailingSlashes = () => { 29 | const location = useLocation(); 30 | return location.pathname.match('/.*/$') && ( 31 | 37 | ); 38 | }; 39 | 40 | const AppRouting: FC = () => { 41 | const { features } = useContext(FeaturesContext); 42 | 43 | return ( 44 | 45 | 46 | 47 | } 50 | /> 51 | } 54 | /> 55 | {features.security && 56 | 61 | 62 | 63 | } 64 | />} 65 | 69 | 70 | 71 | } 72 | /> 73 | 74 | 75 | ); 76 | }; 77 | 78 | export default AppRouting; 79 | -------------------------------------------------------------------------------- /interface/src/AuthenticatedRouting.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useCallback, useContext, useEffect } from 'react'; 2 | import { Navigate, Routes, Route, useNavigate, useLocation } from 'react-router-dom'; 3 | import { AxiosError } from 'axios'; 4 | 5 | import { FeaturesContext } from './contexts/features'; 6 | import * as AuthenticationApi from './api/authentication'; 7 | import { PROJECT_PATH } from './api/env'; 8 | import { AXIOS } from './api/endpoints'; 9 | import { Layout, RequireAdmin } from './components'; 10 | 11 | import ProjectRouting from './project/ProjectRouting'; 12 | 13 | import WiFiConnection from './framework/wifi/WiFiConnection'; 14 | import AccessPoint from './framework/ap/AccessPoint'; 15 | import NetworkTime from './framework/ntp/NetworkTime'; 16 | import Mqtt from './framework/mqtt/Mqtt'; 17 | import System from './framework/system/System'; 18 | import Security from './framework/security/Security'; 19 | 20 | const AuthenticatedRouting: FC = () => { 21 | const { features } = useContext(FeaturesContext); 22 | const location = useLocation(); 23 | const navigate = useNavigate(); 24 | 25 | const handleApiResponseError = useCallback((error: AxiosError) => { 26 | if (error.response && error.response.status === 401) { 27 | AuthenticationApi.storeLoginRedirect(location); 28 | navigate("/unauthorized"); 29 | } 30 | return Promise.reject(error); 31 | }, [location, navigate]); 32 | 33 | useEffect(() => { 34 | const axiosHandlerId = AXIOS.interceptors.response.use((response) => response, handleApiResponseError); 35 | return () => AXIOS.interceptors.response.eject(axiosHandlerId); 36 | }, [handleApiResponseError]); 37 | 38 | return ( 39 | 40 | 41 | {features.project && ( 42 | } /> 43 | )} 44 | } /> 45 | } /> 46 | {features.ntp && ( 47 | } /> 48 | )} 49 | {features.mqtt && ( 50 | } /> 51 | )} 52 | {features.security && ( 53 | 57 | 58 | 59 | } 60 | /> 61 | )} 62 | } /> 63 | } /> 64 | 65 | 66 | ); 67 | }; 68 | 69 | export default AuthenticatedRouting; 70 | -------------------------------------------------------------------------------- /interface/src/CustomTheme.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { CssBaseline } from '@mui/material'; 4 | import { createTheme, responsiveFontSizes, ThemeProvider } from '@mui/material/styles'; 5 | import { indigo, blueGrey, orange, red, green } from '@mui/material/colors'; 6 | 7 | import { RequiredChildrenProps } from './utils'; 8 | 9 | const theme = responsiveFontSizes( 10 | createTheme({ 11 | palette: { 12 | background: { 13 | default: "#fafafa" 14 | }, 15 | primary: indigo, 16 | secondary: blueGrey, 17 | info: { 18 | main: indigo[500] 19 | }, 20 | warning: { 21 | main: orange[500] 22 | }, 23 | error: { 24 | main: red[500] 25 | }, 26 | success: { 27 | main: green[500] 28 | } 29 | } 30 | }) 31 | ); 32 | 33 | const CustomTheme: FC = ({ children }) => ( 34 | 35 | 36 | {children} 37 | 38 | ); 39 | 40 | export default CustomTheme; 41 | -------------------------------------------------------------------------------- /interface/src/api/ap.ts: -------------------------------------------------------------------------------- 1 | import { AxiosPromise } from "axios"; 2 | 3 | import { APSettings, APStatus } from "../types"; 4 | import { AXIOS } from "./endpoints"; 5 | 6 | export function readAPStatus(): AxiosPromise { 7 | return AXIOS.get('/apStatus'); 8 | } 9 | 10 | export function readAPSettings(): AxiosPromise { 11 | return AXIOS.get('/apSettings'); 12 | } 13 | 14 | export function updateAPSettings(apSettings: APSettings): AxiosPromise { 15 | return AXIOS.post('/apSettings', apSettings); 16 | } 17 | -------------------------------------------------------------------------------- /interface/src/api/authentication.ts: -------------------------------------------------------------------------------- 1 | import { AxiosPromise } from "axios"; 2 | import * as H from 'history'; 3 | import jwtDecode from 'jwt-decode'; 4 | import { Path } from "react-router-dom"; 5 | 6 | import { Features, Me, SignInRequest, SignInResponse } from "../types"; 7 | 8 | import { ACCESS_TOKEN, AXIOS } from "./endpoints"; 9 | import { PROJECT_PATH } from './env'; 10 | 11 | export const SIGN_IN_PATHNAME = 'loginPathname'; 12 | export const SIGN_IN_SEARCH = 'loginSearch'; 13 | 14 | export const getDefaultRoute = (features: Features) => features.project ? `/${PROJECT_PATH}` : "/wifi"; 15 | 16 | export function verifyAuthorization(): AxiosPromise { 17 | return AXIOS.get('/verifyAuthorization'); 18 | } 19 | 20 | export function signIn(request: SignInRequest): AxiosPromise { 21 | return AXIOS.post('/signIn', request); 22 | } 23 | 24 | /** 25 | * Fallback to sessionStorage if localStorage is absent. WebView may not have local storage enabled. 26 | */ 27 | export function getStorage() { 28 | return localStorage || sessionStorage; 29 | } 30 | 31 | export function storeLoginRedirect(location?: H.Location) { 32 | if (location) { 33 | getStorage().setItem(SIGN_IN_PATHNAME, location.pathname); 34 | getStorage().setItem(SIGN_IN_SEARCH, location.search); 35 | } 36 | } 37 | 38 | export function clearLoginRedirect() { 39 | getStorage().removeItem(SIGN_IN_PATHNAME); 40 | getStorage().removeItem(SIGN_IN_SEARCH); 41 | } 42 | 43 | export function fetchLoginRedirect(features: Features): Partial { 44 | const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME); 45 | const signInSearch = getStorage().getItem(SIGN_IN_SEARCH); 46 | clearLoginRedirect(); 47 | return { 48 | pathname: signInPathname || getDefaultRoute(features), 49 | search: (signInPathname && signInSearch) || undefined 50 | }; 51 | } 52 | 53 | export const clearAccessToken = () => localStorage.removeItem(ACCESS_TOKEN); 54 | export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken) as Me; 55 | 56 | export function addAccessTokenParameter(url: string) { 57 | const accessToken = getStorage().getItem(ACCESS_TOKEN); 58 | if (!accessToken) { 59 | return url; 60 | } 61 | const parsedUrl = new URL(url); 62 | parsedUrl.searchParams.set(ACCESS_TOKEN, accessToken); 63 | return parsedUrl.toString(); 64 | } 65 | -------------------------------------------------------------------------------- /interface/src/api/endpoints.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosPromise, CancelToken } from 'axios'; 2 | 3 | export const WS_BASE_URL = '/ws/'; 4 | export const API_BASE_URL = '/rest/'; 5 | export const ACCESS_TOKEN = 'access_token'; 6 | export const WEB_SOCKET_ROOT = calculateWebSocketRoot(WS_BASE_URL); 7 | 8 | export const AXIOS = axios.create({ 9 | baseURL: API_BASE_URL, 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | }, 13 | transformRequest: [(data, headers) => { 14 | if (headers) { 15 | if (localStorage.getItem(ACCESS_TOKEN)) { 16 | headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN); 17 | } 18 | if (headers['Content-Type'] !== 'application/json') { 19 | return data; 20 | } 21 | } 22 | return JSON.stringify(data); 23 | }] 24 | }); 25 | 26 | function calculateWebSocketRoot(webSocketPath: string) { 27 | const location = window.location; 28 | const webProtocol = location.protocol === "https:" ? "wss:" : "ws:"; 29 | return webProtocol + "//" + location.host + webSocketPath; 30 | } 31 | 32 | export interface FileUploadConfig { 33 | cancelToken?: CancelToken; 34 | onUploadProgress?: (progressEvent: ProgressEvent) => void; 35 | } 36 | 37 | export const uploadFile = (url: string, file: File, config?: FileUploadConfig): AxiosPromise => { 38 | const formData = new FormData(); 39 | formData.append('file', file); 40 | 41 | return AXIOS.post(url, formData, { 42 | headers: { 43 | 'Content-Type': 'multipart/form-data' 44 | }, 45 | ...(config || {}) 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /interface/src/api/env.ts: -------------------------------------------------------------------------------- 1 | export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME || 'ESP8266 React'; 2 | export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH || 'project'; 3 | -------------------------------------------------------------------------------- /interface/src/api/features.ts: -------------------------------------------------------------------------------- 1 | import { AxiosPromise } from 'axios'; 2 | 3 | import { Features } from '../types'; 4 | import { AXIOS } from './endpoints'; 5 | 6 | export function readFeatures(): AxiosPromise { 7 | return AXIOS.get('/features'); 8 | } 9 | -------------------------------------------------------------------------------- /interface/src/api/mqtt.ts: -------------------------------------------------------------------------------- 1 | import { AxiosPromise } from "axios"; 2 | 3 | import { MqttSettings, MqttStatus } from "../types"; 4 | import { AXIOS } from "./endpoints"; 5 | 6 | export function readMqttStatus(): AxiosPromise { 7 | return AXIOS.get('/mqttStatus'); 8 | } 9 | 10 | export function readMqttSettings(): AxiosPromise { 11 | return AXIOS.get('/mqttSettings'); 12 | } 13 | 14 | export function updateMqttSettings(ntpSettings: MqttSettings): AxiosPromise { 15 | return AXIOS.post('/mqttSettings', ntpSettings); 16 | } 17 | -------------------------------------------------------------------------------- /interface/src/api/ntp.ts: -------------------------------------------------------------------------------- 1 | import { AxiosPromise } from "axios"; 2 | 3 | import { NTPSettings, NTPStatus, Time } from "../types"; 4 | import { AXIOS } from "./endpoints"; 5 | 6 | export function readNTPStatus(): AxiosPromise { 7 | return AXIOS.get('/ntpStatus'); 8 | } 9 | 10 | export function readNTPSettings(): AxiosPromise { 11 | return AXIOS.get('/ntpSettings'); 12 | } 13 | 14 | export function updateNTPSettings(ntpSettings: NTPSettings): AxiosPromise { 15 | return AXIOS.post('/ntpSettings', ntpSettings); 16 | } 17 | 18 | export function updateTime(time: Time): AxiosPromise