rewrite to 6.0.0

This commit is contained in:
Bevan Hunt 2021-03-20 02:25:29 -07:00
parent d0485b4c7d
commit 1d4459eed2
32 changed files with 1992 additions and 106452 deletions

3003
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,37 +1,31 @@
[package]
name = "broker"
version = "5.0.0"
version = "6.0.0"
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"

View File

@ -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

View File

@ -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

View File

@ -1 +0,0 @@
web: ./target/release/sse-server

101
README.md
View File

@ -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
@ -95,10 +88,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,7 +99,7 @@ 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":{...}}
@ -120,71 +112,25 @@ will return
```
- where {...} is the event
#### 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 +144,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

View File

@ -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-----

View File

@ -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-----

View File

@ -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

View File

@ -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

23
example/.gitignore vendored
View File

@ -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*

View File

@ -1 +0,0 @@
## Example App

15829
example/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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>

View File

@ -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"
}

View File

@ -1,2 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

View File

@ -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} />
);
}
}

View File

@ -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();
});

View File

@ -1,6 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -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();

View File

@ -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

View File

@ -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();
});
}
}

View File

@ -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';

View File

@ -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); }
}

File diff suppressed because it is too large Load Diff

View File

@ -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')()
],
}

View File

@ -1,7 +0,0 @@
const tailwindcss = require('tailwindcss');
module.exports = {
plugins: [
tailwindcss('./tailwind.js'),
require('autoprefixer'),
],
};

View File

@ -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
}
}

View File

@ -1,7 +1,395 @@
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;
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(())
}
// 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()
}
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(())
}
#[tokio::main]
pub async fn main() {
broker().await
}

View File

@ -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);
}