mirror of https://github.com/apibillme/broker
Compare commits
5 Commits
d0485b4c7d
...
c7128c45be
Author | SHA1 | Date |
---|---|---|
Bevan Hunt | c7128c45be | |
Bevan Hunt | a5bfd5b274 | |
Bevan Hunt | 884bd1124e | |
Bevan Hunt | 9a1daa2173 | |
Bevan Hunt | 1d4459eed2 |
|
@ -2,6 +2,8 @@ target
|
|||
target/*
|
||||
tmp/*
|
||||
tmp
|
||||
example
|
||||
example/*
|
||||
|
||||
# gitginore template for creating Snap packages
|
||||
# website: https://snapcraft.io/
|
||||
|
|
File diff suppressed because it is too large
Load Diff
30
Cargo.toml
30
Cargo.toml
|
@ -1,37 +1,31 @@
|
|||
[package]
|
||||
name = "broker"
|
||||
version = "5.0.0"
|
||||
version = "6.0.3"
|
||||
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
keywords = ["sse", "api", "baas", "broker", "real-time"]
|
||||
description = "Real-time BaaS (Backend as a Service)"
|
||||
repository = "https://github.com/apibillme/broker"
|
||||
homepage = "https://apibill.me"
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
warp = { version = "0.2", features = ["tls"] }
|
||||
futures = "0.3"
|
||||
tokio = { version = "0.2", features = ["full"] }
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
tide = "0.16.0"
|
||||
async-std = { version = "1.9", features = ["attributes"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_derive = "1"
|
||||
json = "0.12"
|
||||
sled = "0.31"
|
||||
pretty_env_logger = "0.3"
|
||||
rocksdb = "0.15"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
bcrypt = "0.6"
|
||||
jsonwebtoken = "7.0.1"
|
||||
go-flag = "0.1"
|
||||
envy = "0.4"
|
||||
lazy_static = "1.4"
|
||||
crossbeam = "0.7"
|
||||
bus = "2.2"
|
||||
broker-ntp = "0.0.1"
|
||||
Inflector = "0.11"
|
||||
json-patch = "0.2"
|
||||
base64 = "0.12"
|
||||
|
||||
[dev-dependencies]
|
||||
reqwest = { version = "0.10", features = ["json"] }
|
||||
rust-argon2 = "0.8"
|
||||
anyhow = "1"
|
||||
rmp-serde = "0.15"
|
||||
driftwood = "0.0.6"
|
||||
http-types = "2"
|
||||
tide-rustls = "0.2"
|
||||
futures = "0.3"
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) [2019] [Bevan Hunt]
|
||||
Copyright (c) [2021] [Bevan Hunt]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
14
Makefile
14
Makefile
|
@ -1,14 +0,0 @@
|
|||
run:
|
||||
SAVE_PATH=./tmp/broker_data ORIGIN=* cargo run
|
||||
release:
|
||||
SAVE_PATH=./tmp/broker_data cargo run --release
|
||||
build:
|
||||
sudo snapcraft
|
||||
edge:
|
||||
sudo snapcraft push --release edge *.snap
|
||||
publish:
|
||||
sudo snapcraft push --release stable *.snap
|
||||
cover:
|
||||
cargo tarpaulin
|
||||
purge:
|
||||
sudo multipass delete snapcraft-broker && sudo multipass purge
|
119
README.md
119
README.md
|
@ -4,7 +4,7 @@
|
|||
|
||||
### Purpose
|
||||
|
||||
The purpose of this library is to be your real-time BaaS (Backend as a Service).
|
||||
The purpose of this service is to be your real-time BaaS (Backend as a Service).
|
||||
|
||||
Broker is a SSE message broker that requires you write no backend code to have a full real-time API.
|
||||
|
||||
|
@ -14,34 +14,27 @@ Broker follows an insert-only/publish/subscribe paradigm rather than a REST CRUD
|
|||
|
||||
### Features
|
||||
|
||||
* Very performant with a low memory footprint that uses about 20MB and 2 CPU threads
|
||||
* Under 1,000 lines of code
|
||||
* Very performant with a almost no CPU and memory usage
|
||||
* Under 500 lines of code
|
||||
* Secure Real-time Event Stream via SSE - requires the use of [broker-client](https://www.npmjs.com/package/broker-client)
|
||||
* Multi-tenanted
|
||||
* Supports CORS
|
||||
* Supports SSL - full end-to-end encryption
|
||||
* Provides user authentication with JWTs or HTTP Basic with stored Bcrypt(ed) passwords
|
||||
* Handles future events via Epoch UNIX timestamp
|
||||
* Provides user authentication with JWTs with stored Argon2 passwords
|
||||
* Uses Global NTP servers and doesn't rely on your local server time
|
||||
* Stateful immutable event persistence
|
||||
* Insert event via JSON POST request
|
||||
* Sync latest events on SSE client connection
|
||||
* Event log via GET request
|
||||
* Event cancellation via GET request
|
||||
|
||||
### How it works
|
||||
|
||||
In Broker you create a user, login, then insert an event with its data, a collection_id, and a timestamp. Broker publishes the event when the timestamp is reached to the event stream via SSE. Broker keeps all events its database that can be viewed in collections (by collection_id). Broker can also cancel future events.
|
||||
In Broker you create a user, login, then insert an event with its data. Broker then publishes the event via SSE.
|
||||
|
||||
When the client first subscribes to the SSE connection all the latest events and data is sent to the client. Combined with sending the latest event via SSE when subscribed negates the necessity to do any GET API requests in the lifecycle of an event.
|
||||
|
||||
The side-effect of this system is that the latest event is the schema. Old events are saved in the database and are not changed but the latest event is the schema for the front-end. This is pure NoSQL as the backend is agnostic to the event data.
|
||||
The side-effect of this system is that the latest event is the schema. This is pure NoSQL as the backend is agnostic to the event data.
|
||||
|
||||
### Recommeded Services/Libraries to use with Broker
|
||||
* [broker-client](https://www.npmjs.com/package/broker-client) - the official front-end client for broker
|
||||
* [broker-hook](https://www.npmjs.com/package/broker-hook) - the official react hook for broker
|
||||
* [broker-grid](https://www.npmjs.com/package/broker-grid) - the official data grid for broker
|
||||
* [Integromat](https://www.integromat.com/) - No-code Event Scheduler that supports many apps like GitHub, Meetup, and etc.
|
||||
* [React Hook Form](https://react-hook-form.com/) - Best form library for React
|
||||
* [React Debounce Input](https://www.npmjs.com/package/react-debounce-input) - React input for Real-time Submission (Edit in Place forms)
|
||||
|
||||
|
@ -54,7 +47,7 @@ The side-effect of this system is that the latest event is the schema. Old event
|
|||
|
||||
Yes with React Native. There may be native 3rd party libraries for SSE that work. In the future official libraries may be made available for native platforms.
|
||||
|
||||
### API
|
||||
### Use
|
||||
|
||||
#### Step 1 - create a user
|
||||
|
||||
|
@ -68,11 +61,7 @@ POST /users
|
|||
```
|
||||
- where {...} is for username and string, password a string, collection_id is the uuid of the event collection for user info, tenant_id is the uuid of the tenant
|
||||
|
||||
will return
|
||||
```json
|
||||
{"id":{...}}
|
||||
```
|
||||
- where {...} is the uuid (string) of the user
|
||||
will return `200` or `500` or `400`
|
||||
|
||||
#### For JWT Auth: Step 2 - login with the user
|
||||
|
||||
|
@ -95,10 +84,9 @@ will return
|
|||
#### Step 3 - connect to SSE
|
||||
|
||||
```html
|
||||
GET /events/{id}
|
||||
GET /sse
|
||||
```
|
||||
- where {id} is the tenant_id
|
||||
- authenticated endpoint (Authorization: Bearer {jwt}) or (Authorization: Basic {base64})
|
||||
- authenticated endpoint (Authorization: Bearer {jwt})
|
||||
- connect your sse-client to this endpoint using [broker-client](https://www.npmjs.com/package/broker-client)
|
||||
- note: broker-client uses fetch as eventsource doesn't support headers
|
||||
|
||||
|
@ -107,84 +95,34 @@ GET /events/{id}
|
|||
```html
|
||||
POST /insert
|
||||
```
|
||||
- authenticated endpoint (Authorization: Bearer {jwt}) or (Authorization: Basic {base64})
|
||||
- authenticated endpoint (Authorization: Bearer {jwt})
|
||||
- POST JSON to insert an event
|
||||
```json
|
||||
{"event":{...}, "tenant_id":{...}, "collection_id":{...}, "timestamp":{...}, "data":{...}}
|
||||
{"event":{...}, "data":{...}}
|
||||
```
|
||||
- where {...} is for the event a string, tenant_id is an assigned uuid v4 for the tenant, collection_id is an assigned uuid v4 for the event collection, timestamp is the epoch unix timestamp when you want the event to become the current event, and data is any JSON you want
|
||||
- where {...} is for the event a string and data is any JSON you want
|
||||
|
||||
will return
|
||||
```json
|
||||
{"event":{...}}
|
||||
```
|
||||
- where {...} is the event
|
||||
will return: `200` or `500` or `400` or `401`
|
||||
|
||||
#### Optional Endpoints
|
||||
### Install
|
||||
|
||||
```html
|
||||
GET /collections/{collection_id}
|
||||
```
|
||||
- authenticated endpoint (Authorization: Bearer {jwt}) or (Authorization: Basic {base64})
|
||||
- do a GET request where {collection_id} is the uuid of the collection you want (sorted by ascending timestamp) for the user's tenant
|
||||
|
||||
will return
|
||||
```json
|
||||
{"events":{...}}
|
||||
```
|
||||
- where {...} is the array of events
|
||||
|
||||
```html
|
||||
GET /user_events
|
||||
```
|
||||
- authenticated endpoint (Authorization: Bearer {jwt}) or (Authorization: Basic {base64})
|
||||
- do a GET request to get the user event collections (sorted by ascending timestamp)
|
||||
|
||||
will return
|
||||
```json
|
||||
{"info": {...}, "events":{...}}
|
||||
```
|
||||
- where (...) is for info a list of events for user info and events a list of all events that the user inserted
|
||||
|
||||
```html
|
||||
GET /cancel/{id}
|
||||
```
|
||||
- authenticated endpoint (Authorization: Bearer {jwt}) or (Authorization: Basic {base64})
|
||||
- do a GET request where id is the uuid of the event to cancel a future event for the user's tenant
|
||||
|
||||
will return
|
||||
```json
|
||||
{"event":{...}}
|
||||
```
|
||||
- where {...} is the event
|
||||
|
||||
### Use
|
||||
|
||||
```rust
|
||||
use broker::broker;
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() {
|
||||
broker().await
|
||||
}
|
||||
```
|
||||
OR
|
||||
``` cargo install broker ```
|
||||
|
||||
- the origin needs to be passed in as a flag - wildcard is supported - default http://localhost:3000
|
||||
- the port needs to be passed in as a flag - default 8080
|
||||
- the expiry (for jwts) needs to be passed in as a flag - default 3600
|
||||
- the secret (for jwts) needs to be passed in as a flag - default secret
|
||||
- the conection needs to passed in as a flag (http or https) - default http
|
||||
- the key-path needs to passed in as a flag if connection https - default ./broker.rsa
|
||||
- the cert-path needs to passed in as a flag if connection https - default ./broker.pem
|
||||
- the save_path where the embedded database will save needs to be passed in as an environment variable
|
||||
- example: SAVE_PATH=./tmp/broker_data broker --port 8080 --connection https --origin http://localhost:3000 --expiry 3600 --secret secret --key-path ./broker.rsa --cert-path ./broker.pem
|
||||
- the origin can be passed in as a flag - default *
|
||||
- the port can be passed in as a flag - default 8080
|
||||
- the jwt_expiry (for jwts) can be passed in as a flag - default 86400
|
||||
- the jwt_secret (for jwts) should be passed in as a flag - default secret
|
||||
- the secure can be passed in as a flag (true or false) - default false
|
||||
- the key_path can be passed in as a flag if connection https - default ./broker.rsa
|
||||
- the cert_path can be passed in as a flag if connection https - default ./broker.pem
|
||||
- the db can be passed in as a flag where the embedded database will be saved - default tmp
|
||||
- example: `./broker --db="tmp" --port="443" --secure="true" --origin="*" --jwt_expiry="86400" --jwt_secret="secret" --key_path="broker.rsa" --cert_path="broker.pem"`
|
||||
|
||||
### Under the Hood
|
||||
|
||||
- [warp](https://crates.io/crates/warp) - web framework
|
||||
- [sled](https://crates.io/crates/sled) - embedded database
|
||||
### TechStack
|
||||
|
||||
* [Tide](https://crates.io/crates/tide)
|
||||
* [RocksDB](https://crates.io/crates/rocksdb)
|
||||
|
||||
### Inspiration
|
||||
|
||||
|
@ -198,6 +136,7 @@ OR
|
|||
|
||||
### Migrations
|
||||
|
||||
- from 5.0 to 6.0: is a full rewrite - there is no upgrade path from 5.0 to 6.0
|
||||
- from 4.0 to 5.0: multi-tenancy has been added and sled has been upgraded - there is no upgrade path from 4.0 to 5.0
|
||||
- from 3.0 to 4.0: the sse endpoint now returns all events with all collections with the latest collection event rather than just the latest event data for all event types
|
||||
- from 2.0 to 3.0: the sse endpoint is now secure and requires the use of the [broker-client](https://www.npmjs.com/package/broker-client) library
|
||||
|
|
24
broker.pem
24
broker.pem
|
@ -1,24 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIEADCCAmigAwIBAgICAcgwDQYJKoZIhvcNAQELBQAwLDEqMCgGA1UEAwwhcG9u
|
||||
eXRvd24gUlNBIGxldmVsIDIgaW50ZXJtZWRpYXRlMB4XDTE2MDgxMzE2MDcwNFoX
|
||||
DTIyMDIwMzE2MDcwNFowGTEXMBUGA1UEAwwOdGVzdHNlcnZlci5jb20wggEiMA0G
|
||||
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpVhh1/FNP2qvWenbZSghari/UThwe
|
||||
dynfnHG7gc3JmygkEdErWBO/CHzHgsx7biVE5b8sZYNEDKFojyoPHGWK2bQM/FTy
|
||||
niJCgNCLdn6hUqqxLAml3cxGW77hAWu94THDGB1qFe+eFiAUnDmob8gNZtAzT6Ky
|
||||
b/JGJdrEU0wj+Rd7wUb4kpLInNH/Jc+oz2ii2AjNbGOZXnRz7h7Kv3sO9vABByYe
|
||||
LcCj3qnhejHMqVhbAT1MD6zQ2+YKBjE52MsQKU/xhUpu9KkUyLh0cxkh3zrFiKh4
|
||||
Vuvtc+n7aeOv2jJmOl1dr0XLlSHBlmoKqH6dCTSbddQLmlK7dms8vE01AgMBAAGj
|
||||
gb4wgbswDAYDVR0TAQH/BAIwADALBgNVHQ8EBAMCBsAwHQYDVR0OBBYEFMeUzGYV
|
||||
bXwJNQVbY1+A8YXYZY8pMEIGA1UdIwQ7MDmAFJvEsUi7+D8vp8xcWvnEdVBGkpoW
|
||||
oR6kHDAaMRgwFgYDVQQDDA9wb255dG93biBSU0EgQ0GCAXswOwYDVR0RBDQwMoIO
|
||||
dGVzdHNlcnZlci5jb22CFXNlY29uZC50ZXN0c2VydmVyLmNvbYIJbG9jYWxob3N0
|
||||
MA0GCSqGSIb3DQEBCwUAA4IBgQBsk5ivAaRAcNgjc7LEiWXFkMg703AqDDNx7kB1
|
||||
RDgLalLvrjOfOp2jsDfST7N1tKLBSQ9bMw9X4Jve+j7XXRUthcwuoYTeeo+Cy0/T
|
||||
1Q78ctoX74E2nB958zwmtRykGrgE/6JAJDwGcgpY9kBPycGxTlCN926uGxHsDwVs
|
||||
98cL6ZXptMLTR6T2XP36dAJZuOICSqmCSbFR8knc/gjUO36rXTxhwci8iDbmEVaf
|
||||
BHpgBXGU5+SQ+QM++v6bHGf4LNQC5NZ4e4xvGax8ioYu/BRsB/T3Lx+RlItz4zdU
|
||||
XuxCNcm3nhQV2ZHquRdbSdoyIxV5kJXel4wCmOhWIq7A2OBKdu5fQzIAzzLi65EN
|
||||
RPAKsKB4h7hGgvciZQ7dsMrlGw0DLdJ6UrFyiR5Io7dXYT/+JP91lP5xsl6Lhg9O
|
||||
FgALt7GSYRm2cZdgi9pO9rRr83Br1VjQT1vHz6yoZMXSqc4A2zcN2a2ZVq//rHvc
|
||||
FZygs8miAhWPzqnpmgTj1cPiU1M=
|
||||
-----END CERTIFICATE-----
|
27
broker.rsa
27
broker.rsa
|
@ -1,27 +0,0 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAqVYYdfxTT9qr1np22UoIWq4v1E4cHncp35xxu4HNyZsoJBHR
|
||||
K1gTvwh8x4LMe24lROW/LGWDRAyhaI8qDxxlitm0DPxU8p4iQoDQi3Z+oVKqsSwJ
|
||||
pd3MRlu+4QFrveExwxgdahXvnhYgFJw5qG/IDWbQM0+ism/yRiXaxFNMI/kXe8FG
|
||||
+JKSyJzR/yXPqM9ootgIzWxjmV50c+4eyr97DvbwAQcmHi3Ao96p4XoxzKlYWwE9
|
||||
TA+s0NvmCgYxOdjLEClP8YVKbvSpFMi4dHMZId86xYioeFbr7XPp+2njr9oyZjpd
|
||||
Xa9Fy5UhwZZqCqh+nQk0m3XUC5pSu3ZrPLxNNQIDAQABAoIBAFKtZJgGsK6md4vq
|
||||
kyiYSufrcBLaaEQ/rkQtYCJKyC0NAlZKFLRy9oEpJbNLm4cQSkYPXn3Qunx5Jj2k
|
||||
2MYz+SgIDy7f7KHgr52Ew020dzNQ52JFvBgt6NTZaqL1TKOS1fcJSSNIvouTBerK
|
||||
NCSXHzfb4P+MfEVe/w1c4ilE+kH9SzdEo2jK/sRbzHIY8TX0JbmQ4SCLLayr22YG
|
||||
usIxtIYcWt3MMP/G2luRnYzzBCje5MXdpAhlHLi4TB6x4h5PmBKYc57uOVNngKLd
|
||||
YyrQKcszW4Nx5v0a4HG3A5EtUXNCco1+5asXOg2lYphQYVh2R+1wgu5WiDjDVu+6
|
||||
EYgjFSkCgYEA0NBk6FDoxE/4L/4iJ4zIhu9BptN8Je/uS5c6wRejNC/VqQyw7SHb
|
||||
hRFNrXPvq5Y+2bI/DxtdzZLKAMXOMjDjj0XEgfOIn2aveOo3uE7zf1i+njxwQhPu
|
||||
uSYA9AlBZiKGr2PCYSDPnViHOspVJjxRuAgyWM1Qf+CTC0D95aj0oz8CgYEAz5n4
|
||||
Cb3/WfUHxMJLljJ7PlVmlQpF5Hk3AOR9+vtqTtdxRjuxW6DH2uAHBDdC3OgppUN4
|
||||
CFj55kzc2HUuiHtmPtx8mK6G+otT7Lww+nLSFL4PvZ6CYxqcio5MPnoYd+pCxrXY
|
||||
JFo2W7e4FkBOxb5PF5So5plg+d0z/QiA7aFP1osCgYEAtgi1rwC5qkm8prn4tFm6
|
||||
hkcVCIXc+IWNS0Bu693bXKdGr7RsmIynff1zpf4ntYGpEMaeymClCY0ppDrMYlzU
|
||||
RBYiFNdlBvDRj6s/H+FTzHRk2DT/99rAhY9nzVY0OQFoQIXK8jlURGrkmI/CYy66
|
||||
XqBmo5t4zcHM7kaeEBOWEKkCgYAYnO6VaRtPNQfYwhhoFFAcUc+5t+AVeHGW/4AY
|
||||
M5qlAlIBu64JaQSI5KqwS0T4H+ZgG6Gti68FKPO+DhaYQ9kZdtam23pRVhd7J8y+
|
||||
xMI3h1kiaBqZWVxZ6QkNFzizbui/2mtn0/JB6YQ/zxwHwcpqx0tHG8Qtm5ZAV7PB
|
||||
eLCYhQKBgQDALJxU/6hMTdytEU5CLOBSMby45YD/RrfQrl2gl/vA0etPrto4RkVq
|
||||
UrkDO/9W4mZORClN3knxEFSTlYi8YOboxdlynpFfhcs82wFChs+Ydp1eEsVHAqtu
|
||||
T+uzn0sroycBiBfVB949LExnzGDFUkhG0i2c2InarQYLTsIyHCIDEA==
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -1,4 +0,0 @@
|
|||
REACT_APP_API=http://localhost:8080
|
||||
REACT_APP_PASSWORD=password
|
||||
REACT_APP_USERNAME=user
|
||||
REACT_APP_TENANT=112718d1-a0be-4468-b902-0749c3d964ae
|
|
@ -1,4 +0,0 @@
|
|||
REACT_APP_API=https://demo-api.apibill.me
|
||||
REACT_APP_PASSWORD=password
|
||||
REACT_APP_USERNAME=user
|
||||
REACT_APP_TENANT=112718d1-a0be-4468-b902-0749c3d964ae
|
|
@ -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 +0,0 @@
|
|||
## Example App
|
File diff suppressed because it is too large
Load Diff
|
@ -1,43 +0,0 @@
|
|||
{
|
||||
"name": "client",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
"autoprefixer": "^9.7.6",
|
||||
"broker-grid": "1.0.5",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-hook-form": "^4.10.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "3.3.0",
|
||||
"tailwindcss": "^1.4.6",
|
||||
"tailwindcss-spinner": "^1.0.0",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build:style": "tailwind build src/index.css -o src/tailwind.css",
|
||||
"start": "npm run build:style && react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 2.0 KiB |
|
@ -1,43 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Broker Demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
|
@ -1,138 +0,0 @@
|
|||
import React, {useEffect, useState} from 'react';
|
||||
import Grid from 'broker-grid';
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Switch,
|
||||
Route
|
||||
} from "react-router-dom";
|
||||
import { useForm } from "react-hook-form";
|
||||
import Logo from './logo.svg';
|
||||
import uuid from 'uuid/v4';
|
||||
import './tailwind.css';
|
||||
import './spinner.css';
|
||||
|
||||
function Insert(props) {
|
||||
const { handleSubmit, register } = useForm();
|
||||
const stamp = Math.floor(Date.now() / 1000);
|
||||
const id = uuid();
|
||||
const onSubmit = values => {
|
||||
const apiEndpoint = process.env.REACT_APP_API + '/insert';
|
||||
const vals = JSON.stringify(values);
|
||||
const v = `{"collection_id": "${id}", "tenant_id":"112718d1-a0be-4468-b902-0749c3d964ae", "event": "covid", "timestamp": ${stamp}, "data": ${vals} }`;
|
||||
fetch(apiEndpoint, {
|
||||
method: 'post',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${props.jwt}`
|
||||
},
|
||||
body: v
|
||||
}).then(response => {
|
||||
return response.json();
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<form class="w-full max-w-sm mx-20" onSubmit={handleSubmit(onSubmit)}>
|
||||
<img src={Logo} alt="logo" class="mb-10 mt-5" />
|
||||
<div class="md:flex md:items-center mb-6">
|
||||
<div class="md:w-2/3">
|
||||
<input
|
||||
ref={register}
|
||||
name="Username"
|
||||
placeholder="Username"
|
||||
class="bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:flex md:items-center mb-6">
|
||||
<div class="md:w-2/3">
|
||||
<input
|
||||
ref={register}
|
||||
name="message"
|
||||
placeholder="Message"
|
||||
class="bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:flex md:items-center mb-6">
|
||||
<div class="md:w-2/3">
|
||||
<input
|
||||
ref={register}
|
||||
name="location"
|
||||
placeholder="Location"
|
||||
class="bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:flex md:items-center">
|
||||
<div class="md:w-1/3"></div>
|
||||
<div class="md:w-2/3">
|
||||
<button class="shadow bg-teal-500 hover:bg-teal-400 focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded" type="submit">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function Load(props) {
|
||||
const { handleSubmit, register, errors } = useForm();
|
||||
const sseURL = `${process.env.REACT_APP_API}/events/${process.env.REACT_APP_TENANT}`;
|
||||
const insertURL = `${process.env.REACT_APP_API}/insert`;
|
||||
return (
|
||||
<Router>
|
||||
<div>
|
||||
<Switch>
|
||||
<Route path="/">
|
||||
<Insert jwt={props.jwt} />
|
||||
{ props.jwt.length == 0 &&
|
||||
<div class="spinner mt-20">
|
||||
</div>
|
||||
}
|
||||
{props.jwt.length > 0 &&
|
||||
<div class="mt-20 mx-20">
|
||||
<Grid sseEndpoint={sseURL} tenantID={process.env.REACT_APP_TENANT} insertEndpoint={insertURL} eventListen={'covid'} title={'Fight Covid'} token={props.jwt} />
|
||||
</div>
|
||||
}
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [data, setData] = useState({jwt: ''});
|
||||
useEffect(() => {
|
||||
const apiEndpoint = process.env.REACT_APP_API + '/login';
|
||||
const v = `{"username": "${process.env.REACT_APP_USERNAME}" ,"password": "${process.env.REACT_APP_PASSWORD}" }`;
|
||||
let jwt = fetch(apiEndpoint, {
|
||||
method: 'post',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: v
|
||||
}).then(response => {
|
||||
return response.json();
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
jwt.then(res => {
|
||||
setData(res);
|
||||
})
|
||||
}, []);
|
||||
|
||||
if (data === undefined) {
|
||||
return (
|
||||
<div>Loading...</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Load jwt={data.jwt} />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
const { getByText } = render(<App />);
|
||||
const linkElement = getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
@tailwind base;
|
||||
|
||||
@tailwind components;
|
||||
|
||||
@tailwind utilities;
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
serviceWorker.unregister();
|
|
@ -1,165 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 564.29999 116.99999"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="apibillme.svg"
|
||||
width="564.29999"
|
||||
height="116.99999"
|
||||
inkscape:version="0.92.2 5c3e80d, 2017-08-06"><metadata
|
||||
id="metadata4179"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs4177" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="640"
|
||||
inkscape:window-height="480"
|
||||
id="namedview4175"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:zoom="0.42228739"
|
||||
inkscape:cx="267.3"
|
||||
inkscape:cy="69.6"
|
||||
inkscape:window-x="672"
|
||||
inkscape:window-y="167"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="Layer_1" /><style
|
||||
type="text/css"
|
||||
id="style4130">
|
||||
.st0{fill:#8C8C8C;}
|
||||
.st1{fill:#3DD8D8;}
|
||||
.st2{fill:#EF3D06;}
|
||||
.st3{fill:#F2C300;}
|
||||
.st4{fill:#0AAEAB;}
|
||||
.st5{fill:#255C65;}
|
||||
.st6{fill:#B4CF37;}
|
||||
</style><g
|
||||
id="g4172"
|
||||
transform="translate(-73.699997,-182.10001)"><g
|
||||
id="g4152"><path
|
||||
class="st0"
|
||||
d="m 262.8,219.7 h 11.6 V 266 h -11.6 v -4.9 c -2.3,2.2 -4.5,3.7 -6.8,4.7 -2.3,1 -4.8,1.4 -7.4,1.4 -6,0 -11.2,-2.3 -15.5,-7 -4.3,-4.7 -6.6,-10.4 -6.6,-17.3 0,-7.2 2.1,-13 6.3,-17.6 4.2,-4.6 9.4,-6.9 15.4,-6.9 2.8,0 5.4,0.5 7.8,1.6 2.4,1.1 4.7,2.6 6.8,4.7 z m -12.3,9.5 c -3.6,0 -6.6,1.3 -9,3.8 -2.4,2.5 -3.6,5.8 -3.6,9.8 0,4 1.2,7.3 3.6,9.9 2.4,2.6 5.4,3.9 8.9,3.9 3.7,0 6.7,-1.3 9.1,-3.8 2.4,-2.5 3.6,-5.9 3.6,-10 0,-4 -1.2,-7.3 -3.6,-9.8 -2.2,-2.5 -5.3,-3.8 -9,-3.8 z"
|
||||
id="path4132"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8c8c8c" /><path
|
||||
class="st0"
|
||||
d="m 297.7,219.7 v 5.1 c 2.1,-2.1 4.4,-3.7 6.8,-4.7 2.4,-1.1 5.1,-1.6 7.8,-1.6 6.1,0 11.2,2.3 15.4,6.9 4.2,4.6 6.4,10.4 6.4,17.6 0,6.9 -2.2,12.7 -6.6,17.3 -4.4,4.6 -9.6,7 -15.6,7 -2.7,0 -5.2,-0.5 -7.5,-1.4 -2.3,-1 -4.6,-2.5 -6.9,-4.7 V 283 H 286 v -63.3 z m 12.3,9.5 c -3.7,0 -6.7,1.3 -9.1,3.8 -2.4,2.5 -3.6,5.8 -3.6,9.8 0,4.1 1.2,7.5 3.6,10 2.4,2.5 5.5,3.8 9.1,3.8 3.6,0 6.5,-1.3 9,-3.9 2.4,-2.6 3.6,-5.9 3.6,-9.9 0,-4 -1.2,-7.2 -3.6,-9.8 -2.4,-2.6 -5.4,-3.8 -9,-3.8 z"
|
||||
id="path4134"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8c8c8c" /><path
|
||||
class="st0"
|
||||
d="m 348.3,200.6 c 2,0 3.8,0.7 5.2,2.2 1.5,1.5 2.2,3.3 2.2,5.4 0,2.1 -0.7,3.8 -2.2,5.3 -1.4,1.5 -3.2,2.2 -5.2,2.2 -2.1,0 -3.8,-0.7 -5.3,-2.2 -1.5,-1.5 -2.2,-3.3 -2.2,-5.4 0,-2 0.7,-3.8 2.2,-5.2 1.6,-1.6 3.3,-2.3 5.3,-2.3 z m -5.8,19.1 h 11.6 V 266 h -11.6 z"
|
||||
id="path4136"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8c8c8c" /><path
|
||||
class="st0"
|
||||
d="m 376.3,201.8 v 23 c 2.1,-2.1 4.4,-3.7 6.8,-4.7 2.4,-1.1 5.1,-1.6 7.8,-1.6 6.1,0 11.2,2.3 15.4,6.9 4.2,4.6 6.4,10.4 6.4,17.6 0,6.9 -2.2,12.7 -6.6,17.3 -4.4,4.6 -9.6,7 -15.6,7 -2.7,0 -5.2,-0.5 -7.5,-1.4 -2.3,-1 -4.6,-2.5 -6.9,-4.7 v 4.9 h -11.5 v -64.3 z m 12.2,27.4 c -3.7,0 -6.7,1.3 -9.1,3.8 -2.4,2.5 -3.6,5.8 -3.6,9.8 0,4.1 1.2,7.5 3.6,10 2.4,2.5 5.5,3.8 9.1,3.8 3.6,0 6.5,-1.3 9,-3.9 2.4,-2.6 3.6,-5.9 3.6,-9.9 0,-4 -1.2,-7.2 -3.6,-9.8 -2.4,-2.6 -5.4,-3.8 -9,-3.8 z"
|
||||
id="path4138"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8c8c8c" /><path
|
||||
class="st0"
|
||||
d="m 426.9,200.6 c 2,0 3.8,0.7 5.2,2.2 1.5,1.5 2.2,3.3 2.2,5.4 0,2.1 -0.7,3.8 -2.2,5.3 -1.4,1.5 -3.2,2.2 -5.2,2.2 -2.1,0 -3.8,-0.7 -5.3,-2.2 -1.5,-1.5 -2.2,-3.3 -2.2,-5.4 0,-2 0.7,-3.8 2.2,-5.2 1.5,-1.6 3.2,-2.3 5.3,-2.3 z m -5.8,19.1 h 11.6 V 266 h -11.6 z"
|
||||
id="path4140"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8c8c8c" /><path
|
||||
class="st0"
|
||||
d="m 442,201.8 h 11.6 V 266 H 442 Z"
|
||||
id="path4142"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8c8c8c" /><path
|
||||
class="st0"
|
||||
d="m 462.9,201.8 h 11.6 V 266 h -11.6 z"
|
||||
id="path4144"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8c8c8c" /><path
|
||||
class="st0"
|
||||
d="m 491.5,253.9 c 1.8,0 3.4,0.6 4.7,1.9 1.3,1.3 2,2.9 2,4.7 0,1.8 -0.7,3.4 -2,4.7 -1.3,1.3 -2.9,2 -4.7,2 -1.8,0 -3.4,-0.7 -4.7,-2 -1.3,-1.3 -2,-2.9 -2,-4.7 0,-1.8 0.7,-3.4 2,-4.7 1.2,-1.3 2.8,-1.9 4.7,-1.9 z"
|
||||
id="path4146"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8c8c8c" /><path
|
||||
class="st0"
|
||||
d="m 509.5,219.7 h 11.6 v 5.3 c 2,-2.2 4.2,-3.8 6.6,-4.9 2.4,-1.1 5.1,-1.6 8,-1.6 2.9,0 5.5,0.7 7.8,2.1 2.3,1.4 4.2,3.5 5.6,6.2 1.8,-2.7 4.1,-4.8 6.8,-6.2 2.7,-1.4 5.6,-2.1 8.8,-2.1 3.3,0 6.2,0.8 8.7,2.3 2.5,1.5 4.3,3.5 5.4,6 1.1,2.5 1.6,6.5 1.6,12.1 V 266 h -11.7 v -23.5 c 0,-5.3 -0.7,-8.8 -2,-10.7 -1.3,-1.9 -3.3,-2.8 -5.9,-2.8 -2,0 -3.8,0.6 -5.3,1.7 -1.6,1.1 -2.7,2.7 -3.5,4.7 -0.8,2 -1.2,5.2 -1.2,9.7 V 266 h -11.7 v -22.5 c 0,-4.1 -0.3,-7.2 -0.9,-9 -0.6,-1.9 -1.5,-3.2 -2.8,-4.2 -1.2,-0.9 -2.7,-1.4 -4.4,-1.4 -1.9,0 -3.7,0.6 -5.3,1.7 -1.6,1.2 -2.8,2.8 -3.5,4.8 -0.8,2.1 -1.2,5.3 -1.2,9.8 V 266 h -11.6 v -46.3 z"
|
||||
id="path4148"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8c8c8c" /><path
|
||||
class="st0"
|
||||
d="m 638,246.2 h -37.4 c 0.5,3.3 2,5.9 4.3,7.9 2.3,1.9 5.3,2.9 9,2.9 4.3,0 8.1,-1.5 11.2,-4.6 l 9.8,4.6 c -2.4,3.5 -5.4,6 -8.8,7.7 -3.4,1.7 -7.5,2.5 -12.1,2.5 -7.3,0 -13.2,-2.3 -17.8,-6.9 -4.6,-4.6 -6.9,-10.3 -6.9,-17.2 0,-7.1 2.3,-12.9 6.8,-17.6 4.6,-4.7 10.3,-7 17.2,-7 7.3,0 13.2,2.3 17.8,7 4.6,4.7 6.9,10.8 6.9,18.5 z m -11.6,-9.1 c -0.8,-2.6 -2.3,-4.7 -4.5,-6.3 -2.3,-1.6 -4.9,-2.4 -7.9,-2.4 -3.2,0 -6.1,0.9 -8.5,2.7 -1.5,1.1 -3,3.1 -4.3,6 z"
|
||||
id="path4150"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#8c8c8c" /></g><g
|
||||
id="g4170"><polygon
|
||||
class="st1"
|
||||
points="139.8,204.4 139.8,226.5 131.6,226.5 131.6,242.1 137.7,242.1 137.7,254.9 131.6,254.9 131.6,261.2 149,261.2 149,283.3 131.6,283.3 131.6,299.1 220.2,299.1 220.2,191.7 131.6,191.7 131.6,204.4 "
|
||||
id="polygon4154"
|
||||
style="fill:#3dd8d8" /><rect
|
||||
x="105.1"
|
||||
y="243.89999"
|
||||
class="st2"
|
||||
width="17.4"
|
||||
height="17.4"
|
||||
id="rect4156"
|
||||
style="fill:#ef3d06" /><rect
|
||||
x="124.7"
|
||||
y="266"
|
||||
class="st3"
|
||||
width="17.4"
|
||||
height="17.4"
|
||||
id="rect4158"
|
||||
style="fill:#f2c300" /><rect
|
||||
x="118.9"
|
||||
y="204.39999"
|
||||
class="st3"
|
||||
width="17.4"
|
||||
height="17.4"
|
||||
id="rect4160"
|
||||
style="fill:#f2c300" /><rect
|
||||
x="95.699997"
|
||||
y="204.39999"
|
||||
class="st4"
|
||||
width="8.6999998"
|
||||
height="8.6999998"
|
||||
id="rect4162"
|
||||
style="fill:#0aaeab" /><rect
|
||||
x="73.699997"
|
||||
y="200.10001"
|
||||
class="st5"
|
||||
width="8.6999998"
|
||||
height="8.6999998"
|
||||
id="rect4164"
|
||||
style="fill:#255c65" /><rect
|
||||
x="87.099998"
|
||||
y="182.10001"
|
||||
class="st2"
|
||||
width="6.0999999"
|
||||
height="6.0999999"
|
||||
id="rect4166"
|
||||
style="fill:#ef3d06" /><rect
|
||||
x="85.400002"
|
||||
y="223.2"
|
||||
class="st6"
|
||||
width="11.9"
|
||||
height="11.9"
|
||||
id="rect4168"
|
||||
style="fill:#b4cf37" /></g></g></svg>
|
Before Width: | Height: | Size: 8.0 KiB |
|
@ -1,137 +0,0 @@
|
|||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export function register(config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' }
|
||||
})
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom/extend-expect';
|
|
@ -1,24 +0,0 @@
|
|||
.spinner {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.spinner::after {
|
||||
content: '';
|
||||
position: absolute !important;
|
||||
top: calc(50% - (1em / 2));
|
||||
left: calc(50% - (1em / 2));
|
||||
display: block;
|
||||
width: 7em;
|
||||
height: 7em;
|
||||
border: 2px solid currentColor;
|
||||
border-radius: 9999px;
|
||||
border-right-color: transparent;
|
||||
border-top-color: transparent;
|
||||
animation: spinAround 500ms infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spinAround {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
86854
example/src/tailwind.css
86854
example/src/tailwind.css
File diff suppressed because it is too large
Load Diff
|
@ -1,502 +0,0 @@
|
|||
module.exports = {
|
||||
prefix: '',
|
||||
important: false,
|
||||
separator: ':',
|
||||
theme: {
|
||||
spinner: (theme) => ({
|
||||
default: {
|
||||
color: '#dae1e7', // color you want to make the spinner
|
||||
size: '1em', // size of the spinner (used for both width and height)
|
||||
border: '2px', // border-width of the spinner (shouldn't be bigger than half the spinner's size)
|
||||
speed: '500ms', // the speed at which the spinner should rotate
|
||||
},
|
||||
},
|
||||
screens: {
|
||||
sm: '640px',
|
||||
md: '768px',
|
||||
lg: '1024px',
|
||||
xl: '1280px',
|
||||
},
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
|
||||
black: '#000',
|
||||
white: '#fff',
|
||||
|
||||
gray: {
|
||||
100: '#f7fafc',
|
||||
200: '#edf2f7',
|
||||
300: '#e2e8f0',
|
||||
400: '#cbd5e0',
|
||||
500: '#a0aec0',
|
||||
600: '#718096',
|
||||
700: '#4a5568',
|
||||
800: '#2d3748',
|
||||
900: '#1a202c',
|
||||
},
|
||||
red: {
|
||||
100: '#fff5f5',
|
||||
200: '#fed7d7',
|
||||
300: '#feb2b2',
|
||||
400: '#fc8181',
|
||||
500: '#f56565',
|
||||
600: '#e53e3e',
|
||||
700: '#c53030',
|
||||
800: '#9b2c2c',
|
||||
900: '#742a2a',
|
||||
},
|
||||
orange: {
|
||||
100: '#fffaf0',
|
||||
200: '#feebc8',
|
||||
300: '#fbd38d',
|
||||
400: '#f6ad55',
|
||||
500: '#ed8936',
|
||||
600: '#dd6b20',
|
||||
700: '#c05621',
|
||||
800: '#9c4221',
|
||||
900: '#7b341e',
|
||||
},
|
||||
yellow: {
|
||||
100: '#fffff0',
|
||||
200: '#fefcbf',
|
||||
300: '#faf089',
|
||||
400: '#f6e05e',
|
||||
500: '#ecc94b',
|
||||
600: '#d69e2e',
|
||||
700: '#b7791f',
|
||||
800: '#975a16',
|
||||
900: '#744210',
|
||||
},
|
||||
green: {
|
||||
100: '#f0fff4',
|
||||
200: '#c6f6d5',
|
||||
300: '#9ae6b4',
|
||||
400: '#68d391',
|
||||
500: '#48bb78',
|
||||
600: '#38a169',
|
||||
700: '#2f855a',
|
||||
800: '#276749',
|
||||
900: '#22543d',
|
||||
},
|
||||
teal: {
|
||||
100: '#e6fffa',
|
||||
200: '#b2f5ea',
|
||||
300: '#81e6d9',
|
||||
400: '#4fd1c5',
|
||||
500: '#38b2ac',
|
||||
600: '#319795',
|
||||
700: '#2c7a7b',
|
||||
800: '#285e61',
|
||||
900: '#234e52',
|
||||
},
|
||||
blue: {
|
||||
100: '#ebf8ff',
|
||||
200: '#bee3f8',
|
||||
300: '#90cdf4',
|
||||
400: '#63b3ed',
|
||||
500: '#4299e1',
|
||||
600: '#3182ce',
|
||||
700: '#2b6cb0',
|
||||
800: '#2c5282',
|
||||
900: '#2a4365',
|
||||
},
|
||||
indigo: {
|
||||
100: '#ebf4ff',
|
||||
200: '#c3dafe',
|
||||
300: '#a3bffa',
|
||||
400: '#7f9cf5',
|
||||
500: '#667eea',
|
||||
600: '#5a67d8',
|
||||
700: '#4c51bf',
|
||||
800: '#434190',
|
||||
900: '#3c366b',
|
||||
},
|
||||
purple: {
|
||||
100: '#faf5ff',
|
||||
200: '#e9d8fd',
|
||||
300: '#d6bcfa',
|
||||
400: '#b794f4',
|
||||
500: '#9f7aea',
|
||||
600: '#805ad5',
|
||||
700: '#6b46c1',
|
||||
800: '#553c9a',
|
||||
900: '#44337a',
|
||||
},
|
||||
pink: {
|
||||
100: '#fff5f7',
|
||||
200: '#fed7e2',
|
||||
300: '#fbb6ce',
|
||||
400: '#f687b3',
|
||||
500: '#ed64a6',
|
||||
600: '#d53f8c',
|
||||
700: '#b83280',
|
||||
800: '#97266d',
|
||||
900: '#702459',
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
px: '1px',
|
||||
'0': '0',
|
||||
'1': '0.25rem',
|
||||
'2': '0.5rem',
|
||||
'3': '0.75rem',
|
||||
'4': '1rem',
|
||||
'5': '1.25rem',
|
||||
'6': '1.5rem',
|
||||
'8': '2rem',
|
||||
'10': '2.5rem',
|
||||
'12': '3rem',
|
||||
'16': '4rem',
|
||||
'20': '5rem',
|
||||
'24': '6rem',
|
||||
'32': '8rem',
|
||||
'40': '10rem',
|
||||
'48': '12rem',
|
||||
'56': '14rem',
|
||||
'64': '16rem',
|
||||
},
|
||||
backgroundColor: theme => theme('colors'),
|
||||
backgroundPosition: {
|
||||
bottom: 'bottom',
|
||||
center: 'center',
|
||||
left: 'left',
|
||||
'left-bottom': 'left bottom',
|
||||
'left-top': 'left top',
|
||||
right: 'right',
|
||||
'right-bottom': 'right bottom',
|
||||
'right-top': 'right top',
|
||||
top: 'top',
|
||||
},
|
||||
backgroundSize: {
|
||||
auto: 'auto',
|
||||
cover: 'cover',
|
||||
contain: 'contain',
|
||||
},
|
||||
borderColor: theme => ({
|
||||
...theme('colors'),
|
||||
default: theme('colors.gray.300', 'currentColor'),
|
||||
}),
|
||||
borderRadius: {
|
||||
none: '0',
|
||||
sm: '0.125rem',
|
||||
default: '0.25rem',
|
||||
lg: '0.5rem',
|
||||
full: '9999px',
|
||||
},
|
||||
borderWidth: {
|
||||
default: '1px',
|
||||
'0': '0',
|
||||
'2': '2px',
|
||||
'4': '4px',
|
||||
'8': '8px',
|
||||
},
|
||||
boxShadow: {
|
||||
default: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
|
||||
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
||||
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
|
||||
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
|
||||
'2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
||||
inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
|
||||
outline: '0 0 0 3px rgba(66, 153, 225, 0.5)',
|
||||
none: 'none',
|
||||
},
|
||||
container: {},
|
||||
cursor: {
|
||||
auto: 'auto',
|
||||
default: 'default',
|
||||
pointer: 'pointer',
|
||||
wait: 'wait',
|
||||
text: 'text',
|
||||
move: 'move',
|
||||
'not-allowed': 'not-allowed',
|
||||
},
|
||||
fill: {
|
||||
current: 'currentColor',
|
||||
},
|
||||
flex: {
|
||||
'1': '1 1 0%',
|
||||
auto: '1 1 auto',
|
||||
initial: '0 1 auto',
|
||||
none: 'none',
|
||||
},
|
||||
flexGrow: {
|
||||
'0': '0',
|
||||
default: '1',
|
||||
},
|
||||
flexShrink: {
|
||||
'0': '0',
|
||||
default: '1',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"Segoe UI"',
|
||||
'Roboto',
|
||||
'"Helvetica Neue"',
|
||||
'Arial',
|
||||
'"Noto Sans"',
|
||||
'sans-serif',
|
||||
'"Apple Color Emoji"',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
'"Noto Color Emoji"',
|
||||
],
|
||||
serif: [
|
||||
'Georgia',
|
||||
'Cambria',
|
||||
'"Times New Roman"',
|
||||
'Times',
|
||||
'serif',
|
||||
],
|
||||
mono: [
|
||||
'Menlo',
|
||||
'Monaco',
|
||||
'Consolas',
|
||||
'"Liberation Mono"',
|
||||
'"Courier New"',
|
||||
'monospace',
|
||||
],
|
||||
},
|
||||
fontSize: {
|
||||
xs: '0.75rem',
|
||||
sm: '0.875rem',
|
||||
base: '1rem',
|
||||
lg: '1.125rem',
|
||||
xl: '1.25rem',
|
||||
'2xl': '1.5rem',
|
||||
'3xl': '1.875rem',
|
||||
'4xl': '2.25rem',
|
||||
'5xl': '3rem',
|
||||
'6xl': '4rem',
|
||||
},
|
||||
fontWeight: {
|
||||
hairline: '100',
|
||||
thin: '200',
|
||||
light: '300',
|
||||
normal: '400',
|
||||
medium: '500',
|
||||
semibold: '600',
|
||||
bold: '700',
|
||||
extrabold: '800',
|
||||
black: '900',
|
||||
},
|
||||
height: theme => ({
|
||||
auto: 'auto',
|
||||
...theme('spacing'),
|
||||
full: '100%',
|
||||
screen: '100vh',
|
||||
}),
|
||||
inset: {
|
||||
'0': '0',
|
||||
auto: 'auto',
|
||||
},
|
||||
letterSpacing: {
|
||||
tighter: '-0.05em',
|
||||
tight: '-0.025em',
|
||||
normal: '0',
|
||||
wide: '0.025em',
|
||||
wider: '0.05em',
|
||||
widest: '0.1em',
|
||||
},
|
||||
lineHeight: {
|
||||
none: '1',
|
||||
tight: '1.25',
|
||||
snug: '1.375',
|
||||
normal: '1.5',
|
||||
relaxed: '1.625',
|
||||
loose: '2',
|
||||
},
|
||||
listStyleType: {
|
||||
none: 'none',
|
||||
disc: 'disc',
|
||||
decimal: 'decimal',
|
||||
},
|
||||
margin: (theme, { negative }) => ({
|
||||
auto: 'auto',
|
||||
...theme('spacing'),
|
||||
...negative(theme('spacing')),
|
||||
}),
|
||||
maxHeight: {
|
||||
full: '100%',
|
||||
screen: '100vh',
|
||||
},
|
||||
maxWidth: {
|
||||
xs: '20rem',
|
||||
sm: '24rem',
|
||||
md: '28rem',
|
||||
lg: '32rem',
|
||||
xl: '36rem',
|
||||
'2xl': '42rem',
|
||||
'3xl': '48rem',
|
||||
'4xl': '56rem',
|
||||
'5xl': '64rem',
|
||||
'6xl': '72rem',
|
||||
full: '100%',
|
||||
},
|
||||
minHeight: {
|
||||
'0': '0',
|
||||
full: '100%',
|
||||
screen: '100vh',
|
||||
},
|
||||
minWidth: {
|
||||
'0': '0',
|
||||
full: '100%',
|
||||
},
|
||||
objectPosition: {
|
||||
bottom: 'bottom',
|
||||
center: 'center',
|
||||
left: 'left',
|
||||
'left-bottom': 'left bottom',
|
||||
'left-top': 'left top',
|
||||
right: 'right',
|
||||
'right-bottom': 'right bottom',
|
||||
'right-top': 'right top',
|
||||
top: 'top',
|
||||
},
|
||||
opacity: {
|
||||
'0': '0',
|
||||
'25': '0.25',
|
||||
'50': '0.5',
|
||||
'75': '0.75',
|
||||
'100': '1',
|
||||
},
|
||||
order: {
|
||||
first: '-9999',
|
||||
last: '9999',
|
||||
none: '0',
|
||||
'1': '1',
|
||||
'2': '2',
|
||||
'3': '3',
|
||||
'4': '4',
|
||||
'5': '5',
|
||||
'6': '6',
|
||||
'7': '7',
|
||||
'8': '8',
|
||||
'9': '9',
|
||||
'10': '10',
|
||||
'11': '11',
|
||||
'12': '12',
|
||||
},
|
||||
padding: theme => theme('spacing'),
|
||||
placeholderColor: theme => theme('colors'),
|
||||
stroke: {
|
||||
current: 'currentColor',
|
||||
},
|
||||
textColor: theme => theme('colors'),
|
||||
width: theme => ({
|
||||
auto: 'auto',
|
||||
...theme('spacing'),
|
||||
'1/2': '50%',
|
||||
'1/3': '33.333333%',
|
||||
'2/3': '66.666667%',
|
||||
'1/4': '25%',
|
||||
'2/4': '50%',
|
||||
'3/4': '75%',
|
||||
'1/5': '20%',
|
||||
'2/5': '40%',
|
||||
'3/5': '60%',
|
||||
'4/5': '80%',
|
||||
'1/6': '16.666667%',
|
||||
'2/6': '33.333333%',
|
||||
'3/6': '50%',
|
||||
'4/6': '66.666667%',
|
||||
'5/6': '83.333333%',
|
||||
'1/12': '8.333333%',
|
||||
'2/12': '16.666667%',
|
||||
'3/12': '25%',
|
||||
'4/12': '33.333333%',
|
||||
'5/12': '41.666667%',
|
||||
'6/12': '50%',
|
||||
'7/12': '58.333333%',
|
||||
'8/12': '66.666667%',
|
||||
'9/12': '75%',
|
||||
'10/12': '83.333333%',
|
||||
'11/12': '91.666667%',
|
||||
full: '100%',
|
||||
screen: '100vw',
|
||||
}),
|
||||
zIndex: {
|
||||
auto: 'auto',
|
||||
'0': '0',
|
||||
'10': '10',
|
||||
'20': '20',
|
||||
'30': '30',
|
||||
'40': '40',
|
||||
'50': '50',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
spinner: ['responsive'],
|
||||
accessibility: ['responsive', 'focus'],
|
||||
alignContent: ['responsive'],
|
||||
alignItems: ['responsive'],
|
||||
alignSelf: ['responsive'],
|
||||
appearance: ['responsive'],
|
||||
backgroundAttachment: ['responsive'],
|
||||
backgroundColor: ['responsive', 'hover', 'focus'],
|
||||
backgroundPosition: ['responsive'],
|
||||
backgroundRepeat: ['responsive'],
|
||||
backgroundSize: ['responsive'],
|
||||
borderCollapse: ['responsive'],
|
||||
borderColor: ['responsive', 'hover', 'focus'],
|
||||
borderRadius: ['responsive'],
|
||||
borderStyle: ['responsive'],
|
||||
borderWidth: ['responsive'],
|
||||
boxShadow: ['responsive', 'hover', 'focus'],
|
||||
cursor: ['responsive'],
|
||||
display: ['responsive'],
|
||||
fill: ['responsive'],
|
||||
flex: ['responsive'],
|
||||
flexDirection: ['responsive'],
|
||||
flexGrow: ['responsive'],
|
||||
flexShrink: ['responsive'],
|
||||
flexWrap: ['responsive'],
|
||||
float: ['responsive'],
|
||||
fontFamily: ['responsive'],
|
||||
fontSize: ['responsive'],
|
||||
fontSmoothing: ['responsive'],
|
||||
fontStyle: ['responsive'],
|
||||
fontWeight: ['responsive', 'hover', 'focus'],
|
||||
height: ['responsive'],
|
||||
inset: ['responsive'],
|
||||
justifyContent: ['responsive'],
|
||||
letterSpacing: ['responsive'],
|
||||
lineHeight: ['responsive'],
|
||||
listStylePosition: ['responsive'],
|
||||
listStyleType: ['responsive'],
|
||||
margin: ['responsive'],
|
||||
maxHeight: ['responsive'],
|
||||
maxWidth: ['responsive'],
|
||||
minHeight: ['responsive'],
|
||||
minWidth: ['responsive'],
|
||||
objectFit: ['responsive'],
|
||||
objectPosition: ['responsive'],
|
||||
opacity: ['responsive', 'hover', 'focus'],
|
||||
order: ['responsive'],
|
||||
outline: ['responsive', 'focus'],
|
||||
overflow: ['responsive'],
|
||||
padding: ['responsive'],
|
||||
placeholderColor: ['responsive', 'focus'],
|
||||
pointerEvents: ['responsive'],
|
||||
position: ['responsive'],
|
||||
resize: ['responsive'],
|
||||
stroke: ['responsive'],
|
||||
tableLayout: ['responsive'],
|
||||
textAlign: ['responsive'],
|
||||
textColor: ['responsive', 'hover', 'focus'],
|
||||
textDecoration: ['responsive', 'hover', 'focus'],
|
||||
textTransform: ['responsive'],
|
||||
userSelect: ['responsive'],
|
||||
verticalAlign: ['responsive'],
|
||||
visibility: ['responsive'],
|
||||
whitespace: ['responsive'],
|
||||
width: ['responsive'],
|
||||
wordBreak: ['responsive'],
|
||||
zIndex: ['responsive'],
|
||||
},
|
||||
corePlugins: {},
|
||||
plugins: [
|
||||
require('tailwindcss-spinner')()
|
||||
],
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
const tailwindcss = require('tailwindcss');
|
||||
module.exports = {
|
||||
plugins: [
|
||||
tailwindcss('./tailwind.js'),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
};
|
832
src/lib.rs
832
src/lib.rs
|
@ -1,832 +0,0 @@
|
|||
use tokio::stream::StreamExt;
|
||||
use tokio::time::interval;
|
||||
use std::iter::Iterator;
|
||||
use std::collections::{HashSet, HashMap, VecDeque};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
use bcrypt::{DEFAULT_COST, hash, verify};
|
||||
use warp::{Filter, http::StatusCode, sse::ServerSentEvent};
|
||||
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
|
||||
use std::convert::Infallible;
|
||||
use std::time::Duration;
|
||||
use lazy_static::lazy_static;
|
||||
use bus::Bus;
|
||||
use crossbeam::channel::unbounded;
|
||||
use inflector::Inflector;
|
||||
use json_patch::merge;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use base64::{decode as base64_decode};
|
||||
|
||||
// init database as lazy
|
||||
lazy_static! {
|
||||
static ref TREE: HashMap<String, sled::Db> = {
|
||||
let configure = config();
|
||||
let tree = sled::open(configure.save_path).unwrap();
|
||||
let mut m = HashMap::new();
|
||||
m.insert("tree".to_owned(), tree);
|
||||
m
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Token {
|
||||
pub jwt: String
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Record {
|
||||
pub event: Event,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UserCollection {
|
||||
pub info: Vec<Event>,
|
||||
pub events: Vec<Event>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Collection {
|
||||
pub events: Vec<Event>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SSE {
|
||||
pub event: String,
|
||||
pub data: String,
|
||||
pub id: String,
|
||||
pub retry: Duration,
|
||||
pub tenant_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub port: u16,
|
||||
pub expiry: i64,
|
||||
pub origin: String,
|
||||
pub secret: String,
|
||||
pub save_path: String,
|
||||
pub connection: String,
|
||||
pub cert_path: String,
|
||||
pub key_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct JWT {
|
||||
check: bool,
|
||||
claims: Claims,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct User {
|
||||
id: uuid::Uuid,
|
||||
username: String,
|
||||
password: String,
|
||||
collection_id: uuid::Uuid,
|
||||
tenant_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UserForm {
|
||||
username: String,
|
||||
password: String,
|
||||
collection_id: uuid::Uuid,
|
||||
tenant_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Login {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Claims {
|
||||
sub: String,
|
||||
company: String,
|
||||
exp: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Cfg {
|
||||
pub save_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Event {
|
||||
pub id: uuid::Uuid,
|
||||
pub user_id: uuid::Uuid,
|
||||
pub collection_id: uuid::Uuid,
|
||||
pub tenant_id: uuid::Uuid,
|
||||
pub event: String,
|
||||
pub timestamp: i64,
|
||||
pub published: bool,
|
||||
pub cancelled: bool,
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct EventForm {
|
||||
collection_id: uuid::Uuid,
|
||||
tenant_id: uuid::Uuid,
|
||||
event: String,
|
||||
timestamp: i64,
|
||||
data: serde_json::Value,
|
||||
}
|
||||
|
||||
// helper function to create sse events
|
||||
fn get_events(tenant_id: uuid::Uuid) -> Vec<SSE> {
|
||||
let tree = TREE.get(&"tree".to_owned()).unwrap();
|
||||
let mut vals : Vec<Event> = tree.iter().into_iter().filter(|x| {
|
||||
let p = x.as_ref().unwrap();
|
||||
let k = std::str::from_utf8(&p.0).unwrap().to_owned();
|
||||
if k.contains("_v_") {
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
let evt : Event = serde_json::from_str(&v).unwrap();
|
||||
if !evt.cancelled && evt.tenant_id == tenant_id {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}).map(|x| {
|
||||
let p = x.as_ref().unwrap();
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
let evt : Event = serde_json::from_str(&v).unwrap();
|
||||
evt
|
||||
}).collect();
|
||||
|
||||
vals.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
|
||||
|
||||
let mut uniques : HashSet<String> = HashSet::new();
|
||||
for evt in &vals {
|
||||
uniques.insert(evt.clone().event);
|
||||
}
|
||||
|
||||
let mut sse_events : Vec<SSE> = Vec::new();
|
||||
|
||||
for evt in uniques {
|
||||
let mut events : HashMap<String, Event> = HashMap::new();
|
||||
for event in &vals {
|
||||
if evt == event.event {
|
||||
events.insert(event.clone().collection_id.to_string(), event.clone());
|
||||
}
|
||||
}
|
||||
let mut evts : Vec<Event> = Vec::new();
|
||||
let mut uniq_data_keys : HashSet<String> = HashSet::new();
|
||||
let mut rows : Vec<serde_json::Value> = Vec::new();
|
||||
for (_, v) in events {
|
||||
if v.clone().data.is_object() {
|
||||
evts.push(v.clone());
|
||||
let mut data = v.clone().data;
|
||||
let j = json!({"timestamp": v.clone().timestamp.to_string()});
|
||||
merge(&mut data, &j);
|
||||
let j = json!({"collection_id": v.clone().collection_id});
|
||||
merge(&mut data, &j);
|
||||
rows.push(data);
|
||||
for (k, _) in v.clone().data.as_object().unwrap() {
|
||||
uniq_data_keys.insert(k.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rows.sort_by(|a, b| a.get("timestamp").unwrap().to_string().cmp(&b.get("timestamp").unwrap().to_string()));
|
||||
rows.reverse();
|
||||
|
||||
let mut columns : VecDeque<serde_json::Value> = VecDeque::new();
|
||||
for uniq_key in uniq_data_keys {
|
||||
if uniq_key != "collection_id" && uniq_key != "timestamp" {
|
||||
columns.push_back(json!({"title": Inflector::to_sentence_case(&uniq_key), "field": uniq_key}));
|
||||
}
|
||||
}
|
||||
|
||||
let mut cols : Vec<&serde_json::Value> = columns.iter().collect();
|
||||
cols.sort_by(|a, b| a.to_string().cmp(&b.to_string()));
|
||||
let mut colz : VecDeque<serde_json::Value> = VecDeque::new();
|
||||
for col in cols {
|
||||
colz.push_back(col.clone());
|
||||
}
|
||||
colz.push_front(json!({"title": "collection_id", "field": "collection_id"}));
|
||||
colz.push_front(json!({"title": "Timestamp", "field": "timestamp"}));
|
||||
|
||||
let guid = Uuid::new_v4().to_string();
|
||||
let events_json = json!({"events": evts, "columns": colz, "rows": rows});
|
||||
sse_events.push(SSE{id: guid, event: evt, data: serde_json::to_string(&events_json).unwrap(), retry: Duration::from_millis(5000), tenant_id: tenant_id});
|
||||
}
|
||||
sse_events
|
||||
}
|
||||
|
||||
// get ntp time from global servers (cloudflare primary and fallback pool)
|
||||
pub fn get_ntp_time() -> i64 {
|
||||
let pool_ntp = "pool.ntp.org:123";
|
||||
let cf_ntp = "time.cloudflare.com:123";
|
||||
let response = match broker_ntp::request(cf_ntp) {
|
||||
Ok(res) => res,
|
||||
Err(_) => broker_ntp::request(pool_ntp).unwrap()
|
||||
};
|
||||
let timestamp = response.transmit_timestamp;
|
||||
broker_ntp::unix_time::Instant::from(timestamp).secs()
|
||||
}
|
||||
|
||||
// cancel future event
|
||||
fn cancel(tree: sled::Db, event_id: String, user_id: String) -> String {
|
||||
|
||||
let versioned = format!("_u_{}", user_id);
|
||||
let g = tree.get(&versioned.as_bytes()).unwrap().unwrap();
|
||||
let v = std::str::from_utf8(&g).unwrap().to_owned();
|
||||
let user : User = serde_json::from_str(&v).unwrap();
|
||||
|
||||
let versioned = format!("_v_{}", event_id);
|
||||
let g = tree.get(&versioned.as_bytes()).unwrap().unwrap();
|
||||
let v = std::str::from_utf8(&g).unwrap().to_owned();
|
||||
let mut json : Event = serde_json::from_str(&v).unwrap();
|
||||
let j = json.clone();
|
||||
if json.tenant_id == user.tenant_id {
|
||||
json.cancelled = true;
|
||||
let _ = tree.compare_and_swap(versioned.as_bytes(), Some(serde_json::to_string(&j).unwrap().as_bytes()), Some(serde_json::to_string(&json).unwrap().as_bytes()));
|
||||
let _ = tree.flush();
|
||||
}
|
||||
json!({"event": json}).to_string()
|
||||
}
|
||||
|
||||
// display user collection of events
|
||||
fn user_collection(tree: sled::Db, id: String) -> String {
|
||||
|
||||
let versioned = format!("_u_{}", id);
|
||||
let g = tree.get(&versioned.as_bytes()).unwrap().unwrap();
|
||||
let v = std::str::from_utf8(&g).unwrap().to_owned();
|
||||
let user : User = serde_json::from_str(&v).unwrap();
|
||||
|
||||
// turn iVec(s) to String(s) and make HashMap
|
||||
let mut info: Vec<Event> = tree.iter().into_iter().filter(|x| {
|
||||
let p = x.as_ref().unwrap();
|
||||
let k = std::str::from_utf8(&p.0).unwrap().to_owned();
|
||||
if k.contains(&"_v_") {
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
let j : Event = serde_json::from_str(&v).unwrap();
|
||||
if j.collection_id.to_string() == user.collection_id.to_string() {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}).map(|x| {
|
||||
let p = x.unwrap();
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
let j : Event = serde_json::from_str(&v).unwrap();
|
||||
j
|
||||
}).collect();
|
||||
|
||||
info.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
|
||||
|
||||
// turn iVec(s) to String(s) and make HashMap
|
||||
let mut owned: Vec<Event> = tree.iter().into_iter().filter(|x| {
|
||||
let p = x.as_ref().unwrap();
|
||||
let k = std::str::from_utf8(&p.0).unwrap().to_owned();
|
||||
if k.contains(&"_v_") {
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
let j : Event = serde_json::from_str(&v).unwrap();
|
||||
if j.user_id.to_string() == user.id.to_string() {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}).map(|x| {
|
||||
let p = x.unwrap();
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
let j : Event = serde_json::from_str(&v).unwrap();
|
||||
j
|
||||
}).collect();
|
||||
|
||||
owned.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
|
||||
|
||||
let c = UserCollection{info: info, events: owned};
|
||||
let data : String = serde_json::to_string(&c).unwrap();
|
||||
data
|
||||
}
|
||||
|
||||
// display collection of events based on collection_id
|
||||
fn collection(tree: sled::Db, collection_id: String, user_id: String) -> String {
|
||||
|
||||
let versioned = format!("_u_{}", user_id);
|
||||
let g = tree.get(&versioned.as_bytes()).unwrap().unwrap();
|
||||
let v = std::str::from_utf8(&g).unwrap().to_owned();
|
||||
let user : User = serde_json::from_str(&v).unwrap();
|
||||
|
||||
let mut records: Vec<Event> = tree.iter().into_iter().filter(|x| {
|
||||
let p = x.as_ref().unwrap();
|
||||
let k = std::str::from_utf8(&p.0).unwrap().to_owned();
|
||||
if k.contains(&"_v_") {
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
let j : Event = serde_json::from_str(&v).unwrap();
|
||||
if j.collection_id.to_string() == collection_id && j.tenant_id == user.tenant_id {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}).map(|x| {
|
||||
let p = x.unwrap();
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
let j : Event = serde_json::from_str(&v).unwrap();
|
||||
j
|
||||
}).collect();
|
||||
|
||||
records.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
|
||||
|
||||
let c = Collection{events: records};
|
||||
let data : String = serde_json::to_string(&c).unwrap();
|
||||
data
|
||||
}
|
||||
|
||||
// create a user
|
||||
fn user_create(tree: sled::Db, user_form: UserForm) -> (bool, String) {
|
||||
|
||||
let records : HashMap<String, String> = tree.iter().into_iter().filter(|x| {
|
||||
let p = x.as_ref().unwrap();
|
||||
let k = std::str::from_utf8(&p.0).unwrap().to_owned();
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
if k.contains("_u_") {
|
||||
let user : User = serde_json::from_str(&v).unwrap();
|
||||
if user.username == user_form.username {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}).map(|x| {
|
||||
let p = x.unwrap();
|
||||
let k = std::str::from_utf8(&p.0).unwrap().to_owned();
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
(k, v)
|
||||
}).collect();
|
||||
|
||||
if records.len() > 0 {
|
||||
let j = json!({"error": "username already taken"}).to_string();
|
||||
return (false, j)
|
||||
} else {
|
||||
// set as future value
|
||||
let uuid = Uuid::new_v4();
|
||||
let versioned = format!("_u_{}", uuid.to_string());
|
||||
let hashed = hash(user_form.clone().password, DEFAULT_COST).unwrap();
|
||||
let new_user = User{id: uuid, username: user_form.clone().username, password: hashed, collection_id: user_form.clone().collection_id, tenant_id: user_form.clone().tenant_id };
|
||||
|
||||
let _ = tree.compare_and_swap(versioned.as_bytes(), None as Option<&[u8]>, Some(serde_json::to_string(&new_user).unwrap().as_bytes()));
|
||||
let _ = tree.flush();
|
||||
let j = json!({"id": uuid.to_string()}).to_string();
|
||||
return (true, j)
|
||||
}
|
||||
}
|
||||
|
||||
// login with user creds
|
||||
fn login(tree: sled::Db, login: Login, config: Config) -> (bool, String) {
|
||||
|
||||
let now = get_ntp_time();
|
||||
let expi = now + config.expiry;
|
||||
let expiry = expi as usize;
|
||||
|
||||
let records : HashMap<String, String> = tree.iter().into_iter().filter(|x| {
|
||||
let p = x.as_ref().unwrap();
|
||||
let k = std::str::from_utf8(&p.0).unwrap().to_owned();
|
||||
if k.contains(&"_u_") {
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
let user : User = serde_json::from_str(&v).unwrap();
|
||||
if user.username == login.username {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}).map(|x| {
|
||||
let p = x.unwrap();
|
||||
let k = std::str::from_utf8(&p.0).unwrap().to_owned();
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
(k, v)
|
||||
}).collect();
|
||||
|
||||
for (_k, v) in records {
|
||||
let user : User = serde_json::from_str(&v).unwrap();
|
||||
let verified = verify(login.password, &user.password).unwrap();
|
||||
if verified {
|
||||
let my_claims = Claims{company: "".to_owned(), sub: user.id.to_string(), exp: expiry};
|
||||
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(config.secret.as_ref())).unwrap();
|
||||
let j = json!({"jwt": token}).to_string();
|
||||
return (true, j)
|
||||
} else {
|
||||
return (false, "".to_owned())
|
||||
}
|
||||
}
|
||||
(false, "".to_owned())
|
||||
}
|
||||
|
||||
// config based on sane local dev defaults (uses double dashes for flags)
|
||||
fn config() -> Config {
|
||||
|
||||
let mut port : u16 = 8080;
|
||||
let mut expiry : i64 = 3600;
|
||||
let mut connection = "http".to_owned();
|
||||
let mut origin = "http://localhost:3000".to_owned();
|
||||
let mut secret = "secret".to_owned();
|
||||
let mut key_path = "./broker.rsa".to_owned();
|
||||
let mut cert_path = "./broker.pem".to_owned();
|
||||
let _ : Vec<String> = go_flag::parse(|flags| {
|
||||
flags.add_flag("port", &mut port);
|
||||
flags.add_flag("origin", &mut origin);
|
||||
flags.add_flag("expiry", &mut expiry);
|
||||
flags.add_flag("secret", &mut secret);
|
||||
flags.add_flag("connection", &mut connection);
|
||||
flags.add_flag("key-path", &mut key_path);
|
||||
flags.add_flag("cert-path", &mut cert_path);
|
||||
});
|
||||
|
||||
let save_path = match envy::from_env::<Cfg>() {
|
||||
Ok(cfg) => cfg.save_path,
|
||||
Err(_) => "./tmp/broker_data".to_owned()
|
||||
};
|
||||
|
||||
Config{port: port, secret: secret, origin: origin, save_path: save_path, expiry: expiry, connection: connection, key_path: key_path, cert_path: cert_path}
|
||||
}
|
||||
|
||||
// verify the exp and key of the JWT or the HTTP Basic Username/Password
|
||||
fn jwt_verify(config: Config, token: String) -> JWT {
|
||||
|
||||
let mut parts = token.split(" ");
|
||||
let auth_type = parts.next().unwrap();
|
||||
if auth_type == "Bearer" {
|
||||
let token = parts.next().unwrap();
|
||||
let _ = match decode::<Claims>(&token, &DecodingKey::from_secret(config.secret.as_ref()), &Validation::default()) {
|
||||
Ok(c) => {
|
||||
return JWT{check: true, claims: c.claims};
|
||||
},
|
||||
Err(_) => {
|
||||
return JWT{check: false, claims: Claims{company: "".to_owned(), exp: 0, sub: "".to_owned()}};
|
||||
}
|
||||
};
|
||||
} else if auth_type == "Basic" {
|
||||
let token = parts.next().unwrap();
|
||||
let _ = match &base64_decode(token) {
|
||||
Ok(c) => {
|
||||
let _ = match std::str::from_utf8(&c) {
|
||||
Ok(d) => {
|
||||
let mut username_password = d.split(":");
|
||||
let username = username_password.next().unwrap();
|
||||
let password = username_password.next().unwrap();
|
||||
let tree = TREE.get(&"tree".to_owned()).unwrap();
|
||||
|
||||
let records : HashMap<String, String> = tree.iter().into_iter().filter(|x| {
|
||||
let p = x.as_ref().unwrap();
|
||||
let k = std::str::from_utf8(&p.0).unwrap().to_owned();
|
||||
if k.contains(&"_u_") {
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
let user : User = serde_json::from_str(&v).unwrap();
|
||||
if user.username == username {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}).map(|x| {
|
||||
let p = x.unwrap();
|
||||
let k = std::str::from_utf8(&p.0).unwrap().to_owned();
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
(k, v)
|
||||
}).collect();
|
||||
|
||||
for (_k, v) in records {
|
||||
let user : User = serde_json::from_str(&v).unwrap();
|
||||
let verified = verify(password, &user.password).unwrap();
|
||||
if verified {
|
||||
return JWT{check: true, claims: Claims{company: "".to_owned(), exp: 0, sub: user.id.to_string()}};
|
||||
} else {
|
||||
return JWT{check: false, claims: Claims{company: "".to_owned(), exp: 0, sub: "".to_owned()}};
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
return JWT{check: false, claims: Claims{company: "".to_owned(), exp: 0, sub: "".to_owned()}};
|
||||
}
|
||||
};
|
||||
},
|
||||
Err(_) => {
|
||||
return JWT{check: false, claims: Claims{company: "".to_owned(), exp: 0, sub: "".to_owned()}};
|
||||
}
|
||||
};
|
||||
}
|
||||
JWT{check: false, claims: Claims{company: "".to_owned(), exp: 0, sub: "".to_owned()}}
|
||||
}
|
||||
|
||||
// insert an event
|
||||
fn insert(tree: sled::Db, user_id: String, evt: EventForm) -> String {
|
||||
|
||||
// get user
|
||||
let versioned = format!("_u_{}", user_id);
|
||||
let g = tree.get(&versioned.as_bytes()).unwrap().unwrap();
|
||||
let v = std::str::from_utf8(&g).unwrap().to_owned();
|
||||
let user : User = serde_json::from_str(&v).unwrap();
|
||||
|
||||
// build event object
|
||||
let id = Uuid::new_v4();
|
||||
let j = Event{id: id, published: false, cancelled: false, data: evt.data, event: evt.event, timestamp: evt.timestamp, user_id: user.id, collection_id: evt.collection_id, tenant_id: evt.tenant_id};
|
||||
let new_value_string = serde_json::to_string(&j).unwrap();
|
||||
let new_value = new_value_string.as_bytes();
|
||||
let versioned = format!("_v_{}", id.to_string());
|
||||
|
||||
// only write if form tenant_id and user tenant_id
|
||||
if user.tenant_id == evt.tenant_id {
|
||||
let _ = tree.compare_and_swap(versioned, None as Option<&[u8]>, Some(new_value.clone()));
|
||||
let _ = tree.flush();
|
||||
return json!({"event": j}).to_string()
|
||||
}
|
||||
|
||||
json!({"error": "trying to write to wrong tenant"}).to_string()
|
||||
}
|
||||
|
||||
// create a sse event
|
||||
fn event_stream(rx: crossbeam::channel::Receiver<SSE>, allowed: bool) -> Result<impl ServerSentEvent, Infallible> {
|
||||
|
||||
if allowed {
|
||||
let sse = match rx.try_recv() {
|
||||
Ok(sse) => sse,
|
||||
Err(_) => {
|
||||
let id = Uuid::new_v4();
|
||||
let guid = id.to_string();
|
||||
let polling = json!({"status": "polling"});
|
||||
SSE{id: guid, event: "internal_status".to_owned(), data: polling.to_string(), retry: Duration::from_millis(5000), tenant_id: id}
|
||||
}
|
||||
};
|
||||
Ok((
|
||||
warp::sse::id(sse.id),
|
||||
warp::sse::data(sse.data),
|
||||
warp::sse::event(sse.event),
|
||||
warp::sse::retry(sse.retry),
|
||||
))
|
||||
} else {
|
||||
let id = Uuid::new_v4();
|
||||
let guid = id.to_string();
|
||||
let denied = json!({"error": "denied"});
|
||||
let sse = SSE{id: guid, event: "internal_status".to_owned(), data: denied.to_string(), retry: Duration::from_millis(5000), tenant_id: id};
|
||||
Ok((
|
||||
warp::sse::id(sse.id),
|
||||
warp::sse::data(sse.data),
|
||||
warp::sse::event(sse.event),
|
||||
warp::sse::retry(sse.retry),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// main function
|
||||
pub async fn broker() {
|
||||
|
||||
// start logging
|
||||
pretty_env_logger::init();
|
||||
|
||||
// user create route
|
||||
let user_create_route = warp::post()
|
||||
.and(warp::path("users"))
|
||||
.and(warp::body::json())
|
||||
.map(move |user: UserForm| {
|
||||
let tree = TREE.get(&"tree".to_owned()).unwrap();
|
||||
let (check, value) = user_create(tree.clone(), user.clone());
|
||||
if check {
|
||||
let reply = warp::reply::with_status(value, StatusCode::OK);
|
||||
warp::reply::with_header(reply, "Content-Type", "application/json")
|
||||
} else {
|
||||
let reply = warp::reply::with_status(value, StatusCode::BAD_REQUEST);
|
||||
warp::reply::with_header(reply, "Content-Type", "application/json")
|
||||
}
|
||||
});
|
||||
|
||||
// auth check middleware
|
||||
let auth_check = warp::header::<String>("authorization").map(|token| {
|
||||
let configure = config();
|
||||
jwt_verify(configure, token)
|
||||
});
|
||||
|
||||
// login route
|
||||
let login_route = warp::post()
|
||||
.and(warp::path("login"))
|
||||
.and(warp::body::json())
|
||||
.map(move |login_form: Login| {
|
||||
let configure = config();
|
||||
let tree = TREE.get(&"tree".to_owned()).unwrap();
|
||||
let (check, value) = login(tree.clone(), login_form.clone(), configure.clone());
|
||||
if check {
|
||||
let reply = warp::reply::with_status(value, StatusCode::OK);
|
||||
warp::reply::with_header(reply, "Content-Type", "application/json")
|
||||
} else {
|
||||
let reply = warp::reply::with_status(value, StatusCode::UNAUTHORIZED);
|
||||
warp::reply::with_header(reply, "Content-Type", "application/json")
|
||||
}
|
||||
});
|
||||
|
||||
// insert route
|
||||
let insert_route = warp::post()
|
||||
.and(warp::path("insert"))
|
||||
.and(auth_check)
|
||||
.and(warp::body::json())
|
||||
.map(move |jwt: JWT, event_form: EventForm| {
|
||||
if jwt.check {
|
||||
let tree = TREE.get(&"tree".to_owned()).unwrap();
|
||||
let record = insert(tree.clone(), jwt.claims.sub, event_form);
|
||||
let reply = warp::reply::with_status(record, StatusCode::OK);
|
||||
warp::reply::with_header(reply, "Content-Type", "application/json")
|
||||
} else {
|
||||
let reply = warp::reply::with_status("".to_owned(), StatusCode::UNAUTHORIZED);
|
||||
warp::reply::with_header(reply, "Content-Type", "application/json")
|
||||
}
|
||||
});
|
||||
|
||||
// create thread-safe broadcast bus
|
||||
let mix_tx = Bus::new(100);
|
||||
let tx = Arc::new(Mutex::new(mix_tx));
|
||||
let tx2 = tx.clone();
|
||||
|
||||
// create tokio worker thread that will dispatch events to bus
|
||||
let _ = tokio::spawn(async move {
|
||||
loop {
|
||||
// get events that have not been published or cancelled
|
||||
let tree = TREE.get(&"tree".to_owned()).unwrap();
|
||||
let vals : HashMap<String, Event> = tree.iter().into_iter().filter(|x| {
|
||||
let p = x.as_ref().unwrap();
|
||||
let k = std::str::from_utf8(&p.0).unwrap().to_owned();
|
||||
if k.contains("_v_") {
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
let evt : Event = serde_json::from_str(&v).unwrap();
|
||||
if !evt.published && !evt.cancelled {
|
||||
let now = get_ntp_time();
|
||||
if evt.timestamp <= now {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}).map(|x| {
|
||||
let p = x.as_ref().unwrap();
|
||||
let k = std::str::from_utf8(&p.0).unwrap().to_owned();
|
||||
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
|
||||
let evt : Event = serde_json::from_str(&v).unwrap();
|
||||
let evt_cloned = evt.clone();
|
||||
(k, evt_cloned)
|
||||
}).collect();
|
||||
|
||||
// publish these filtered events to bus
|
||||
for (k, v) in vals {
|
||||
let old_json = v.clone();
|
||||
let old_json_clone = old_json.clone();
|
||||
let mut new_json = v.clone();
|
||||
new_json.published = true;
|
||||
let newest_json = new_json.clone();
|
||||
let newer_json = newest_json.clone();
|
||||
let tree_cloned = tree.clone();
|
||||
|
||||
let _ = tokio::spawn(async move {
|
||||
let _ = tree_cloned.compare_and_swap(k, Some(serde_json::to_string(&old_json_clone).unwrap().as_bytes()), Some(serde_json::to_string(&newest_json).unwrap().as_bytes()));
|
||||
let _ = tree_cloned.flush();
|
||||
}).await;
|
||||
|
||||
tx2.lock().unwrap().broadcast(newer_json);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// create bus middleware
|
||||
let with_sender = warp::any().map(move || tx.clone());
|
||||
|
||||
// sse route
|
||||
let sse_route = warp::path("events")
|
||||
.and(auth_check)
|
||||
.and(with_sender)
|
||||
.and(warp::path::param::<uuid::Uuid>())
|
||||
.and(warp::get()).map(move |jwt: JWT, tx_main: Arc<Mutex<bus::Bus<Event>>>, tenant_id: uuid::Uuid| {
|
||||
|
||||
// create recv for bus (each sse instance must have its own)
|
||||
let mut rx_main = tx_main.lock().unwrap().add_rx();
|
||||
|
||||
// create local sse channel
|
||||
let (tx, rx) = unbounded();
|
||||
|
||||
// loop through sse events to send on load of sse route
|
||||
for event in get_events(tenant_id) {
|
||||
let _ = tx.send(event);
|
||||
}
|
||||
|
||||
// every 100ms check the bus and if any messages send to local channel also check local channel and publish to stream (sse route)
|
||||
let event_stream = interval(Duration::from_millis(100)).map(move |_| {
|
||||
let evt = match rx_main.try_recv() {
|
||||
Ok(evt) => {
|
||||
if tenant_id == evt.tenant_id {
|
||||
evt
|
||||
} else {
|
||||
let id = Uuid::new_v4();
|
||||
Event{id: id, published: false, cancelled: false, data: json!({"test": "test"}), event: "fake".to_owned(), timestamp: 123, user_id: id, collection_id: id, tenant_id: id}
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
let id = Uuid::new_v4();
|
||||
Event{id: id, published: false, cancelled: false, data: json!({"test": "test"}), event: "fake".to_owned(), timestamp: 123, user_id: id, collection_id: id, tenant_id: id}
|
||||
}
|
||||
};
|
||||
for event in get_events(evt.tenant_id) {
|
||||
let _ = tx.send(event);
|
||||
}
|
||||
event_stream(rx.clone(), jwt.check)
|
||||
});
|
||||
warp::sse::reply(event_stream)
|
||||
});
|
||||
|
||||
// cancel route
|
||||
let cancel_route = warp::get()
|
||||
.and(warp::path("cancel"))
|
||||
.and(auth_check)
|
||||
.and(warp::path::param::<String>())
|
||||
.map(move |jwt: JWT, event_id: String| {
|
||||
if jwt.check {
|
||||
let tree = TREE.get(&"tree".to_owned()).unwrap();
|
||||
let record = cancel(tree.clone(), event_id, jwt.claims.sub);
|
||||
let reply = warp::reply::with_status(record, StatusCode::OK);
|
||||
warp::reply::with_header(reply, "Content-Type", "application/json")
|
||||
} else {
|
||||
let reply = warp::reply::with_status("".to_owned(), StatusCode::UNAUTHORIZED);
|
||||
warp::reply::with_header(reply, "Content-Type", "application/json")
|
||||
}
|
||||
});
|
||||
|
||||
// collections route
|
||||
let collections_route = warp::get()
|
||||
.and(warp::path("collections"))
|
||||
.and(auth_check)
|
||||
.and(warp::path::param::<String>())
|
||||
.map(move |jwt: JWT, collection_id: String| {
|
||||
if jwt.check {
|
||||
let tree = TREE.get(&"tree".to_owned()).unwrap();
|
||||
let record = collection(tree.clone(), collection_id, jwt.claims.sub);
|
||||
let reply = warp::reply::with_status(record, StatusCode::OK);
|
||||
warp::reply::with_header(reply, "Content-Type", "application/json")
|
||||
} else {
|
||||
let reply = warp::reply::with_status("".to_owned(), StatusCode::UNAUTHORIZED);
|
||||
warp::reply::with_header(reply, "Content-Type", "application/json")
|
||||
}
|
||||
});
|
||||
|
||||
// user collection route
|
||||
let user_collection_route = warp::get()
|
||||
.and(warp::path("user_events"))
|
||||
.and(auth_check)
|
||||
.map(move |jwt: JWT| {
|
||||
if jwt.check {
|
||||
let tree = TREE.get(&"tree".to_owned()).unwrap();
|
||||
let record = user_collection(tree.clone(), jwt.claims.sub);
|
||||
let reply = warp::reply::with_status(record, StatusCode::OK);
|
||||
warp::reply::with_header(reply, "Content-Type", "application/json")
|
||||
} else {
|
||||
let reply = warp::reply::with_status("".to_owned(), StatusCode::UNAUTHORIZED);
|
||||
warp::reply::with_header(reply, "Content-Type", "application/json")
|
||||
}
|
||||
});
|
||||
|
||||
// create cors wrapper
|
||||
let configure = config();
|
||||
let mut cors = warp::cors().allow_origin(&*configure.origin).allow_methods(vec!["GET", "POST"]).allow_headers(vec![warp::http::header::AUTHORIZATION, warp::http::header::CONTENT_TYPE]);
|
||||
|
||||
// handle allow any origin case
|
||||
if configure.origin == "*" {
|
||||
cors = warp::cors().allow_any_origin().allow_methods(vec!["GET", "POST"]).allow_headers(vec![warp::http::header::AUTHORIZATION, warp::http::header::CONTENT_TYPE]);
|
||||
}
|
||||
|
||||
// create routes
|
||||
let routes = warp::any().and(login_route).or(user_create_route).or(insert_route).or(sse_route).or(cancel_route).or(collections_route).or(user_collection_route).with(cors);
|
||||
|
||||
// set ip and port
|
||||
let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), configure.port);
|
||||
|
||||
// start server based on https or http
|
||||
if configure.connection == "https" {
|
||||
return warp::serve(routes)
|
||||
.tls()
|
||||
.cert_path(&configure.cert_path)
|
||||
.key_path(&configure.key_path)
|
||||
.run(socket).await
|
||||
} else {
|
||||
return warp::serve(routes).run(socket).await
|
||||
}
|
||||
}
|
393
src/main.rs
393
src/main.rs
|
@ -1,7 +1,390 @@
|
|||
mod lib;
|
||||
use lib::broker;
|
||||
use std::{collections::HashMap, iter::Iterator};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
use argon2::{self, Config as Argon2Config};
|
||||
use anyhow::Result;
|
||||
use jsonwebtoken::{encode, TokenData, decode, Header, Validation, EncodingKey, DecodingKey};
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Arc;
|
||||
use tide::Request;
|
||||
use http_types::headers::HeaderValue;
|
||||
use tide::security::{CorsMiddleware, Origin};
|
||||
use tide_rustls::TlsListener;
|
||||
use async_std::stream;
|
||||
use std::time::Duration;
|
||||
use futures::StreamExt;
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() {
|
||||
broker().await
|
||||
lazy_static! {
|
||||
static ref DB : Arc<rocksdb::DB> = {
|
||||
|
||||
let prefix_extractor = rocksdb::SliceTransform::create_fixed_prefix(3);
|
||||
|
||||
let mut opts = rocksdb::Options::default();
|
||||
opts.create_if_missing(true);
|
||||
opts.set_prefix_extractor(prefix_extractor);
|
||||
|
||||
let configure = env_var_config();
|
||||
let db = rocksdb::DB::open(&opts, configure.db).unwrap();
|
||||
Arc::new(db)
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct EnvVarConfig {
|
||||
pub port: u16,
|
||||
pub jwt_expiry: i64,
|
||||
pub origin: String,
|
||||
pub jwt_secret: String,
|
||||
pub db: String,
|
||||
pub secure: bool,
|
||||
pub cert_path: String,
|
||||
pub key_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct User {
|
||||
id: uuid::Uuid,
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UserForm {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct LoginForm {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
pub exp: i64,
|
||||
pub iat: i64,
|
||||
pub iss: String,
|
||||
pub sub: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct Event {
|
||||
pub id: uuid::Uuid,
|
||||
pub user_id: uuid::Uuid,
|
||||
pub event: String,
|
||||
pub timestamp: i64,
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct EventForm {
|
||||
event: String,
|
||||
data: serde_json::Value,
|
||||
}
|
||||
|
||||
fn replace(key: String, value: Vec<u8>) -> Result<()> {
|
||||
DB.put(key.clone(), value.clone())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_user_by_username(user_username: String) -> Result<Option<User>> {
|
||||
let users = get_users()?;
|
||||
Ok(users.into_iter().filter(|user| user.username == user_username).last())
|
||||
}
|
||||
|
||||
fn get_users() -> Result<Vec<User>> {
|
||||
let prefix = "users".as_bytes();
|
||||
let i = DB.prefix_iterator(prefix);
|
||||
let res : Vec<User> = i.map(|(_, v)| {
|
||||
let data: User = rmp_serde::from_read_ref(&v).unwrap();
|
||||
data
|
||||
}).collect();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn puts_user(user: User) -> Result<()> {
|
||||
let key = format!("users_{}", user.id);
|
||||
let value = rmp_serde::to_vec_named(&user)?;
|
||||
replace(key, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_user_unique(user_username: String) -> Result<bool> {
|
||||
let users = get_users()?;
|
||||
for user in users {
|
||||
if user.username == user_username {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn get_events() -> Result<Vec<Event>> {
|
||||
let prefix = "events".as_bytes();
|
||||
let i = DB.prefix_iterator(prefix);
|
||||
let res : Vec<Event> = i.map(|(_, v)| {
|
||||
let data: Event = rmp_serde::from_read_ref(&v).unwrap();
|
||||
data
|
||||
}).collect();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn puts_event(event: Event) -> Result<()> {
|
||||
let key = format!("events_{}", event.event);
|
||||
let value = rmp_serde::to_vec_named(&event)?;
|
||||
replace(key, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_ntp_time() -> i64 {
|
||||
let pool_ntp = "pool.ntp.org:123";
|
||||
let response = broker_ntp::request(pool_ntp).unwrap();
|
||||
let timestamp = response.transmit_timestamp;
|
||||
broker_ntp::unix_time::Instant::from(timestamp).secs()
|
||||
}
|
||||
|
||||
fn user_create(user_form: UserForm) -> Result<Option<String>> {
|
||||
|
||||
if !is_user_unique(user_form.clone().username)? {
|
||||
let j = json!({"error": "username already taken"}).to_string();
|
||||
return Ok(Some(j));
|
||||
} else {
|
||||
// set as future value
|
||||
let uuid = Uuid::new_v4();
|
||||
let config = Argon2Config::default();
|
||||
let uuid_string = Uuid::new_v4().to_string();
|
||||
let salt = uuid_string.as_bytes();
|
||||
let password = user_form.password.as_bytes();
|
||||
let hashed = argon2::hash_encoded(password, salt, &config).unwrap();
|
||||
let new_user = User{id: uuid, username: user_form.clone().username, password: hashed };
|
||||
|
||||
puts_user(new_user).unwrap();
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
fn create_jwt(login: LoginForm) -> Result<Option<String>> {
|
||||
|
||||
let user_value = get_user_by_username(login.username)?;
|
||||
match user_value {
|
||||
Some(user) => {
|
||||
let verified = argon2::verify_encoded(&user.password, login.password.as_bytes())?;
|
||||
if verified {
|
||||
let app = env_var_config();
|
||||
let iat = get_ntp_time();
|
||||
let exp = iat + app.jwt_expiry;
|
||||
let iss = "Dispatcher".to_string();
|
||||
let my_claims = Claims{sub: user.clone().username, exp, iat, iss};
|
||||
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(app.jwt_secret.as_ref()))?;
|
||||
Ok(Some(token))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
},
|
||||
None => { Ok(None) }
|
||||
}
|
||||
}
|
||||
|
||||
fn env_var_config() -> EnvVarConfig {
|
||||
|
||||
let mut port : u16 = 8080;
|
||||
let mut jwt_expiry : i64 = 86400;
|
||||
let mut secure = false;
|
||||
let mut origin = "*".to_string();
|
||||
let mut jwt_secret = "secret".to_string();
|
||||
let mut key_path = "./broker.rsa".to_string();
|
||||
let mut cert_path = "./broker.pem".to_string();
|
||||
let mut db: String = "tmp".to_string();
|
||||
let _ : Vec<String> = go_flag::parse(|flags| {
|
||||
flags.add_flag("port", &mut port);
|
||||
flags.add_flag("origin", &mut origin);
|
||||
flags.add_flag("jwt_expiry", &mut jwt_expiry);
|
||||
flags.add_flag("jwt_secret", &mut jwt_secret);
|
||||
flags.add_flag("secure", &mut secure);
|
||||
flags.add_flag("key_path", &mut key_path);
|
||||
flags.add_flag("cert_path", &mut cert_path);
|
||||
flags.add_flag("db", &mut db);
|
||||
});
|
||||
|
||||
EnvVarConfig{port, origin, jwt_expiry, jwt_secret, secure, key_path, cert_path, db}
|
||||
}
|
||||
|
||||
fn jwt_verify(token: String) -> Result<Option<TokenData<Claims>>> {
|
||||
|
||||
let configure = env_var_config();
|
||||
|
||||
let mut parts = token.split(" ");
|
||||
let auth_type = parts.next().unwrap();
|
||||
if auth_type == "Bearer" {
|
||||
let token = parts.next().unwrap();
|
||||
let _ = match decode::<Claims>(&token, &DecodingKey::from_secret(configure.jwt_secret.as_ref()), &Validation::default()) {
|
||||
Ok(c) => {
|
||||
return Ok(Some(c));
|
||||
},
|
||||
Err(_) => {
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
// insert an event
|
||||
async fn insert(user_username: String, event_form: EventForm) -> Result<bool> {
|
||||
|
||||
let user_value = get_user_by_username(user_username)?;
|
||||
|
||||
match user_value {
|
||||
Some(user) => {
|
||||
let timestamp = get_ntp_time();
|
||||
let id = uuid::Uuid::new_v4();
|
||||
|
||||
let event = Event{
|
||||
id,
|
||||
data: event_form.data,
|
||||
event: event_form.event,
|
||||
user_id: user.id,
|
||||
timestamp,
|
||||
};
|
||||
|
||||
puts_event(event.clone())?;
|
||||
Ok(true)
|
||||
},
|
||||
None => {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_user(mut req: Request<()>) -> tide::Result {
|
||||
let r = req.body_string().await.unwrap();
|
||||
let user_form : UserForm = serde_json::from_str(&r).unwrap();
|
||||
match user_create(user_form)? {
|
||||
Some(err) => {
|
||||
let err = format!("error: {}", err);
|
||||
Ok(tide::Response::builder(400).body(err).header("content-type", "application/json").build())
|
||||
},
|
||||
None => {
|
||||
Ok(tide::Response::builder(200).body("").header("content-type", "application/json").build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn login_user(mut req: Request<()>) -> tide::Result {
|
||||
let r = req.body_string().await?;
|
||||
let login_form : LoginForm = serde_json::from_str(&r)?;
|
||||
match create_jwt(login_form)? {
|
||||
Some(jwt) => {
|
||||
let msg = format!("jwt: {}", jwt);
|
||||
Ok(tide::Response::builder(200).body(msg).header("content-type", "application/json").build())
|
||||
},
|
||||
None => {
|
||||
Ok(tide::Response::builder(401).header("content-type", "application/json").build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn insert_event(mut req: Request<()>) -> tide::Result {
|
||||
let token_value = req.header("authorization");
|
||||
match token_value {
|
||||
Some(token_header) => {
|
||||
let token = token_header.last().to_string();
|
||||
let jwt_value = jwt_verify(token).unwrap();
|
||||
match jwt_value {
|
||||
Some(jwt) => {
|
||||
let r = req.body_string().await.unwrap();
|
||||
let event_form : EventForm = serde_json::from_str(&r).unwrap();
|
||||
insert(jwt.claims.sub, event_form).await.unwrap();
|
||||
Ok(tide::Response::builder(200).header("content-type", "application/json").build())
|
||||
},
|
||||
None => { Ok(tide::Response::builder(401).header("content-type", "application/json").build()) }
|
||||
}
|
||||
},
|
||||
None => { Ok(tide::Response::builder(401).header("content-type", "application/json").build()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() -> tide::Result<()> {
|
||||
|
||||
let configure = env_var_config();
|
||||
|
||||
let cors = CorsMiddleware::new()
|
||||
.allow_methods("GET, POST, OPTIONS".parse::<HeaderValue>().unwrap())
|
||||
.allow_headers("authorization".parse::<HeaderValue>().unwrap())
|
||||
.allow_origin(Origin::from(configure.origin))
|
||||
.allow_credentials(false);
|
||||
|
||||
let mut app = tide::new();
|
||||
app.with(driftwood::DevLogger);
|
||||
app.with(cors);
|
||||
app.at("/insert").post(insert_event);
|
||||
app.at("/users").post(create_user);
|
||||
app.at("/login").post(login_user);
|
||||
|
||||
app.at("/sse").get(tide::sse::endpoint(|req: Request<()>, sender| async move {
|
||||
|
||||
let token_value = req.header("authorization");
|
||||
match token_value {
|
||||
Some(token_header) => {
|
||||
let token = token_header.last().to_string();
|
||||
let jwt_value = jwt_verify(token).unwrap();
|
||||
match jwt_value {
|
||||
Some(_) => {
|
||||
|
||||
let mut cache: HashMap<String, Event> = HashMap::new();
|
||||
|
||||
let mut interval = stream::interval(Duration::from_millis(100));
|
||||
while let Some(_) = interval.next().await {
|
||||
let events = get_events()?;
|
||||
|
||||
for evt in events {
|
||||
if !cache.contains_key(&evt.event) {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
sender.send(&evt.event, evt.data.to_string(), Some(&id.to_string())).await?;
|
||||
cache.insert(evt.event.clone(), evt.clone());
|
||||
}
|
||||
else {
|
||||
let value_maybe = cache.get_key_value(&evt.event);
|
||||
match value_maybe {
|
||||
Some((_, v)) => {
|
||||
if &evt != v {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
sender.send(&evt.event, evt.data.to_string(), Some(&id.to_string())).await?;
|
||||
cache.insert(evt.event.clone(), evt.clone());
|
||||
}
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
None => { Ok(()) }
|
||||
}
|
||||
},
|
||||
None => { Ok(()) }
|
||||
}
|
||||
}));
|
||||
|
||||
let ip = format!("0.0.0.0:{}", configure.port);
|
||||
|
||||
if configure.secure {
|
||||
app.listen(
|
||||
TlsListener::build()
|
||||
.addrs(ip)
|
||||
.cert(configure.cert_path)
|
||||
.key(configure.key_path)
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
app.listen(ip).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
178
tests/tests.rs
178
tests/tests.rs
|
@ -1,178 +0,0 @@
|
|||
extern crate broker;
|
||||
use serde_json::json;
|
||||
use base64::encode;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test1() {
|
||||
|
||||
let user1 = json!({"username": "rust22", "password": "rust", "collection_id":"3ca76743-8d99-4d3f-b85c-633ea456f90c", "tenant_id": "e69d88c2-135e-4280-9cd8-d4a5edd8642a"});
|
||||
let user2 = json!({"username": "rust23", "password": "rust", "collection_id":"3ca76743-8d99-4d3f-b85c-633ea456f90d", "tenant_id": "e69d88c2-135e-4280-9cd8-d4a5edd8642a"});
|
||||
let user1_login = json!({"username": "rust22", "password": "rust"});
|
||||
let event1 = json!({"event": "test", "tenant_id": "e69d88c2-135e-4280-9cd8-d4a5edd8642a", "collection_id": "3ca76743-8d99-4d3f-b85c-633ea456f90c", "timestamp": 1578667309, "data": "{}"});
|
||||
let now = broker::get_ntp_time();
|
||||
let x = now + 1000;
|
||||
let event2 = json!({"event": "user", "tenant_id": "e69d88c2-135e-4280-9cd8-d4a5edd8642a", "collection_id": "3ca76743-8d99-4d3f-b85c-633ea456f90d", "timestamp": x, "data": "{}"});
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let basic_token = encode("rust22:rust");
|
||||
let basic = format!("Basic {}", basic_token);
|
||||
|
||||
// create user 1 - want success
|
||||
let res = client.post("http://localhost:8080/users")
|
||||
.json(&user1)
|
||||
.send().await.unwrap();
|
||||
assert_eq!(res.status(), 200);
|
||||
|
||||
// create user 2 - want success
|
||||
let res = client.post("http://localhost:8080/users")
|
||||
.json(&user2)
|
||||
.send().await.unwrap()
|
||||
.status();
|
||||
assert_eq!(res, 200);
|
||||
|
||||
// try to create user 2 again - want failure
|
||||
let res = client.post("http://localhost:8080/users")
|
||||
.json(&user1)
|
||||
.send().await.unwrap()
|
||||
.status();
|
||||
assert_eq!(res, 400);
|
||||
|
||||
// login for user 1 - want success
|
||||
let res = client.post("http://localhost:8080/login")
|
||||
.json(&user1_login)
|
||||
.send().await.unwrap()
|
||||
.text().await.unwrap();
|
||||
|
||||
let token: broker::Token = serde_json::from_str(&res).unwrap();
|
||||
let bearer = format!("Bearer {}", token.jwt);
|
||||
|
||||
// try posting event without auth - want failure
|
||||
let res = client.post("http://localhost:8080/insert")
|
||||
.json(&event1)
|
||||
.send().await.unwrap()
|
||||
.status();
|
||||
assert_eq!(res, 400);
|
||||
|
||||
// try posting event with bad auth - want failure
|
||||
let res = client.post("http://localhost:8080/insert")
|
||||
.header("Authorization", "foo")
|
||||
.json(&event1)
|
||||
.send().await.unwrap()
|
||||
.status();
|
||||
assert_eq!(res, 401);
|
||||
|
||||
// try posting event with bad auth - want failure
|
||||
let res = client.post("http://localhost:8080/insert")
|
||||
.header("Authorization", "Bearer 1234")
|
||||
.json(&event1)
|
||||
.send().await.unwrap()
|
||||
.status();
|
||||
assert_eq!(res, 401);
|
||||
|
||||
// post event with JWT - want success
|
||||
let res = client.post("http://localhost:8080/insert")
|
||||
.header("Authorization", &bearer)
|
||||
.json(&event1)
|
||||
.send().await.unwrap();
|
||||
assert_eq!(res.status(), 200);
|
||||
let event : broker::Record = serde_json::from_str(&res.text().await.unwrap()).unwrap();
|
||||
assert_eq!(event.event.published, false);
|
||||
|
||||
// post event with JWT - want success
|
||||
let res = client.post("http://localhost:8080/insert")
|
||||
.header("Authorization", &bearer)
|
||||
.json(&event2)
|
||||
.send().await.unwrap();
|
||||
assert_eq!(res.status(), 200);
|
||||
let event2 : broker::Record = serde_json::from_str(&res.text().await.unwrap()).unwrap();
|
||||
assert_eq!(event2.event.published, false);
|
||||
|
||||
// post event with HTTP Basic - want success
|
||||
let res = client.post("http://localhost:8080/insert")
|
||||
.header("Authorization", &basic)
|
||||
.json(&event1)
|
||||
.send().await.unwrap();
|
||||
assert_eq!(res.status(), 200);
|
||||
let event : broker::Record = serde_json::from_str(&res.text().await.unwrap()).unwrap();
|
||||
assert_eq!(event.event.published, false);
|
||||
|
||||
// try getting collection without auth - want failure
|
||||
let res = client.get("http://localhost:8080/collections/123")
|
||||
.send().await.unwrap()
|
||||
.status();
|
||||
assert_eq!(res, 400);
|
||||
|
||||
// pause for a second to process job
|
||||
let half_second = std::time::Duration::from_millis(500);
|
||||
std::thread::sleep(half_second);
|
||||
|
||||
// get collection with JWT - want success
|
||||
let res = client.get("http://localhost:8080/collections/3ca76743-8d99-4d3f-b85c-633ea456f90c")
|
||||
.header("Authorization", &bearer)
|
||||
.send().await.unwrap();
|
||||
assert_eq!(res.status(), 200);
|
||||
let events : broker::Collection = serde_json::from_str(&res.text().await.unwrap()).unwrap();
|
||||
assert_eq!(events.events[0].published, true);
|
||||
|
||||
// get collection with HTTP Basic - want success
|
||||
let res = client.get("http://localhost:8080/collections/3ca76743-8d99-4d3f-b85c-633ea456f90c")
|
||||
.header("Authorization", &basic)
|
||||
.send().await.unwrap();
|
||||
assert_eq!(res.status(), 200);
|
||||
let events : broker::Collection = serde_json::from_str(&res.text().await.unwrap()).unwrap();
|
||||
assert_eq!(events.events[0].published, true);
|
||||
|
||||
// try getting user without auth - want failure
|
||||
let res = client.get("http://localhost:8080/user_events")
|
||||
.send().await.unwrap()
|
||||
.status();
|
||||
assert_eq!(res, 400);
|
||||
|
||||
// get user collection - want success
|
||||
let res = client.get("http://localhost:8080/user_events")
|
||||
.header("Authorization", &bearer)
|
||||
.send().await.unwrap()
|
||||
.status();
|
||||
assert_eq!(res, 200);
|
||||
|
||||
// try cancelling without auth - want failure
|
||||
let res = client.get("http://localhost:8080/cancel/123")
|
||||
.send().await.unwrap()
|
||||
.status();
|
||||
assert_eq!(res, 400);
|
||||
|
||||
// cancel with JWT - want success
|
||||
let url = format!("http://localhost:8080/cancel/{}", event2.event.id);
|
||||
let res = client.get(&url)
|
||||
.header("Authorization", &bearer)
|
||||
.send().await.unwrap();
|
||||
assert_eq!(res.status(), 200);
|
||||
let event : broker::Record = serde_json::from_str(&res.text().await.unwrap()).unwrap();
|
||||
assert_eq!(event.event.cancelled, true);
|
||||
|
||||
// cancel with HTTP Basic - want success
|
||||
let url = format!("http://localhost:8080/cancel/{}", event2.event.id);
|
||||
let res = client.get(&url)
|
||||
.header("Authorization", &basic)
|
||||
.send().await.unwrap();
|
||||
assert_eq!(res.status(), 200);
|
||||
let event : broker::Record = serde_json::from_str(&res.text().await.unwrap()).unwrap();
|
||||
assert_eq!(event.event.cancelled, true);
|
||||
|
||||
// get collection with JWT - want success
|
||||
let res = client.get("http://localhost:8080/collections/3ca76743-8d99-4d3f-b85c-633ea456f90d")
|
||||
.header("Authorization", &bearer)
|
||||
.send().await.unwrap();
|
||||
assert_eq!(res.status(), 200);
|
||||
let events : broker::Collection = serde_json::from_str(&res.text().await.unwrap()).unwrap();
|
||||
assert_eq!(events.events[0].published, false);
|
||||
|
||||
// get collection with HTTP Basic - want success
|
||||
let res = client.get("http://localhost:8080/collections/3ca76743-8d99-4d3f-b85c-633ea456f90d")
|
||||
.header("Authorization", &basic)
|
||||
.send().await.unwrap();
|
||||
assert_eq!(res.status(), 200);
|
||||
let events : broker::Collection = serde_json::from_str(&res.text().await.unwrap()).unwrap();
|
||||
assert_eq!(events.events[0].published, false);
|
||||
}
|
Loading…
Reference in New Issue