├── .dockerignore
├── .env
├── .gitignore
├── LICENSE
├── README.md
├── backend
├── Dockerfile
├── go.mod
├── go.sum
└── go
│ ├── cmd
│ └── main.go
│ ├── pkg
│ ├── db
│ │ └── badger.go
│ └── util
│ │ ├── log.go
│ │ └── util.go
│ └── services
│ ├── action_handler.go
│ ├── event_listener.go
│ ├── shutdown.go
│ ├── web_routes.go
│ └── web_server.go
├── docker-compose.yml
├── frontend.env
└── frontend
├── Dockerfile
├── README.md
├── nginx.conf
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.js
├── assets
│ ├── avt.svg
│ ├── check.svg
│ ├── left.svg
│ ├── logoD.svg
│ ├── logoW.svg
│ ├── overview.svg
│ ├── overviewA.svg
│ ├── payment.svg
│ ├── paymentA.svg
│ ├── recentActivity
│ │ ├── active.svg
│ │ ├── activeD.svg
│ │ ├── noneActive.svg
│ │ └── noneActiveD.svg
│ ├── release.svg
│ ├── releaseA.svg
│ ├── right.svg
│ ├── setting.svg
│ ├── settingA.svg
│ ├── states
│ │ ├── i1.svg
│ │ ├── i2.svg
│ │ ├── i3.svg
│ │ └── i4.svg
│ ├── statesd
│ │ ├── i1.svg
│ │ ├── i2.svg
│ │ ├── i3.svg
│ │ ├── i4.svg
│ │ └── i5.svg
│ ├── upload.svg
│ ├── uploadD.svg
│ ├── user.svg
│ ├── userA.svg
│ └── wave.svg
├── components
│ ├── Action.jsx
│ ├── ActionTable.jsx
│ ├── AuditTable.jsx
│ ├── DatabaseSetup.jsx
│ ├── SettingsDisplay.jsx
│ ├── Sidebar.jsx
│ └── Welcome.jsx
├── index.js
├── layout
│ └── index.jsx
├── pages
│ ├── ConnectDatabase.jsx
│ ├── LayoutAction.jsx
│ ├── LayoutActionTable.jsx
│ ├── LayoutAuditTable.jsx
│ ├── LayoutSettings.jsx
│ ├── LayoutWelcome.jsx
│ └── NotFound.jsx
├── styles
│ ├── style.css
│ ├── style.css.map
│ └── style.scss
└── util
│ ├── HealthCheck.js
│ └── api.js
└── yarn.lock
/.dockerignore:
--------------------------------------------------------------------------------
1 | .dockerignore
2 | .git
3 | .idea
4 | **/build
5 | **/node_modules
6 | **/*.md
7 | Dockerfile.*
8 | docker-compose*.yaml
9 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # If provided, the on-disk database (BadgerDB) will use AES encryption. If used, must be set at first startup.
2 | # The type of AES is used based on the key size. For example 16 bytes will use AES-128. 24 bytes will use AES-192. 32 bytes will use AES-256.
3 | DISK_DB_ENCRYPTION_KEY=badgerkey16bytes
4 | # Used by the front end to gently authenticate requests. This entire application should be within your VPC and should NOT be externally accessible.
5 | # Leaving this empty will allow unauthenticated requests to the API server.
6 | # NOTE: The frontend does not support injecting the ENV variables with docker, do not use this for now...
7 | #HTTP_API_SERVER_AUTH_TOKEN=db-webhooks
8 | # CSV of origin(s) a cross-domain request can be executed from the internal API server. * will allow all.
9 | HTTP_API_SERVER_CORS_ALLOW_ORIGINS=*
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.exe
2 | *.iml
3 | *.swp
4 | *.swo
5 | *.dll
6 | *.so
7 | *.dylib
8 | *.test
9 | *.out
10 | *.log
11 |
12 | npm-debug.log*
13 | yarn-debug.log*
14 | yarn-error.log*
15 |
16 | node_modules/
17 | vendor/
18 | build/
19 |
20 | .DS_Store
21 | .idea
22 | .vscode
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | WWWWWW||WWWWWW
4 | W W W||W W W
5 | ||
6 | ( OO )__________
7 | / | \
8 | /o o| MIT \
9 | \___/||_||__||_|| *
10 | || || || ||
11 | _||_|| _||_||
12 | (__|__|(__|__|
13 |
14 | Copyright (c) 2023 Portola Labs, Inc.
15 |
16 | Permission is hereby granted, free of charge, to any person obtaining a copy
17 | of this software and associated documentation files (the "Software"), to deal
18 | in the Software without restriction, including without limitation the rights
19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20 | copies of the Software, and to permit persons to whom the Software is
21 | furnished to do so, subject to the following conditions:
22 |
23 | The above copyright notice and this permission notice shall be included in all
24 | copies or substantial portions of the Software.
25 |
26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32 | SOFTWARE.
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
🪝 DB Webhooks
3 | Real-time events for Postgres
4 |
5 |
6 |
7 | DB Webhooks is a utility for Postgres that triggers webhooks when rows are inserted, updated, or deleted. It uses
8 | database triggers that send low-latency websocket messages to a Go application. This application then calls
9 | the configured webhook(s) with a JSON payload that includes specified values from the database row.
10 |
11 | ### How It Works
12 |
13 | 1. Data is modified in a Postgres table (INSERT, UPDATE, DELETE)
14 | 2. A Postgres trigger notifies the DB Webhooks web server via a websocket message
15 | 3. DB Webhooks formats, filters, and sends the data to configured webhook(s)
16 |
17 | 
18 |
19 | ## Get Started
20 |
21 | ### Run DB Webhooks locally
22 |
23 | You can run DB Webhooks locally with Docker.
24 |
25 | ```bash
26 | git clone --depth 1 https://github.com/tableflowhq/db-webhooks.git
27 | cd db-webhooks
28 | docker-compose up -d
29 | ```
30 |
31 | Then open [http://localhost:3000](http://localhost:3000) to access DB Webhooks.
32 |
33 |
34 | **Note**: When connecting your database, if your Postgres host is `localhost`, you must use `host.docker.internal`
35 | instead to access it when running with Docker.
36 |
37 | ### Run DB Webhooks on AWS (EC2)
38 |
39 | **Note**: Make sure this instance is only accessible within your VPC.\
40 | **Note**: Make sure your local machine is able to connect to the server on port 3000 (the web server) and 3003 (the API
41 | server) over HTTP.\
42 | **Note**: These instructions are for Amazon Linux 2 AMI (HVM).
43 |
44 | #### Option 1 (one-line install)
45 |
46 | ```bash
47 | sudo yum update -y && \
48 | sudo yum install -y docker && \
49 | sudo service docker start && \
50 | sudo usermod -a -G docker $USER && \
51 | sudo curl -L "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose && \
52 | sudo mv /usr/local/bin/docker-compose /usr/bin/docker-compose && \
53 | sudo chmod +x /usr/bin/docker-compose && \
54 | mkdir db-webhooks && cd db-webhooks && \
55 | wget https://raw.githubusercontent.com/tableflowhq/db-webhooks/main/{.env,docker-compose.yml,.dockerignore,frontend.env} && \
56 | sg docker -c 'docker-compose up -d'
57 |
58 | ```
59 |
60 | #### Option 2 (guided install)
61 |
62 | 1. To install Docker, run the following command in your SSH session on the instance terminal:
63 |
64 | ```bash
65 | sudo yum update -y
66 | sudo yum install -y docker
67 | sudo service docker start
68 | sudo usermod -a -G docker $USER
69 | logout # Needed to close the SSH session so Docker does not have to be run as root
70 | ```
71 |
72 | 2. To install `docker-compose`, run the following command in your ssh session on the instance terminal:
73 |
74 | ```bash
75 | sudo curl -L "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
76 | sudo mv /usr/local/bin/docker-compose /usr/bin/docker-compose
77 | sudo chmod +x /usr/bin/docker-compose
78 | docker-compose version
79 | ```
80 |
81 | 3. Install and run DB Webhooks
82 |
83 | ```bash
84 | mkdir db-webhooks && cd db-webhooks
85 | wget https://raw.githubusercontent.com/tableflowhq/db-webhooks/main/{.env,docker-compose.yml,.dockerignore,frontend.env}
86 | docker-compose up -d
87 | ```
88 |
89 | ## Features
90 |
91 | ### Template Strings
92 |
93 | Embed variables from database rows in your actions
94 | When adding an action, you can insert data from the row into the response body of the POST request by using template
95 | strings.
96 |
97 | For instance, if your table has a column called `email`, you would put the value `${email}` in the request
98 | body: `{"text":"User created: ${email}!"}`
99 |
100 | The prefixes `new.` and `old.` can be used if a new (INSERT, UPDATE) or old (UPDATE, DELETE) row is available. If a
101 | prefix is not specified, the new or old values will be used depending on the event.
102 | Example: `{"text":"User updated: ${old.email} is now ${new.email}!"}`
103 |
104 | Meta values can also be used to get more query information. The following are available:
105 |
106 | 1. `meta.table` (table name)
107 | 2. `meta.schema` (schema name)
108 | 3. `meta.event` (INSERT, UPDATE, or DELETE)
109 | 4. `meta.user` (the Postgres user who ran the query that triggered the trigger)
110 | 5. `meta.event_summary` (a formatted summary of the user, table, and event)
111 | 6. `meta.changed` (the values changed for an UPDATE)
112 |
113 | ## Postgres Configuration
114 |
115 | ### Create a Postgres User
116 |
117 | When setting up your database in DB Webhooks, you'll need a Postgres user to connect to the database. You can either use
118 | an existing user or create a new one. This guide shows how to create a new Postgres user with the correct permissions
119 | needed for DB Webhooks.
120 |
121 | If you're using an existing user, make sure the user has the `create` permission on the schema(s) you want to use.
122 |
123 | **Note:** If you want DB Webhooks to also clean up the triggers and functions it creates when all actions are removed on
124 | a table, the user needs to be an owner as Postgres does not have a "drop" permission.
125 |
126 | ### Why is the `create` Permission Needed?
127 |
128 | DB Webhooks uses the create permission to create triggers and functions. A trigger (and corresponding function) is
129 | created whenever a new action on a table is added which doesn't have the DB Webhooks trigger already.
130 |
131 | DB Webhooks will delete the trigger and function it created if the last action on a table is removed, so it doesn't
132 | leave any extra triggers on your database.
133 |
134 | You can see how the trigger works by looking at its implementation in `util.go`.
135 |
136 | #### 1. Create the User
137 |
138 | ```sql
139 | create user db_webhooks with encrypted password 'XXXXXXXXXXXXXXX';
140 | ```
141 |
142 | #### 2. Grant Create Access
143 |
144 | ```sql
145 | grant create on schema public to db_webhooks;
146 | -- If you have tables in a schema other than `public`, add the schema here
147 | grant create on schema your_other_schema_if_needed to db_webhooks;
148 | ```
149 |
--------------------------------------------------------------------------------
/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.19-buster AS backend
2 | WORKDIR /app
3 | COPY go.mod go.sum ./
4 | RUN go mod download && go mod verify
5 | COPY . .
6 | RUN GOOS=linux go build -o build ./go/cmd
7 |
8 | FROM gcr.io/distroless/base AS final
9 | COPY --from=backend /app/build /
10 | EXPOSE 3003
11 | ENTRYPOINT ["/build"]
12 |
--------------------------------------------------------------------------------
/backend/go.mod:
--------------------------------------------------------------------------------
1 | module db-webhooks
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/dgraph-io/badger/v3 v3.2103.5
7 | github.com/gin-contrib/cors v1.4.0
8 | github.com/gin-gonic/gin v1.8.2
9 | github.com/google/uuid v1.3.0
10 | github.com/jackc/pgx v3.6.2+incompatible
11 | github.com/joho/godotenv v1.4.0
12 | go.uber.org/zap v1.24.0
13 | )
14 |
15 | require (
16 | github.com/cespare/xxhash v1.1.0 // indirect
17 | github.com/cespare/xxhash/v2 v2.1.1 // indirect
18 | github.com/cockroachdb/apd v1.1.0 // indirect
19 | github.com/dgraph-io/ristretto v0.1.1 // indirect
20 | github.com/dustin/go-humanize v1.0.0 // indirect
21 | github.com/gin-contrib/sse v0.1.0 // indirect
22 | github.com/go-playground/locales v0.14.1 // indirect
23 | github.com/go-playground/universal-translator v0.18.1 // indirect
24 | github.com/go-playground/validator/v10 v10.11.2 // indirect
25 | github.com/goccy/go-json v0.10.0 // indirect
26 | github.com/gofrs/uuid v4.4.0+incompatible // indirect
27 | github.com/gogo/protobuf v1.3.2 // indirect
28 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
29 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect
30 | github.com/golang/protobuf v1.5.0 // indirect
31 | github.com/golang/snappy v0.0.3 // indirect
32 | github.com/google/flatbuffers v1.12.1 // indirect
33 | github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
34 | github.com/json-iterator/go v1.1.12 // indirect
35 | github.com/klauspost/compress v1.12.3 // indirect
36 | github.com/leodido/go-urn v1.2.1 // indirect
37 | github.com/lib/pq v1.10.7 // indirect
38 | github.com/mattn/go-isatty v0.0.17 // indirect
39 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
40 | github.com/modern-go/reflect2 v1.0.2 // indirect
41 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect
42 | github.com/pkg/errors v0.9.1 // indirect
43 | github.com/shopspring/decimal v1.3.1 // indirect
44 | github.com/ugorji/go/codec v1.2.9 // indirect
45 | go.opencensus.io v0.22.5 // indirect
46 | go.uber.org/atomic v1.7.0 // indirect
47 | go.uber.org/multierr v1.6.0 // indirect
48 | golang.org/x/crypto v0.6.0 // indirect
49 | golang.org/x/net v0.6.0 // indirect
50 | golang.org/x/sys v0.5.0 // indirect
51 | golang.org/x/text v0.7.0 // indirect
52 | google.golang.org/protobuf v1.28.1 // indirect
53 | gopkg.in/yaml.v2 v2.4.0 // indirect
54 | )
55 |
--------------------------------------------------------------------------------
/backend/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
3 | github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
5 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
6 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
7 | github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
8 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
9 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
10 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
11 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
12 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
13 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
14 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
15 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
16 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
17 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
18 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
20 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
21 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
22 | github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg=
23 | github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw=
24 | github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
25 | github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
26 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
27 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
28 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
29 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
30 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
31 | github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
32 | github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
33 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
34 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
35 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
36 | github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
37 | github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398=
38 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
39 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
40 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
41 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
42 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
43 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
44 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
45 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
46 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
47 | github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
48 | github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
49 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
50 | github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
51 | github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
52 | github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
53 | github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
54 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
55 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
56 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
57 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
58 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
59 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
60 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
61 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
62 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
63 | github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
64 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
65 | github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
66 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
67 | github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
68 | github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
69 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
70 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
71 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
72 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
73 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
74 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
75 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
76 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
77 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
78 | github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
79 | github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
80 | github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
81 | github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
82 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
83 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
84 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
85 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
86 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
87 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
88 | github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU=
89 | github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
90 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
91 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
92 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
93 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
94 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
95 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
96 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
97 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
98 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
99 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
100 | github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
101 | github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
102 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
103 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
104 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
105 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
106 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
107 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
108 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
109 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
110 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
111 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
112 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
113 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
114 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
115 | github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
116 | github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
117 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
118 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
119 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
120 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
121 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
122 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
123 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
124 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
125 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
126 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
127 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
128 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
129 | github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
130 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
131 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
132 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
133 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
134 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
135 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
136 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
137 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
138 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
139 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
140 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
141 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
142 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
143 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
144 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
145 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
146 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
147 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
148 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
149 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
150 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
151 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
152 | github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
153 | github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
154 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
155 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
156 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
157 | go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
158 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
159 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
160 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
161 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
162 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
163 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
164 | go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
165 | go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
166 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
167 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
168 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
169 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
170 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
171 | golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
172 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
173 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
174 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
175 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
176 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
177 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
178 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
179 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
180 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
181 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
182 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
183 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
184 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
185 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
186 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
187 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
188 | golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
189 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
190 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
191 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
192 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
193 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
194 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
195 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
196 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
197 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
198 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
199 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
200 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
201 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
202 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
203 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
204 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
205 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
206 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
207 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
208 | golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
209 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
210 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
211 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
212 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
213 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
214 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
215 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
216 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
217 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
218 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
219 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
220 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
221 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
222 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
223 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
224 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
225 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
226 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
227 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
228 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
229 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
230 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
231 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
232 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
233 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
234 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
235 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
236 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
237 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
238 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
239 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
240 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
241 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
242 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
243 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
244 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
245 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
246 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
247 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
248 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
249 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
250 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
251 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
252 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
253 |
--------------------------------------------------------------------------------
/backend/go/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "db-webhooks/go/pkg/db"
6 | "db-webhooks/go/pkg/util"
7 | "db-webhooks/go/services"
8 | "github.com/dgraph-io/badger/v3"
9 | "github.com/joho/godotenv"
10 | "os"
11 | "os/signal"
12 | "syscall"
13 | )
14 |
15 | func main() {
16 | services.ShutdownCtx, services.ShutdownCancelFunc = context.WithCancel(context.Background())
17 |
18 | /* Logger and ENV */
19 | util.InitLogger()
20 | envErr := godotenv.Load()
21 | if envErr != nil {
22 | util.Log.Warnw("Could not load .env file", "error", envErr.Error())
23 | }
24 |
25 | /* BadgerDB */
26 | services.ShutdownWaitGroup.Add(1)
27 | err := db.InitBadgerDB(services.ShutdownCtx, &services.ShutdownWaitGroup)
28 | if err != nil {
29 | util.Log.Fatalw("Error initializing BadgerDB", "error", err)
30 | return
31 | }
32 |
33 | // Check if the database configuration has been set up
34 | connConfig, err := services.GetDatabaseConnectionConfig()
35 | if err != nil {
36 | // No connection config exists, wait to start the event listener until this is added
37 | if err == badger.ErrKeyNotFound {
38 | util.Log.Debugw("Database connection config does not exist yet, waiting to start event listener")
39 | } else {
40 | util.Log.Fatalw("Could not retrieve database connection config", "error", err)
41 | return
42 | }
43 | }
44 |
45 | /* Event Listener */
46 | if connConfig != nil {
47 | // Start the event listener only if a connection config already exists
48 | services.ShutdownWaitGroup.Add(1)
49 | err = services.InitEventListener(services.ShutdownCtx, connConfig)
50 |
51 | if err != nil {
52 | util.Log.Fatalw("Error initializing database event listener", "error", err)
53 | return
54 | }
55 | }
56 |
57 | /* Action Handler */
58 | services.ShutdownWaitGroup.Add(1)
59 | err = services.InitActionHandler(services.ShutdownCtx)
60 | if err != nil {
61 | util.Log.Fatalw("Error initializing action handler", "error", err)
62 | return
63 | }
64 |
65 | /* Web Server */
66 | services.ShutdownWaitGroup.Add(1)
67 | err = services.InitWebServer(services.ShutdownCtx)
68 | if err != nil {
69 | util.Log.Fatalw("Error initializing web server", "error", err)
70 | return
71 | }
72 |
73 | util.Log.Infow("Services started")
74 | go func() {
75 | // Wait for interrupt signal to gracefully shut down services with a timeout
76 | quit := make(chan os.Signal)
77 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
78 | <-quit
79 | util.Log.Infow("Shutting down services...")
80 | services.ShutdownCancelFunc()
81 | }()
82 | services.ShutdownWaitGroup.Wait()
83 | util.Log.Infow("Services shutdown")
84 | }
85 |
--------------------------------------------------------------------------------
/backend/go/pkg/db/badger.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "context"
5 | "db-webhooks/go/pkg/util"
6 | "fmt"
7 | "github.com/dgraph-io/badger/v3"
8 | "os"
9 | "sync"
10 | "time"
11 | )
12 |
13 | const (
14 | // Default BadgerDB discardRatio. It represents the discard ratio for the BadgerDB GC.
15 | // Ref: https://godoc.org/github.com/dgraph-io/badger#DB.RunValueLogGC
16 | badgerDiscardRatio = 0.5
17 | // Default BadgerDB GC interval
18 | badgerGCInterval = 10 * time.Minute
19 | badgerDefaultDir = "/tmp/badger"
20 | )
21 |
22 | var (
23 | DB *BadgerDB
24 | DefaultConnectionName = "postgres"
25 | NamespaceConnections = "connections"
26 | NamespaceActions = "actions"
27 | NamespaceAudit = "audit"
28 | )
29 |
30 | type (
31 | // BadgerDBInt defines an embedded key/value store database interface.
32 | BadgerDBInt interface {
33 | Get(namespace, key string) (value []byte, err error)
34 | Has(namespace, key string) (bool, error)
35 | Find(namespace string) (map[string][]byte, error)
36 | Set(namespace, key string, value []byte) error
37 | Delete(namespace, key string) error
38 | Close() error
39 | }
40 | // BadgerDB is a wrapper around a BadgerDB backend database that implements the BadgerDBInt interface.
41 | BadgerDB struct {
42 | db *badger.DB
43 | ctx context.Context
44 | cancelFunc context.CancelFunc
45 | }
46 | )
47 |
48 | // InitBadgerDB returns a new initialized BadgerDB database implementing the BadgerDBInt interface.
49 | // If the database cannot be initialized, an error will be returned.
50 | func InitBadgerDB(ctx context.Context, wg *sync.WaitGroup) error {
51 | dir := os.Getenv("DISK_DB_LOCATION")
52 | if len(dir) == 0 {
53 | dir = badgerDefaultDir
54 | }
55 | if err := os.MkdirAll(dir, 0774); err != nil {
56 | return err
57 | }
58 | opts := badger.DefaultOptions(dir).
59 | WithValueDir(dir).
60 | WithSyncWrites(false).
61 | WithLoggingLevel(badger.WARNING).
62 | WithNumVersionsToKeep(0).
63 | WithCompactL0OnClose(true).
64 | WithValueLogFileSize(1024 * 1024 * 16).
65 | WithMemTableSize(1024 * 1024 * 32)
66 |
67 | encryptionKey := os.Getenv("DISK_DB_ENCRYPTION_KEY")
68 | if len(encryptionKey) != 0 {
69 | opts = opts.WithEncryptionKey([]byte(encryptionKey)).
70 | WithIndexCacheSize(100 << 20)
71 | }
72 | badgerDB, err := badger.Open(opts)
73 | if err != nil {
74 | return err
75 | }
76 | DB = &BadgerDB{
77 | db: badgerDB,
78 | }
79 | DB.ctx, DB.cancelFunc = context.WithCancel(ctx)
80 | go DB.run(wg)
81 | util.Log.Debugw("BadgerDB started")
82 | return nil
83 | }
84 |
85 | // Get attempts to get a value for a given key and namespace.
86 | // If the key does not exist in the provided namespace, an error is returned, otherwise the retrieved value.
87 | func (bdb *BadgerDB) Get(namespace, key string) (value []byte, err error) {
88 | err = bdb.db.View(func(txn *badger.Txn) error {
89 | item, err := txn.Get(namespaceKey(namespace, key))
90 | if err != nil {
91 | return err
92 | }
93 | return item.Value(func(v []byte) error {
94 | value = make([]byte, len(v))
95 | copy(value, v)
96 | return nil
97 | })
98 | })
99 | if err != nil {
100 | return nil, err
101 | }
102 | return value, nil
103 | }
104 |
105 | // Has returns a boolean reflecting if the database has a given key for a namespace or not.
106 | // An error is only returned if an error to Get would be returned that is not of type badger.ErrKeyNotFound.
107 | func (bdb *BadgerDB) Has(namespace, key string) (ok bool, err error) {
108 | _, err = bdb.Get(namespace, key)
109 | switch err {
110 | case badger.ErrKeyNotFound:
111 | ok, err = false, nil
112 | case nil:
113 | ok, err = true, nil
114 | }
115 | return
116 | }
117 |
118 | // Find attempts to get a map[key]values for a given namespace.
119 | func (bdb *BadgerDB) Find(namespace string) (values map[string][]byte, err error) {
120 | values = make(map[string][]byte)
121 | err = bdb.db.View(func(txn *badger.Txn) error {
122 | it := txn.NewIterator(badger.DefaultIteratorOptions)
123 | defer it.Close()
124 | prefix := []byte(namespace)
125 | for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
126 | item := it.Item()
127 | k := string(item.KeyCopy(nil))
128 | v, err := item.ValueCopy(nil)
129 | if err != nil {
130 | return err
131 | }
132 | values[k] = v
133 | }
134 | return nil
135 | })
136 | if err != nil {
137 | return nil, err
138 | }
139 | return values, nil
140 | }
141 |
142 | // Set attempts to store a value for a given key and namespace.
143 | // If the key/value pair cannot be saved, an error is returned.
144 | func (bdb *BadgerDB) Set(namespace, key string, value []byte) error {
145 | err := bdb.db.Update(func(txn *badger.Txn) error {
146 | return txn.Set(namespaceKey(namespace, key), value)
147 | })
148 | if err != nil {
149 | util.Log.Debugw("Failed to set key", "key", key, "namespace", namespace, "error", err)
150 | return err
151 | }
152 | return nil
153 | }
154 |
155 | // Delete attempts to store a value for a given key and namespace.
156 | // If the key/value pair cannot be saved, an error is returned.
157 | func (bdb *BadgerDB) Delete(namespace, key string) error {
158 | err := bdb.db.Update(func(txn *badger.Txn) error {
159 | return txn.Delete(namespaceKey(namespace, key))
160 | })
161 | if err != nil {
162 | util.Log.Debugw("Failed to delete key", "key", key, "namespace", namespace, "error", err)
163 | return err
164 | }
165 | return nil
166 | }
167 |
168 | // Close closes the connection to the underlying BadgerDB database as well as invoking the context's cancel function.
169 | func (bdb *BadgerDB) Close() error {
170 | bdb.cancelFunc()
171 | if err := bdb.db.Close(); err != nil {
172 | util.Log.Errorw("Failed to close BadgerDB", "error", err)
173 | return err
174 | }
175 | util.Log.Debugw("BadgerDB shutdown")
176 | return nil
177 | }
178 |
179 | // run triggers the garbage collection for the BadgerDB backend database and listens for context cancellation.
180 | func (bdb *BadgerDB) run(wg *sync.WaitGroup) {
181 | defer wg.Done()
182 | ticker := time.NewTicker(badgerGCInterval)
183 | for {
184 | select {
185 | case <-ticker.C:
186 | err := bdb.db.RunValueLogGC(badgerDiscardRatio)
187 | if err != nil {
188 | // Don't report an error when GC didn't result in any cleanup
189 | if err == badger.ErrNoRewrite {
190 | //util.Log.Debugw("No BadgerDB GC occurred", "error", err)
191 | } else {
192 | //util.Log.Debugw("Failed to GC BadgerDB", "error", err)
193 | }
194 | }
195 | case <-bdb.ctx.Done():
196 | bdb.Close()
197 | return
198 | }
199 | }
200 | }
201 |
202 | // namespaceKey returns a composite key used for lookup and storage for a given namespace and key.
203 | func namespaceKey(namespace, key string) []byte {
204 | return []byte(fmt.Sprintf("%s/%s", namespace, key))
205 | }
206 |
--------------------------------------------------------------------------------
/backend/go/pkg/util/log.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "go.uber.org/zap"
5 | )
6 |
7 | var Log *zap.SugaredLogger
8 | var initialized bool
9 |
10 | func InitLogger() {
11 | if initialized {
12 | return
13 | }
14 | initialized = true
15 | zapLogger, _ := zap.NewDevelopment()
16 | defer func(zapLogger *zap.Logger) {
17 | _ = zapLogger.Sync()
18 | }(zapLogger)
19 | Log = zapLogger.Sugar()
20 | }
21 |
--------------------------------------------------------------------------------
/backend/go/pkg/util/util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "regexp"
8 | "strings"
9 | )
10 |
11 | func JsonPrettyPrint(in string) string {
12 | var out bytes.Buffer
13 | err := json.Indent(&out, []byte(in), "", "\t")
14 | if err != nil {
15 | return in
16 | }
17 | return out.String()
18 | }
19 |
20 | func IsValidJSON(str string) bool {
21 | var js json.RawMessage
22 | return json.Unmarshal([]byte(str), &js) == nil
23 | }
24 |
25 | // FillTemplateValues replaces template keys with the provided values
26 | // Format examples: ${name} ${new.name} ${old.name}
27 | func FillTemplateValues(template string, values map[string]interface{}) string {
28 | result := template
29 | rex := regexp.MustCompile(`\${(\w+|\w+\.\w+)}`)
30 | matches := rex.FindAllStringSubmatch(template, -1)
31 | for _, match := range matches {
32 | token := match[0]
33 | tokenValue := match[1]
34 | // Replace the value even if it doesn't exist in the values map
35 | value, exists := values[tokenValue]
36 | if !exists {
37 | value = ""
38 | }
39 | result = strings.ReplaceAll(result, token, fmt.Sprintf("%v", value))
40 | }
41 | return result
42 | }
43 |
44 | func GetTriggerDropSQL(schema, table string) string {
45 | template := fmt.Sprintf(`
46 | drop trigger if exists pg_notify_trigger_event_%[1]s_%[2]s_update on %[1]s.%[2]s;
47 | drop trigger if exists pg_notify_trigger_event_%[1]s_%[2]s_insert on %[1]s.%[2]s;
48 | drop trigger if exists pg_notify_trigger_event_%[1]s_%[2]s_delete on %[1]s.%[2]s;
49 | drop function if exists pg_notify_trigger_event_%[1]s_%[2]s();
50 | `, schema, table)
51 | return template
52 | }
53 |
54 | func GetTriggerCreationSQL(schema, table string) string {
55 | template := fmt.Sprintf(`
56 | do
57 | $$
58 | begin
59 | if not exists(select 1 from pg_proc where proname = 'pg_notify_trigger_event_%[1]s_%[2]s') then
60 | create or replace function pg_notify_trigger_event_%[1]s_%[2]s() returns trigger as
61 | $FN$
62 | declare
63 | hasNew bool = false;
64 | hasOld bool = false;
65 | payload jsonb;
66 | begin
67 | if TG_OP = 'INSERT' then
68 | hasNew = true;
69 | elseif TG_OP = 'UPDATE' then
70 | hasNew = true;
71 | hasOld = true;
72 | else
73 | hasOld = true;
74 | end if;
75 | payload = jsonb_build_object(
76 | 'table', TG_TABLE_NAME,
77 | 'schema', TG_TABLE_SCHEMA,
78 | 'event', to_jsonb(TG_OP),
79 | 'user', current_user
80 | );
81 | if hasNew then
82 | payload = jsonb_set(payload, '{new}', to_jsonb(NEW), true);
83 | end if;
84 | if hasOld then
85 | payload = jsonb_set(payload, '{old}', to_jsonb(OLD), true);
86 | end if;
87 | perform pg_notify('pg_notify_trigger_event', payload::text);
88 | return NEW;
89 | end;
90 | $FN$ language plpgsql;
91 | end if;
92 | if not exists(select 1 from pg_trigger where tgname = 'pg_notify_trigger_event_%[1]s_%[2]s_update') then
93 | create trigger pg_notify_trigger_event_%[1]s_%[2]s_update
94 | after update
95 | on %[1]s.%[2]s
96 | for each row
97 | execute procedure pg_notify_trigger_event_%[1]s_%[2]s();
98 | end if;
99 | if not exists(select 1 from pg_trigger where tgname = 'pg_notify_trigger_event_%[1]s_%[2]s_insert') then
100 | create trigger pg_notify_trigger_event_%[1]s_%[2]s_insert
101 | after insert
102 | on %[1]s.%[2]s
103 | for each row
104 | execute procedure pg_notify_trigger_event_%[1]s_%[2]s();
105 | end if;
106 | if not exists(select 1 from pg_trigger where tgname = 'pg_notify_trigger_event_%[1]s_%[2]s_delete') then
107 | create trigger pg_notify_trigger_event_%[1]s_%[2]s_delete
108 | after delete
109 | on %[1]s.%[2]s
110 | for each row
111 | execute procedure pg_notify_trigger_event_%[1]s_%[2]s();
112 | end if;
113 | end
114 | $$;`, schema, table)
115 | return template
116 | }
117 |
118 | func GetTriggerCreationSQLV2(schema, table string) string {
119 | template := fmt.Sprintf(`
120 | do
121 | $$
122 | begin
123 | if not exists(select 1 from pg_proc where proname = 'pg_notify_trigger_event_%[1]s_%[2]s') then
124 | create or replace function pg_notify_trigger_event_%[1]s_%[2]s() returns trigger as
125 | $FN$
126 | declare
127 | hasNew bool = false;
128 | hasOld bool = false;
129 | payload jsonb;
130 | begin
131 | if TG_OP = 'INSERT' then
132 | hasNew = true;
133 | elseif TG_OP = 'UPDATE' then
134 | hasNew = true;
135 | hasOld = true;
136 | else
137 | hasOld = true;
138 | end if;
139 | payload = jsonb_build_object(
140 | 'table', TG_TABLE_NAME,
141 | 'schema', TG_TABLE_SCHEMA,
142 | 'event', to_jsonb(TG_OP),
143 | 'user', current_user
144 | );
145 | if hasNew then
146 | payload = jsonb_set(payload, '{new}', to_jsonb(NEW), true);
147 | end if;
148 | if hasOld then
149 | payload = jsonb_set(payload, '{old}', to_jsonb(OLD), true);
150 | end if;
151 | perform pg_notify('pg_notify_trigger_event', payload::text);
152 | return NEW;
153 | end;
154 | $FN$ language plpgsql;
155 | end if;
156 | if not exists(select 1 from pg_trigger where tgname = 'pg_notify_trigger_event_%[1]s_%[2]s_update') then
157 | create trigger pg_notify_trigger_event_%[1]s_%[2]s_update
158 | after update
159 | on %[1]s.%[2]s
160 | for each row
161 | execute procedure pg_notify_trigger_event_%[1]s_%[2]s();
162 | end if;
163 | if not exists(select 1 from pg_trigger where tgname = 'pg_notify_trigger_event_%[1]s_%[2]s_insert') then
164 | create trigger pg_notify_trigger_event_%[1]s_%[2]s_insert
165 | after insert
166 | on %[1]s.%[2]s
167 | for each row
168 | execute procedure pg_notify_trigger_event_%[1]s_%[2]s();
169 | end if;
170 | if not exists(select 1 from pg_trigger where tgname = 'pg_notify_trigger_event_%[1]s_%[2]s_delete') then
171 | create trigger pg_notify_trigger_event_%[1]s_%[2]s_delete
172 | after delete
173 | on %[1]s.%[2]s
174 | for each row
175 | execute procedure pg_notify_trigger_event_%[1]s_%[2]s();
176 | end if;
177 | end
178 | $$;`, schema, table)
179 | return template
180 | }
181 |
182 | func GetTriggerCreationSQLV3(schema, table string) string {
183 | template := fmt.Sprintf(`
184 | do
185 | $$
186 | begin
187 | if not exists(select 1 from pg_proc where proname = 'pg_notify_trigger_event_%[1]s_%[2]s') then
188 | create or replace function pg_notify_trigger_event_%[1]s_%[2]s() returns trigger as
189 | $FN$
190 | declare
191 | hasNew bool = false;
192 | hasOld bool = false;
193 | payload jsonb;
194 | begin
195 | if TG_OP = 'INSERT' then
196 | hasNew = true;
197 | elseif TG_OP = 'UPDATE' then
198 | hasNew = true;
199 | hasOld = true;
200 | else
201 | hasOld = true;
202 | end if;
203 | payload = jsonb_build_object(
204 | 'table', TG_TABLE_NAME,
205 | 'schema', TG_TABLE_SCHEMA,
206 | 'event', to_jsonb(TG_OP),
207 | 'user', current_user
208 | );
209 | if hasNew then
210 | payload = jsonb_set(payload, '{new}', to_jsonb(NEW), true);
211 | end if;
212 | if hasOld then
213 | payload = jsonb_set(payload, '{old}', to_jsonb(OLD), true);
214 | end if;
215 | perform pg_notify('pg_notify_trigger_event', payload::text);
216 | return NEW;
217 | end;
218 | $FN$ language plpgsql;
219 | end if;
220 | if not exists(select 1 from pg_trigger where tgname = 'pg_notify_trigger_event_%[1]s_%[2]s_update') then
221 | create trigger pg_notify_trigger_event_%[1]s_%[2]s_update
222 | after update
223 | on %[1]s.%[2]s
224 | for each row
225 | execute procedure pg_notify_trigger_event_%[1]s_%[2]s();
226 | end if;
227 | if not exists(select 1 from pg_trigger where tgname = 'pg_notify_trigger_event_%[1]s_%[2]s_insert') then
228 | create trigger pg_notify_trigger_event_%[1]s_%[2]s_insert
229 | after insert
230 | on %[1]s.%[2]s
231 | for each row
232 | execute procedure pg_notify_trigger_event_%[1]s_%[2]s();
233 | end if;
234 | if not exists(select 1 from pg_trigger where tgname = 'pg_notify_trigger_event_%[1]s_%[2]s_delete') then
235 | create trigger pg_notify_trigger_event_%[1]s_%[2]s_delete
236 | after delete
237 | on %[1]s.%[2]s
238 | for each row
239 | execute procedure pg_notify_trigger_event_%[1]s_%[2]s();
240 | end if;
241 | end
242 | $$;`, schema, table)
243 | return template
244 | }
245 |
246 | func GetTriggerCreationSQLV4(schema, table string) string {
247 | template := fmt.Sprintf(`
248 | do
249 | $$
250 | begin
251 | if not exists(select 1 from pg_proc where proname = 'pg_notify_trigger_event_%[1]s_%[2]s') then
252 | create or replace function pg_notify_trigger_event_%[1]s_%[2]s() returns trigger as
253 | $FN$
254 | declare
255 | hasNew bool = false;
256 | hasOld bool = false;
257 | payload jsonb;
258 | begin
259 | if TG_OP = 'INSERT' then
260 | hasNew = true;
261 | elseif TG_OP = 'UPDATE' then
262 | hasNew = true;
263 | hasOld = true;
264 | else
265 | hasOld = true;
266 | end if;
267 | payload = jsonb_build_object(
268 | 'table', TG_TABLE_NAME,
269 | 'schema', TG_TABLE_SCHEMA,
270 | 'event', to_jsonb(TG_OP),
271 | 'user', current_user
272 | );
273 | if hasNew then
274 | payload = jsonb_set(payload, '{new}', to_jsonb(NEW), true);
275 | end if;
276 | if hasOld then
277 | payload = jsonb_set(payload, '{old}', to_jsonb(OLD), true);
278 | end if;
279 | perform pg_notify('pg_notify_trigger_event', payload::text);
280 | return NEW;
281 | end;
282 | $FN$ language plpgsql;
283 | end if;
284 | if not exists(select 1 from pg_trigger where tgname = 'pg_notify_trigger_event_%[1]s_%[2]s_update') then
285 | create trigger pg_notify_trigger_event_%[1]s_%[2]s_update
286 | after update
287 | on %[1]s.%[2]s
288 | for each row
289 | execute procedure pg_notify_trigger_event_%[1]s_%[2]s();
290 | end if;
291 | if not exists(select 1 from pg_trigger where tgname = 'pg_notify_trigger_event_%[1]s_%[2]s_insert') then
292 | create trigger pg_notify_trigger_event_%[1]s_%[2]s_insert
293 | after insert
294 | on %[1]s.%[2]s
295 | for each row
296 | execute procedure pg_notify_trigger_event_%[1]s_%[2]s();
297 | end if;
298 | if not exists(select 1 from pg_trigger where tgname = 'pg_notify_trigger_event_%[1]s_%[2]s_delete') then
299 | create trigger pg_notify_trigger_event_%[1]s_%[2]s_delete
300 | after delete
301 | on %[1]s.%[2]s
302 | for each row
303 | execute procedure pg_notify_trigger_event_%[1]s_%[2]s();
304 | end if;
305 | end
306 | $$;`, schema, table)
307 | return template
308 | }
309 |
--------------------------------------------------------------------------------
/backend/go/services/action_handler.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "db-webhooks/go/pkg/db"
7 | "db-webhooks/go/pkg/util"
8 | "encoding/json"
9 | "fmt"
10 | "net/http"
11 | "strings"
12 | "time"
13 | )
14 |
15 | const (
16 | TriggerActionHTTP = "HTTP"
17 | TriggerActionAudit = "AUDIT"
18 | triggerEventInsert = "INSERT"
19 | triggerEventUpdate = "UPDATE"
20 | triggerEventDelete = "DELETE"
21 | )
22 |
23 | type TriggerAction struct {
24 | Name string `json:"name"`
25 | Table string `json:"table"`
26 | Schema string `json:"schema"`
27 | TriggerEvents []string `json:"trigger_events"`
28 | Action struct {
29 | Type string `json:"type"`
30 | URL string `json:"url"`
31 | Method string `json:"method"`
32 | Body string `json:"body"`
33 | } `json:"action"`
34 | Filters struct {
35 | ExcludeUsers []string `json:"exclude_users"`
36 | } `json:"filters"`
37 | }
38 |
39 | type Audit struct {
40 | Table string `json:"table"`
41 | Schema string `json:"schema"`
42 | User string `json:"user"`
43 | Event string `json:"event"`
44 | Changed map[string]ChangedRow `json:"changed"`
45 | }
46 |
47 | type ChangedRow struct {
48 | Old interface{} `json:"old"`
49 | New interface{} `json:"new"`
50 | }
51 |
52 | func InitActionHandler(ctx context.Context) error {
53 | // TODO: Use a persistent queue for handling events
54 | util.Log.Debugw("Action handler started")
55 | go func() {
56 | defer ShutdownWaitGroup.Done()
57 | for {
58 | select {
59 | case <-ctx.Done():
60 | util.Log.Debugw("Action handler shutdown")
61 | return
62 | }
63 | }
64 | }()
65 | return nil
66 | }
67 |
68 | func HandleNotifyEventReceived(payload NotifyPayload) {
69 | if len(payload.Table) == 0 {
70 | return
71 | }
72 | // Look up actions that exist for table
73 | // TODO: These can be cached in memory
74 | actionsData, err := db.DB.Find(db.NamespaceActions)
75 | if err != nil {
76 | util.Log.Debugw("No trigger actions found", "error", err)
77 | return
78 | }
79 | // Parse action byte data
80 | var matchedActions []TriggerAction
81 | for _, actionData := range actionsData {
82 | action := TriggerAction{}
83 | if jsonErr := json.Unmarshal(actionData, &action); jsonErr != nil {
84 | util.Log.Errorw("Could no unmarshal action data", "error", jsonErr)
85 | continue
86 | }
87 | if action.Table != payload.Table {
88 | continue
89 | }
90 | if action.Schema != payload.Schema {
91 | continue
92 | }
93 | // Check action filters
94 | filterAction := false
95 | for _, user := range action.Filters.ExcludeUsers {
96 | if user == payload.User {
97 | util.Log.Debugw("Not executing action due to filter match",
98 | "filter", "exclude_users",
99 | "user", payload.User,
100 | )
101 | filterAction = true
102 | }
103 | }
104 | if filterAction {
105 | continue
106 | }
107 | // Add the action if the matching table and schema contains a trigger event matching the trigger operation
108 | for _, triggerEvent := range action.TriggerEvents {
109 | if payload.Event == triggerEvent {
110 | matchedActions = append(matchedActions, action)
111 | break
112 | }
113 | }
114 | }
115 | columnTemplateValues := mergeColumnTemplateValues(payload)
116 | for _, action := range matchedActions {
117 | switch action.Action.Type {
118 | case TriggerActionHTTP:
119 | triggerActionHTTPPOST(action, columnTemplateValues)
120 | break
121 | case TriggerActionAudit:
122 | triggerActionAuditLog(action, columnTemplateValues)
123 | break
124 | default:
125 | util.Log.Debugw("Attempted to fire unsupported trigger action", "type", action.Action.Type)
126 | }
127 | }
128 | }
129 |
130 | // mergeColumnTemplateValues creates a merged map of all available template values.
131 | // The prefixes "new." and "old." can be used if a new (INSERT, UPDATE) or old (UPDATE, DELETE) row is available.
132 | // If a prefix is not specified, the new or old values will be used depending on the event:
133 | // INSERT: ${name} == ${new.name}
134 | // UPDATE: ${name} == ${new.name}
135 | // DELETE: ${name} == ${old.name}
136 | func mergeColumnTemplateValues(payload NotifyPayload) map[string]interface{} {
137 | columnTemplateValues := make(map[string]interface{})
138 | if payload.New != nil {
139 | for k, v := range payload.New {
140 | columnTemplateValues[fmt.Sprintf("new.%v", k)] = v
141 | }
142 | }
143 | if payload.Old != nil {
144 | for k, v := range payload.Old {
145 | columnTemplateValues[fmt.Sprintf("old.%v", k)] = v
146 | }
147 | }
148 | // Find the changed columns
149 | changedValuesStr := make([]string, 0)
150 | changedValuesMap := make(map[string]ChangedRow)
151 | if payload.Event == triggerEventUpdate && payload.New != nil && payload.Old != nil {
152 | oldValueMap := make(map[string]interface{}, len(payload.Old))
153 | for k, v := range payload.Old {
154 | oldValueMap[k] = v
155 | }
156 | for k, newVal := range payload.New {
157 | if oldVal, exists := oldValueMap[k]; exists && oldVal != newVal {
158 | changedValuesStr = append(changedValuesStr, fmt.Sprintf("-- %v --\\nold: %v\\nnew: %v\\n", k, oldVal, newVal))
159 | changedValuesMap[k] = ChangedRow{
160 | Old: oldVal,
161 | New: newVal,
162 | }
163 | }
164 | }
165 | }
166 | var actionStr string
167 | switch payload.Event {
168 | case triggerEventDelete, triggerEventUpdate:
169 | actionStr = fmt.Sprintf("%vd", strings.ToLower(payload.Event))
170 | break
171 | case triggerEventInsert:
172 | actionStr = fmt.Sprintf("%ved", strings.ToLower(payload.Event))
173 | break
174 | }
175 | switch payload.Event {
176 | case triggerEventInsert, triggerEventUpdate:
177 | for k, v := range payload.New {
178 | columnTemplateValues[k] = v
179 | }
180 | break
181 | case triggerEventDelete:
182 | for k, v := range payload.Old {
183 | columnTemplateValues[k] = v
184 | }
185 | break
186 | }
187 | // Add metadata values
188 | columnTemplateValues["meta.table"] = payload.Table
189 | columnTemplateValues["meta.schema"] = payload.Schema
190 | columnTemplateValues["meta.event"] = payload.Event
191 | columnTemplateValues["meta.user"] = payload.User
192 | columnTemplateValues["meta.event_summary"] = fmt.Sprintf("User *%v* %v a row in table `%v`", payload.User, actionStr, payload.Table)
193 | columnTemplateValues["meta.changed"] = strings.Join(changedValuesStr, "\\n")
194 | columnTemplateValues["meta.changed_raw"] = changedValuesMap
195 | return columnTemplateValues
196 | }
197 |
198 | func triggerActionHTTPPOST(action TriggerAction, templateValues map[string]interface{}) {
199 | postRequestBody := util.FillTemplateValues(action.Action.Body, templateValues)
200 | resp, err := http.Post(action.Action.URL, "application/json", bytes.NewBuffer([]byte(postRequestBody)))
201 | if err != nil {
202 | util.Log.Errorw("An error occurred making a trigger action POST request",
203 | "url", action.Action.URL,
204 | "error", err,
205 | )
206 | return
207 | }
208 | if resp != nil {
209 | defer resp.Body.Close()
210 | }
211 | }
212 |
213 | func triggerActionAuditLog(action TriggerAction, templateValues map[string]interface{}) {
214 | audit := Audit{
215 | Table: action.Table,
216 | Schema: action.Schema,
217 | User: templateValues["meta.user"].(string),
218 | Event: templateValues["meta.event"].(string),
219 | Changed: templateValues["meta.changed_raw"].(map[string]ChangedRow),
220 | }
221 | auditJson, err := json.Marshal(&audit)
222 | if err != nil {
223 | util.Log.Warnw("Error creating audit log. Could not marshal JSON", "error", err)
224 | return
225 | }
226 | key := fmt.Sprintf("%v", time.Now().UnixMicro())
227 | if err = db.DB.Set(db.NamespaceAudit, key, auditJson); err != nil {
228 | util.Log.Warnw("Error creating audit log", "error", err)
229 | return
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/backend/go/services/event_listener.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "context"
5 | "db-webhooks/go/pkg/db"
6 | "db-webhooks/go/pkg/util"
7 | "encoding/json"
8 | "errors"
9 | "github.com/jackc/pgx"
10 | "time"
11 | )
12 |
13 | const defaultListenChannel = "pg_notify_trigger_event"
14 |
15 | var initialized bool
16 |
17 | type ConnConfig struct {
18 | Host string `json:"host"`
19 | Port uint16 `json:"port"`
20 | Database string `json:"database"`
21 | User string `json:"user"`
22 | Password string `json:"password"`
23 | }
24 |
25 | type NotifyPayload struct {
26 | Table string `json:"table"`
27 | Schema string `json:"schema"`
28 | Event string `json:"event"`
29 | New map[string]interface{} `json:"new"`
30 | Old map[string]interface{} `json:"old"`
31 | User string `json:"user"`
32 | }
33 |
34 | var ConnPool *pgx.ConnPool
35 |
36 | func InitEventListener(ctx context.Context, ccfg *ConnConfig) (err error) {
37 | if initialized {
38 | return
39 | }
40 | initialized = true
41 | // TODO: Add logic to reconnect if connection fails if pgx does not do this automatically
42 | pgxConnConfig := pgx.ConnConfig{
43 | Host: ccfg.Host,
44 | Port: ccfg.Port,
45 | Database: ccfg.Database,
46 | User: ccfg.User,
47 | Password: ccfg.Password,
48 | }
49 | ConnPool, err = pgx.NewConnPool(pgx.ConnPoolConfig{
50 | ConnConfig: pgxConnConfig,
51 | MaxConnections: 3,
52 | AcquireTimeout: 30 * time.Second,
53 | })
54 | if err != nil {
55 | return err
56 | }
57 | util.Log.Debugw("Connected to database", "name", ccfg.Database)
58 | go listen(ctx)
59 | return nil
60 | }
61 |
62 | func listen(ctx context.Context) {
63 | conn, err := ConnPool.Acquire()
64 | if err != nil {
65 | util.Log.Errorw("Could not acquire database connection to LISTEN", "error", err)
66 | return
67 | }
68 | defer func(conn *pgx.Conn) {
69 | ConnPool.Release(conn)
70 | // TODO: Handle ConnPool management elsewhere
71 | ConnPool.Close()
72 | ShutdownWaitGroup.Done()
73 | }(conn)
74 | err = conn.Listen(defaultListenChannel)
75 | if err != nil {
76 | util.Log.Errorw("Could not establish LISTEN/NOTIFY channel", "error", err, "channel", defaultListenChannel)
77 | return
78 | }
79 | util.Log.Debugw("Event listener started")
80 | for {
81 | // If ctx is done, err will be non-nil and this function will return
82 | msg, err := conn.WaitForNotification(ctx)
83 | if err != nil {
84 | util.Log.Debugw("Event listener shutdown")
85 | return
86 | }
87 | notifyPayload := NotifyPayload{}
88 | err = json.Unmarshal([]byte(msg.Payload), ¬ifyPayload)
89 | if err != nil {
90 | util.Log.Errorw("Could not unmarshal NOTIFY payload from database", "error", err)
91 | continue
92 | }
93 | util.Log.Debugw("Received NOTIFY event from database, forwarding to action handler",
94 | "channel", msg.Channel,
95 | "schema", notifyPayload.Schema,
96 | "table", notifyPayload.Table,
97 | "event", notifyPayload.Event,
98 | )
99 | go HandleNotifyEventReceived(notifyPayload)
100 | }
101 | }
102 |
103 | func GetDatabaseConnectionConfig() (*ConnConfig, error) {
104 | // TODO: Modify to support multiple database connection configs using a UUID key
105 | val, err := db.DB.Get(db.NamespaceConnections, db.DefaultConnectionName)
106 | if err != nil {
107 | return nil, err
108 | }
109 | connConfig := ConnConfig{}
110 | err = json.Unmarshal(val, &connConfig)
111 | if err != nil {
112 | return nil, err
113 | }
114 | return &connConfig, nil
115 | }
116 |
117 | func TestDatabaseConnection(ccfg *ConnConfig) error {
118 | pgxConnConfig := pgx.ConnConfig{
119 | Host: ccfg.Host,
120 | Port: ccfg.Port,
121 | Database: ccfg.Database,
122 | User: ccfg.User,
123 | Password: ccfg.Password,
124 | }
125 | conn, err := pgx.Connect(pgxConnConfig)
126 | if err != nil {
127 | return err
128 | }
129 | if !conn.IsAlive() {
130 | return errors.New("connection error")
131 | }
132 | err = conn.Close()
133 | if err != nil {
134 | return err
135 | }
136 | return nil
137 | }
138 |
--------------------------------------------------------------------------------
/backend/go/services/shutdown.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "context"
5 | "sync"
6 | )
7 |
8 | var ShutdownWaitGroup sync.WaitGroup
9 | var ShutdownCtx context.Context
10 | var ShutdownCancelFunc context.CancelFunc
11 |
--------------------------------------------------------------------------------
/backend/go/services/web_routes.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "db-webhooks/go/pkg/db"
5 | "db-webhooks/go/pkg/util"
6 | "encoding/json"
7 | "github.com/dgraph-io/badger/v3"
8 | "github.com/gin-gonic/gin"
9 | "github.com/google/uuid"
10 | "net/http"
11 | "strings"
12 | )
13 |
14 | type ObjectID struct {
15 | ID string `json:"id"`
16 | }
17 |
18 | func Health(c *gin.Context) {
19 | c.JSON(http.StatusOK, gin.H{
20 | "message": "ok",
21 | })
22 | }
23 |
24 | func ConnectionGet(c *gin.Context) {
25 | connConfig, err := GetDatabaseConnectionConfig()
26 | if err != nil {
27 | if err != badger.ErrKeyNotFound {
28 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "error retrieving connection", "error": err.Error()})
29 | return
30 | }
31 | }
32 | if connConfig == nil {
33 | connConfig = &ConnConfig{}
34 | }
35 | c.JSON(http.StatusOK, connConfig)
36 | }
37 |
38 | func ConnectionCreate(c *gin.Context) {
39 | connConfig := ConnConfig{}
40 | if err := c.BindJSON(&connConfig); err != nil {
41 | util.Log.Warnw("Could not bind JSON", "error", err)
42 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "invalid json body", "error": err.Error()})
43 | return
44 | }
45 | connConfigJson, err := json.Marshal(&connConfig)
46 | if err != nil {
47 | util.Log.Warnw("Could not marshal JSON", "error", err)
48 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "could not marshal connection object", "error": err.Error()})
49 | return
50 | }
51 | if ok, _ := db.DB.Has(db.NamespaceConnections, db.DefaultConnectionName); ok {
52 | util.Log.Warnw("Attempted to create connection that already exists")
53 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "connection already exists", "error": "connection already exists"})
54 | return
55 | }
56 | if err = TestDatabaseConnection(&connConfig); err != nil {
57 | util.Log.Warnw("Could not connect to database for connection creation")
58 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "connection failed", "error": err.Error()})
59 | return
60 | }
61 | if err = db.DB.Set(db.NamespaceConnections, db.DefaultConnectionName, connConfigJson); err != nil {
62 | util.Log.Warnw("Error creating connection", "error", err)
63 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "error creating connection", "error": err.Error()})
64 | return
65 | }
66 | if err = InitEventListener(ShutdownCtx, &connConfig); err != nil {
67 | util.Log.Errorw("Could not initialize event listener", "error", err)
68 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "error initializing event listener", "error": err.Error()})
69 | return
70 | }
71 | c.JSON(http.StatusOK, gin.H{
72 | "message": "connection created",
73 | })
74 | }
75 |
76 | func ConnectionDelete(c *gin.Context) {
77 | if err := db.DB.Delete(db.NamespaceConnections, db.DefaultConnectionName); err != nil {
78 | util.Log.Warnw("Error deleting connection", "error", err)
79 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "error deleting connection", "error": err.Error()})
80 | return
81 | }
82 | // TODO: Shutdown event listener on connection delete
83 | c.JSON(http.StatusOK, gin.H{
84 | "message": "connection deleted",
85 | })
86 | }
87 |
88 | func ActionCreate(c *gin.Context) {
89 | action := TriggerAction{}
90 | if err := c.BindJSON(&action); err != nil {
91 | util.Log.Warnw("Could not bind JSON", "error", err)
92 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "invalid json body", "error": err.Error()})
93 | return
94 | }
95 | actionJson, err := json.Marshal(&action)
96 | if err != nil {
97 | util.Log.Warnw("Could not marshal JSON", "error", err)
98 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "could not marshal connection object", "error": err.Error()})
99 | return
100 | }
101 | if len(action.Table) == 0 {
102 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "no table provided", "error": "no table provided"})
103 | return
104 | }
105 | if len(action.Schema) == 0 {
106 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "no schema provided", "error": "no schema provided"})
107 | return
108 | }
109 | if len(action.TriggerEvents) == 0 {
110 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "no trigger events provided", "error": "no trigger events provided"})
111 | return
112 | }
113 | if len(action.Action.Type) == 0 {
114 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "no action type provided", "error": "no action type provided"})
115 | return
116 | }
117 | if action.Action.Type == TriggerActionHTTP {
118 | isPostRequestBodyValidJSON := util.IsValidJSON(action.Action.Body) || len(action.Action.Body) == 0
119 | if !isPostRequestBodyValidJSON {
120 | util.Log.Infow("Invalid request body JSON when creating action")
121 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "error creating action", "error": "request body must be valid JSON or empty"})
122 | return
123 | }
124 | if len(action.Action.URL) == 0 {
125 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "no URL provided", "error": "no URL provided"})
126 | return
127 | }
128 | }
129 | // Create db trigger if one does not already exist (handled by the SQL using `if not exists`)
130 | _, err = ConnPool.Exec(util.GetTriggerCreationSQL(action.Schema, action.Table))
131 | if err != nil {
132 | util.Log.Warnw("Could not create database trigger", "error", err)
133 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "could not create database trigger", "error": err.Error()})
134 | return
135 | }
136 | // If an ID is provided as a request param, check if the action exists and update it if so
137 | actionId := uuid.New().String()
138 | id := c.Query("id")
139 | if len(id) != 0 {
140 | _, existsErr := db.DB.Get(db.NamespaceActions, id)
141 | if existsErr != nil {
142 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "action does not exist", "error": err.Error()})
143 | return
144 | }
145 | actionId = id
146 | }
147 | if err = db.DB.Set(db.NamespaceActions, actionId, actionJson); err != nil {
148 | util.Log.Warnw("Error creating action", "error", err)
149 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "error creating action", "error": err.Error()})
150 | return
151 | }
152 | c.JSON(http.StatusOK, gin.H{
153 | "message": "ok",
154 | "id": actionId,
155 | })
156 | }
157 |
158 | func ActionList(c *gin.Context) {
159 | actions := make(map[string]TriggerAction)
160 | actionsData, err := db.DB.Find(db.NamespaceActions)
161 | if err != nil {
162 | c.JSON(http.StatusOK, actions)
163 | return
164 | }
165 | for key, actionData := range actionsData {
166 | action := TriggerAction{}
167 | if jsonErr := json.Unmarshal(actionData, &action); jsonErr != nil {
168 | util.Log.Errorw("Could no unmarshal action data", "error", jsonErr)
169 | continue
170 | }
171 | id := strings.Split(key, "/")[1]
172 | actions[id] = action
173 | }
174 | c.JSON(http.StatusOK, actions)
175 | }
176 |
177 | func AuditList(c *gin.Context) {
178 | audits := make(map[string]Audit)
179 | auditsData, err := db.DB.Find(db.NamespaceAudit)
180 | if err != nil {
181 | c.JSON(http.StatusOK, audits)
182 | return
183 | }
184 | for key, auditData := range auditsData {
185 | audit := Audit{}
186 | if jsonErr := json.Unmarshal(auditData, &audit); jsonErr != nil {
187 | util.Log.Errorw("Could no unmarshal audit data", "error", jsonErr)
188 | continue
189 | }
190 | id := strings.Split(key, "/")[1]
191 | audits[id] = audit
192 | }
193 | c.JSON(http.StatusOK, audits)
194 | }
195 |
196 | func ActionGet(c *gin.Context) {
197 | id := c.Query("id")
198 | if len(id) == 0 {
199 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "no id provided"})
200 | return
201 | }
202 | actionData, err := db.DB.Get(db.NamespaceActions, id)
203 | if err != nil {
204 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "action does not exist", "error": err.Error()})
205 | return
206 | }
207 | action := TriggerAction{}
208 | if jsonErr := json.Unmarshal(actionData, &action); jsonErr != nil {
209 | util.Log.Errorw("Could no unmarshal action data", "error", jsonErr)
210 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "could not unmarshal action data", "error": err.Error()})
211 | return
212 | }
213 | c.JSON(http.StatusOK, action)
214 | }
215 |
216 | func ActionDelete(c *gin.Context) {
217 | id := ObjectID{}
218 | if err := c.BindJSON(&id); err != nil {
219 | util.Log.Warnw("Could not bind JSON", "error", err)
220 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "invalid json body", "error": err.Error()})
221 | return
222 | }
223 |
224 | // Lookup action to see if exists
225 | actionToDeleteData, err := db.DB.Get(db.NamespaceActions, id.ID)
226 | if err != nil {
227 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": "action does not exist", "error": err.Error()})
228 | return
229 | }
230 | actionToDelete := TriggerAction{}
231 | if jsonErr := json.Unmarshal(actionToDeleteData, &actionToDelete); jsonErr != nil {
232 | util.Log.Errorw("Could no unmarshal action data", "error", jsonErr)
233 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "could not unmarshal action data", "error": err.Error()})
234 | return
235 | }
236 | // Get all actions
237 | // If this is the last action for the table, delete the underlying database trigger
238 | actionsData, err := db.DB.Find(db.NamespaceActions)
239 | if err != nil {
240 | util.Log.Warnw("Error deleting action, could not look up existing actions", "error", err)
241 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "error deleting action, could not look up existing actions", "error": err.Error()})
242 | return
243 | }
244 | shouldDeleteDatabaseTrigger := true
245 | for key, actionData := range actionsData {
246 | a := TriggerAction{}
247 | if jsonErr := json.Unmarshal(actionData, &a); jsonErr != nil {
248 | util.Log.Errorw("Could no unmarshal action data", "error", jsonErr)
249 | continue
250 | }
251 | actionId := strings.Split(key, "/")[1]
252 | if actionId == id.ID {
253 | // Move on so the action that will be deleted is not considered for deleting the database trigger
254 | continue
255 | }
256 | if actionToDelete.Table == a.Table && actionToDelete.Schema == a.Schema {
257 | shouldDeleteDatabaseTrigger = false
258 | break
259 | }
260 | }
261 | if err = db.DB.Delete(db.NamespaceActions, id.ID); err != nil {
262 | util.Log.Warnw("Error deleting action", "error", err)
263 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "error deleting action", "error": err.Error()})
264 | return
265 | }
266 | // If no other actions exist with same schema/table, delete the underlying database trigger
267 | if shouldDeleteDatabaseTrigger {
268 | _, err = ConnPool.Exec(util.GetTriggerDropSQL(actionToDelete.Schema, actionToDelete.Table))
269 | if err != nil {
270 | util.Log.Warnw("Could not drop database triggers", "error", err)
271 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "could not drop database triggers", "error": err.Error()})
272 | return
273 | }
274 | }
275 | c.JSON(http.StatusOK, gin.H{
276 | "message": "action deleted",
277 | })
278 | }
279 |
280 | func TableList(c *gin.Context) {
281 | tables := make(map[string][]string)
282 | rows, err := ConnPool.Query("select schemaname, tablename from pg_catalog.pg_tables where schemaname not in ('pg_catalog', 'information_schema');")
283 | if err != nil {
284 | util.Log.Warnw("Error listing tables", "error", err)
285 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "could not list tables", "error": err.Error()})
286 | return
287 | }
288 | for rows.Next() {
289 | var schemaName string
290 | var tableName string
291 | if err := rows.Scan(&schemaName, &tableName); err != nil {
292 | continue
293 | }
294 | tables[schemaName] = append(tables[schemaName], tableName)
295 | }
296 | c.JSON(http.StatusOK, tables)
297 | }
298 |
299 | func DatabaseUserList(c *gin.Context) {
300 | users := make([]string, 0)
301 | rows, err := ConnPool.Query("select usename from pg_catalog.pg_user;")
302 | if err != nil {
303 | util.Log.Warnw("Error listing users", "error", err)
304 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": "could not list users", "error": err.Error()})
305 | return
306 | }
307 | for rows.Next() {
308 | var useName string
309 | if err := rows.Scan(&useName); err != nil {
310 | continue
311 | }
312 | users = append(users, useName)
313 | }
314 | c.JSON(http.StatusOK, users)
315 | }
316 |
--------------------------------------------------------------------------------
/backend/go/services/web_server.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "context"
5 | "db-webhooks/go/pkg/util"
6 | "errors"
7 | "fmt"
8 | "github.com/gin-contrib/cors"
9 | "github.com/gin-gonic/gin"
10 | "net/http"
11 | "os"
12 | "strings"
13 | "time"
14 | )
15 |
16 | const (
17 | httpServerReadTimeout = 30 * time.Second
18 | httpServerWriteTimeout = 30 * time.Second
19 | httpDefaultAuthorizationToken = "db-webhooks"
20 | httpDefaultServerPort = "3003"
21 | httpDefaultAllowOrigins = "*"
22 | )
23 |
24 | var authorizationHeaderToken string
25 |
26 | func InitWebServer(ctx context.Context) error {
27 | util.Log.Debugw("Starting API server")
28 | gin.SetMode(gin.ReleaseMode)
29 | router := gin.New()
30 |
31 | allowOrigins := strings.Split(os.Getenv("HTTP_API_SERVER_CORS_ALLOW_ORIGINS"), ",")
32 | if len(allowOrigins[0]) == 0 {
33 | allowOrigins = []string{httpDefaultAllowOrigins}
34 | }
35 | router.Use(cors.New(cors.Config{
36 | AllowOrigins: allowOrigins,
37 | AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"},
38 | AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization", "Accept"},
39 | AllowCredentials: true,
40 | AllowWildcard: true,
41 | MaxAge: 12 * time.Hour,
42 | }))
43 | router.Use(gin.Logger())
44 | router.Use(gin.Recovery())
45 | port := os.Getenv("HTTP_API_SERVER_PORT")
46 | if len(port) == 0 {
47 | port = httpDefaultServerPort
48 | }
49 | authorizationHeaderToken = os.Getenv("HTTP_API_SERVER_AUTH_TOKEN")
50 | if len(authorizationHeaderToken) == 0 {
51 | authorizationHeaderToken = httpDefaultAuthorizationToken
52 | }
53 | server := &http.Server{
54 | Addr: fmt.Sprintf(":%s", port),
55 | Handler: router,
56 | ReadTimeout: httpServerReadTimeout,
57 | WriteTimeout: httpServerWriteTimeout,
58 | MaxHeaderBytes: 1 << 20,
59 | }
60 |
61 | /* --------------------------- Public routes --------------------------- */
62 |
63 | public := router.Group("/api")
64 | public.GET("/health", Health)
65 |
66 | /* --------------------------- Private routes --------------------------- */
67 |
68 | v1 := router.Group("/api/v1")
69 | v1.Use(AuthMiddleware())
70 |
71 | /* Connection */
72 | v1.GET("/connection", ConnectionGet)
73 | v1.POST("/connection", ConnectionCreate)
74 | v1.DELETE("/connection", ConnectionDelete)
75 | /* Action */
76 | v1.GET("/action/list", ActionList)
77 | v1.GET("/action", ActionGet)
78 | v1.POST("/action", ActionCreate)
79 | v1.DELETE("/action", ActionDelete)
80 | /* Table */
81 | v1.GET("/table/list", TableList)
82 | /* Database User */
83 | v1.GET("/db-user/list", DatabaseUserList)
84 | /* Audit */
85 | v1.GET("/audit/list", AuditList)
86 |
87 | // Initialize the server in a goroutine so that it won't block shutdown handling
88 | go func() {
89 | if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
90 | util.Log.Debugw("HTTP server closed", "error", err)
91 | }
92 | }()
93 | util.Log.Debugw("API server started")
94 | go func() {
95 | defer ShutdownWaitGroup.Done()
96 | for {
97 | select {
98 | case <-ctx.Done():
99 | if err := server.Shutdown(ctx); err != nil {
100 | util.Log.Fatalw("API server forced to shutdown", "error", err)
101 | }
102 | util.Log.Debugw("API server shutdown")
103 | return
104 | }
105 | }
106 | }()
107 | return nil
108 | }
109 |
110 | func AuthMiddleware() gin.HandlerFunc {
111 | return func(c *gin.Context) {
112 | if len(authorizationHeaderToken) == 0 {
113 | // If not authorization header token is provided in the env, allow unauthenticated requests
114 | return
115 | }
116 | authorizationHeader := c.GetHeader("Authorization")
117 | if len(authorizationHeader) == 0 {
118 | util.Log.Debugw("Missing authorization header in request")
119 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "unauthorized"})
120 | return
121 | }
122 | if authorizationHeader != authorizationHeaderToken {
123 | util.Log.Debugw("Unable to authorize user")
124 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "unauthorized"})
125 | return
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | # Un-comment if you want use below postgres.
5 | #postgres:
6 | # image: postgres
7 | # container_name: postgres
8 | # ports:
9 | # - "5432:5432"
10 | # networks:
11 | # - db-webhooks
12 | # environment:
13 | # - POSTGRES_PASSWORD=postgres
14 | backend:
15 | #build: "./backend"
16 | image: inqueryio/backend:latest
17 | platform: linux/amd64
18 | # Un-comment if using Apple silicon
19 | #platform: linux/arm64
20 | container_name: backend
21 | stop_signal: SIGTERM
22 | stop_grace_period: 30s
23 | restart: on-failure
24 | ports:
25 | - "3003:3003"
26 | networks:
27 | - db-webhooks
28 | volumes:
29 | - db-webhooks:/tmp/badger
30 | environment:
31 | - DISK_DB_ENCRYPTION_KEY=${DISK_DB_ENCRYPTION_KEY}
32 | - DISK_DB_LOCATION=/tmp/badger
33 | - HTTP_API_SERVER_PORT=3003
34 | - HTTP_API_SERVER_CORS_ALLOW_ORIGINS=${HTTP_API_SERVER_CORS_ALLOW_ORIGINS}
35 | - HTTP_API_SERVER_AUTH_TOKEN=${HTTP_API_SERVER_AUTH_TOKEN}
36 | frontend:
37 | #build: "./frontend"
38 | image: inqueryio/frontend:latest
39 | platform: linux/amd64
40 | # Un-comment if using Apple silicon
41 | #platform: linux/arm64
42 | container_name: frontend
43 | ports:
44 | - "3000:80"
45 | networks:
46 | - db-webhooks
47 | volumes:
48 | - ${PWD}/frontend.env:/usr/share/nginx/html/.env
49 | volumes:
50 | db-webhooks:
51 | networks:
52 | db-webhooks:
53 |
--------------------------------------------------------------------------------
/frontend.env:
--------------------------------------------------------------------------------
1 | REACT_APP_API_AUTH_TOKEN=db-webhooks
2 |
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16-alpine AS frontend
2 | WORKDIR /app
3 | COPY package.json yarn.lock ./
4 | RUN yarn install
5 | COPY . .
6 | RUN yarn build
7 |
8 | FROM nginx:stable-alpine AS final
9 | COPY --from=frontend /app/build /usr/share/nginx/html
10 | COPY nginx.conf /etc/nginx/conf.d/default.conf
11 | RUN apk add --update nodejs npm
12 | RUN npm install -g runtime-env-cra
13 | WORKDIR /usr/share/nginx/html
14 | EXPOSE 80
15 | CMD ["/bin/sh", "-c", "NODE_ENV=development runtime-env-cra && nginx -g \"daemon off;\""]
16 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # DB Webhooks Frontend
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `yarn start`
8 |
9 | Runs the app in the development mode.\
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits. You will also see any lint errors in the console.
13 |
14 | ### `yarn test`
15 |
16 | Launches the test runner in the interactive watch mode.
17 |
18 | ### `yarn build`
19 |
20 | Builds the app for production to the `build` folder.\
21 | It correctly bundles React in production mode and optimizes the build for the best performance.
22 | The build is minified and the filenames include the hashes.
23 |
24 |
--------------------------------------------------------------------------------
/frontend/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 |
3 | listen 80;
4 |
5 | location / {
6 | root /usr/share/nginx/html;
7 | index index.html index.htm;
8 | try_files $uri $uri/ /index.html;
9 | }
10 |
11 | error_page 500 502 503 504 /50x.html;
12 |
13 | location = /50x.html {
14 | root /usr/share/nginx/html;
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adminp",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "bootstrap": "^5.1.1",
10 | "react": "^17.0.2",
11 | "react-dom": "^17.0.2",
12 | "react-icons": "^4.2.0",
13 | "react-loading-overlay": "^1.0.1",
14 | "react-multi-select-component": "^4.3.4",
15 | "react-router-dom": "^5.3.0",
16 | "react-scripts": "4.0.3",
17 | "web-vitals": "^1.0.1"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test",
23 | "eject": "react-scripts eject"
24 | },
25 | "eslintConfig": {
26 | "extends": [
27 | "react-app",
28 | "react-app/jest"
29 | ]
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.2%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tableflowhq/db-webhooks/2eab35538421d2f2c5ed2018bdcd859069c1981d/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | DB Webhooks
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tableflowhq/db-webhooks/2eab35538421d2f2c5ed2018bdcd859069c1981d/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tableflowhq/db-webhooks/2eab35538421d2f2c5ed2018bdcd859069c1981d/frontend/public/logo512.png
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "DB Webhooks",
3 | "name": "DB Webhooks",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | // IMPORTING CSS
2 | import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
3 | import LayoutWelcome from "./pages/LayoutWelcome";
4 | import "./styles/style.css";
5 |
6 | // IMPORTING ROUTER AND SWITCH
7 | import { Redirect, Route, Switch } from "react-router-dom";
8 | import { useEffect, useState } from "react";
9 | import LayoutAction from "./pages/LayoutAction";
10 | import LayoutActionTable from "./pages/LayoutActionTable";
11 | import LoadingOverlay from "react-loading-overlay";
12 | import ConnectDatabase from "./pages/ConnectDatabase";
13 | import LayoutSettings from "./pages/LayoutSettings";
14 | import LayoutAuditTable from "./pages/LayoutAuditTable";
15 | import NotFound from "./pages/NotFound";
16 |
17 | // import the health check component, serivng as a protection guard to ensure BE uptime
18 | import CheckBEHealth from "./util/HealthCheck";
19 |
20 | function App() {
21 | const [mode, setMode] = useState(1);
22 | const [loading, setLoading] = useState(false);
23 |
24 | let localMode = localStorage.getItem("modeLocal");
25 |
26 | useEffect(() => {
27 | if (!localMode) {
28 | localStorage.setItem("modeLocal", "1");
29 | setMode(1);
30 | }
31 | if (!localMode) {
32 | document.body.style.background = "#f5f6f8";
33 | } else if (localMode === "1") {
34 | document.body.style.background = "#f5f6f8";
35 | } else if (localMode && localMode === "2") {
36 | document.body.style.background = "#212121";
37 | }
38 | }, [localMode, mode]);
39 |
40 | return (
41 |
46 | }
49 | spinner={true}
50 | text="Loading..."
51 | >
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | {/* CheckBEHealth to ensure uptime before routing, pass in the component as props */}
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
80 |
81 |
82 |
83 |
88 |
89 |
90 |
91 |
92 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | );
105 | }
106 |
107 | export default App;
108 |
--------------------------------------------------------------------------------
/frontend/src/assets/avt.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/frontend/src/assets/check.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/assets/left.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/assets/logoD.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/frontend/src/assets/logoW.svg:
--------------------------------------------------------------------------------
1 |
26 |
--------------------------------------------------------------------------------
/frontend/src/assets/overview.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/assets/overviewA.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/assets/payment.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/frontend/src/assets/paymentA.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/frontend/src/assets/recentActivity/active.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/assets/recentActivity/activeD.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/assets/recentActivity/noneActive.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/assets/recentActivity/noneActiveD.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/assets/release.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/frontend/src/assets/releaseA.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/frontend/src/assets/right.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/assets/setting.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/assets/settingA.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/assets/states/i1.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/frontend/src/assets/states/i2.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/frontend/src/assets/states/i3.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/frontend/src/assets/states/i4.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/assets/statesd/i1.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/frontend/src/assets/statesd/i2.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/frontend/src/assets/statesd/i3.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/frontend/src/assets/statesd/i4.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/assets/statesd/i5.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/assets/upload.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/frontend/src/assets/uploadD.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/frontend/src/assets/user.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/frontend/src/assets/userA.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/frontend/src/assets/wave.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/frontend/src/components/Action.jsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState} from "react";
2 | import {httpGet, httpPost} from "../util/api";
3 | import {useHistory} from "react-router-dom";
4 | import {BsChevronDown, BsChevronUp} from "react-icons/all";
5 | import {MultiSelect} from "react-multi-select-component";
6 |
7 | const Action = ({setLoading, type}) => {
8 | const [error, setError] = useState(null)
9 | const [formData, setFormData] = useState({
10 | name: "",
11 | schema: "",
12 | table: "",
13 | type: "HTTP",
14 | events: [],
15 | url: "",
16 | body: "",
17 | });
18 | const [collapse, setCollapse] = useState(true)
19 | const [tables, setTables] = useState({})
20 | const [users, setUsers] = useState([]);
21 | const [selectedUser, setSelectedUser] = useState([]);
22 | const history = useHistory()
23 | const queryParams = new URLSearchParams(window.location.search)
24 | const id = queryParams.get("id")
25 | const events = ["INSERT", "UPDATE", "DELETE"]
26 | const typeAudit = "AUDIT"
27 | const handleChange = (e) => {
28 | setFormData({
29 | ...formData,
30 | [e.target.name]: e.target.value,
31 | });
32 | };
33 | const handleEvents = (i) => {
34 | setFormData({
35 | ...formData,
36 | events: formData.events.includes(i)
37 | ? formData.events.filter((content) => content !== i)
38 | : [...formData.events, i],
39 | });
40 | };
41 | // TODO: Clean this up
42 | useEffect(() => {
43 | httpGet("table/list", (data) => {
44 | setTables(data)
45 | httpGet("db-user/list", (data) => {
46 | setUsers(data.map((userName) => {
47 | return {label: userName, value: userName}
48 | }))
49 | if(!id) {
50 | return
51 | }
52 | httpGet(`action?id=${id}`, (data) => {
53 | setFormData({
54 | ...formData,
55 | name: data.name,
56 | schema: data.schema,
57 | table: data.table,
58 | events: data.trigger_events,
59 | type: data.action.type,
60 | url: data.action.url,
61 | body: data.action.body,
62 | })
63 | if(data.filters && data.filters.exclude_users) {
64 | setSelectedUser(data.filters.exclude_users.map((userName) => {
65 | return {label: userName, value: userName}
66 | }))
67 | }
68 | }, (data) => {
69 | console.log(data)
70 | history.push("/")
71 | // TODO: Return error page in case of failure here
72 | })
73 | }, (data) => {
74 | console.log(data)
75 | })
76 | }, (data) => {
77 | console.log(data)
78 | // TODO: Return error page in case of failure here
79 | })
80 | }, []);
81 |
82 | const createAction = () => {
83 | setError(null)
84 | // setLoading(true)
85 | console.log(formData)
86 | const body = {
87 | "name": formData.name,
88 | "table": formData.table,
89 | "schema": formData.schema,
90 | "trigger_events": formData.type === typeAudit ? events : formData.events,
91 | "action": {
92 | "type": formData.type,
93 | "url": formData.type === typeAudit ? "" : formData.url,
94 | "method": formData.type === typeAudit ? "" : "POST",
95 | "body": formData.type === typeAudit ? "" : formData.body
96 | },
97 | "filters": {
98 | "exclude_users": selectedUser.map(u => u.value)
99 | }
100 | }
101 | let path = "action"
102 | if(id) {
103 | path = path + "?id=" + id
104 | }
105 | httpPost(path, body, (data) => {
106 | history.push("/")
107 | // setTimeout(function() {
108 | // // setLoading(false);
109 | // }, 100);
110 | }, (data) => {
111 | // setLoading(false);
112 | if(data.error) {
113 | setError(data.error)
114 | } else {
115 | setError("An unknown error occurred")
116 | }
117 | })
118 | }
119 |
120 | return (
121 |
122 |
123 |
124 |
125 |
126 |
133 |
134 |
135 |
136 |
144 |
145 |
146 |
147 |
158 |
159 |
160 |
161 |
162 |
163 | {events.map((content) => {
164 | return (
165 |
handleEvents(content)}
167 | className="d-flex align-items-center pointer"
168 | >
169 | {formData.events.includes(content) ? (
170 |
171 | ) : (
172 |
173 | )}
174 |
175 |
176 | );
177 | })}
178 |
179 |
180 |
181 |
182 |
183 |
187 |
188 |
189 |
190 |
193 |
194 |
195 |
202 |
203 |
204 |
205 |
214 |
215 |
216 |
217 |
setCollapse(!collapse)}>
218 | {collapse ?
:
}
219 |
{collapse ? 'Show' : 'Hide'} Filters
220 |
221 |
222 |
223 |
224 |
225 |
s.label).join(", ")}}
234 | />
235 |
236 |
237 | {error ? (
238 |
239 | Error: {error}
240 |
241 | ) : null}
242 |
243 |
249 |
250 |
251 |
252 | );
253 | };
254 |
255 | export default Action;
256 |
--------------------------------------------------------------------------------
/frontend/src/components/ActionTable.jsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState} from "react";
2 | import {httpDelete, httpGet} from "../util/api";
3 | import {RiDeleteBinLine, RiEditBoxLine} from "react-icons/all";
4 | import {useHistory} from "react-router-dom";
5 |
6 | const ActionTable = () => {
7 | const [actions, setActions] = useState({});
8 | const history = useHistory();
9 |
10 | useEffect(() => {
11 | httpGet("action/list", (data) => {
12 | setActions(data)
13 | }, (data) => {
14 | console.log(data)
15 | // TODO: Return error page in case of failure here
16 | })
17 | }, []);
18 |
19 | const deleteAction = (id) => {
20 | httpDelete("action", {"id": id}, (data) => {
21 | window.location.reload()
22 | }, (data) => {
23 | console.log(data)
24 | // TODO: Return error page in case of failure here
25 | window.location.reload()
26 | })
27 | }
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
35 | Name |
36 | Table |
37 | Events |
38 | URL |
39 | |
40 | |
41 |
42 |
43 |
44 | {Object.entries(actions).map(([id, action]) => {
45 | return (
46 |
47 | {action.name} |
48 | {action.table} |
49 | {action.trigger_events.join(", ")} |
50 | {action.action.url} |
51 |
52 | history.push(`/edit-action?id=${id}`)}
56 | />
57 | |
58 |
59 | deleteAction(id)}
63 | />
64 | |
65 |
66 | );
67 | })}
68 |
69 |
70 |
71 |
72 | );
73 | };
74 |
75 | export default ActionTable;
76 |
--------------------------------------------------------------------------------
/frontend/src/components/AuditTable.jsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState} from "react";
2 | import {httpGet} from "../util/api";
3 | import {BsChevronDown, BsChevronUp} from "react-icons/all";
4 |
5 | const AuditTable = () => {
6 | const [audits, setAudits] = useState({});
7 | const [rowExpand, setRowExpand] = useState({})
8 | const [render, setRender] = useState(false)
9 |
10 | useEffect(() => {
11 | httpGet("audit/list", (data) => {
12 | setAudits(data)
13 | setRender(true)
14 | }, (data) => {
15 | console.log(data)
16 | setRender(true)
17 | // TODO: Return error page in case of failure here
18 | })
19 | }, []);
20 |
21 | if(!render) {
22 | return null
23 | }
24 | if(Object.keys(audits).length === 0) {
25 | return (
26 |
27 | {/* LOGIN FORM START */}
28 |
29 |
30 |
31 |
Track Database Changes
32 |
33 |
34 |
35 |
No audit logs exist. Create an action with the type "Audit Log" to get
36 | started.
37 |
38 |
39 |
40 |
41 | )
42 | }
43 | return (
44 |
45 |
46 |
47 |
48 |
49 | Time |
50 | Table |
51 | User |
52 | Event |
53 | Changed |
54 |
55 |
56 |
57 | {Object.entries(audits).map(([id, action]) => {
58 | const changed = action.changed
59 | let changedColumns = []
60 | if(changed) {
61 | changedColumns = Object.entries(changed).map(([k]) => k)
62 | }
63 | const date = new Date(Number(id) / 1000)
64 | return (
65 |
66 | {date.toLocaleString()} |
67 | {action.table} |
68 | {action.user} |
69 | {action.event} |
70 |
71 | {changedColumns.length === 0 ? null : (
72 |
73 | setRowExpand({...rowExpand, [id]: !rowExpand[id]})}>
75 | {!rowExpand[id] ? : }
76 |
77 |
78 | {changedColumns.join(", ")}
79 |
80 |
81 | {Object.entries(changed).map(([columnName, values]) => {
82 | return (
83 |
84 |
85 |
86 | {columnName}
88 | |
89 |
90 |
91 |
92 |
93 | new: {values.new} |
95 |
96 |
97 | old: {values.old} |
99 |
100 |
101 |
102 | )
103 | })}
104 |
105 |
106 | )}
107 | |
108 |
109 | );
110 | })}
111 |
112 |
113 |
114 |
115 | );
116 | }
117 | ;
118 |
119 | export default AuditTable;
120 |
--------------------------------------------------------------------------------
/frontend/src/components/DatabaseSetup.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const DatabaseSetup = () => {
4 | return (
5 |
6 |
Setup
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | export default DatabaseSetup;
59 |
--------------------------------------------------------------------------------
/frontend/src/components/SettingsDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const DisplaySettings = ({setMode, mode, localMode}) => {
4 | return (
5 |
6 |
Display Settings
7 |
8 |
9 |
10 |
11 |
28 |
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/frontend/src/components/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect} from "react";
2 | import {NavLink} from "react-router-dom";
3 | import {FaRegTimesCircle} from "react-icons/fa";
4 | import logoD from "../assets/logoD.svg";
5 | import logoW from "../assets/logoW.svg";
6 |
7 | const Sidebar = ({sideBar, setSideBar}) => {
8 | useEffect(() => {
9 | if(sideBar) {
10 | document.body.style.overflow = "hidden";
11 | } else {
12 | document.body.style.overflow = "auto";
13 | }
14 | }, [sideBar]);
15 |
16 | return (
17 |
22 |
23 |
24 |
25 |

26 |

27 |
28 |
29 |
setSideBar(!sideBar)}
31 | fontSize="1.8rem"
32 | className="pointer hamb color3"
33 | />
34 |
35 |
36 |
37 |
Manage
38 |
39 |
43 |
44 |
45 |
46 |
47 |
Maintenence
48 |
49 |
53 |
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | export default Sidebar;
61 |
--------------------------------------------------------------------------------
/frontend/src/components/Welcome.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {NavLink} from "react-router-dom";
3 | import logoD from "../assets/logoD.svg";
4 | import logoW from "../assets/logoW.svg";
5 |
6 | const Welcome = ({setLoading}) => {
7 | return (
8 |
9 |
10 |
11 |

12 |

13 |
14 |
15 |
16 |
Welcome to DB Webhooks!
17 |
18 | Looks like everything's working. Let's connect to your database and
19 | start triggering actions.
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default Welcome;
36 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 | import {BrowserRouter as Route} from "react-router-dom";
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById("root")
11 | );
12 |
--------------------------------------------------------------------------------
/frontend/src/layout/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {FiSettings} from "react-icons/fi";
3 | import {NavLink} from "react-router-dom";
4 | import logoD from "../assets/logoD.svg";
5 | import logoW from "../assets/logoW.svg";
6 |
7 | const Layout = ({children}) => {
8 | return (
9 |
10 |
11 |
13 |
14 |
15 |

16 |

17 |
18 |
19 |
25 |
31 |
32 |
33 |
37 |
38 |
39 |
40 | {/* CHILDREN */}
41 | {children}
42 |
43 |
44 | );
45 | };
46 |
47 | export default Layout;
48 |
--------------------------------------------------------------------------------
/frontend/src/pages/ConnectDatabase.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from "react";
2 | import wave from "../assets/wave.svg";
3 | import {httpPost} from "../util/api";
4 | import {useHistory} from "react-router-dom";
5 |
6 |
7 | const ConnectDatabase = ({setLoading}) => {
8 | const [databaseInfo, setDatabaseInfo] = useState({
9 | host: "",
10 | port: 5432,
11 | database: "",
12 | user: "",
13 | password: "",
14 | });
15 | const [error, setError] = useState(null)
16 | const history = useHistory();
17 | const handleChange = (event) => {
18 | setDatabaseInfo({...databaseInfo, [event.target.name]: event.target.value});
19 | };
20 | const handleSubmit = (event) => {
21 | setError(null)
22 | setLoading(true)
23 | databaseInfo.port = Number(databaseInfo.port)
24 | httpPost("connection", databaseInfo,
25 | (data) => {
26 | setTimeout(function() {
27 | setLoading(false);
28 | history.push("/")
29 | }, 500);
30 | }, (data) => {
31 | setLoading(false);
32 | if(data.error) {
33 | setError(data.error)
34 | } else {
35 | setError("An unknown error occurred")
36 | }
37 | })
38 | };
39 |
40 | return (
41 |
42 | {/* LOGIN FORM START */}
43 |
44 |
45 |
46 |
Connect to Your Database
47 |
48 |
49 |
132 |
133 |
134 | {/*
*/}
135 | {/* LOGIN FORM END */}
136 |
137 | {/* BOTTOM IMAGE START */}
138 |
139 |

140 |
141 | {/* BOTTOM IMAGE END */}
142 |
143 | );
144 | };
145 |
146 | export default ConnectDatabase;
147 |
--------------------------------------------------------------------------------
/frontend/src/pages/LayoutAction.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Layout from "../layout";
3 | import Action from "../components/Action";
4 |
5 | const LayoutAction = ({localMode, setLoading, type}) => {
6 | return (
7 |
8 |
9 |
{type} Action
10 | {/* BOTTOM SECTION */}
11 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default LayoutAction;
24 |
--------------------------------------------------------------------------------
/frontend/src/pages/LayoutActionTable.jsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState} from "react";
2 | import Layout from "../layout";
3 | import ActionTable from "../components/ActionTable";
4 | import {Link, Redirect} from "react-router-dom";
5 | import {httpGet} from "../util/api";
6 |
7 | const LayoutActionTable = () => {
8 | const [data, setData] = useState({});
9 | const [isLoading, setLoading] = useState(true);
10 |
11 | useEffect(() => {
12 | httpGet("connection", (data) => {
13 | setData(data);
14 | setLoading(false);
15 | }, (data) => {
16 | // TODO: Return error page in case of failure here
17 | setLoading(false);
18 | })
19 | }, []);
20 |
21 | if(isLoading) {
22 | return null
23 | }
24 | if(!data.host) {
25 | return ()
26 | }
27 | return (
28 |
29 |
30 |
31 |
Actions
32 |
33 | {/*
39 |
40 | {/* BOTTOM SECTION */}
41 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default LayoutActionTable;
54 |
--------------------------------------------------------------------------------
/frontend/src/pages/LayoutAuditTable.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Layout from "../layout";
3 | import AuditTable from "../components/AuditTable";
4 |
5 | const LayoutAuditTable = () => {
6 | return (
7 |
8 |
9 |
10 |
Audit Log
11 |
12 | {/* BOTTOM SECTION */}
13 |
20 |
21 |
22 | );
23 | };
24 |
25 | export default LayoutAuditTable;
26 |
--------------------------------------------------------------------------------
/frontend/src/pages/LayoutSettings.jsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect} from "react";
2 | import Layout from "../layout";
3 | import {DisplaySettings,} from "../components/SettingsDisplay";
4 |
5 | const LayoutSettings = ({setMode, mode, localMode}) => {
6 | useEffect(() => {
7 | }, [localMode, mode]);
8 | return (
9 |
10 |
11 |
Settings
12 |
13 | {/* BOTTOM SECTION */}
14 |
15 |
16 |
17 |
18 |
19 | {/*
20 |
*/}
21 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default LayoutSettings;
37 |
--------------------------------------------------------------------------------
/frontend/src/pages/LayoutWelcome.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import wave from "../assets/wave.svg";
3 |
4 | // COMPONENTS
5 | import Welcome from "../components/Welcome";
6 |
7 | const LayoutWelcome = ({setLoading}) => {
8 | return (
9 |
10 | {/* LOGIN FORM START */}
11 |
12 | {/* LOGIN FORM END */}
13 |
14 | {/* BOTTOM IMAGE START */}
15 |
16 |

17 |
18 | {/* BOTTOM IMAGE END */}
19 |
20 | );
21 | };
22 |
23 | export default LayoutWelcome;
24 |
--------------------------------------------------------------------------------
/frontend/src/pages/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import wave from "../assets/wave.svg";
3 |
4 | const NotFound = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
Page Not Found
11 |
12 | Click here to return home.
13 |
14 |
15 |
16 |
17 |
18 |

19 |
20 |
21 | );
22 | };
23 |
24 | export default NotFound;
25 |
--------------------------------------------------------------------------------
/frontend/src/styles/style.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["style.scss","style.css"],"names":[],"mappings":"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;EDwZA;IACE,WAAA;ECtZF;EDwZA;IACE,gBAAA;ECtZF;EDwZA;IACE,gBAAA;ECtZF;EDwZA;IACE,gBAAA;ECtZF;EDwZA;IACE,gBAAA;ECtZF;EDwZA;IACE,gBAAA;ECtZF;EDwZA;IACE,gBAAA;ECtZF;EDwZA;IACE,8BAAA;ECtZF;EDwZA;IACE,gBAAA;ECtZF;EDwZA;IACE,gBAAA;ECtZF;EDwZA;IACE,gBAAA;ECtZF;EDwZA;IACE,gBAAA;ECtZF;EDwZA;IACE,gBAAA;ECtZF;EDwZA;IACE,gBAAA;ECtZF;EDwZA;IACE,gBAAA;ECtZF;ED0ZA;IACE,WAAA;ECxZF;ED0ZA;IACE,gBAAA;ECxZF;ED0ZA;IACE,gBAAA;ECxZF;ED0ZA;IACE,gBAAA;ECxZF;ED0ZA;IACE,gBAAA;ECxZF;ED0ZA;IACE,gBAAA;ECxZF;ED0ZA;IACE,gBAAA;ECxZF;ED0ZA;IACE,gCAAA;ECxZF;ED0ZA;IACE,gBAAA;ECxZF;ED0ZA;IACE,gBAAA;ECxZF;ED0ZA;IACE,gBAAA;ECxZF;ED0ZA;IACE,gBAAA;ECxZF;ED0ZA;IACE,gBAAA;ECxZF;ED0ZA;IACE,gBAAA;ECxZF;ED0ZA;IACE,gBAAA;ECxZF;ED0ZA;IACE,gBAAA;ECxZF;AACF;AD2ZA;EACE;IACE,WAAA;ECzZF;ED2ZA;IACE,WAAA;ECzZF;ED2ZA;IACE,WAAA;ECzZF;ED6ZA;IACE,WAAA;EC3ZF;ED6ZA;IACE,WAAA;EC3ZF;ED6ZA;IACE,WAAA;EC3ZF;AACF","file":"style.css"}
--------------------------------------------------------------------------------
/frontend/src/util/HealthCheck.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import LoadingOverlay from "react-loading-overlay";
3 |
4 | import logoD from "../assets/logoD.svg";
5 | import logoW from "../assets/logoW.svg";
6 |
7 | import { getAPIBaseURL, healthCheck, httpGet } from "../util/api";
8 |
9 | const CheckBEHealth = ({ children }) => {
10 | const [connectionData, setConnectionData] = useState({});
11 | const [loading, setLoading] = useState(false);
12 | const [error, setError] = useState(null);
13 | const [render, setRender] = useState(false);
14 |
15 | useEffect(() => {
16 | setLoading(true);
17 | healthCheck(
18 | (data) => {
19 | setError(null);
20 | // Check to see if a connection has been set up
21 | httpGet(
22 | "connection",
23 | (data) => {
24 | setConnectionData(data);
25 | setLoading(false);
26 | setRender(true);
27 | },
28 | (data) => {
29 | setLoading(false);
30 | setRender(true);
31 | }
32 | );
33 | },
34 | (data) => {
35 | setError("error");
36 | setRender(true);
37 | setLoading(false);
38 | console.log(data);
39 | }
40 | );
41 | }, [setLoading]);
42 |
43 | if (loading) {
44 | return (
45 |
52 |
53 |
54 | );
55 | }
56 |
57 | if (error || !render) {
58 | return (
59 |
60 |
61 |
62 |

63 |

64 |
65 |
66 |
67 |
Connection Error
68 |
69 | The frontend was unable to connect to the API server
70 |
71 |
84 |
85 |
86 |
87 |
88 |
89 | );
90 | } else {
91 | return children;
92 | }
93 | };
94 |
95 | export default CheckBEHealth;
96 |
--------------------------------------------------------------------------------
/frontend/src/util/api.js:
--------------------------------------------------------------------------------
1 | const env = {
2 | ...process?.env,
3 | ...window?.__RUNTIME_CONFIG__,
4 | }
5 | const defaultBaseURL = getAPIBaseURL("v1")
6 | const authToken = getAPIAuthToken()
7 |
8 | export function getAPIBaseURL(version) {
9 | let url = process.env.REACT_APP_API_BASE_URL
10 | if(url) {
11 | if(!url.endsWith("/")) {
12 | url = url + "/"
13 | }
14 | return url + "api/" + (version ? version + "/" : "")
15 | }
16 | let host = window.location.host
17 | if(host.indexOf(":") > 0) {
18 | host = host.substring(0, host.indexOf(":"))
19 | }
20 | return `${window.location.protocol}//${host}:3003/api/${version ? version + "/" : ""}`
21 | }
22 |
23 | function getAPIAuthToken() {
24 | let token = env.REACT_APP_API_AUTH_TOKEN
25 | if(!token) {
26 | token = "db-webhooks"
27 | }
28 | return token
29 | }
30 |
31 | export function healthCheck(success, failure) {
32 | const requestOptions = {
33 | method: "GET",
34 | headers: {
35 | "Content-Type": "application/json",
36 | },
37 | };
38 | apiRequest(getAPIBaseURL(), "health", requestOptions, success, failure)
39 | }
40 |
41 | export function httpGet(path, success, failure) {
42 | const requestOptions = {
43 | method: "GET",
44 | headers: {
45 | "Content-Type": "application/json",
46 | "Authorization": authToken
47 | },
48 | };
49 | apiRequest(defaultBaseURL, path, requestOptions, success, failure)
50 | }
51 |
52 | export function httpPost(path, body, success, failure) {
53 | apiRequest(defaultBaseURL, path, getRequestOptions("POST", body), success, failure)
54 | }
55 |
56 | export function httpDelete(path, body, success, failure) {
57 | apiRequest(defaultBaseURL, path, getRequestOptions("DELETE", body), success, failure)
58 | }
59 |
60 | function getRequestOptions(method, body) {
61 | return {
62 | method: method,
63 | headers: {
64 | "Content-Type": "application/json",
65 | "Authorization": authToken
66 | },
67 | body: body ? JSON.stringify(body) : ""
68 | }
69 | }
70 |
71 | function apiRequest(baseURL, path, requestOptions, success, failure) {
72 | fetchWithTimeout(`${baseURL}${path}`, requestOptions)
73 | .then(response => {
74 | if(!response.ok) {
75 | return Promise.reject(response);
76 | }
77 | return response.json();
78 | })
79 | .then(data => {
80 | if(success !== undefined) {
81 | success(data)
82 | }
83 | })
84 | .catch(error => {
85 | if(typeof error.json === "function") {
86 | error.json().then(serverError => {
87 | if(failure !== undefined) {
88 | failure(serverError)
89 | }
90 | }).catch(_ => {
91 | if(failure !== undefined) {
92 | failure(error)
93 | }
94 | });
95 | } else {
96 | console.log(error);
97 | failure(error)
98 | }
99 | });
100 | }
101 |
102 | async function fetchWithTimeout(resource, options = {}) {
103 | const {timeout = 8000} = options;
104 | const controller = new AbortController();
105 | const id = setTimeout(() => controller.abort(), timeout);
106 | const response = await fetch(resource, {
107 | ...options,
108 | signal: controller.signal
109 | });
110 | clearTimeout(id);
111 | return response;
112 | }
113 |
--------------------------------------------------------------------------------