Basic REST API

This commit is contained in:
Jean Jacques Avril 2022-02-19 03:06:28 +01:00
parent e2c7a65e7c
commit f2fd5bc3b6
11 changed files with 319 additions and 68 deletions

View File

@ -9,8 +9,19 @@
- Allow integrity check for duplicate pin numbers, rfid etc.
* Use text templates for Display
* Rebuild WEB UI -> Mobile first approach. More limited features but therefore easy to understand
### 19.02.2022
* CSV upload
* Admin authentification
* Api for device settings
* capitve portal
* (Optional) REST user update
* Create WEB UI without React
------
## Log
### 13.02.2022
+ Finally I found a solution for using a json document with variable file size. Import and export works well - till you have more than 300 users.
### 19.02.2022
+ Stable and fast csv database implemented.
+ Basic REST API: read, create and delete users / read and drop csv db

12
data/static/error404.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Error 404: Page not found.</h1>
</body>
</html>

12
data/static/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Welcome on ESP8266</h1>
</body>
</html>

View File

@ -13,6 +13,9 @@ platform = espressif8266
board = nodemcuv2
framework = arduino
board_build.filesystem = littlefs
monitor_speed = 115200 ; set to the baud rate you pass to Serial.begin(…)
monitor_filters = esp8266_exception_decoder, default
build_type = debug
lib_deps =
xreef/PCF8574 library@^2.2.1
marcoschwartz/LiquidCrystal_I2C@^1.1.4

View File

@ -41,6 +41,10 @@ void Interface::showMessage(String msg1, String msg2, unsigned long display_time
this->_msg_display_time = display_time;
this->setState(states::MSG);
}
int Interface::getState(){
return _display_state;
}
void Interface::render()
{
if (this->_keyboard->available())

View File

@ -37,6 +37,7 @@ public:
void begin(Keyboard* Keyboard);
void render();
void setState(states state);
int getState();
void greetUser(String username);
String getPin();
bool pinAvailable();

View File

@ -14,10 +14,19 @@ bool UserDb::add_user(User &user)
{
File db = LittleFS.open(filename, "a");
db.write("\n" + String(user.uid) + ";" + user.first_name + ";" + user.last_name + ";" + user.rfid_uid + ";" + user.user_pin + ";" + user.enabled ? "1" : "0");
if (db)
{
if (db.size() > 0)
db.write("\n");
String csvline = user.toCSVString();
db.write(csvline.c_str());
db.close();
return true;
}
else
return false;
}
User UserDb::user_by_pin(String cmp)
{
#ifdef DEBUG
@ -27,7 +36,7 @@ User UserDb::user_by_pin(String cmp)
do
{
User temp = *it;
if (temp.enabled)
if (temp.match)
return temp;
++it;
} while (it.has_next());
@ -42,7 +51,7 @@ User UserDb::user_by_rfid(String cmp)
do
{
User temp = *it;
if (temp.enabled)
if (temp.match)
return temp;
++it;
} while (it.has_next());
@ -50,15 +59,20 @@ User UserDb::user_by_rfid(String cmp)
}
User UserDb::user_by_uid(unsigned long cmp)
{
String cmps = String(cmp);
return user_by_uid(cmps);
}
User UserDb::user_by_uid(String cmp)
{
#ifdef DEBUG
Serial.println("Searching for user with user id " + String(cmp));
Serial.println("Searching for user with id " + cmp);
#endif
Iterator it = begin();
Iterator it = begin_with_filter(&cmp, USERATTRIBUTES::USER_ID);
do
{
User temp = *it;
if (temp.uid == cmp)
if (temp.match)
return temp;
++it;
} while (it.has_next());

View File

@ -28,12 +28,20 @@ namespace userdb
mutable String rfid_uid;
mutable String user_pin;
mutable bool enabled = false;
mutable bool match = false;
bool operator<(const User &o) const { return uid < o.uid; }
bool operator==(const User &o) const { return uid == o.uid; }
String toString()
{
return "ID: " + String(uid) + " Name: " + last_name + " " + first_name + " Enabled: " + (enabled ? "Yes" : "No") + " LineNo.: " + String(line) + "[RFID/PIN]" + rfid_uid + "//" + user_pin;
}
String toCSVString()
{
return String(uid) + ";" + first_name + ";" + last_name + ";" + rfid_uid + ";" + user_pin + ";" + (enabled ? "1" : "0");
}
String toJSONString(){
return "{\"uid\":\""+String(uid) + "\",\"first_name\":\"" + first_name + "\",\"last_name\":\"" + last_name + "\",\"rfid_uid\":\"" + rfid_uid + "\",\"user_pin\":\"" + user_pin + "\",\"enabled\":" + (enabled ? "true" : "false")+"}";
}
};
class UserDb
@ -49,45 +57,12 @@ namespace userdb
User user_by_pin(String);
User user_by_rfid(String);
User user_by_uid(unsigned long);
inline User read_csv_line(String &instring, uint32_t &line, String *match, USERATTRIBUTES attr)
{
int locations[5];
int location_index = 0;
int str_length = instring.length() - 1;
for (int i = 0; i < str_length; i++)
{
if (instring.charAt(i) == ';')
{
locations[location_index] = i;
location_index++;
}
}
User res;
if (attr == RFID_UID)
{
res.rfid_uid = instring.substring(locations[2] + 1, locations[3]);
if (!res.rfid_uid.equals(*match))
return res;
res.user_pin = instring.substring(locations[3] + 1, locations[4]);
}
else if (attr == USER_PIN)
{
res.user_pin = instring.substring(locations[3] + 1, locations[4]);
if (!res.user_pin.equals(*match))
return res;
res.rfid_uid = instring.substring(locations[2] + 1, locations[3]);
}
else
{
res.rfid_uid = instring.substring(locations[2] + 1, locations[3]);
res.user_pin = instring.substring(locations[3] + 1, locations[4]);
}
res.uid = instring.substring(0, locations[0]).toInt();
res.first_name = instring.substring(locations[0] + 1, locations[1]);
res.last_name = instring.substring(locations[1] + 1, locations[2]);
res.enabled = instring.charAt(locations[4] + 1) == '1' ? true : false;
res.line = line;
return res;
User user_by_uid(String cmp);
bool drop(){
LittleFS.remove(filename);
File n = LittleFS.open(filename,"a");
n.close();
return true;
}
static inline User read_csv_line(File &instream, uint32_t &line, String *match, USERATTRIBUTES attr)
{
@ -106,6 +81,7 @@ namespace userdb
return res;
res.enabled = instream.read() == '1' ? true : false;
res.line = line;
res.match = true;
return res;
}
bool remove_user(User &user);
@ -119,7 +95,7 @@ namespace userdb
while (old_db.available())
{
if (curr_line == line)
while (old_db.read() != '\n')
while (old_db.available()&&old_db.read() != '\n')
{
}
else
@ -194,10 +170,8 @@ namespace userdb
{
if (db_file.available())
{
// String temp = db_file.readStringUntil('\n');
// current = read_csv_line(temp, line, match, filter_attr);
// line++;
current = read_csv_line(db_file, line, match, filter_attr);
// Position stream pointer to beginning of the next line
while (db_file.available())
{
if (db_file.read() == '\n')
@ -223,7 +197,7 @@ namespace userdb
bool available = true;
uint32_t line = 0;
};
Iterator begin() { return Iterator(LittleFS.open("userdb.csv", "r")); }
Iterator begin_with_filter(String *match, USERATTRIBUTES attr) { return Iterator(LittleFS.open("userdb.csv", "r"), match, attr); }
Iterator begin() { return Iterator(LittleFS.open(filename, "r")); }
Iterator begin_with_filter(String *match, USERATTRIBUTES attr) { return Iterator(LittleFS.open(filename, "r"), match, attr); }
};
}

176
src/WebConsole.cpp Normal file
View File

@ -0,0 +1,176 @@
#include "WebConsole.h"
using namespace webconsole;
WebConsole::WebConsole() {}
WebConsole::~WebConsole()
{
_server->close();
delete (_server);
}
bool WebConsole::init(userdb::UserDb *userdb)
{
_server = new ESP8266WebServer(80);
this->userdb = userdb;
_server->begin();
_server->on("/userdb", HTTPMethod::HTTP_DELETE, std::bind(&WebConsole::_dropUserDb, this));
_server->on("/userdb", HTTPMethod::HTTP_GET, std::bind(&WebConsole::_getUserDb, this));
_server->on("/rfid", std::bind(&WebConsole::_catchRFID, this));
_server->on(UriBraces("/user/{}"), HTTPMethod::HTTP_DELETE, std::bind(&WebConsole::_deleteUser, this));
_server->on(UriBraces("/user/{}"), HTTPMethod::HTTP_GET, std::bind(&WebConsole::_getUser, this));
_server->on(UriBraces("/user/{}"), HTTPMethod::HTTP_PUT, std::bind(&WebConsole::_createUser, this));
_server->on(UriBraces("/get/{}"), std::bind(&WebConsole::_deleteUser, this));
//_server->on("/bypin",std::bind(&WebConsole::_findPin,this));
_server->onNotFound(std::bind(&WebConsole::_handleStatic, this));
return true;
}
void WebConsole::attachRfid(Rfid *rfid){
this->rfid=rfid;
}
void WebConsole::serve()
{
if (catch_rfid && rfid != nullptr && rfid->available())
{
rfid_buffer = rfid->getID();
catch_rfid_updated = true;
catch_rfid = false;
}
_server->handleClient();
}
bool WebConsole::isInterceptingRfid(){
return catch_rfid;
}
void WebConsole::_handleStatic()
{
String path = path_prefix + _server->uri();
Serial.print("Request " + path);
File src = LittleFS.open(path, "r");
if (src.isDirectory())
{
path += "index.html";
src.close();
src = LittleFS.open(path, "r");
}
if (!src)
{
path = "/static/error404.html";
src.close();
src = LittleFS.open(path, "r");
}
Serial.println(" resolved to " + path);
if (src)
{
String content_type = "text/html";
_server->streamFile(src, content_type);
src.close();
}
else
{
_server->send(500, "text/plain", "Internal error 500");
}
}
void WebConsole::_getUserDb()
{
File src = LittleFS.open("userdb.csv", "r");
if (src)
{
_server->streamFile(src, "text/csv");
}
src.close();
}
void WebConsole::_deleteUser()
{
if (userdb == nullptr)
{
_server->send(500, "text/json", "{\"error\":\"UserDb not initialized\"}");
return;
}
String uid = _server->pathArg(0);
userdb::User del = userdb->user_by_uid(uid);
if (del.match == true)
{
userdb->remove_user(del);
_server->send(200, "text/plain", del.toJSONString());
}
else
{
_server->send(500, "text/json", "{\"error\":\"User not found.\"}");
}
}
void WebConsole::_getUser()
{
if (userdb == nullptr)
{
_server->send(500, "text/json", "{\"error\":\"UserDb not initialized\"}");
return;
}
String uid = _server->pathArg(0);
userdb::User res = userdb->user_by_uid(uid);
if (res.match == true)
{
_server->send(200, "text/json", res.toJSONString());
}
else
{
_server->send(500, "text/json", "{\"error\":\"User not found.\"}");
}
}
void WebConsole::_createUser()
{
userdb::User created;
String body = _server->arg("plain");
const int capacity = 1024;
StaticJsonDocument<capacity> doc;
DeserializationError err = deserializeJson(doc, body);
if (err.code() != err.Ok)
{
Serial.println(err.c_str());
Serial.println(body);
String response = "{\"error\":\"";
response += err.c_str();
response += "\"}";
_server->send(500, "text/json", response);
return;
}
if (!_server->pathArg(0).isEmpty())
created.uid = _server->pathArg(0).toInt();
else if (!doc["uid"].isNull())
created.uid = doc["uid"].as<unsigned long>();
else
{
_server->send(500, "text/json", "{\"error\":\"UserId not provided.\"}");
return;
}
created.first_name = doc["first_name"].as<String>();
created.last_name = doc["last_name"].as<String>();
created.rfid_uid = doc["rfid_uid"].as<String>();
created.user_pin = doc["user_pin"].as<String>();
created.enabled = doc["enabled"].as<bool>();
userdb->add_user(created);
_server->send(200, "text/json", created.toJSONString());
}
void WebConsole::_dropUserDb()
{
if (userdb->drop())
_server->send(500, "text/json", "{\"ok\":\"UserDb dropped.\"}");
else
_server->send(500, "text/json", "{\"error\":\"UserDb could not be dropped.\"}");
}
void WebConsole::_catchRFID()
{
if (catch_rfid_updated)
{
String response = "{\"rfid_uid\":\"" + rfid_buffer + "\"}";
_server->send(500, "text/json", response);
catch_rfid_updated = false;
}
else if(catch_rfid){
_server->send(500, "text/json", "{\"ok\":\"already activated\"}");
}
else{
catch_rfid = true;
_server->send(500, "text/json", "{\"ok\":\"now activated\"}");
}
}

40
src/WebConsole.h Normal file
View File

@ -0,0 +1,40 @@
#pragma once
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <uri/UriBraces.h>
#include <uri/UriRegex.h>
#include "LittleFS.h"
#include "ArduinoJson.h"
#include "UserDb.h"
#include "Rfid.h"
namespace webconsole
{
static const char path_prefix[] PROGMEM = "/static";
static ESP8266WebServer _server(80);
class WebConsole
{
public:
WebConsole();
~WebConsole();
bool init(userdb::UserDb *userdb);
void attachRfid(Rfid *rfid);
void serve();
bool isInterceptingRfid();
private:
void _handleStatic();
void _getUserDb();
void _deleteUser();
void _getUser();
void _createUser();
void _dropUserDb();
void _catchRFID();
ESP8266WebServer* _server;
userdb::UserDb *userdb = nullptr;
bool catch_rfid = false;
bool catch_rfid_updated = false;
String rfid_buffer;
Rfid *rfid = nullptr;
};
}

View File

@ -5,13 +5,13 @@
#include "Rfid.h"
#include "Interface.h"
#include <ESP8266WiFi.h>
#include <vector>
//#include "Users.h"
#include "WebConsole.h"
#include "UserDb.h"
#include "Persistence.h"
// File persistence
Persistence persistence;
userdb::UserDb userdatabase("userdb.csv");
webconsole::WebConsole web;
// Rfid
Rfid rfid;
// i2C Bus
@ -32,52 +32,56 @@ IPAddress subnet(255, 255, 255, 0);
void setup()
{
Persistence::Configuration config = persistence.loadConfig();
// users.ImportFromPersistence();
Serial.begin(9600);
Serial.begin(115200);
Serial.println("Starting System");
Serial.println("\t1. Network config: " + WiFi.softAPConfig(local_IP, gateway, subnet) ? "Ready" : "Failed!");
Serial.print("\t1. Network config ->");
Serial.println(WiFi.softAPConfig(local_IP, gateway, subnet) ? "Ready" : "Failed!");
Serial.print("\t2 AP setup " + String(config.SSID)+ " -> ");
if (strlen(config.PASS) > 0)
Serial.println("\t1. AP setup with password: " + WiFi.softAP(config.SSID, config.PASS) ? "Ready" : "Failed!");
Serial.println(WiFi.softAP(config.SSID, config.PASS) ? "Ready" : "Failed!");
else
Serial.println("\t1. AP setup without password: " +WiFi.softAP(config.SSID) ? "Ready" : "Failed!");
delay(250);
Serial.println(WiFi.softAP(config.SSID) ? "Ready" : "Failed!");
delay(150);
#ifdef DEBUG
userdatabase.print_to_serial();
#endif
web.init(&userdatabase);
keyboard.begin(&Wire);
rfid.begin();
web.attachRfid(&rfid);
iface.begin(&keyboard);
userdatabase.remove_user_by_line("userdb.csv",2);
}
void loop()
{
keyboard.scanAsync();
rfid.scan();
web.serve();
keyboard.scanAsync();
userdb::User login_user;
if (iface.pinAvailable())
if(web.isInterceptingRfid()&&iface.getState()==0){
iface.showMessage("WebUI connected", "Awaiting RFID", 3000);
}
else if (iface.pinAvailable())
{
unsigned long delta = millis();
login_user = userdatabase.user_by_pin(iface.getPin());
Serial.println("Query duration: " +String(millis()-delta));
Serial.println("Query duration: " + String(millis() - delta));
if (login_user.enabled == false)
{
iface.showMessage("Login failed!", "-> Pin incorrect", 3000);
return;
}
}
else if (rfid.available())
{
unsigned long delta = millis();
login_user = userdatabase.user_by_rfid(rfid.getID());
Serial.println("Query duration: " +String(millis()-delta));
Serial.println("Query duration: " + String(millis() - delta));
if (login_user.enabled == false)
{
iface.showMessage("Login failed!", "-> Unkown Card", 3000);
return;
}
}
if (login_user.enabled == true)