├── .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