Compare commits
No commits in common. "master" and "304844daef7704ed40dcf179e9538b296ed684fe" have entirely different histories.
master
...
304844daef
3
.vscode/extensions.json
vendored
@ -3,8 +3,5 @@
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
]
|
||||
}
|
||||
|
49
Notes.md
@ -1,49 +0,0 @@
|
||||
# Notes
|
||||
|
||||
|
||||
## Todo's
|
||||
|
||||
* Create REST API
|
||||
* Create Persistence
|
||||
- Change userpersitence to an iterator over a csv file, that does not require to keep the whole database in ram
|
||||
- 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
|
||||
|
||||
### 19.03.2022
|
||||
+ Settings pos
|
||||
+ WifiSSID - terminator 0x00 - Pos.: 0-31 (00 - 1F)
|
||||
+ WifiPassword - terminator 0x00 - Pos.: 32-63 (20 - 3F)
|
||||
+ Device IP - 40-43 uint32
|
||||
+ Subnet 44 - 47
|
||||
+ Gateway 48 - 4B
|
||||
+ Mode 0 - station; 1 - client 4C uint8
|
||||
+ AuthError Timeout (secs) 4D
|
||||
+ OpenLock hold (secs) 4E
|
||||
### 20.03.2022
|
||||
+ settings API implemented and tested
|
||||
+ TODO: factory reset
|
||||
|
||||
### 10.04.2022
|
||||
+ Toni:
|
||||
+ Reed Contact -> Door closed?
|
||||
+ Tracking in->out
|
||||
+ Stats
|
||||
+ Sensors
|
||||
|
@ -5,14 +5,9 @@ Adress 0x21
|
||||
### 1.1 Wiring
|
||||
* Red: 3.3V
|
||||
* Black GND
|
||||
* Green SDA -> D2
|
||||
* Grey SCL -> D1
|
||||
* Green SDA -> D3
|
||||
* Grey SCL -> D4
|
||||
|
||||
## 2.0 LCD
|
||||
The LCD display is driven on the same i2c bus as the keypad.
|
||||
|
||||
nterface Definition
|
||||
BYTE BIT
|
||||
7 (MSB) 6 5 4 3 2 1 0 (LSB)
|
||||
I2C slave address L H L L A2 A1 A0 R/W
|
||||
I/O data bus P7 P6 P5 P4 P3 P2 P1 P0
|
||||
|
BIN
data/admin
12
data/s/a.svg
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.2" baseProfile="tiny" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" viewBox="0 0 500 500" overflow="visible" xml:space="preserve">
|
||||
<polyline fill="#CCCCCC" stroke="#000000" stroke-miterlimit="10" points="181.5,395.5 86.5,395.5 86.5,300.5 "/>
|
||||
<g>
|
||||
|
||||
<rect x="355.37" y="40.63" transform="matrix(0.7071 -0.7071 0.7071 0.7071 31.9147 294.7168)" fill="#CCCCCC" stroke="#000000" stroke-width="1" stroke-miterlimit="9.9999" width="32.69" height="136.4"/>
|
||||
|
||||
<rect x="94.59" y="168.11" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -95.5641 241.9096)" fill="#CCCCCC" stroke="#000000" stroke-width="1" stroke-miterlimit="9.9999" width="299.29" height="136.4"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 881 B |
10
data/s/b.svg
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.2" baseProfile="tiny" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" viewBox="0 0 500 500" overflow="visible" xml:space="preserve">
|
||||
<path fill="#CCCCCC" stroke="#000000" stroke-linecap="round" stroke-miterlimit="10" d="M349.5,421.5h-199c-8.28,0-15-6.72-15-15
|
||||
v-255h229v255C364.5,414.78,357.78,421.5,349.5,421.5z"/>
|
||||
<path fill="#CCCCCC" stroke="#000000" stroke-linecap="round" stroke-miterlimit="10" d="M386.81,99.5H314.5V80.38
|
||||
c0-9.88-8.01-17.88-17.88-17.88h-92.24c-9.88,0-17.88,8.01-17.88,17.88V99.5h-73.31c-5.35,0-9.69,4.34-9.69,9.69v20.62
|
||||
c0,5.35,4.34,9.69,9.69,9.69h273.62c5.35,0,9.69-4.34,9.69-9.69v-20.62C396.5,103.84,392.16,99.5,386.81,99.5z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 874 B |
@ -1 +0,0 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>doorlock_pwa</title><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-capable" content="yes"><link rel="apple-touch-icon" href="/assets/icons/apple-touch-icon.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#673ab8"><style>*{box-sizing:border-box}html{font-family:Helvetica,sans-serif;font-size:16px}body,html{height:100%}body{background-color:#fff;margin:0;padding:0;width:100%}</style><link href="/bundle.3bf6c.css" rel="stylesheet" media="only x" onload="this.media='all'"><noscript><link rel="stylesheet" href="/bundle.3bf6c.css"></noscript></head><body><script defer="defer" src="/bundle.4bdf3.js"></script><script nomodule="" src="/polyfills.914a6.js"></script></body></html>
|
BIN
data/settings
1218
data/userdb.csv
@ -12,10 +12,6 @@
|
||||
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
|
||||
|
@ -1,34 +0,0 @@
|
||||
#include "AdminAuth.h"
|
||||
using namespace webconsole;
|
||||
bool AdminAuth::isAuth(const char *token)
|
||||
{
|
||||
return tokenbuffer->exists(token) != -1;
|
||||
}
|
||||
bool AdminAuth::logout(const char *token)
|
||||
{
|
||||
int16_t tid = tokenbuffer->exists(token);
|
||||
tokenbuffer->setnull(tid);
|
||||
return tid != -1;
|
||||
}
|
||||
char *AdminAuth::login(const String& username, const String& password)
|
||||
{
|
||||
char *res = nullptr;
|
||||
File adminfile = LittleFS.open("admin", "r");
|
||||
if (username.equals(adminfile.readStringUntil('\0')) && password.equals(adminfile.readStringUntil('\0')))
|
||||
res = tokenbuffer->newToken();
|
||||
adminfile.close();
|
||||
return res;
|
||||
}
|
||||
bool AdminAuth::setAuth(const String& username, const String& password)
|
||||
{
|
||||
if (username.length() == 0 || username.length() > MAX_USERNAMEPASSWORD_LENGTH || password.length()==0 || password.length() > MAX_USERNAMEPASSWORD_LENGTH)
|
||||
return false;
|
||||
File adminfile = LittleFS.open("admin", "w+");
|
||||
adminfile.print(username);
|
||||
adminfile.print('\0');
|
||||
adminfile.print(password);
|
||||
adminfile.print('\0');
|
||||
delay(1);
|
||||
adminfile.close();
|
||||
return true;
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
#pragma once
|
||||
#include "LittleFS.h"
|
||||
#define TOKENBUFFERCAPACITY 3
|
||||
#define TOKENLENGHT 10
|
||||
#define MAX_USERNAMEPASSWORD_LENGTH 25
|
||||
namespace webconsole
|
||||
{
|
||||
|
||||
struct TokenBuffer
|
||||
{
|
||||
|
||||
TokenBuffer()
|
||||
{
|
||||
for (uint16_t j = 0; j < TOKENBUFFERCAPACITY; j++)
|
||||
setnull(j);
|
||||
}
|
||||
char *newToken()
|
||||
{
|
||||
if (next >= TOKENBUFFERCAPACITY)
|
||||
next = 0;
|
||||
for (uint16_t i = 0; i < TOKENLENGHT; i++)
|
||||
token[next][i] = randomChar();
|
||||
token[next][TOKENLENGHT] = 0x00;
|
||||
|
||||
return &token[next++][0];
|
||||
}
|
||||
int16_t exists(const char *cmp)
|
||||
{
|
||||
for (uint16_t j = 0; j < TOKENBUFFERCAPACITY; j++)
|
||||
{
|
||||
bool match = true;
|
||||
for (uint16_t i = 0; match && i < TOKENLENGHT; i++)
|
||||
if (token[j][i] != cmp[i])
|
||||
match = false;
|
||||
if (match)
|
||||
return j;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
void setnull(uint16_t index)
|
||||
{
|
||||
if (index >= 0 && index < TOKENBUFFERCAPACITY)
|
||||
for (uint16_t i = 0; i < TOKENLENGHT; i++)
|
||||
token[index][i] = 0x00; // initialize with nullbytes
|
||||
}
|
||||
|
||||
private:
|
||||
uint16_t next = 0;
|
||||
char token[TOKENBUFFERCAPACITY][TOKENLENGHT + 1];
|
||||
char randomChar()
|
||||
{
|
||||
auto charrype = random(0, 3);
|
||||
switch (charrype)
|
||||
{
|
||||
case 0:
|
||||
return random(0x30, 0x3A); // Numbers
|
||||
break;
|
||||
case 1:
|
||||
return random(0x41, 0x5B); // Capital letters
|
||||
break;
|
||||
case 2:
|
||||
return random(0x61, 0x7B); // Lowercase letters
|
||||
break;
|
||||
}
|
||||
return 0x21;
|
||||
}
|
||||
};
|
||||
class AdminAuth
|
||||
{
|
||||
public:
|
||||
//char *login(const char *username, const char *password);
|
||||
char *login(const String &username,const String &password);
|
||||
bool isAuth(const char *token);
|
||||
bool logout(const char *token);
|
||||
bool setAuth(const String &username, const String &password);
|
||||
|
||||
private:
|
||||
TokenBuffer *tokenbuffer = new TokenBuffer();
|
||||
};
|
||||
}
|
106
src/Config.cpp
@ -1,106 +0,0 @@
|
||||
#include "Config.h"
|
||||
|
||||
Config::Config(const char* filepath): _filepath(filepath)
|
||||
{
|
||||
if (!LittleFS.begin())
|
||||
{
|
||||
Serial.println("An Error has occurred while mounting LittleFS");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Config::~Config()
|
||||
{
|
||||
free(buffer);
|
||||
}
|
||||
bool Config::setFilePath(const char* filepath){
|
||||
this->_filepath = filepath;
|
||||
return loadBin();
|
||||
}
|
||||
bool Config::loadBin()
|
||||
{
|
||||
File config_file = LittleFS.open(_filepath, "r");
|
||||
//if (buffer == nullptr) // Allocate only once
|
||||
// buffer = (uint8_t *)malloc(CONFIG_SIZE);
|
||||
if (config_file.available())
|
||||
{
|
||||
config_file.read(buffer, CONFIG_SIZE);
|
||||
buffer[OFFSET_SSID + 0x1F] = 0x00; // ensure ssid and password are terminated with a null character
|
||||
buffer[OFFSET_PASS + 0x1F] = 0x00;
|
||||
}
|
||||
config_file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Config::saveBin()
|
||||
{
|
||||
File config_file = LittleFS.open(_filepath, "w");
|
||||
config_file.write(buffer, CONFIG_SIZE);
|
||||
delay(100);
|
||||
config_file.close();
|
||||
return true;
|
||||
}
|
||||
bool Config::setSSID(const char *ssid)
|
||||
{
|
||||
size_t offset = 0;
|
||||
bool copy = true;
|
||||
for (int i = 0; i < 31; i++)
|
||||
{
|
||||
if (ssid[i] == 0x00)
|
||||
copy = false;
|
||||
buffer[offset + i] = copy ? ssid[i] : 0x00;
|
||||
}
|
||||
buffer[offset + 31] = '\n';
|
||||
return true;
|
||||
}
|
||||
bool Config::setPASS(const char *pass)
|
||||
{
|
||||
size_t offset = 0x20;
|
||||
bool copy = true;
|
||||
for (int i = 0; i < 31; i++)
|
||||
{
|
||||
if (pass[i] == 0x00)
|
||||
copy = false;
|
||||
buffer[offset + i] = copy ? pass[i] : 0x00;
|
||||
}
|
||||
buffer[offset + 31] = '\n';
|
||||
return true;
|
||||
}
|
||||
void Config::print()
|
||||
{
|
||||
Serial.print("BufferHEX ");
|
||||
for (int i = 0; i < CONFIG_SIZE; i++)
|
||||
{
|
||||
Serial.print(i, HEX);
|
||||
Serial.print(">");
|
||||
Serial.print(buffer[i], HEX);
|
||||
Serial.print("|");
|
||||
}
|
||||
Serial.print("SSID: ");
|
||||
Serial.println(SSID);
|
||||
Serial.print("PASS: ");
|
||||
Serial.println(PASS);
|
||||
Serial.print("IP");
|
||||
Serial.print(*((uint8_t *)&ip), DEC);
|
||||
Serial.print(".");
|
||||
Serial.print(* (((uint8_t *)&ip + 1)), DEC);
|
||||
Serial.print(".");
|
||||
Serial.print(* ((uint8_t *)&ip + 2), DEC);
|
||||
Serial.print(".");
|
||||
Serial.println( * ((uint8_t *)&ip + 3), DEC);
|
||||
Serial.print("GW");
|
||||
Serial.print( * ((uint8_t *)&gw), DEC);
|
||||
Serial.print(".");
|
||||
Serial.print( * ((uint8_t *)&gw + 1), DEC);
|
||||
Serial.print(".");
|
||||
Serial.print( * ((uint8_t *)&gw + 2), DEC);
|
||||
Serial.print(".");
|
||||
Serial.println( * ((uint8_t *)&gw + 3), DEC);
|
||||
Serial.print("SUBNET");
|
||||
Serial.print( * ((uint8_t *)&subnet), DEC);
|
||||
Serial.print(".");
|
||||
Serial.print( * ((uint8_t *)&subnet + 1), DEC);
|
||||
Serial.print(".");
|
||||
Serial.print( * ((uint8_t *)&subnet + 2), DEC);
|
||||
Serial.print(".");
|
||||
Serial.println( * ((uint8_t *)&subnet + 3), DEC);
|
||||
}
|
37
src/Config.h
@ -1,37 +0,0 @@
|
||||
#pragma once
|
||||
#include "LittleFS.h"
|
||||
#include "ArduinoJson.h"
|
||||
#define OFFSET_SSID 0x00
|
||||
#define OFFSET_PASS 0x20
|
||||
#define OFFSET_IP 0x40
|
||||
#define OFFSET_SUBNET 0x44
|
||||
#define OFFSET_GW 0x48
|
||||
#define OFFSET_MODE 0x4C
|
||||
#define OFFSET_FAIL_TIMEOUT 0x4D
|
||||
#define OFFSET_HOLD_TIME 0x4E
|
||||
#define CONFIG_SIZE 0x4F
|
||||
class Config
|
||||
{
|
||||
private:
|
||||
uint8_t *buffer = (uint8_t *)malloc(CONFIG_SIZE);
|
||||
|
||||
public:
|
||||
Config(const char* filepath);
|
||||
~Config();
|
||||
const char *SSID = (char *)(buffer + OFFSET_SSID);
|
||||
const char *PASS = (char *)(buffer + OFFSET_PASS);
|
||||
uint32_t &ip = *((uint32_t*)(buffer+OFFSET_IP));
|
||||
uint32_t &subnet = *((uint32_t*)(buffer+OFFSET_SUBNET));
|
||||
uint32_t &gw = *((uint32_t*)(buffer+OFFSET_GW));
|
||||
uint8_t &mode = *(buffer + OFFSET_MODE);
|
||||
uint8_t &fail_timeout = *(buffer+OFFSET_FAIL_TIMEOUT);
|
||||
uint8_t &hold_time = *(buffer+OFFSET_HOLD_TIME);
|
||||
const char* _filepath;
|
||||
bool setFilePath(const char* filepath);
|
||||
bool loadConfig();
|
||||
bool loadBin();
|
||||
bool saveBin();
|
||||
void print();
|
||||
bool setSSID(const char *ssid);
|
||||
bool setPASS(const char *pass);
|
||||
};
|
@ -41,15 +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())
|
||||
{
|
||||
this->_inputTimeout = millis();
|
||||
if (this->_keyboard->getLastChr() == 'X')
|
||||
{
|
||||
this->_keyboard->clear();
|
||||
@ -80,12 +75,7 @@ void Interface::render()
|
||||
//this->setState(states::VALIDATE_PIN);
|
||||
}
|
||||
}
|
||||
if(this->_display_state==states::ENTER_PIN_ADD_NUM||this->_display_state==states::ENTER_PIN_START){
|
||||
if(millis()-_inputTimeout>10000){
|
||||
this->_keyboard->clear();
|
||||
this->setState(states::ABORT);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->_displayUpdate)
|
||||
{
|
||||
switch (this->_display_state)
|
||||
|
@ -12,7 +12,6 @@ private:
|
||||
unsigned long _lastDisplayUpdate = 0;
|
||||
unsigned long _displayTimer1 = 0;
|
||||
unsigned long _displayTimer2 = 0;
|
||||
unsigned long _inputTimeout = 0;
|
||||
bool _displayUpdate = true;
|
||||
String _username = "";
|
||||
unsigned int _scroll_index = 0;
|
||||
@ -32,13 +31,12 @@ public:
|
||||
GREET,
|
||||
MSG
|
||||
|
||||
};
|
||||
};;
|
||||
Interface(/* args */);
|
||||
~Interface();
|
||||
void begin(Keyboard* Keyboard);
|
||||
void render();
|
||||
void setState(states state);
|
||||
int getState();
|
||||
void greetUser(String username);
|
||||
String getPin();
|
||||
bool pinAvailable();
|
||||
|
@ -1,6 +1,8 @@
|
||||
#include "Keyboard.h"
|
||||
|
||||
|
||||
//#define DEBUG
|
||||
#define PIN_WIRE_SDA D3
|
||||
#define PIN_WIRE_SCL D4
|
||||
Keyboard::Keyboard(uint8_t _debounce)
|
||||
{
|
||||
this->keybind.insert({
|
||||
@ -21,7 +23,7 @@ Keyboard::Keyboard(uint8_t _debounce)
|
||||
}
|
||||
void Keyboard::begin(TwoWire *databus)
|
||||
{
|
||||
pcf8574 = new PCF8574(databus, 0x21);
|
||||
pcf8574 = new PCF8574(databus, 0x21, PIN_WIRE_SDA, PIN_WIRE_SCL);
|
||||
pcf8574->pinMode(0, OUTPUT);
|
||||
for (int i = 1; i < 8; i++)
|
||||
{
|
||||
@ -82,9 +84,7 @@ void Keyboard::scanColumn(uint8_t *key_ptr, uint8_t start, uint8_t stop)
|
||||
_timeElapsed = millis();
|
||||
this->_lastKey = *key_ptr;
|
||||
this->_buffer.push_back(mapChr(*key_ptr));
|
||||
#ifdef DEBUG
|
||||
Serial.print(*key_ptr);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
#include "Relais.h"
|
||||
|
||||
Relais::Relais(uint8_t pin)
|
||||
{
|
||||
pinMode(pin, OUTPUT);
|
||||
digitalWrite(_pin, 1);
|
||||
_pin = pin;
|
||||
}
|
||||
void Relais::cylce()
|
||||
{
|
||||
if (!_state)
|
||||
return;
|
||||
if (millis() > _call_time)
|
||||
{
|
||||
digitalWrite(_pin, 1);
|
||||
_state = false;
|
||||
Serial.println("Relay released.");
|
||||
}
|
||||
}
|
||||
void Relais::activate(uint8_t seconds)
|
||||
{
|
||||
Serial.println("Relay activated for "+String(seconds)+" Seconds.");
|
||||
digitalWrite(_pin, 0);
|
||||
_state = true;
|
||||
_call_time = millis() + seconds * 1000;
|
||||
}
|
16
src/Relais.h
@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
class Relais
|
||||
{
|
||||
public:
|
||||
Relais(uint8_t pin);
|
||||
void cylce();
|
||||
void activate(uint8_t seconds);
|
||||
|
||||
private:
|
||||
unsigned long _call_time = 0;
|
||||
bool _state = false;
|
||||
uint8_t _pin;
|
||||
};
|
11
src/Rfid.cpp
@ -1,8 +1,8 @@
|
||||
#include "Rfid.h"
|
||||
#define SS_PIN D8
|
||||
#define RST_PIN D0
|
||||
#define RST_PIN D1
|
||||
#define RFID_TIMEOUT 3000
|
||||
Rfid::Rfid(/* args */) : _mfrc522(SS_PIN, RST_PIN)
|
||||
Rfid::Rfid(/* args */) : _mfrc522(SS_PIN)
|
||||
{
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ void Rfid::scan()
|
||||
this->_rfid = "";
|
||||
for (byte i = 0; i < this->_mfrc522.uid.size; i++)
|
||||
{
|
||||
this->_rfid += this->_mfrc522.uid.uidByte[i] < 0x10 ? "0" : "";
|
||||
this->_rfid += this->_mfrc522.uid.uidByte[i] < 0x10 ? " 0" : " ";
|
||||
this->_rfid += String(this->_mfrc522.uid.uidByte[i], HEX);
|
||||
}
|
||||
this->_rfid.trim();
|
||||
@ -32,13 +32,10 @@ void Rfid::scan()
|
||||
if (this->_rfid != this->_lastRfid || millis() > this->_lastRfidScan + RFID_TIMEOUT)
|
||||
{
|
||||
_status = 1;
|
||||
#ifdef DEBUG
|
||||
_mfrc522.PICC_DumpToSerial(&(_mfrc522.uid));
|
||||
#endif
|
||||
Serial.print(this->_rfid);
|
||||
this->_lastRfid = this->_rfid;
|
||||
this->_lastRfidScan = millis();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
185
src/UserDb.cpp
@ -1,185 +0,0 @@
|
||||
#include "UserDb.h"
|
||||
//#define DEBUG
|
||||
using namespace userdb;
|
||||
|
||||
UserDb::UserDb(const char *filename) : filename(filename)
|
||||
{
|
||||
}
|
||||
void UserDb::coursor_to_end_of_line(File &f)
|
||||
{
|
||||
while (f.available() && f.read() != '\n')
|
||||
{
|
||||
}
|
||||
}
|
||||
bool UserDb::remove_user(User &user)
|
||||
{
|
||||
return update_db_line(filename, user.line);
|
||||
}
|
||||
bool UserDb::update_user(User *user)
|
||||
{
|
||||
return update_db_line(filename, user->line, user);;
|
||||
}
|
||||
bool UserDb::update_db_line(const char *file, linenumber line, User *replace)
|
||||
{
|
||||
const char *temp_file = ".tmp.csv";
|
||||
File old_db = LittleFS.open(file, "r");
|
||||
File new_db = LittleFS.open(temp_file, "w+");
|
||||
linenumber curr_line = 0;
|
||||
bool line_found = false;
|
||||
while (old_db.available())
|
||||
{
|
||||
if (curr_line == line)
|
||||
{
|
||||
|
||||
#ifdef DEBUG
|
||||
Serial.println("Found line: " + String(line));
|
||||
#endif
|
||||
if (replace != nullptr)
|
||||
{
|
||||
String insert = replace->toCSVString();
|
||||
#ifdef DEBUG
|
||||
Serial.println("Inserting: " + insert);
|
||||
#endif
|
||||
if (new_db.position() > 0)
|
||||
new_db.write('\n');
|
||||
new_db.write(insert.c_str());
|
||||
}
|
||||
coursor_to_end_of_line(old_db);
|
||||
line_found = true;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if (new_db.position() > 0)
|
||||
new_db.write('\n');
|
||||
old_db.sendUntil(new_db, '\n');
|
||||
}
|
||||
curr_line++;
|
||||
}
|
||||
old_db.close();
|
||||
#ifdef DEBUG
|
||||
Serial.println("NewDB:");
|
||||
new_db.seek(0);
|
||||
while (new_db.available())
|
||||
new_db.sendAvailable(Serial);
|
||||
#endif
|
||||
new_db.close();
|
||||
if (LittleFS.remove(file) && LittleFS.rename(temp_file, file))
|
||||
Serial.println("UserDb updated");
|
||||
else
|
||||
Serial.println("Could not delete old userdb");
|
||||
return line_found;
|
||||
}
|
||||
bool UserDb::add_user(User &user)
|
||||
{
|
||||
|
||||
File db = LittleFS.open(filename, "a");
|
||||
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
|
||||
Serial.println("Searching for user with pin number " + cmp);
|
||||
#endif
|
||||
Iterator it = begin_with_filter(&cmp, USERATTRIBUTES::USER_PIN);
|
||||
do
|
||||
{
|
||||
User temp = *it;
|
||||
if ((*it).match && (*it).enabled)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
Serial.println(" -> Found match");
|
||||
#endif
|
||||
it.close_file();
|
||||
return *it;
|
||||
}
|
||||
++it;
|
||||
} while (it.has_next());
|
||||
return User{.enabled = false};
|
||||
}
|
||||
User UserDb::user_by_rfid(String cmp)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
Serial.println("Searching for user with rfid " + cmp);
|
||||
#endif
|
||||
Iterator it = begin_with_filter(&cmp, USERATTRIBUTES::RFID_UID);
|
||||
do
|
||||
{
|
||||
if ((*it).match && (*it).enabled)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
Serial.println(" -> Found match");
|
||||
#endif
|
||||
it.close_file();
|
||||
return *it;
|
||||
}
|
||||
++it;
|
||||
} while (it.has_next());
|
||||
return User{.enabled = false};
|
||||
}
|
||||
|
||||
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 id " + cmp);
|
||||
#endif
|
||||
Iterator it = begin_with_filter(&cmp, USERATTRIBUTES::USER_ID);
|
||||
do
|
||||
{
|
||||
if ((*it).match)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
Serial.println(" -> Found match");
|
||||
#endif
|
||||
it.close_file();
|
||||
return *it;
|
||||
}
|
||||
++it;
|
||||
} while (it.has_next());
|
||||
return User{.enabled = false};
|
||||
}
|
||||
User UserDb::user_by_line(linenumber cmp)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
Serial.println("Searching for user with line number " + String(cmp));
|
||||
#endif
|
||||
Iterator it = begin_with_filter(cmp, USERATTRIBUTES::LINE_ID);
|
||||
do
|
||||
{
|
||||
if ((*it).match)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
Serial.println(" -> Found match");
|
||||
#endif
|
||||
it.close_file();
|
||||
return (*it);
|
||||
}
|
||||
++it;
|
||||
} while (it.has_next());
|
||||
return User{.enabled = false};
|
||||
}
|
||||
void UserDb::print_to_serial()
|
||||
{
|
||||
Serial.println("Starting UserDB Test Read");
|
||||
for (Iterator it = begin(); it.has_next(); ++it)
|
||||
{
|
||||
User temp = *it;
|
||||
Serial.println(temp.toString());
|
||||
}
|
||||
}
|
206
src/UserDb.h
@ -1,206 +0,0 @@
|
||||
#pragma once
|
||||
#include "Config.h"
|
||||
#include "LittleFS.h"
|
||||
#include <sstream>
|
||||
#include <iterator>
|
||||
#include <cstddef>
|
||||
|
||||
namespace userdb
|
||||
{
|
||||
typedef uint16_t linenumber;
|
||||
enum USERATTRIBUTES
|
||||
{
|
||||
LINE_ID,
|
||||
USER_ID,
|
||||
FIRST_NAME,
|
||||
LAST_NAME,
|
||||
RFID_UID,
|
||||
USER_PIN,
|
||||
ENABLED,
|
||||
NONE
|
||||
};
|
||||
struct User
|
||||
{
|
||||
// User(unsigned long new_uid): uid(new_uid){}
|
||||
unsigned long uid = -1;
|
||||
linenumber line = UINT16_MAX;
|
||||
mutable String first_name;
|
||||
mutable String last_name;
|
||||
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") + ",\"line\":\"" + line + "\"}";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
class UserDb
|
||||
{
|
||||
private:
|
||||
const char *filename;
|
||||
|
||||
public:
|
||||
UserDb(const char *filename);
|
||||
void print_to_serial();
|
||||
void PrintAll();
|
||||
// User find_user();
|
||||
User user_by_pin(String);
|
||||
User user_by_rfid(String);
|
||||
User user_by_uid(unsigned long);
|
||||
User user_by_uid(String cmp);
|
||||
User user_by_line(linenumber cmp);
|
||||
bool drop()
|
||||
{
|
||||
LittleFS.remove(filename);
|
||||
File n = LittleFS.open(filename, "a");
|
||||
n.close();
|
||||
return true;
|
||||
}
|
||||
static inline User read_csv_line(User &res, File &instream, linenumber &line, String *match, USERATTRIBUTES attr)
|
||||
{
|
||||
String uid = instream.readStringUntil(';');
|
||||
if (attr == USER_ID && !uid.equals(*match))
|
||||
return res;
|
||||
res.uid = uid.toInt();
|
||||
res.first_name = instream.readStringUntil(';');
|
||||
res.last_name = instream.readStringUntil(';');
|
||||
res.rfid_uid = instream.readStringUntil(';');
|
||||
if (attr == RFID_UID && !res.rfid_uid.equals(*match))
|
||||
return res;
|
||||
res.user_pin = instream.readStringUntil(';');
|
||||
if (attr == USER_PIN && !res.user_pin.equals(*match))
|
||||
return res;
|
||||
res.enabled = instream.read() == '1' ? true : false;
|
||||
res.line = line;
|
||||
res.match = true;
|
||||
return res;
|
||||
}
|
||||
|
||||
void inline static coursor_to_end_of_line(File &f);
|
||||
bool remove_user(User &user);
|
||||
bool update_user(User *user);
|
||||
static bool update_db_line(const char *file, linenumber line, User *replace = nullptr);
|
||||
bool add_user(User &user);
|
||||
struct Iterator
|
||||
{
|
||||
using iterator_category = std::input_iterator_tag;
|
||||
using pointer = void;
|
||||
using value_type = User;
|
||||
using reference = User;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
|
||||
Iterator(File data) : Iterator(data, nullptr, NONE)
|
||||
{
|
||||
}
|
||||
Iterator(File data, linenumber m, USERATTRIBUTES attr) : db_file(data), filter_attr(attr), matchline(m)
|
||||
{
|
||||
if (!data)
|
||||
{
|
||||
Serial.println("Failed to open userdb while creating an iterator");
|
||||
}
|
||||
else
|
||||
{
|
||||
next();
|
||||
}
|
||||
}
|
||||
Iterator(File data, String *m, USERATTRIBUTES attr) : db_file(data), filter_attr(attr), match(m)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
if (filter_attr == NONE)
|
||||
Serial.println("Started user iterator with ");
|
||||
else
|
||||
Serial.println("Started filtered user iterator with " + String(filter_attr) + " query: " + *match);
|
||||
#endif
|
||||
|
||||
if (!data)
|
||||
{
|
||||
Serial.println("Failed to open userdb while creating an iterator");
|
||||
}
|
||||
else
|
||||
{
|
||||
next();
|
||||
}
|
||||
}
|
||||
reference operator*() const { return current; }
|
||||
Iterator &operator++()
|
||||
{
|
||||
next();
|
||||
return *this;
|
||||
}
|
||||
Iterator operator++(int)
|
||||
{
|
||||
next();
|
||||
return *this;
|
||||
}
|
||||
~Iterator()
|
||||
{
|
||||
close_file();
|
||||
}
|
||||
void close_file()
|
||||
{
|
||||
db_file.close();
|
||||
}
|
||||
bool has_next()
|
||||
{
|
||||
return available;
|
||||
}
|
||||
void next()
|
||||
{
|
||||
if (db_file.available())
|
||||
{
|
||||
if (filter_attr == USERATTRIBUTES::LINE_ID)
|
||||
{
|
||||
if (matchline == line)
|
||||
{
|
||||
current = read_csv_line(current, db_file, line, match, filter_attr);
|
||||
current.match = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
current = read_csv_line(current, db_file, line, match, filter_attr);
|
||||
// Position stream pointer to beginning of the next line
|
||||
while (db_file.available())
|
||||
{
|
||||
if (db_file.read() == '\n')
|
||||
{
|
||||
++line;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
available = false;
|
||||
current = User{};
|
||||
db_file.close();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
File db_file;
|
||||
User current;
|
||||
USERATTRIBUTES filter_attr;
|
||||
String *match;
|
||||
bool available = true;
|
||||
linenumber line = 0;
|
||||
linenumber matchline = 0;
|
||||
};
|
||||
Iterator begin() { return Iterator(LittleFS.open(filename, "r")); }
|
||||
Iterator begin_with_filter(String *match, USERATTRIBUTES attr) { return Iterator(LittleFS.open(filename, "r"), match, attr); }
|
||||
Iterator begin_with_filter(linenumber match, USERATTRIBUTES attr) { return Iterator(LittleFS.open(filename, "r"), match, attr); }
|
||||
};
|
||||
}
|
65
src/Users.cpp
Normal file
@ -0,0 +1,65 @@
|
||||
#include "Users.h"
|
||||
|
||||
Users::Users(/* args */)
|
||||
{
|
||||
this->_userdb.insert(user_account{.uid = 1, .first_name = "John", .last_name = "Doe", .rfid_uid = "8C 75 6C 17", .user_pin = "2424"});
|
||||
this->_userdb.insert(user_account{.uid = 2, .first_name = "Max", .last_name = "Mustermann and Sons", .rfid_uid = "D3 4E 03 0B", .user_pin = "1212"});
|
||||
}
|
||||
|
||||
Users::~Users()
|
||||
{
|
||||
}
|
||||
bool Users::checkPin(String pin_code, std::vector<Users::User> *logon_users)
|
||||
{
|
||||
std::copy_if(this->_userdb.begin(), this->_userdb.end(), inserter(*logon_users, logon_users->end()), [=](user_account user)
|
||||
{ return pin_code == user.user_pin; });
|
||||
return (logon_users->size() > 0);
|
||||
}
|
||||
|
||||
bool Users::checkRfid(String rfid_code, std::vector<Users::User> *logon_users)
|
||||
{
|
||||
std::copy_if(this->_userdb.begin(), this->_userdb.end(), inserter(*logon_users, logon_users->end()), [=](user_account user)
|
||||
{ return rfid_code == user.rfid_uid; });
|
||||
return (logon_users->size() > 0);
|
||||
}
|
||||
|
||||
unsigned long Users::addUser(String first_name, String last_name, String rifd_uid, String user_pin)
|
||||
{
|
||||
unsigned long new_id = this->_userdb.rbegin()->uid + 1;
|
||||
this->_userdb.insert(user_account{.uid = new_id, .first_name = first_name, .last_name = last_name, .rfid_uid = rifd_uid, .user_pin = user_pin});
|
||||
return new_id;
|
||||
}
|
||||
|
||||
bool Users::delUser(unsigned long id)
|
||||
{
|
||||
return this->_userdb.erase(user_account{.uid = id})>0;
|
||||
}
|
||||
|
||||
bool Users::updateUser(unsigned long id, ATTRIBUTES attr, String value)
|
||||
{
|
||||
auto modify = this->_userdb.find(user_account{.uid = id});
|
||||
switch (attr)
|
||||
{
|
||||
case FIRST_NAME:
|
||||
modify->first_name = value;
|
||||
break;
|
||||
case LAST_NAME:
|
||||
modify->last_name = value;
|
||||
break;
|
||||
case RFID_UID:
|
||||
modify->rfid_uid = value;
|
||||
break;
|
||||
case USER_PIN:
|
||||
modify->user_pin = value;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String Users::toString(User *user)
|
||||
{
|
||||
return "UID: " + String(user->uid) + " Name: " + user->first_name + " " + user->last_name;
|
||||
}
|
40
src/Users.h
Normal file
@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
class Users
|
||||
{
|
||||
public:
|
||||
enum ATTRIBUTES{
|
||||
FIRST_NAME,
|
||||
LAST_NAME,
|
||||
RFID_UID,
|
||||
USER_PIN
|
||||
};
|
||||
typedef struct User
|
||||
{
|
||||
//User(unsigned long new_uid): uid(new_uid){}
|
||||
unsigned long uid;
|
||||
mutable String first_name;
|
||||
mutable String last_name;
|
||||
mutable String rfid_uid;
|
||||
mutable String user_pin;
|
||||
bool operator < (const User &o) const { return uid <o.uid; }
|
||||
bool operator == (const User &o) const { return uid ==o.uid; }
|
||||
|
||||
} user_account;
|
||||
|
||||
private:
|
||||
std::set<User> _userdb;
|
||||
|
||||
/* data */
|
||||
public:
|
||||
Users(/* args */);
|
||||
~Users();
|
||||
bool checkRfid(String rfid_code, std::vector<Users::User> *logon_users);
|
||||
bool checkPin(String pin_code, std::vector<Users::User> *logon_users);
|
||||
unsigned long addUser(String first_name, String last_name, String rifd_uid, String user_pin);
|
||||
bool delUser(unsigned long id);
|
||||
bool updateUser(unsigned long id, ATTRIBUTES attr, String value);
|
||||
String toString(User *user);
|
||||
};
|
@ -1,451 +0,0 @@
|
||||
#include "WebConsole.h"
|
||||
|
||||
using namespace webconsole;
|
||||
|
||||
WebConsole::WebConsole()
|
||||
{
|
||||
}
|
||||
WebConsole::~WebConsole()
|
||||
{
|
||||
_server->close();
|
||||
delete (_server);
|
||||
}
|
||||
bool WebConsole::init(Config *config, userdb::UserDb *userdb)
|
||||
{
|
||||
this->userdb = userdb;
|
||||
this->_config = config;
|
||||
this->userdb = userdb;
|
||||
// Wifi Setup
|
||||
this->_dnsServer = new DNSServer;
|
||||
WiFi.mode(WIFI_AP);
|
||||
Serial.print("\t1. Network config... ");
|
||||
Serial.println(WiFi.softAPConfig(_config->ip, _config->gw, _config->subnet) ? "Ready" : "Failed!");
|
||||
Serial.print("\t2. DNS config... ");
|
||||
Serial.println(_dnsServer->start(53, "*", _config->ip) ? "Ready" : "Failed!");
|
||||
Serial.print("\t3 AP setup SSID:\"" + String(_config->SSID) + "\"... ");
|
||||
if (strlen(_config->PASS) > 0)
|
||||
Serial.println(WiFi.softAP(_config->SSID, _config->PASS) ? "Ready" : "Failed!");
|
||||
else
|
||||
Serial.println(WiFi.softAP(_config->SSID) ? "Ready" : "Failed!");
|
||||
WiFi.hostname("Doorlock");
|
||||
Serial.println("Please connect via http://" + WiFi.softAPIP().toString() + "/");
|
||||
|
||||
// Webserver Setup
|
||||
this->_server = new ESP8266WebServer(80);
|
||||
const char *headerkeys[] = {"Authentification"};
|
||||
size_t headerkeyssize = sizeof(headerkeys) / sizeof(char *);
|
||||
_server->collectHeaders(headerkeys, headerkeyssize);
|
||||
_server->begin();
|
||||
_server->on("/api/auth", HTTPMethod::HTTP_POST, std::bind(&WebConsole::_auth, this));
|
||||
_server->on("/api/userdb", HTTPMethod::HTTP_DELETE, std::bind(&WebConsole::_dropUserDb, this));
|
||||
_server->on("/api/userdb", HTTPMethod::HTTP_GET, std::bind(&WebConsole::_getUserDb, this));
|
||||
_server->on("/api/rfid", HTTPMethod::HTTP_GET, std::bind(&WebConsole::_catchRFID, this));
|
||||
_server->on("/api/debug/printfile", std::bind(&WebConsole::_print_db_raw, this));
|
||||
_server->on(UriBraces("/api/user/{}"), HTTPMethod::HTTP_DELETE, std::bind(&WebConsole::_deleteUser, this));
|
||||
_server->on(UriBraces("/api/user/{}"), HTTPMethod::HTTP_GET, std::bind(&WebConsole::_getUser, this));
|
||||
_server->on(UriBraces("/api/user/{}"), HTTPMethod::HTTP_PUT, std::bind(&WebConsole::_createUser, this));
|
||||
_server->on(UriBraces("/api/user/{}"), HTTPMethod::HTTP_POST, std::bind(&WebConsole::_updateUser, this));
|
||||
_server->on(UriBraces("/api/config"), HTTPMethod::HTTP_POST, std::bind(&WebConsole::_settings, this));
|
||||
_server->serveStatic("/", LittleFS, "/s/");
|
||||
_server->onNotFound(std::bind(&WebConsole::_handleUnknown, this));
|
||||
return true;
|
||||
}
|
||||
void WebConsole::attachRfid(Rfid *rfid)
|
||||
{
|
||||
this->rfid = rfid;
|
||||
}
|
||||
void WebConsole::serve()
|
||||
{
|
||||
_dnsServer->processNextRequest();
|
||||
if (catch_rfid)
|
||||
{
|
||||
if (millis() - catch_rfid_millis > 2000)
|
||||
{
|
||||
catch_rfid = 0;
|
||||
}
|
||||
else if (catch_rfid > 1 && rfid != nullptr && rfid->available())
|
||||
{
|
||||
rfid_buffer = rfid->getID();
|
||||
catch_rfid = 1;
|
||||
}
|
||||
}
|
||||
_server->handleClient();
|
||||
}
|
||||
uint8_t WebConsole::isInterceptingRfid()
|
||||
{
|
||||
if (catch_rfid == 3)
|
||||
{
|
||||
catch_rfid = 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
return catch_rfid;
|
||||
}
|
||||
bool WebConsole::_isAuth()
|
||||
{
|
||||
if (_server->method() == HTTPMethod::HTTP_OPTIONS)
|
||||
{
|
||||
_server->send(204);
|
||||
return false;
|
||||
}
|
||||
if (!_server->hasHeader("Authentification"))
|
||||
{
|
||||
_server->send(401, "text/plain", "Error 401: Unauthorized (missing auth token)");
|
||||
return false;
|
||||
}
|
||||
const char *token = _server->header("Authentification").c_str();
|
||||
bool res = auth.isAuth(token);
|
||||
if (!res)
|
||||
_server->send(401, "text/plain", "Error 401: Unauthorized (missing auth token)");
|
||||
return res;
|
||||
}
|
||||
void WebConsole::_auth()
|
||||
{
|
||||
_sendCORS();
|
||||
String action = _server->arg("action");
|
||||
if (action.equals("check"))
|
||||
{
|
||||
const char *token = _server->arg("token").c_str();
|
||||
bool res = auth.isAuth(token);
|
||||
_server->send(200, "text/plain", res ? "valid" : "invalid");
|
||||
}
|
||||
else if (action.equals("login"))
|
||||
{
|
||||
char *token = auth.login(_server->arg("username"), _server->arg("password"));
|
||||
if (token == nullptr)
|
||||
_server->send(401, "text/plain", "login_failed");
|
||||
else
|
||||
_server->send(200, "text/plain", token);
|
||||
}
|
||||
else if (action.equals("logout"))
|
||||
{
|
||||
const char *token = _server->arg("token").c_str();
|
||||
bool res = auth.logout(token);
|
||||
_server->send(200, "text/plain", res ? "success" : "failed");
|
||||
}
|
||||
else if (action.equals("update"))
|
||||
{
|
||||
// if (!_isAuth())
|
||||
// return;
|
||||
bool res = auth.setAuth(_server->arg("username"), _server->arg("password"));
|
||||
_server->send(200, "text/plain", res ? "success" : "failed");
|
||||
}
|
||||
else
|
||||
_server->send(404, "text/plain", "unknown action");
|
||||
}
|
||||
void WebConsole::_settings()
|
||||
{
|
||||
_sendCORS();
|
||||
if (!_isAuth())
|
||||
return;
|
||||
String action = _server->arg("action");
|
||||
if (action.equals("update"))
|
||||
{
|
||||
uint8_t error = 0b00000000;
|
||||
uint8_t sucess = 0b00000000;
|
||||
if (_server->hasArg("SSID"))
|
||||
{
|
||||
_config->setSSID(_server->arg("SSID").c_str());
|
||||
int length = strlen(_config->SSID);
|
||||
if (length == 0 || length > 31)
|
||||
error |= 0b00000001;
|
||||
else
|
||||
sucess |= 0b00000001;
|
||||
}
|
||||
if (_server->hasArg("PASS"))
|
||||
{
|
||||
_config->setSSID(_server->arg("PASS").c_str());
|
||||
int length = strlen(_config->SSID);
|
||||
if (length == 0 || length > 31)
|
||||
error |= 0b00000010;
|
||||
else
|
||||
sucess |= 0b00000010;
|
||||
}
|
||||
|
||||
if (_server->hasArg("ip"))
|
||||
{
|
||||
IPAddress temp;
|
||||
if (temp.fromString(_server->arg("ip")))
|
||||
{
|
||||
_config->ip = temp.v4();
|
||||
sucess |= 0b00000100;
|
||||
}
|
||||
else
|
||||
error |= 0b00000100;
|
||||
}
|
||||
if (_server->hasArg("subnet"))
|
||||
{
|
||||
IPAddress temp;
|
||||
if (temp.fromString(_server->arg("subnet")))
|
||||
{
|
||||
_config->subnet = temp.v4();
|
||||
sucess |= 0b00001000;
|
||||
}
|
||||
else
|
||||
error |= 0b00001000;
|
||||
}
|
||||
if (_server->hasArg("gw"))
|
||||
{
|
||||
IPAddress temp;
|
||||
if (temp.fromString(_server->arg("gw")))
|
||||
{
|
||||
_config->gw = temp.v4();
|
||||
sucess |= 0b00010000;
|
||||
}
|
||||
else
|
||||
error |= 0b00010000;
|
||||
}
|
||||
if (_server->hasArg("mode"))
|
||||
{
|
||||
_config->mode = _server->arg("mode").toInt();
|
||||
if (_config->mode >= 0 && _config->mode <= 1)
|
||||
sucess |= 0b00100000;
|
||||
else
|
||||
error |= 0b00100000;
|
||||
}
|
||||
if (_server->hasArg("fail_timeout"))
|
||||
{
|
||||
_config->fail_timeout = _server->arg("fail_timeout").toInt();
|
||||
if (_config->fail_timeout >= 0 && _config->fail_timeout <= 250)
|
||||
sucess |= 0b01000000;
|
||||
else
|
||||
error |= 0b01000000;
|
||||
}
|
||||
if (_server->hasArg("hold_time"))
|
||||
{
|
||||
_config->hold_time = _server->arg("hold_time").toInt();
|
||||
if (_config->hold_time > 0 && _config->hold_time <= 250)
|
||||
sucess |= 0b10000000;
|
||||
else
|
||||
error |= 0b10000000;
|
||||
}
|
||||
if (error == 0 && sucess > 0)
|
||||
{ // Save
|
||||
_config->saveBin();
|
||||
_server->send(200, "text/json", "{\"status\":\"ok\", \"sucess\":\"" + String(sucess) + "\"}");
|
||||
}
|
||||
else if (error > 0)
|
||||
{ // Rollback
|
||||
_config->loadBin();
|
||||
_server->send(200, "text/json", "{\"status\":\"failed\", \"sucess\":\"" + String(sucess) + "\", \"error\":\"" + String(error) + "\"}");
|
||||
}
|
||||
else
|
||||
_server->send(200, "text/json", "{\"status\":\"noting_todo\"}");
|
||||
}
|
||||
else if (action.equals("get"))
|
||||
{
|
||||
_server->send(200, "text/json", "{\"SSID\":\"" + String(_config->SSID) + "\", \"PASS\":\"" + String(_config->PASS) + "\", \"ip\":\"" + IPAddress(_config->ip).toString() + "\", \"subnet\":\"" + IPAddress(_config->subnet).toString() + "\", \"gw\":\"" + IPAddress(_config->gw).toString() + "\", \"mode\":\"" + String(_config->mode) + "\", \"fail_timeout\":\"" + String(_config->fail_timeout) + "\", \"hold_time\":\"" + String(_config->hold_time) + "\"}");
|
||||
}
|
||||
else if (action.equals("apply"))
|
||||
{
|
||||
_server->send(200, "text/json", "{\"status\":\"restarting\"}");
|
||||
ESP.restart();
|
||||
}
|
||||
else
|
||||
_server->send(404, "text/plain", "unknown action");
|
||||
}
|
||||
void WebConsole::_sendCORS()
|
||||
{
|
||||
_server->sendHeader("Access-Control-Allow-Origin", "*");
|
||||
_server->sendHeader("Access-Control-Max-Age", "10000");
|
||||
_server->sendHeader("Access-Control-Allow-Methods", "PUT,POST,GET,OPTIONS,DELETE");
|
||||
_server->sendHeader("Access-Control-Allow-Headers", "*");
|
||||
}
|
||||
void WebConsole::_handleUnknown()
|
||||
{
|
||||
_sendCORS();
|
||||
if (_server->method() == HTTP_OPTIONS)
|
||||
{
|
||||
_server->send(204);
|
||||
}
|
||||
else
|
||||
{
|
||||
File src = LittleFS.open("s/index.html", "r");
|
||||
_server->streamFile(src, "text/html");
|
||||
src.close();
|
||||
}
|
||||
}
|
||||
|
||||
void WebConsole::_getUserDb()
|
||||
{
|
||||
_sendCORS();
|
||||
if (!_isAuth())
|
||||
return;
|
||||
File src = LittleFS.open("userdb.csv", "r");
|
||||
if (src)
|
||||
{
|
||||
_server->streamFile(src, "text/csv");
|
||||
}
|
||||
src.close();
|
||||
}
|
||||
void WebConsole::_deleteUser()
|
||||
{
|
||||
_sendCORS();
|
||||
if (!_isAuth())
|
||||
return;
|
||||
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()
|
||||
{
|
||||
_sendCORS();
|
||||
if (!_isAuth())
|
||||
return;
|
||||
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::_updateUser()
|
||||
{
|
||||
_sendCORS();
|
||||
if (!_isAuth())
|
||||
return;
|
||||
userdb::User updated;
|
||||
String body = _server->arg("plain");
|
||||
const int capacity = 256;
|
||||
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 (doc.containsKey("line"))
|
||||
{
|
||||
updated = userdb->user_by_line(doc["line"].as<userdb::linenumber>());
|
||||
}
|
||||
if (!updated.match && doc.containsKey("uid"))
|
||||
{
|
||||
updated = userdb->user_by_uid(doc["uid"].as<unsigned long>());
|
||||
}
|
||||
if (!updated.match && !_server->pathArg(0).isEmpty())
|
||||
{
|
||||
updated = userdb->user_by_uid(_server->pathArg(0).toInt());
|
||||
}
|
||||
|
||||
if (updated.match == false)
|
||||
{
|
||||
_server->send(500, "text/json", "{\"error\":\"user could not be found.\"}");
|
||||
return;
|
||||
}
|
||||
// created.line = doc["line"].as<uint32_t>();
|
||||
if (doc.containsKey("uid"))
|
||||
updated.uid = doc["uid"].as<unsigned long>();
|
||||
if (doc.containsKey("first_name"))
|
||||
updated.first_name = doc["first_name"].as<String>();
|
||||
if (doc.containsKey("last_name"))
|
||||
updated.last_name = doc["last_name"].as<String>();
|
||||
if (doc.containsKey("rfid_uid"))
|
||||
updated.rfid_uid = doc["rfid_uid"].as<String>();
|
||||
if (doc.containsKey("user_pin"))
|
||||
updated.user_pin = doc["user_pin"].as<String>();
|
||||
if (doc.containsKey("enabled"))
|
||||
updated.enabled = doc["enabled"].as<bool>();
|
||||
userdb->update_user(&updated);
|
||||
_server->send(200, "text/json", updated.toJSONString().c_str());
|
||||
}
|
||||
void WebConsole::_createUser()
|
||||
{
|
||||
_sendCORS();
|
||||
if (!_isAuth())
|
||||
return;
|
||||
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()
|
||||
{
|
||||
_sendCORS();
|
||||
if (!_isAuth())
|
||||
return;
|
||||
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()
|
||||
{
|
||||
_sendCORS();
|
||||
if (!_isAuth())
|
||||
return;
|
||||
if (rfid == nullptr)
|
||||
{
|
||||
_server->send(500, "text/json", "{\"error\":\"RFID not attached.\",\"state\":\"" + String(catch_rfid) + "\"}");
|
||||
return;
|
||||
}
|
||||
if (catch_rfid == 1)
|
||||
{
|
||||
String response = "{\"rfid_uid\":\"" + rfid_buffer + "\",\"state\":\"" + String(catch_rfid) + "\"}";
|
||||
_server->send(200, "text/json", response);
|
||||
catch_rfid = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
catch_rfid = 3; // 3 - Start listening
|
||||
catch_rfid_millis = millis();
|
||||
_server->send(200, "text/json", "{\"ok\":\"listening\",\"state\":\"" + String(catch_rfid) + "\"}");
|
||||
}
|
||||
}
|
||||
void WebConsole::_updateAdmin()
|
||||
{
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
#pragma once
|
||||
#include <ESP8266mDNS.h>
|
||||
#include <DNSServer.h>
|
||||
#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"
|
||||
#include "Config.h"
|
||||
#include "AdminAuth.h"
|
||||
#include "Interface.h"
|
||||
namespace webconsole
|
||||
{
|
||||
static const char path_prefix[] PROGMEM = "/s";
|
||||
static ESP8266WebServer _server(80);
|
||||
class WebConsole
|
||||
{
|
||||
public:
|
||||
WebConsole();
|
||||
~WebConsole();
|
||||
|
||||
bool init(Config *config, userdb::UserDb *userdb);
|
||||
void attachRfid(Rfid *rfid);
|
||||
void serve();
|
||||
uint8_t isInterceptingRfid();
|
||||
|
||||
private:
|
||||
void _sendCORS();
|
||||
void _settings();
|
||||
void _auth();
|
||||
bool _isAuth();
|
||||
void _handleUnknown();
|
||||
void _getUserDb();
|
||||
void _deleteUser();
|
||||
void _getUser();
|
||||
void _updateUser();
|
||||
void _createUser();
|
||||
void _dropUserDb();
|
||||
void _catchRFID();
|
||||
void _updateAdmin();
|
||||
void _print_db_raw()
|
||||
{
|
||||
File f = LittleFS.open("userdb.csv", "r");
|
||||
while (f.available())
|
||||
{
|
||||
f.sendAvailable(Serial);
|
||||
}
|
||||
_server->send(200, "text/plain", "printed to serial");
|
||||
f.close();
|
||||
}
|
||||
Config *_config;
|
||||
DNSServer *_dnsServer;
|
||||
ESP8266WebServer *_server;
|
||||
userdb::UserDb *userdb = nullptr;
|
||||
unsigned long catch_rfid_millis = 0;
|
||||
uint8_t catch_rfid = 0;
|
||||
bool catch_rfid_updated = false;
|
||||
String rfid_buffer;
|
||||
Rfid *rfid = nullptr;
|
||||
Interface *iface = nullptr;
|
||||
AdminAuth auth;
|
||||
};
|
||||
|
||||
}
|
87
src/main.cpp
@ -1,82 +1,55 @@
|
||||
//#define DEBUG
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include "Keyboard.h"
|
||||
#include "Rfid.h"
|
||||
#include "Interface.h"
|
||||
#include <ESP8266WiFi.h>
|
||||
#include "WebConsole.h"
|
||||
#include "UserDb.h"
|
||||
#include "Config.h"
|
||||
#include "Relais.h"
|
||||
|
||||
// File config
|
||||
Config config("/settings");
|
||||
userdb::UserDb userdatabase("/userdb.csv");
|
||||
webconsole::WebConsole web;
|
||||
#include <vector>
|
||||
#include "Users.h"
|
||||
// Rfid
|
||||
Rfid rfid;
|
||||
// i2C Bus
|
||||
#define PIN_WIRE_SDA D1
|
||||
#define PIN_WIRE_SCL D2
|
||||
Relais relay(D4);
|
||||
#define PIN_WIRE_SDA D3
|
||||
#define PIN_WIRE_SCL D4
|
||||
|
||||
Keyboard keyboard(200);
|
||||
Interface iface;
|
||||
// Wifi control
|
||||
IPAddress local_IP(192, 168, 4, 22);
|
||||
IPAddress gateway(192, 168, 4, 9);
|
||||
IPAddress subnet(255, 255, 255, 0);
|
||||
|
||||
// User DB
|
||||
Users users;
|
||||
|
||||
void setup()
|
||||
{
|
||||
config.loadBin();
|
||||
Serial.begin(115200);
|
||||
Serial.println("Starting System...");
|
||||
#ifdef DEBUG
|
||||
userdatabase.print_to_serial();
|
||||
#endif
|
||||
web.init(&config, &userdatabase);
|
||||
Serial.begin(9600);
|
||||
Serial.print("Starting");
|
||||
Serial.println(WiFi.softAPConfig(local_IP, gateway, subnet) ? "Ready" : "Failed!");
|
||||
Serial.println(WiFi.softAP("Doorlock") ? "Ready" : "Failed!");
|
||||
//delay(500);
|
||||
keyboard.begin(&Wire);
|
||||
rfid.begin();
|
||||
web.attachRfid(&rfid);
|
||||
iface.begin(&keyboard);
|
||||
users.addUser("Harry","Potter","","1234");
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
relay.cylce();
|
||||
rfid.scan();
|
||||
web.serve();
|
||||
keyboard.scanAsync();
|
||||
userdb::User login_user;
|
||||
if (web.isInterceptingRfid() == 3)
|
||||
iface.showMessage("WebUI connected", "Awaiting RFID", 2000);
|
||||
else if (web.isInterceptingRfid() == 1)
|
||||
iface.showMessage("WebUI connected", "RFID Found", 1000);
|
||||
else if (iface.pinAvailable() && iface.getState() != Interface::GREET)
|
||||
rfid.scan();
|
||||
std::vector<Users::User> logon_users;
|
||||
if (iface.pinAvailable() && !users.checkPin(iface.getPin(), &logon_users))
|
||||
iface.showMessage("Login failed!", "-> Pin incorrect", 3000);
|
||||
else if (rfid.available() && !users.checkRfid(rfid.getID(), &logon_users))
|
||||
iface.showMessage("Login failed!", "-> Unkown Card", 3000);
|
||||
else if (logon_users.size() > 0)
|
||||
{
|
||||
|
||||
unsigned long delta = millis();
|
||||
login_user = userdatabase.user_by_pin(iface.getPin());
|
||||
Serial.println("Query duration: " + String(millis() - delta));
|
||||
if (login_user.enabled == false)
|
||||
{
|
||||
iface.showMessage("Login failed!", "-> Pin incorrect", 3000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (rfid.available() && iface.getState() != Interface::GREET)
|
||||
{
|
||||
unsigned long delta = millis();
|
||||
login_user = userdatabase.user_by_rfid(rfid.getID());
|
||||
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)
|
||||
{
|
||||
iface.greetUser(login_user.first_name + " " + login_user.last_name);
|
||||
Serial.println("Logon from User " + login_user.toString());
|
||||
relay.activate(config.hold_time);
|
||||
iface.greetUser(logon_users[0].first_name + " " + logon_users[0].last_name);
|
||||
Serial.print(users.toString(&logon_users[0]));
|
||||
logon_users.clear();
|
||||
}
|
||||
iface.render();
|
||||
}
|
23
web/.gitignore
vendored
@ -1,23 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
"stories": ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||
"addons": ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/preset-create-react-app"],
|
||||
core: {
|
||||
builder: "webpack5"
|
||||
}
|
||||
};
|
@ -1,23 +0,0 @@
|
||||
import { ThemeProvider } from "styled-components";
|
||||
import theme from "../src/theme";
|
||||
|
||||
|
||||
export const decorators = [
|
||||
(Story) => (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `yarn start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `yarn test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `yarn build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `yarn eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `yarn build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
@ -1,68 +0,0 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.2",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.2.3",
|
||||
"eslint-config-react-app": "7",
|
||||
"node-sass": "^7.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-scripts": "^5.0.0",
|
||||
"styled-components": "^5.3.3",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"sb": "start-storybook -p 6006 -s public",
|
||||
"build-sb": "build-storybook -s public"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"**/*.stories.*"
|
||||
],
|
||||
"rules": {
|
||||
"import/no-anonymous-default-export": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "^6.4.18",
|
||||
"@storybook/addon-essentials": "^6.1.21",
|
||||
"@storybook/addon-links": "^6.4.18",
|
||||
"@storybook/builder-webpack5": "^6.4.18",
|
||||
"@storybook/manager-webpack5": "^6.4.18",
|
||||
"@storybook/node-logger": "^6.4.18",
|
||||
"@storybook/preset-create-react-app": "^4.0.1",
|
||||
"@storybook/react": "^6.1.21",
|
||||
"serve": "^13.0.2",
|
||||
"webpack": "5"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 3.8 KiB |
@ -1,19 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Doorlock Management"
|
||||
/>
|
||||
|
||||
<title>Doorlock</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
@ -1,34 +0,0 @@
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Routes ,
|
||||
Route,
|
||||
} from "react-router-dom";
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from "styled-components";
|
||||
import {CreateUserWidget, RecentActivityWidget, SystemStatusWidget, ColourPalette} from './components'
|
||||
import {AccountManager, AdminUI, Logs, System} from './views'
|
||||
import theme from './theme';
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AdminUI>
|
||||
<Routes >
|
||||
<Route path="/" exact element={
|
||||
<>
|
||||
<CreateUserWidget />
|
||||
<RecentActivityWidget />
|
||||
<SystemStatusWidget />
|
||||
</>
|
||||
}/>
|
||||
<Route path="/accounts" element={<AccountManager/>} />
|
||||
<Route path="/log" element={<Logs/>} />
|
||||
<Route path="/system" element={<System/>}/>
|
||||
</Routes >
|
||||
</AdminUI>
|
||||
</ThemeProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
@ -1,22 +0,0 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
const Button = styled.button`
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
color: ${props=>props.disabled?props.theme.colours.color1:props.theme.colours.color3};
|
||||
display: inline-block;
|
||||
padding: 0.3em 0.5em;
|
||||
border: solid .1em ${props=>props.disabled?props.theme.colours.color7:props.theme.colours.color3};
|
||||
background: ${props=>props.disabled?props.theme.colours.color9:props.theme.colours.color4};
|
||||
transition: ease-in-out 225ms;
|
||||
&:hover{
|
||||
background-color: ${props=>props.disabled?props.theme.colours.color9:props.theme.colours.color3};
|
||||
color: ${props=>props.disabled?props.theme.colours.color1:props.theme.colours.color6};
|
||||
border: solid .1em ${props=>props.disabled?props.theme.colours.color7:props.theme.colours.color10};
|
||||
text-shadow: 0 0 .2em ${props=>props.disabled?"transparent":props.theme.colours.color9};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
|
||||
export default Button
|
@ -1 +0,0 @@
|
||||
/* No CSS *//*# sourceMappingURL=Card.css.map */
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"mappings": "",
|
||||
"sources": [
|
||||
"Card.sass"
|
||||
],
|
||||
"names": [],
|
||||
"file": "Card.css"
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
const CardDiv = styled.div`
|
||||
flex-grow: 1;
|
||||
color: ${props=>props.theme.colours.color6};
|
||||
background: ${props=>props.theme.colours.color1};
|
||||
border-radius: 1em;
|
||||
padding: 1em;
|
||||
margin: 1em;
|
||||
display: block;
|
||||
flex-direction: column;
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
export default function Card({children}) {
|
||||
return (
|
||||
<CardDiv>
|
||||
{children}
|
||||
</CardDiv>
|
||||
)
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
import React from 'react';
|
||||
import Hook from './Checkbox.svg'
|
||||
const CheckboxOrigin = styled.input.attrs({ type: 'checkbox' })`
|
||||
visibility: hidden;
|
||||
`;
|
||||
|
||||
|
||||
const CheckboxStyled = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
font-size: 1em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-weight: bold;
|
||||
color: ${props => props.disabled ? props.theme.colours.color1 : props.theme.colours.color3};
|
||||
border: solid .1em ${props => props.disabled ? props.theme.colours.color7 : props.theme.colours.color3};
|
||||
background: ${props => props.disabled ? props.theme.colours.color9 : props.theme.colours.color4};
|
||||
transition: ease-in-out 125ms;
|
||||
|
||||
${CheckboxOrigin}:checked +&{
|
||||
border: solid .1em ${props => props.disabled ? props.theme.colours.color7 : props.theme.colours.color3};
|
||||
background-color: ${props => props.disabled ? props.theme.colours.color9 : props.theme.colours.color3};
|
||||
color: ${props => props.disabled ? props.theme.colours.color1 : props.theme.colours.color6};
|
||||
background-image: url(${Hook});
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
${CheckboxOrigin}:hover +&{
|
||||
background-color: ${props => props.disabled ? props.theme.colours.color9 : props.theme.colours.color3};
|
||||
color: ${props => props.disabled ? props.theme.colours.color1 : props.theme.colours.color6};
|
||||
border: solid .1em ${props => props.disabled ? props.theme.colours.color7 : props.theme.colours.color10};
|
||||
|
||||
}
|
||||
`;
|
||||
const CheckboxWrapper = styled.label`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
`;
|
||||
|
||||
|
||||
const Checkbox = ({ className, checked, ...props }) => (
|
||||
<CheckboxWrapper className={className}>
|
||||
<CheckboxOrigin checked={checked} {...props} />
|
||||
<CheckboxStyled checked={checked} />
|
||||
</CheckboxWrapper>
|
||||
|
||||
);
|
||||
export default Checkbox;
|
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;stroke:#000000;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<path class="st0" d="M26.66,289.39l138.79,155.55c7.35,8.24,20.27,8.1,27.45-0.28l278.92-325.86c5.17-6.04,5.83-14.73,1.63-21.48
|
||||
l-27.59-44.36c-6.48-10.41-21.18-11.59-29.23-2.34L214.5,282.9c-7.55,8.67-21.14,8.28-28.17-0.82l-89.7-116.01
|
||||
c-8.54-11.05-25.83-8.78-31.23,4.1L23.45,270.2C20.73,276.68,21.98,284.14,26.66,289.39z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 770 B |
@ -1,25 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useContext } from 'react';
|
||||
import { ThemeContext } from 'styled-components';
|
||||
const Element = styled.div`
|
||||
//width: 1em;
|
||||
height: 2em;
|
||||
display: block;
|
||||
background: ${props=> props.bg};
|
||||
color: #707070;
|
||||
padding: .5em;
|
||||
|
||||
`;
|
||||
export default function ColourPalette() {
|
||||
var colours = useContext(ThemeContext).colours;
|
||||
return (
|
||||
<div>
|
||||
|
||||
|
||||
{Object.keys(colours).map((name, index) => (
|
||||
<Element bg={colours[name]}>{name}</Element>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import TextInput from './TextInput'
|
||||
import Card from './Card'
|
||||
import Button from './Button'
|
||||
export class CreateUserWidget extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Card>
|
||||
<h4>Add User</h4>
|
||||
<hr />
|
||||
<p>Add new user</p>
|
||||
<TextInput id="first_name" label="First Name" />
|
||||
<TextInput id="last_name" label="Last Name" />
|
||||
<TextInput id="pin" label="PIN" />
|
||||
<TextInput id="rfid" disabled label="RFID" button="Scan" value="Scanning RFID" onClick={(e)=>{console.log("Button Pressed",e)}}/>
|
||||
<TextInput id="Expire_Date" label="Expire Date" type="date" />
|
||||
<Button>Create User</Button><Button disabled>Disabled</Button>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CreateUserWidget
|
@ -1,30 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import Card from './Card'
|
||||
export class RecentActivityWidget extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Card>
|
||||
<h4>Recent Activity</h4>
|
||||
<hr />
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Method</td>
|
||||
<td>Time</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Max Mustermann</td>
|
||||
<td>RFID</td>
|
||||
<td>1.10.2020 08:30</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default RecentActivityWidget
|
@ -1,67 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import styled, {withTheme,} from 'styled-components'
|
||||
export class SearchBox extends Component {
|
||||
|
||||
|
||||
render() {
|
||||
const SearchWrapper = styled.div`
|
||||
margin: .5em;
|
||||
border-radius: .5rem;
|
||||
display: inline-flex;
|
||||
white-space: nowrap;
|
||||
//margin-left: auto;
|
||||
transition: ease-in-out all 500ms;
|
||||
flex-grow: 0;
|
||||
max-width: 20em;
|
||||
&:hover {
|
||||
box-shadow: 0 0 1em ${this.props.theme.colours.color3};
|
||||
flex-grow: 1;
|
||||
input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
input{
|
||||
height: 2rem;
|
||||
transition: ease all 500ms;
|
||||
border-radius: .5rem 0 0 .5rem;
|
||||
display: inline-flex;
|
||||
background: ${this.props.theme.colours.color3};
|
||||
border: none;
|
||||
outline: none;
|
||||
color: ${this.props.theme.colours.color10};
|
||||
flex-grow: 0;
|
||||
margin-left: auto;
|
||||
width: 100%;
|
||||
box-shadow: 0 0 .2em ${this.props.theme.colours.color5};
|
||||
padding: .5em;
|
||||
&:hover, &:not(:placeholder-shown){
|
||||
transition: ease all 500ms;
|
||||
flex-grow: 1;
|
||||
}
|
||||
+ button{
|
||||
height: 2rem;
|
||||
border: none;
|
||||
transition: ease-in-out all 100ms;
|
||||
color: ${this.props.theme.colours.color6};
|
||||
border-radius: 0 .5rem .5rem 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: ${this.props.theme.colours.color2};
|
||||
font-size: 1em;
|
||||
border: .1rem inset ${this.props.theme.colours.color3};
|
||||
border-style: solid;
|
||||
padding: .5em;
|
||||
&:hover{
|
||||
background: ${this.props.theme.colours.color6};
|
||||
color: ${this.props.theme.colours.color3};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
return (
|
||||
<SearchWrapper id="header__search"><input id={this.props.id} type="text" placeholder="..."></input><button>Search</button></SearchWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(SearchBox)
|
@ -1,22 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const Select = styled.select`
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
color: ${props=>props.disabled?props.theme.colours.color1:props.theme.colours.color3};
|
||||
display: inline-block;
|
||||
padding: 0.3em 0.5em;
|
||||
border: solid .1em ${props=>props.disabled?props.theme.colours.color7:props.theme.colours.color3};
|
||||
background: ${props=>props.disabled?props.theme.colours.color9:props.theme.colours.color4};
|
||||
transition: ease-in-out 225ms;
|
||||
|
||||
option {
|
||||
background-color: ${props=>props.disabled?props.theme.colours.color9:props.theme.colours.color3};
|
||||
color: ${props=>props.disabled?props.theme.colours.color1:props.theme.colours.color6};
|
||||
border: solid .1em ${props=>props.disabled?props.theme.colours.color7:props.theme.colours.color10};
|
||||
text-shadow: 0 0 .2em ${props=>props.disabled?"transparent":props.theme.colours.color9};
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default Select;
|
@ -1,17 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import Card from './Card'
|
||||
export class SystemStatusWidget extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Card>
|
||||
<h4>System Status</h4>
|
||||
<hr />
|
||||
<p>Uptime: since 3days 15hours</p>
|
||||
<p>LastDB Update: before 7hours</p>
|
||||
<p>Last administrator login: Admin (12.9.2021 09:30)</p>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SystemStatusWidget
|
@ -1,21 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
const Table = styled.table`
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
thead{
|
||||
background-color: ${props=>props.theme.colours.color9};
|
||||
font-weight: bold;
|
||||
|
||||
}
|
||||
tbody tr:not(:last-child){
|
||||
border-bottom: 1px solid ${props=>props.theme.colours.color9};
|
||||
}
|
||||
tr:nth-child(2){
|
||||
background-color: ${props=>props.theme.colours.color1};
|
||||
}
|
||||
td{
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
`;
|
||||
export default Table;
|
@ -1,122 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { withTheme } from 'styled-components';
|
||||
export class TextInput extends Component {
|
||||
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {value: "value" in props?props.value:""}
|
||||
//this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
handleChange(event){
|
||||
this.setState({value: event.target.value});
|
||||
}
|
||||
render() {
|
||||
const InputWrapper = styled.div`
|
||||
font-size: 1.1em;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin: 0.2em 0;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-align: stretch;
|
||||
-ms-flex-align: stretch;
|
||||
align-items: stretch;
|
||||
background: ${this.props.disabled===undefined ? this.props.theme.colours.color6 : this.props.theme.colours.color7};
|
||||
border-bottom: 0.3em solid ${this.props.disabled===undefined ? this.props.theme.colours.color5 : this.props.theme.colours.color8};
|
||||
:hover {
|
||||
${this.props.disabled===undefined ? "outline: solid "+ this.props.theme.colours.color3 +";\nborder-bottom: 0.3em solid" + this.props.theme.colours.color3 + ";" : null};
|
||||
button:hover{
|
||||
${this.props.btndisabled===undefined ?"background: "+ this.props.theme.colours.color3:""};
|
||||
&::before{
|
||||
${this.props.btndisabled===undefined ?"background: "+ this.props.theme.colours.color3:""};
|
||||
-webkit-transform: translateX(-90%) rotate(45deg) scale(1.2);
|
||||
transform: translateX(-90%) rotate(45deg) scale(1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
input{
|
||||
padding: 1em .2em .1em 0.2em;
|
||||
display: inline-block;
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex-positive: 1;
|
||||
flex-grow: 1;
|
||||
z-index: 1;
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
color: ${this.props.theme.colours.color2};
|
||||
font-size: 1em;
|
||||
${this.props.disabled!==undefined ? "user-select: none;" : null};
|
||||
&:focus + label, &:not(:placeholder-shown) + label {
|
||||
color: ${this.props.theme.colours.color5};
|
||||
font-size: 0.7em;
|
||||
-webkit-transform: translateY(0);
|
||||
transform: translateY(0);
|
||||
bottom: 50%;
|
||||
font-weight: bold;
|
||||
}
|
||||
:disabled{
|
||||
color: ${this.props.theme.colours.color1};
|
||||
&:focus + label, &:not(:placeholder-shown) + label{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
label{
|
||||
padding: 0;
|
||||
color: ${this.props.theme.colours.color2};
|
||||
z-index: 0;
|
||||
position: absolute;
|
||||
left: 0.2em;
|
||||
padding-bottom: 0.2em;
|
||||
bottom: 0;
|
||||
-webkit-transition: all ease-in-out 350ms;
|
||||
transition: all ease-in-out 350ms;
|
||||
}
|
||||
button{
|
||||
position: relative;
|
||||
background: ${this.props.theme.colours.color2};
|
||||
color: ${this.props.theme.colours.color6};
|
||||
border: none;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
padding: 0 .5em;
|
||||
-webkit-transition: ease all 300ms;
|
||||
transition: ease all 300ms;
|
||||
z-index: 1;
|
||||
::before {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
-webkit-clip-path: polygon(0% 0%, 100% 100%, 0% 100%);
|
||||
clip-path: polygon(0% 0%, 100% 100%, 0% 100%);
|
||||
-webkit-transform: translateX(-90%) rotate(45deg);
|
||||
transform: translateX(-90%) rotate(45deg);
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
background: ${this.props.theme.colours.color2};
|
||||
-webkit-transition: ease all 300ms;
|
||||
transition: ease all 300ms;
|
||||
}
|
||||
}
|
||||
`;
|
||||
return (
|
||||
<InputWrapper>
|
||||
<input name={this.props.name} id={this.props.id} placeholder=" " type={this.props.type ? this.props.type : "text"} value={this.state.value} onChange={e=>this.handleChange(e)} disabled={this.props.disabled} />
|
||||
{this.props.label != null ? <label htmlFor={this.props.id}>{this.props.label}</label> : ""}
|
||||
{this.props.button != null ? <button onClick={this.props.onClick}>{this.props.button}</button> : ""}
|
||||
</InputWrapper>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(TextInput)
|
@ -1,12 +0,0 @@
|
||||
import CreateUserWidget from "./CreateUserWidget";
|
||||
import RecentActivityWidget from "./RecentActivityWidget";
|
||||
import SystemStatusWidget from "./SystemStatusWidget";
|
||||
import SearchBox from "./SearchBox";
|
||||
import Card from "./Card";
|
||||
import TextInput from "./TextInput";
|
||||
import Button from "./Button";
|
||||
import Select from "./Select";
|
||||
import Checkbox from "./Checkbox";
|
||||
import ColourPalette from "./ColourPalette";
|
||||
import Table from "./Table";
|
||||
export { CreateUserWidget, RecentActivityWidget, SystemStatusWidget, SearchBox, Card, TextInput, Button, Select, Checkbox, Table, ColourPalette}
|
@ -1,13 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
|
@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import {Button} from '../components'
|
||||
|
||||
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||
export default {
|
||||
title: 'Example/Button',
|
||||
component: Button,
|
||||
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||
argTypes: {
|
||||
children: { control: 'color' },
|
||||
},
|
||||
};
|
||||
|
||||
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||
const Template = (args) => <Button {...args} />;
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Primary.args = {
|
||||
primary: true,
|
||||
children: 'Button',
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
import React from "react";
|
||||
import { Checkbox } from "../components";
|
||||
|
||||
export default {
|
||||
title: 'Input/Checkbox',
|
||||
component: Checkbox,
|
||||
}
|
||||
|
||||
export const Primary = () =><Checkbox/>;
|
@ -1,211 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
import Code from './assets/code-brackets.svg';
|
||||
import Colors from './assets/colors.svg';
|
||||
import Comments from './assets/comments.svg';
|
||||
import Direction from './assets/direction.svg';
|
||||
import Flow from './assets/flow.svg';
|
||||
import Plugin from './assets/plugin.svg';
|
||||
import Repo from './assets/repo.svg';
|
||||
import StackAlt from './assets/stackalt.svg';
|
||||
|
||||
<Meta title="Example/Introduction" />
|
||||
|
||||
<style>{`
|
||||
.subheading {
|
||||
--mediumdark: '#999999';
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
letter-spacing: 6px;
|
||||
line-height: 24px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 12px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.link-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 620px) {
|
||||
.link-list {
|
||||
row-gap: 20px;
|
||||
column-gap: 20px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (-ms-high-contrast:none) {
|
||||
.link-list {
|
||||
display: -ms-grid;
|
||||
-ms-grid-columns: 1fr 1fr;
|
||||
-ms-grid-rows: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.link-item {
|
||||
display: block;
|
||||
padding: 20px 30px 20px 15px;
|
||||
border: 1px solid #00000010;
|
||||
border-radius: 5px;
|
||||
transition: background 150ms ease-out, border 150ms ease-out, transform 150ms ease-out;
|
||||
color: #333333;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.link-item:hover {
|
||||
border-color: #1EA7FD50;
|
||||
transform: translate3d(0, -3px, 0);
|
||||
box-shadow: rgba(0, 0, 0, 0.08) 0 3px 10px 0;
|
||||
}
|
||||
|
||||
.link-item:active {
|
||||
border-color: #1EA7FD;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.link-item strong {
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.link-item img {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
margin-right: 15px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.link-item span {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
display: inline-block;
|
||||
border-radius: 1em;
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
font-weight: 700;
|
||||
background: #E7FDD8;
|
||||
color: #66BF3C;
|
||||
padding: 4px 12px;
|
||||
margin-right: 10px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.tip-wrapper {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.tip-wrapper code {
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
`}</style>
|
||||
|
||||
# Welcome to Storybook
|
||||
|
||||
Storybook helps you build UI components in isolation from your app's business logic, data, and context.
|
||||
That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA.
|
||||
|
||||
Browse example stories now by navigating to them in the sidebar.
|
||||
View their code in the `src/stories` directory to learn how they work.
|
||||
We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages.
|
||||
|
||||
<div className="subheading">Configure</div>
|
||||
|
||||
<div className="link-list">
|
||||
<a
|
||||
className="link-item"
|
||||
href="https://storybook.js.org/docs/react/addons/addon-types"
|
||||
target="_blank"
|
||||
>
|
||||
<img src={Plugin} alt="plugin" />
|
||||
<span>
|
||||
<strong>Presets for popular tools</strong>
|
||||
Easy setup for TypeScript, SCSS and more.
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="link-item"
|
||||
href="https://storybook.js.org/docs/react/configure/webpack"
|
||||
target="_blank"
|
||||
>
|
||||
<img src={StackAlt} alt="Build" />
|
||||
<span>
|
||||
<strong>Build configuration</strong>
|
||||
How to customize webpack and Babel
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="link-item"
|
||||
href="https://storybook.js.org/docs/react/configure/styling-and-css"
|
||||
target="_blank"
|
||||
>
|
||||
<img src={Colors} alt="colors" />
|
||||
<span>
|
||||
<strong>Styling</strong>
|
||||
How to load and configure CSS libraries
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="link-item"
|
||||
href="https://storybook.js.org/docs/react/get-started/setup#configure-storybook-for-your-stack"
|
||||
target="_blank"
|
||||
>
|
||||
<img src={Flow} alt="flow" />
|
||||
<span>
|
||||
<strong>Data</strong>
|
||||
Providers and mocking for data libraries
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="subheading">Learn</div>
|
||||
|
||||
<div className="link-list">
|
||||
<a className="link-item" href="https://storybook.js.org/docs" target="_blank">
|
||||
<img src={Repo} alt="repo" />
|
||||
<span>
|
||||
<strong>Storybook documentation</strong>
|
||||
Configure, customize, and extend
|
||||
</span>
|
||||
</a>
|
||||
<a className="link-item" href="https://storybook.js.org/tutorials/" target="_blank">
|
||||
<img src={Direction} alt="direction" />
|
||||
<span>
|
||||
<strong>In-depth guides</strong>
|
||||
Best practices from leading teams
|
||||
</span>
|
||||
</a>
|
||||
<a className="link-item" href="https://github.com/storybookjs/storybook" target="_blank">
|
||||
<img src={Code} alt="code" />
|
||||
<span>
|
||||
<strong>GitHub project</strong>
|
||||
View the source and add issues
|
||||
</span>
|
||||
</a>
|
||||
<a className="link-item" href="https://discord.gg/storybook" target="_blank">
|
||||
<img src={Comments} alt="comments" />
|
||||
<span>
|
||||
<strong>Discord chat</strong>
|
||||
Chat with maintainers and the community
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="tip-wrapper">
|
||||
<span className="tip">Tip</span>Edit the Markdown in{' '}
|
||||
<code>src/stories/Introduction.stories.mdx</code>
|
||||
</div>
|
@ -1,11 +0,0 @@
|
||||
import React from "react";
|
||||
import { TextInput } from "../components";
|
||||
export default{
|
||||
component: TextInput,
|
||||
title: 'Input/TextInput',
|
||||
}
|
||||
|
||||
export const Primary = (args) => <TextInput {...args}/>;
|
||||
Primary.args = {id: 'Test', name:'test', label: 'Text Field', value: 'Some Input Data'}
|
||||
export const Disabled = () => <TextInput disabled/>;
|
||||
Disabled.args = { label: ' Disabled Text Field', value: 'Disabled'}
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/code-brackets</title><g id="illustration/code-brackets" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#87E6E5" d="M11.4139325,12 C11.7605938,12 12,12.5059743 12,13.3779712 L12,17.4951758 L6.43502246,23.3839989 C5.85499251,23.9978337 5.85499251,25.0021663 6.43502246,25.6160011 L12,31.5048242 L12,35.6220288 C12,36.4939606 11.7605228,37 11.4139325,37 C11.2725831,37 11.1134406,36.9158987 10.9453839,36.7379973 L0.435022463,25.6160011 C-0.145007488,25.0021663 -0.145007488,23.9978337 0.435022463,23.3839989 L10.9453839,12.2620027 C11.1134051,12.0841663 11.2725831,12 11.4139325,12 Z M36.5860675,12 C36.7274169,12 36.8865594,12.0841013 37.0546161,12.2620027 L47.5649775,23.3839989 C48.1450075,23.9978337 48.1450075,25.0021663 47.5649775,25.6160011 L37.0546161,36.7379973 C36.8865949,36.9158337 36.7274169,37 36.5860675,37 C36.2394062,37 36,36.4940257 36,35.6220288 L36,31.5048242 L41.5649775,25.6160011 C42.1450075,25.0021663 42.1450075,23.9978337 41.5649775,23.3839989 L36,17.4951758 L36,13.3779712 C36,12.5060394 36.2394772,12 36.5860675,12 Z"/><rect id="Rectangle-7-Copy-5" width="35.57" height="4" x="5.009" y="22.662" fill="#A0DB77" rx="2" transform="translate(22.793959, 24.662305) rotate(-75.000000) translate(-22.793959, -24.662305)"/></g></svg>
|
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 8.3 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/comments</title><g id="illustration/comments" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Path" fill="#96D07C" d="M2.52730803,17.9196415 C2.44329744,17.9745167 2.36370847,18.000488 2.29303375,18.000488 C2.1197031,18.000488 2,17.8443588 2,17.5752855 L2,4 C2,1.790861 3.790861,3.23296945e-13 6,3.23296945e-13 L33.9995117,3.23296945e-13 C36.2086507,3.23296945e-13 37.9995117,1.790861 37.9995117,4 L37.9995117,9.999512 C37.9995117,12.208651 36.2086507,13.999512 33.9995117,13.999512 L8,13.999512 C7.83499225,13.999512 7.6723181,13.9895206 7.51254954,13.9701099 L2.52730803,17.9196415 Z"/><path id="Path" fill="#73E1E0" d="M7.51066,44.9703679 L2.52730803,47.9186655 C2.44329744,47.9735407 2.36370847,47.999512 2.29303375,47.999512 C2.1197031,47.999512 2,47.8433828 2,47.5743095 L2,35 C2,32.790861 3.790861,31 6,31 L26,31 C28.209139,31 30,32.790861 30,35 L30,41 C30,43.209139 28.209139,45 26,45 L8,45 C7.8343417,45 7.67103544,44.9899297 7.51066,44.9703679 Z"/><path id="Path" fill="#FFD476" d="M46,19.5 L46,33.0747975 C46,33.3438708 45.8802969,33.5 45.7069663,33.5 C45.6362915,33.5 45.5567026,33.4740287 45.472692,33.4191535 L40.4887103,29.4704446 C40.3285371,29.489956 40.1654415,29.5 40,29.5 L18,29.5 C15.790861,29.5 14,27.709139 14,25.5 L14,19.5 C14,17.290861 15.790861,15.5 18,15.5 L42,15.5 C44.209139,15.5 46,17.290861 46,19.5 Z"/></g></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/direction</title><g id="illustration/direction" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#FFD476" d="M23.4917015,33.6030641 L2.93840258,31.4321033 C2.38917316,31.3740904 1.99096346,30.8818233 2.04897631,30.3325939 C2.0747515,30.0885705 2.18934861,29.8625419 2.37095722,29.6975265 L34.2609105,0.721285325 C34.6696614,0.349881049 35.3021022,0.38015648 35.6735064,0.788907393 C35.9232621,1.06377731 36.0001133,1.45442096 35.8730901,1.80341447 L24.5364357,32.9506164 C24.3793473,33.3822133 23.9484565,33.6513092 23.4917015,33.6030641 L23.4917015,33.6030641 Z"/><path id="Combined-Shape-Copy" fill="#FFC445" d="M24.3163597,33.2881029 C24.0306575,33.0138462 23.9337246,32.5968232 24.069176,32.2246735 L35.091923,1.9399251 C35.2266075,1.56988243 35.5659249,1.31333613 35.9586669,1.28460955 C36.5094802,1.24432106 36.9886628,1.65818318 37.0289513,2.20899647 L40.2437557,46.1609256 C40.2644355,46.4436546 40.1641446,46.7218752 39.9678293,46.9263833 C39.5853672,47.3248067 38.9523344,47.3377458 38.5539111,46.9552837 L24.3163597,33.2881029 L24.3163597,33.2881029 Z"/></g></svg>
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/flow</title><g id="illustration/flow" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#79C9FC" fill-rule="nonzero" d="M30,29 C32.7614237,29 35,26.7614237 35,24 C35,14.6111593 27.3888407,7 18,7 C8.61115925,7 1,14.6111593 1,24 C1,33.3888407 8.61115925,41 18,41 C19.3333404,41 20.6447683,40.8466238 21.9154603,40.5471706 C19.5096374,39.3319645 17.5510566,37.8612875 16.0456579,36.1314815 C14.1063138,33.9030427 12.769443,31.0725999 12.0293806,27.6556449 C11.360469,26.565281 11,25.3082308 11,24 C11,20.1340068 14.1340068,17 18,17 C21.8659932,17 25,20.1340068 25,24 C25,26.125 27.7040312,29 30,29 Z"/><path id="Combined-Shape-Copy" fill="#FFC445" fill-rule="nonzero" d="M42,29 C44.7614237,29 47,26.7614237 47,24 C47,14.6111593 39.3888407,7 30,7 C20.6111593,7 13,14.6111593 13,24 C13,33.3888407 20.6111593,41 30,41 C31.3333404,41 32.6447683,40.8466238 33.9154603,40.5471706 C31.5096374,39.3319645 29.4051056,37.9781963 28.0456579,36.1314815 C26.0625,33.4375 23,27.1875 23,24 C23,20.1340068 26.1340068,17 30,17 C33.8659932,17 37,20.1340068 37,24 C37.02301,26.3435241 39.7040312,29 42,29 Z" transform="translate(30.000000, 24.000000) scale(-1, -1) translate(-30.000000, -24.000000)"/></g></svg>
|
Before Width: | Height: | Size: 1.4 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/plugin</title><g id="illustration/plugin" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#79C9FC" d="M26,15.3994248 C26,15.4091303 26,15.4188459 26,15.4285714 L26,21.4694881 C25.8463595,21.4969567 25.6941676,21.51275 25.5873784,21.51275 C25.4974117,21.51275 25.4230979,21.4768034 25.377756,21.4206259 L25.2660784,21.2822603 L25.1317423,21.1657666 C24.2436317,20.3956144 23.100098,19.9633214 21.895551,19.9633214 C19.2039137,19.9633214 17,22.1075558 17,24.7804643 C17,27.4533728 19.2039137,29.5976071 21.895551,29.5976071 C23.1972122,29.5976071 24.3149423,29.2878193 25.1231445,28.3613697 C25.4542273,27.9818463 25.568273,27.9073214 25.5873784,27.9073214 C25.681532,27.9073214 25.8352452,27.9239643 26,27.9524591 L26,32.5714286 C26,32.5811541 26,32.5908697 26,32.6005752 L26,33 C26,35.209139 24.209139,37 22,37 L4,37 C1.790861,37 0,35.209139 0,33 L0,15 C0,12.790861 1.790861,11 4,11 L22,11 C24.209139,11 26,12.790861 26,15 L26,15.3994248 Z"/><path id="Path" fill="#87E6E5" d="M27.9998779,32.5714286 C27.9998779,33.3604068 28.6572726,34 29.4682101,34 L46.5315458,34 C47.3424832,34 47.9998779,33.3604068 47.9998779,32.5714286 L47.9998779,15.4285714 C47.9998779,14.6395932 47.3424832,14 46.5315458,14 L29.4682101,14 C28.6572726,14 27.9998779,14.6395932 27.9998779,15.4285714 L27.9998779,21.8355216 C27.9334367,22.2650514 27.8567585,22.6454496 27.746391,22.8084643 C27.4245309,23.2838571 26.2402709,23.51275 25.5873784,23.51275 C24.8705773,23.51275 24.2322714,23.1857725 23.8214379,22.6767605 C23.3096996,22.2329909 22.6349941,21.9633214 21.895551,21.9633214 C20.2963823,21.9633214 19,23.2245992 19,24.7804643 C19,26.3363293 20.2963823,27.5976071 21.895551,27.5976071 C22.5398535,27.5976071 23.2399343,27.477727 23.6160247,27.0466112 C24.1396029,26.4464286 24.7367044,25.9073214 25.5873784,25.9073214 C26.2402709,25.9073214 27.5912951,26.1766031 27.8226692,26.6116071 C27.8819199,26.7230038 27.9403239,26.921677 27.9998779,27.1556219 L27.9998779,32.5714286 Z"/></g></svg>
|
Before Width: | Height: | Size: 2.1 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/repo</title><g id="illustration/repo" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Rectangle-62-Copy" fill="#B7F0EF" d="M27.2217723,9.04506931 L41.2217723,6.2682098 C43.3886973,5.83840648 45.4937616,7.2466219 45.9235649,9.41354696 C45.9743993,9.66983721 46,9.93049166 46,10.1917747 L46,32.581381 C46,34.4904961 44.650862,36.1335143 42.7782277,36.5049459 L28.7782277,39.2818054 C26.6113027,39.7116087 24.5062384,38.3033933 24.0764351,36.1364682 C24.0256007,35.880178 24,35.6195235 24,35.3582405 L24,12.9686342 C24,11.0595191 25.349138,9.4165009 27.2217723,9.04506931 Z" opacity=".7"/><path id="Combined-Shape" fill="#87E6E5" d="M6.77822775,6.2682098 L20.7782277,9.04506931 C22.650862,9.4165009 24,11.0595191 24,12.9686342 L24,35.3582405 C24,37.5673795 22.209139,39.3582405 20,39.3582405 C19.738717,39.3582405 19.4780625,39.3326398 19.2217723,39.2818054 L5.22177225,36.5049459 C3.34913798,36.1335143 2,34.4904961 2,32.581381 L2,10.1917747 C2,7.98263571 3.790861,6.19177471 6,6.19177471 C6.26128305,6.19177471 6.5219375,6.21737537 6.77822775,6.2682098 Z"/><path id="Rectangle-63-Copy-2" fill="#61C1FD" d="M22,10 C23.1666667,10.2291667 24.0179036,10.625 24.5537109,11.1875 C25.0895182,11.75 25.5716146,12.875 26,14.5625 C26,29.3020833 26,37.5208333 26,39.21875 C26,40.9166667 26.4241536,42.9583333 27.2724609,45.34375 L24.5537109,41.875 L22.9824219,45.34375 C22.327474,43.1979167 22,41.2291667 22,39.4375 C22,37.6458333 22,27.8333333 22,10 Z"/></g></svg>
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/stackalt</title><g id="illustration/stackalt" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#FFAE00" d="M23.8628277,0 L23.8628277,48 L3.32291648,36.2491883 L3.32155653,11.9499781 L23.8628277,0 Z M23.8670509,0 L44.408322,11.9499781 L44.4069621,36.2491883 L23.8670509,48 L23.8670509,0 Z" opacity=".196"/><path id="Rectangle-46-Copy-3" fill="#66BF3C" d="M15.8232279,19.1155258 L24.7368455,21.4714881 C29.6053842,22.7582937 33.4077423,26.5606518 34.694548,31.4291905 L37.0505103,40.3428082 C37.6150232,42.4786032 36.3412474,44.6676353 34.2054524,45.2321482 C33.5569474,45.4035549 32.87575,45.4091235 32.2245294,45.2483418 L23.3459013,43.0562718 C18.2976962,41.809906 14.3561301,37.8683399 13.1097642,32.8201348 L10.9176943,23.9415066 C10.3881737,21.7967682 11.6975664,19.6288529 13.8423049,19.0993322 C14.4935255,18.9385505 15.1747229,18.9441191 15.8232279,19.1155258 Z" opacity=".5" transform="translate(23.999997, 32.166058) rotate(-45.000000) translate(-23.999997, -32.166058)"/><path id="Rectangle-46-Copy-2" fill="#FFAE00" d="M15.8232279,11.2216893 L24.7368455,13.5776516 C29.6053842,14.8644572 33.4077423,18.6668153 34.694548,23.5353541 L37.0505103,32.4489717 C37.6150232,34.5847667 36.3412474,36.7737988 34.2054524,37.3383117 C33.5569474,37.5097184 32.87575,37.515287 32.2245294,37.3545053 L23.3459013,35.1624353 C18.2976962,33.9160695 14.3561301,29.9745034 13.1097642,24.9262983 L10.9176943,16.0476701 C10.3881737,13.9029317 11.6975664,11.7350164 13.8423049,11.2054957 C14.4935255,11.044714 15.1747229,11.0502826 15.8232279,11.2216893 Z" opacity=".5" transform="translate(23.999997, 24.272222) rotate(-45.000000) translate(-23.999997, -24.272222)"/><path id="Rectangle-46-Copy" fill="#FC521F" d="M15.8232279,3.32785281 L24.7368455,5.68381509 C29.6053842,6.97062075 33.4077423,10.7729788 34.694548,15.6415176 L37.0505103,24.5551352 C37.6150232,26.6909302 36.3412474,28.8799623 34.2054524,29.4444752 C33.5569474,29.6158819 32.87575,29.6214505 32.2245294,29.4606688 L23.3459013,27.2685988 C18.2976962,26.022233 14.3561301,22.0806669 13.1097642,17.0324618 L10.9176943,8.15383364 C10.3881737,6.00909519 11.6975664,3.84117987 13.8423049,3.31165925 C14.4935255,3.15087753 15.1747229,3.15644615 15.8232279,3.32785281 Z" opacity=".5" transform="translate(23.999997, 16.378385) rotate(-45.000000) translate(-23.999997, -16.378385)"/></g></svg>
|
Before Width: | Height: | Size: 2.5 KiB |
@ -1,41 +0,0 @@
|
||||
const colours1 = {
|
||||
color1: "#1F1E2E",
|
||||
color2: "#27283A",
|
||||
color3: "#438AF4",
|
||||
color4: "rgba(2,0,36,0.7455182756696428)",
|
||||
color5: "#490428c6",
|
||||
color6: "#ddd9ebbf",
|
||||
color7: "#858192bf",
|
||||
color8: "#3a192ae0",
|
||||
color9: "#110d13",
|
||||
color10: "#fff",
|
||||
}
|
||||
const colours2 = {
|
||||
color1: "#1e2e2e",
|
||||
color2: "#273a37",
|
||||
color3: "#18cb64",
|
||||
color4: "#012400be",
|
||||
color5: "#254904c6",
|
||||
color6: "#daebd9bf",
|
||||
color7: "#839281bf",
|
||||
color8: "#193a1be0",
|
||||
color9: "#110d13",
|
||||
color10: "#fff",
|
||||
}
|
||||
const colours3 = {
|
||||
color1: "#abc4ff", // Contentbox-bg
|
||||
color2: "#b6ccfe", // Site-bg
|
||||
color3: "#edf2fb", // fg-main //hover
|
||||
color4: "#4564AD", // available
|
||||
color5: "#abc4ff", // contrast
|
||||
color6: "#023e8a", // text
|
||||
color7: "#d7e3fc",
|
||||
color8: "#3a192ae0",
|
||||
color9: "#b6ccfe", // disabled
|
||||
color10: "#03045e", // sbox-text
|
||||
}
|
||||
const theme = {
|
||||
colours: colours3
|
||||
}
|
||||
export { theme, colours1 as colours }
|
||||
export default theme
|
@ -1,13 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
|
||||
export class AccountEdit extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default AccountEdit
|
@ -1,72 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import Card from '../components/Card'
|
||||
import styled, { withTheme } from 'styled-components'
|
||||
import { Button, Table, Select, Checkbox } from '../components';
|
||||
|
||||
export class AccountManager extends Component {
|
||||
render() {
|
||||
const ActionSelect = styled.div`
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<h3>Account Manager</h3>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>UserID</td>
|
||||
<td>First Name</td>
|
||||
<td>Last Name</td>
|
||||
<td>Last Access</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>001</td>
|
||||
<td>Anton</td>
|
||||
<td>Stigloher</td>
|
||||
<td>06.10.2021</td>
|
||||
<td><Button>More</Button></td>
|
||||
<td><ActionSelect><Checkbox/></ActionSelect></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>002</td>
|
||||
<td>Max</td>
|
||||
<td>Mustermann</td>
|
||||
<td>07.10.2021</td>
|
||||
<td><Button>More</Button></td>
|
||||
<td><ActionSelect><Checkbox/></ActionSelect></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>003</td>
|
||||
<td>Peter</td>
|
||||
<td>Lustig</td>
|
||||
<td>08.10.2021</td>
|
||||
<td><Button>More</Button></td>
|
||||
<td><ActionSelect><Checkbox/></ActionSelect></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
</Table>
|
||||
<ActionSelect><Select>
|
||||
<option>Extend</option>
|
||||
<option>Enable</option>
|
||||
<option>Disable</option>
|
||||
<option>Delete</option>
|
||||
</Select>
|
||||
<Button>Apply</Button>
|
||||
</ActionSelect>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(AccountManager)
|
@ -1,236 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
//import "./AdminUI.sass"
|
||||
import { Link, NavLink } from 'react-router-dom'
|
||||
import styled, { createGlobalStyle, withTheme } from 'styled-components';
|
||||
import {SearchBox} from '../components';
|
||||
|
||||
|
||||
export class AdminUI extends Component {
|
||||
render() {
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html, body{
|
||||
margin: 0;
|
||||
}
|
||||
#root{
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-rows: auto 1fr 2em;
|
||||
grid-template-areas: "header header header" " menu main ." "footer footer footer";
|
||||
//background: $color2
|
||||
overflow: auto;
|
||||
}
|
||||
`;
|
||||
const Header = styled.header`
|
||||
position: relative;
|
||||
-ms-grid-row: 1;
|
||||
-ms-grid-column: 1;
|
||||
-ms-grid-column-span: 3;
|
||||
grid-area: header;
|
||||
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
min-height: auto;
|
||||
padding: .5em;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: ${this.props.theme.colours.color1};
|
||||
color: ${this.props.theme.colours.color7};
|
||||
h1 {
|
||||
-ms-flex-wrap: nowrap;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
font-size: 1.5em;
|
||||
margin: .5em;
|
||||
margin-right: auto;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
&::after {
|
||||
justify-self: start;
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
background: ${this.props.theme.colours.color3};
|
||||
height: .2em;
|
||||
z-index: 1;
|
||||
}
|
||||
nav{
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-shrink: 1;
|
||||
ul{
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
li{
|
||||
display: inline;
|
||||
a{
|
||||
text-decoration: none;
|
||||
color: ${this.props.theme.colours.color6};
|
||||
padding: .5em;
|
||||
white-space: nowrap;
|
||||
&:hover{
|
||||
color: ${this.props.theme.colours.color3};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const Menu = styled.menu`
|
||||
-ms-grid-row: 2;
|
||||
-ms-grid-column: 1;
|
||||
grid-area: menu;
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
min-height: 100;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
background: ${this.props.theme.colours.color1};
|
||||
padding: .5em 0;
|
||||
overflow: visible;
|
||||
h3{
|
||||
margin: 0;
|
||||
color: #ddd9ebbf;
|
||||
-webkit-text-decoration: bold;
|
||||
text-decoration: bold;
|
||||
}
|
||||
ul{
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin: .4em 0;
|
||||
a{
|
||||
position: relative;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
margin-left: .4em;
|
||||
color: ${this.props.theme.colours.color6};
|
||||
padding: .3em .5em;
|
||||
font-size: 1.05em;
|
||||
-webkit-transition: ease all 500ms;
|
||||
transition: ease all 500ms;
|
||||
&::after{
|
||||
-webkit-transition: ease all 500ms;
|
||||
transition: ease all 500ms;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 100%;
|
||||
width: 0em;
|
||||
content: '';
|
||||
background: ${this.props.theme.colours.color6};
|
||||
}
|
||||
&:hover{
|
||||
background: ${this.props.theme.colours.color3};
|
||||
&::after{
|
||||
width: 0.4em;
|
||||
}
|
||||
}
|
||||
&.menu__active{
|
||||
color: ${this.props.theme.colours.color3};
|
||||
&::after{
|
||||
width: 0.4em;
|
||||
background: ${this.props.theme.colours.color2};
|
||||
}
|
||||
&:hover {
|
||||
color: ${this.props.theme.colours.color10};
|
||||
&::after {
|
||||
width: 0.4em;
|
||||
background: ${this.props.theme.colours.color6};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const Footer = styled.footer`
|
||||
position: relative;
|
||||
grid-area: footer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: ${this.props.theme.colours.color6};
|
||||
text-shadow: 0 0 .5em ${this.props.theme.colours.color3};
|
||||
text-transform: uppercase;
|
||||
background: ${this.props.theme.colours.color2};
|
||||
&::after {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 2em;
|
||||
opacity: 0.5;
|
||||
background: #000;
|
||||
background: linear-gradient(-10deg, ${this.props.theme.colours.color2} 0%, ${this.props.theme.colours.color9}, ${this.props.theme.colours.color5} 50%, ${this.props.theme.colours.color4} 65%, ${this.props.theme.colours.color1} 100%);
|
||||
content: '';
|
||||
}
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 100%;
|
||||
height: .2em;
|
||||
opacity: 0.5;
|
||||
content: '';
|
||||
background: linear-gradient(${this.props.theme.colours.color2}, ${this.props.theme.colours.color3});
|
||||
}
|
||||
`;
|
||||
const Main = styled.div`
|
||||
position: relative;
|
||||
grid-area: main;
|
||||
display: flex;
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
padding: 1em;
|
||||
color: ${props=>props.theme.colours.color6};
|
||||
background: ${this.props.theme.colours.color2};
|
||||
`;
|
||||
return (
|
||||
<>
|
||||
<GlobalStyle />
|
||||
<Header>
|
||||
<h1>Doorlock Systems</h1>
|
||||
<SearchBox />
|
||||
<nav>
|
||||
<ul>
|
||||
<li><Link to="/user_cp">Admin Settings</Link></li>
|
||||
<li><Link to="/logout">Logout</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</Header>
|
||||
<Menu>
|
||||
<h3>Menu{props => this.props.theme}</h3>
|
||||
<ul>
|
||||
<li><NavLink activeClassName="menu__active" exact to="/">Dashboard</NavLink></li>
|
||||
<li><NavLink activeClassName="menu__active" to="/accounts">Accounts</NavLink></li>
|
||||
<li><NavLink activeClassName="menu__active" to="/log">Logs</NavLink></li>
|
||||
<li><NavLink activeClassName="menu__active" to="/system">System</NavLink></li>
|
||||
</ul>
|
||||
</Menu>
|
||||
<Main>
|
||||
{this.props.children}
|
||||
</Main>
|
||||
<Footer>Footer</Footer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(AdminUI)
|
@ -1,42 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import styled, {withTheme} from 'styled-components'
|
||||
import { Table, Button } from '../components'
|
||||
import Card from '../components/Card'
|
||||
export class Logs extends Component {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<h3>Logs</h3>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Time</td>
|
||||
<td>Type</td>
|
||||
<td>Info</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>12.12.12 08:12</td>
|
||||
<td>Authentication</td>
|
||||
<td>[NFC] Max Mustermann</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>12.12.12 08:12</td>
|
||||
<td>Authentication Error</td>
|
||||
<td>[NFC] 92:23:c3:32</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>12.12.12 08:12</td>
|
||||
<td>Admin Login</td>
|
||||
<td>Toni Stigloher</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
<Button>Download full Logfile</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(Logs)
|
@ -1,41 +0,0 @@
|
||||
import React, { Children, Component } from 'react'
|
||||
import styled, {withTheme, createGlobalStyle} from 'styled-components'
|
||||
|
||||
export class Overlay extends Component {
|
||||
render() {
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
#root{
|
||||
overflow : hidden ;
|
||||
//pointer-events:none;
|
||||
//user-select: none;
|
||||
}
|
||||
`;
|
||||
const OverlayWrapper = styled.div`
|
||||
user-select: all;
|
||||
pointer-events: all;
|
||||
filter: blur(0);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: calc(100vw - 4em);
|
||||
margin: 2em;
|
||||
height: calc(100vh - 4em);
|
||||
z-index: 2;
|
||||
border-radius: 1em;
|
||||
padding: 1em;
|
||||
color: ${this.props.theme.colours.color3};
|
||||
background: ${this.props.theme.colours.color9};
|
||||
box-shadow: 0 0 2em ${this.props.theme.colours.color3};
|
||||
border: 1px solid ${this.props.theme.colours.color3};
|
||||
overflow: auto;
|
||||
`;
|
||||
return (
|
||||
<OverlayWrapper>
|
||||
<GlobalStyle/>
|
||||
{this.props.children}
|
||||
</OverlayWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(Overlay)
|
@ -1,26 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import styled, {withTheme} from 'styled-components'
|
||||
import {Button, Card, TextInput} from '../components'
|
||||
export class System extends Component {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<h4>System Settings</h4>
|
||||
<hr />
|
||||
Hello World
|
||||
</Card>
|
||||
<Card>
|
||||
<h4>WiFi</h4>
|
||||
<hr />
|
||||
<TextInput label="SSID"/>
|
||||
<TextInput label="Password" type="password"/>
|
||||
<Button >Save</Button>
|
||||
</Card>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(System)
|
@ -1,6 +0,0 @@
|
||||
import AdminUI from "./AdminUI";
|
||||
import AccountManager from "./AccountManager";
|
||||
import Logs from "./Logs";
|
||||
import System from "./System";
|
||||
import Overlay from "./Overlay";
|
||||
export {AccountManager, AdminUI, Logs, System, Overlay}
|