Newer
Older
smart-home-server / devices / relay / relay_esp8266 / REST_API.h
#ifndef REST_API_H
#define REST_API_H

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

#include "WebPages.h"
#include "WebHandlers.h"

// ---------------------- внешние сущности из .ino / других .h ----------------------
extern ESP8266WebServer server;

extern String savedSSID;
extern String savedPASS;

void saveWiFiConfig(const String &ssid, const String &pass);

String getUniqueID();
String getMAC();


// ---------------------- Колбэки устройства ----------------------
// Устройство МОЖЕТ реализовать эти функции (если не нужно — можно оставить пустыми)

// Добавить свои поля в /status:
// json уже содержит открывающую "{" и какие-то базовые поля.
// Пример: json += ",\"relay_state\":\"on\"";
void appendStatusJsonFields(String &json);

// Добавить свои поля в /about:
void appendAboutJsonFields(String &json);

// Обработка действий устройства:
//
// action — строка, например "set_state"
// paramsJson — сырой JSON params или вообще весь body (как тебе удобнее)
// Если вернуло true → считаем, что действие выполнено успешно
// Если false → errorCode/errorMessage должны быть заполнены
bool deviceHandleAction(const String &action,
                        const String &paramsJson,
                        String &errorCode,
                        String &errorMessage);

// Вызывается при /reset — устройство может выполнить свой специфичный сброс
void deviceHandleReset();

// ---------------------- Вспомогательные функции ----------------------
inline String deviceModeToString(DeviceMode m) {
  switch (m) {
    case DEVICE_MODE_SETUP:    return F("setup");
    case DEVICE_MODE_NORMAL:   return F("normal");
    case DEVICE_MODE_ERROR:    return F("error");
    case DEVICE_MODE_UPDATING: return F("updating");
  }
  return F("error");
}

inline void sendJson(int code, const String &body) {
  server.send(code, F("application/json; charset=utf-8"), body);
}

inline String extractJsonStringValue(const String &body, const String &key) {
  String pattern = "\"" + key + "\"";
  int keyIndex = body.indexOf(pattern);
  if (keyIndex < 0) return "";

  int colonIndex = body.indexOf(':', keyIndex);
  if (colonIndex < 0) return "";

  int firstQuote = body.indexOf('"', colonIndex + 1);
  if (firstQuote < 0) return "";

  int secondQuote = body.indexOf('"', firstQuote + 1);
  if (secondQuote < 0) return "";

  return body.substring(firstQuote + 1, secondQuote);
}

// Попробовать вытащить под-JSON "params": { ... } как сырой текст
inline String extractJsonObject(const String &body, const String &key) {
  String pattern = "\"" + key + "\"";
  int keyIndex = body.indexOf(pattern);
  if (keyIndex < 0) return "";

  int colonIndex = body.indexOf(':', keyIndex);
  if (colonIndex < 0) return "";

  int braceIndex = body.indexOf('{', colonIndex);
  if (braceIndex < 0) return "";

  int depth = 0;
  for (int i = braceIndex; i < (int)body.length(); i++) {
    char c = body[i];
    if (c == '{') depth++;
    else if (c == '}') {
      depth--;
      if (depth == 0) {
        return body.substring(braceIndex, i + 1);
      }
    }
  }
  return "";
}

inline int extractJsonIntValue(const String &body, const String &key) {
  String pattern = "\"" + key + "\"";
  int keyIndex = body.indexOf(pattern);
  if (keyIndex < 0) return -1;

  int colonIndex = body.indexOf(':', keyIndex);
  if (colonIndex < 0) return -1;

  int i = colonIndex + 1;

  // пропускаем пробелы
  while (i < (int)body.length() &&
         (body[i] == ' ' || body[i] == '\t' || body[i] == '\n' || body[i] == '\r')) {
    i++;
  }

  // на случай, если числа вдруг передадутся в кавычках
  bool quoted = (i < (int)body.length() && body[i] == '"');
  if (quoted) i++;

  int sign = 1;
  if (i < (int)body.length() && body[i] == '-') {
    sign = -1;
    i++;
  }

  long value = 0;
  bool anyDigit = false;
  while (i < (int)body.length() && body[i] >= '0' && body[i] <= '9') {
    anyDigit = true;
    value = value * 10 + (body[i] - '0');
    i++;
  }

  if (!anyDigit) return -1;
  return (int)(value * sign);
}


// ---------------------- Авторизация ----------------------
inline bool hasValidToken() {
  if (authToken.length() == 0) return false;

  // 1) Проверяем токен
  String header = server.header(F("Authorization"));
  if (!header.startsWith(F("Bearer "))) return false;

  String token = header.substring(7);
  token.trim();
  if (token != authToken) return false;

  // 2) Проверяем IP, если он был сохранён
  if (serverBaseUrl.length() > 0) {
    IPAddress remote = server.client().remoteIP();
    String ipStr = remote.toString();

    if (ipStr != serverBaseUrl) {
      // Можно залогировать для отладки
      Serial.print(F("Auth IP mismatch: got "));
      Serial.print(ipStr);
      Serial.print(F(", expected "));
      Serial.println(serverBaseUrl);
      return false;
    }
  }

  return true;
}


inline bool requireAuth() {
  if (!hasValidToken()) {
    String json = F("{\"status\":\"error\",\"error\":\"Unauthorized\",\"message\":\"Missing or invalid token\"}");
    sendJson(401, json);
    return false;
  }
  return true;
}

inline void sendNotAvailable() {
  String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Setup mode is not active\"}");
  sendJson(403, json);
}

// ---------------------- /about ----------------------
inline void handleAbout() {
  IPAddress ip = WiFi.localIP();

  String json = "{";

  json += "\"device_name\":\""      + deviceName              + "\",";
  json += "\"device_type\":\""      + String(DEVICE_TYPE)     + "\",";
  json += "\"firmware_version\":\"" + String(FW_VERSION)      + "\",";
  json += "\"device_id\":\""        + getUniqueID()           + "\",";
  json += "\"server\":\""           + serverBaseUrl           + "\",";
  json += "\"status\":\""           + deviceModeToString(deviceMode) + "\",";
  json += "\"ip_address\":\""       + ip.toString()           + "\",";
  json += "\"mac_address\":\""      + getMAC()                + "\",";
  json += "\"uptime\":"             + String(millis() / 1000);

  // Хук для добавления специфичных полей (например, количество каналов, тип реле и т.п.)
  appendAboutJsonFields(json);

  json += "}";

  sendJson(200, json);
}

// ---------------------- /status ----------------------
inline void handleStatus() {
  if (deviceMode == DEVICE_MODE_NORMAL) {
    if (!requireAuth()) return;
  }

  String json = "{";

  // Базовый статус устройства
  json += "\"status\":\"ok\"";

  // Хук: устройство добавляет сюда свои поля (каналы, яркость, что угодно)
  appendStatusJsonFields(json);

  json += "}";

  sendJson(200, json);
}

inline void handleSetDeviceName() {
  // Только в нормальном режиме и только с авторизацией
  if (deviceMode != DEVICE_MODE_NORMAL) {
    String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Not in normal mode\"}");
    sendJson(400, json);
    return;
  }

  if (!requireAuth()) return;

  String body = server.arg("plain");
  body.trim();

  String newName = extractJsonStringValue(body, F("device_name"));
  newName.trim();

  if (newName.length() == 0) {
    String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"device_name is required\"}");
    sendJson(400, json);
    return;
  }

  // Небольшая санитация + ограничение длины
  String sanitized;
  for (uint16_t i = 0; i < newName.length() && sanitized.length() < DEVICE_NAME_MAX_LEN - 1; i++) {
    char c = newName[i];
    // отбрасываем управляющие символы
    if (c >= 32 && c != '\"') {
      sanitized += c;
    }
  }

  if (sanitized.length() == 0) {
    String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Invalid device_name\"}");
    sendJson(400, json);
    return;
  }

  deviceName = sanitized;
  saveDeviceConfig();

  String json = F("{\"status\":\"ok\",\"message\":\"Device name updated\"}");
  sendJson(200, json);
}


// ---------------------- /action ----------------------
// Обработка действий полностью делегируется устройству
inline void handleAction() {
  if (deviceMode != DEVICE_MODE_NORMAL) {
    String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Action not available in this mode\"}");
    sendJson(403, json);
    return;
  }

  if (!requireAuth()) return;

  String body = server.arg("plain");
  body.trim();

  String action = extractJsonStringValue(body, F("action"));
  if (action.length() == 0) {
    String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Action is required\"}");
    sendJson(400, json);
    return;
  }

  // Пытаемся вытащить params как объект, если он есть
  String paramsJson = extractJsonObject(body, F("params"));
  if (paramsJson.length() == 0) {
    // Можно по договорённости считать, что устройство само достанет что нужно из body
    paramsJson = body;
  }

  String errorCode;
  String errorMessage;

  bool ok = deviceHandleAction(action, paramsJson, errorCode, errorMessage);
  if (!ok) {
    if (errorCode.length() == 0)   errorCode = "IllegalActionOrParams";
    if (errorMessage.length() == 0) errorMessage = "Device action failed";

    String json = "{\"status\":\"error\",\"error\":\"" + errorCode +
                  "\",\"message\":\"" + errorMessage + "\"}";
    sendJson(400, json);
    return;
  }

  String json = F("{\"status\":\"ok\",\"message\":\"Action executed\"}");
  sendJson(200, json);
}


// ---------------------- 4. POST /set_token ----------------------
// setup: без токена, первый раз устанавливает токен и переводит в normal
// normal: требует токен, меняет его
inline void handleSetToken() {
  String body = server.arg("plain");
  body.trim();

  String newToken = extractJsonStringValue(body, F("token"));
  if (newToken.length() == 0) {
    String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Token is required\"}");
    sendJson(400, json);
    return;
  }

  if (deviceMode == DEVICE_MODE_SETUP) {
    // IP сервера, с которого пришёл запрос
    IPAddress remote = server.client().remoteIP();
    serverBaseUrl    = remote.toString();   // например "192.168.2.10"
    // В режиме setup всегда разрешаем установить токен, 
    // даже если в EEPROM что-то лежит.
    authToken  = newToken;
    deviceMode = DEVICE_MODE_NORMAL;
    saveDeviceConfig();

    String json = F("{\"status\":\"ok\",\"message\":\"Token set. Device mode: normal\"}");
    sendJson(200, json);
    return;
  }

  // DEVICE_MODE_NORMAL — смена токена только с действующим токеном
  if (deviceMode == DEVICE_MODE_NORMAL) {
    if (!requireAuth()) return;

    authToken = newToken;
    saveDeviceConfig();

    String json = F("{\"status\":\"ok\",\"message\":\"Token updated\"}");
    sendJson(200, json);
    return;
  }

  // Прочие режимы
  String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Cannot set token in current mode\"}");
  sendJson(403, json);
}

// ---------------------- 5. GET /setup ----------------------
// В режиме setup отдаём HTML-страницу настройки Wi-Fi
inline void handleSetupGet() {
  if (deviceMode != DEVICE_MODE_SETUP) {
    sendNotAvailable();
    return;
  }

  // Используем уже готовую HTML-страницу
  server.send(200, F("text/html; charset=utf-8"), wifiSetupPage);
}

// ---------------------- 5. POST /setup ----------------------
// JSON: {"ssid":"...","password":"...","server":"http://..."}
// или form-data, как в текущей веб-форме
inline void handleSetupPost() {
  if (deviceMode != DEVICE_MODE_SETUP) {
    sendNotAvailable();
    return;
  }

  String ssid;
  String pass;
  String serverUrl;

  // 1) Попытка прочитать как форму (из существующей веб-страницы)
  if (server.hasArg(F("ssid"))) {
    ssid = server.arg(F("ssid"));
    pass = server.arg(F("pass"));
    if (server.hasArg(F("server"))) {
      serverUrl = server.arg(F("server"));
    }
  } else {
    // 2) Попытка прочитать JSON
    String body = server.arg("plain");
    body.trim();
    ssid      = extractJsonStringValue(body, F("ssid"));
    pass      = extractJsonStringValue(body, F("password"));
    serverUrl = extractJsonStringValue(body, F("server"));
  }

  if (ssid.length() == 0) {
    String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"SSID is required\"}");
    sendJson(400, json);
    return;
  }

  savedSSID = ssid;
  savedPASS = pass;
  saveWiFiConfig(savedSSID, savedPASS);

  if (serverUrl.length() > 0) {
    serverBaseUrl = serverUrl;
  }

  deviceHandleReset();

  String json = F("{\"status\":\"ok\",\"message\":\"Wi-Fi configured. Connecting...\"}");
  sendJson(200, json);

  // Дальше просто перезапустимся — при старте прошивка сама попробует подключиться к Wi-Fi
  delay(800);
  ESP.restart();
}

// ---------------------- 6. POST /reboot ----------------------
inline void handleRebootApi() {
  if (deviceMode != DEVICE_MODE_NORMAL) {
    String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Reboot available only in normal mode\"}");
    sendJson(403, json);
    return;
  }

  if (!requireAuth()) return;

  String json = F("{\"status\":\"ok\",\"message\":\"Device will reboot now\"}");
  sendJson(200, json);

  Serial.println(F("Reboot requested via REST API"));
  delay(500);
  ESP.restart();
}

// ---------------------- 7. POST /reset ----------------------
// Сброс всех настроек к заводским, переход в setup
inline void handleResetApi() {
  if (deviceMode != DEVICE_MODE_NORMAL) {
    String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Reset available only in normal mode\"}");
    sendJson(403, json);
    return;
  }

  if (!requireAuth()) return;

  // Сбрасываем Wi-Fi
  savedSSID = "";
  savedPASS = "";
  saveWiFiConfig(savedSSID, savedPASS);

  // Сбрасываем токен и режим
  authToken = "";
  serverBaseUrl = "";
  deviceMode = DEVICE_MODE_SETUP;
  saveDeviceConfig();

  String json = F("{\"status\":\"ok\",\"message\":\"Device reset to factory settings. Entering setup mode.\"}");
  sendJson(200, json);

  delay(800);
  ESP.restart();
}

// ---------------------- Регистрация роутов ----------------------
inline void registerRestApiRoutes() {
  server.on(F("/about"),     HTTP_GET,  handleAbout);
  server.on(F("/status"),    HTTP_GET,  handleStatus);
  server.on(F("/action"),    HTTP_POST, handleAction);
  server.on(F("/set_token"), HTTP_POST, handleSetToken);
  server.on(F("/set_device_name"), HTTP_POST, handleSetDeviceName);


  server.on(F("/setup"),     HTTP_GET,  handleSetupGet);
  server.on(F("/setup"),     HTTP_POST, handleSetupPost);

  server.on(F("/reboot"),    HTTP_POST, handleRebootApi);
  server.on(F("/reset"),     HTTP_POST, handleResetApi);
}

#endif // REST_API_H