├── .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 | ![DB Webhooks Create Slack Notification](https://i.imgur.com/1xoorz9.gif) 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/assets/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/logoD.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/src/assets/logoW.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /frontend/src/assets/overview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/overviewA.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/payment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/assets/paymentA.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/assets/recentActivity/active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/recentActivity/activeD.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/recentActivity/noneActive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/recentActivity/noneActiveD.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/release.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/releaseA.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/setting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/settingA.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/states/i1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/assets/states/i2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/states/i3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/assets/states/i4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/statesd/i1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/statesd/i2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/assets/statesd/i3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/statesd/i4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/statesd/i5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/assets/uploadD.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/assets/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/userA.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/assets/wave.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 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 | 180 |
181 | 182 |
183 | 187 |
188 | 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 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {Object.entries(actions).map(([id, action]) => { 45 | return ( 46 | 47 | 48 | 49 | 50 | 51 | 58 | 65 | 66 | ); 67 | })} 68 | 69 |
NameTableEventsURL
{action.name}{action.table}{action.trigger_events.join(", ")}{action.action.url} 52 | history.push(`/edit-action?id=${id}`)} 56 | /> 57 | 59 | deleteAction(id)} 63 | /> 64 |
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 | 50 | 51 | 52 | 53 | 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 | 67 | 68 | 69 | 70 | 108 | 109 | ); 110 | })} 111 | 112 |
TimeTableUserEventChanged
{date.toLocaleString()}{action.table}{action.user}{action.event} 71 | {changedColumns.length === 0 ? null : ( 72 |
73 |
setRowExpand({...rowExpand, [id]: !rowExpand[id]})}> 75 | {!rowExpand[id] ? : } 76 |
77 |
78 |  {changedColumns.join(", ")} 79 |
80 | 105 |
106 | )} 107 |
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 |
40 |
41 |

Actions

42 |
43 |
44 |
45 |
46 |
47 |

Maintenence

48 | 49 |
50 |
51 |

Settings

52 |
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 |
20 | 21 | 23 | 24 |
25 |
26 | 27 |
28 |

Audit

29 |
30 |
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 |
50 |
e.preventDefault()}> 51 |
52 | 53 |
54 | 57 |
58 | 59 |
60 | 68 |
69 |
70 |
71 | 72 |
73 | 81 |
82 |
83 |
84 | 85 |
86 | 94 |
95 | 96 |
97 | 105 |
106 | 107 |
108 | 116 |
117 |
118 |
119 |
120 | 124 |
125 | {error ? ( 126 |

127 | Error: {error} 128 |

129 | ) : null} 130 | 131 |
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 |
12 |
13 |
14 | 15 |
16 |
17 |
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 | {/* 37 | 38 |
39 | 40 | {/* BOTTOM SECTION */} 41 |
42 |
43 |
44 | 45 |
46 |
47 |
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 |
14 |
15 |
16 | 17 |
18 |
19 |
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 | --------------------------------------------------------------------------------