Compare commits
255 Commits
0.1.0-alph
...
feat/open-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2bf6b8f91d | ||
![]() |
776c0fba8b | ||
![]() |
dd64aa2e79 | ||
![]() |
157b13baa7 | ||
![]() |
d1e284116d | ||
![]() |
2f9725d8e1 | ||
![]() |
ee7aea7bee | ||
![]() |
5d73df0040 | ||
![]() |
60cd317e67 | ||
![]() |
f5bdc8db39 | ||
![]() |
9eca697a91 | ||
![]() |
7136ee924d | ||
![]() |
fd9eb7c733 | ||
![]() |
917eaeb2ed | ||
![]() |
3bb90acc9e | ||
![]() |
a69b8e290c | ||
![]() |
674eeeea4e | ||
![]() |
8c2bf6ee0d | ||
![]() |
57bc091499 | ||
![]() |
128a2a8f75 | ||
![]() |
7b09a8817c | ||
![]() |
1d61840c6d | ||
![]() |
4b25e8941c | ||
![]() |
136eda15bf | ||
![]() |
eea6349318 | ||
![]() |
513b5d2948 | ||
![]() |
e61dc2f08a | ||
![]() |
07552bc0b1 | ||
![]() |
0787a3b494 | ||
![]() |
2946428ab8 | ||
![]() |
5c7d32ec16 | ||
![]() |
f0f2e0b6c8 | ||
![]() |
5399ea8f32 | ||
![]() |
4830a7e9ac | ||
![]() |
df1c56bb1c | ||
![]() |
b68d9ce661 | ||
![]() |
145091dce1 | ||
![]() |
ad46210112 | ||
![]() |
4e19f73845 | ||
![]() |
332269ecf9 | ||
![]() |
dfa96f09a0 | ||
![]() |
5bf26f7385 | ||
![]() |
1b269dc6db | ||
![]() |
ce9a115a14 | ||
![]() |
f2f4c72aa6 | ||
![]() |
9970eb16c9 | ||
![]() |
23e53286bd | ||
![]() |
47acff05e2 | ||
![]() |
5572928619 | ||
![]() |
85b4cd6339 | ||
![]() |
f0d38ab260 | ||
![]() |
1276af43ef | ||
![]() |
66d42fc2bc | ||
![]() |
1f058f954d | ||
![]() |
8259fb515c | ||
![]() |
6c2a4c3256 | ||
![]() |
51532657d7 | ||
![]() |
fd56fac353 | ||
![]() |
260f533b41 | ||
![]() |
c09ca3b810 | ||
![]() |
f07fb50eff | ||
![]() |
ea62a20c80 | ||
![]() |
2bb0ded20e | ||
![]() |
fec43f55f2 | ||
![]() |
271e901398 | ||
![]() |
c6f01ce839 | ||
![]() |
2dd404c02c | ||
![]() |
2f29bbcee4 | ||
![]() |
63a8c82535 | ||
![]() |
1f81e45e79 | ||
![]() |
40dcf67de9 | ||
![]() |
32d8b7c038 | ||
![]() |
2a91f3e7d8 | ||
![]() |
36d9861517 | ||
![]() |
d577a21a9a | ||
![]() |
115607cb6b | ||
![]() |
adb969ec04 | ||
![]() |
4e48320cf1 | ||
![]() |
cfe035e96b | ||
![]() |
34a91f3195 | ||
![]() |
ea14cc1743 | ||
![]() |
9d7f5fd9db | ||
![]() |
0520056e43 | ||
![]() |
1821e27692 | ||
![]() |
388530a69c | ||
![]() |
1e32d790b3 | ||
![]() |
681ffc65f1 | ||
![]() |
6331ec7b7a | ||
![]() |
25c9ce03bd | ||
![]() |
ac416bfc7b | ||
![]() |
0049e1380b | ||
![]() |
9bd185a9c6 | ||
![]() |
82bc38923c | ||
![]() |
8df8010b22 | ||
![]() |
b42cc1549a | ||
![]() |
4bc9059b0f | ||
![]() |
87b1cac979 | ||
![]() |
7f18a3f634 | ||
![]() |
0511e737ae | ||
![]() |
003d88fb6c | ||
![]() |
515b72f484 | ||
![]() |
cb96d2d8d1 | ||
![]() |
8a4b2feffe | ||
![]() |
13c038effd | ||
![]() |
38419a4ef4 | ||
![]() |
7785f98dcd | ||
![]() |
5ecc49f861 | ||
![]() |
3cb02dd62c | ||
![]() |
ddda237178 | ||
![]() |
3b4d0d9769 | ||
![]() |
c15b65b473 | ||
![]() |
e07448f670 | ||
![]() |
a596d91ce0 | ||
![]() |
f1b4141367 | ||
![]() |
7ce96367a3 | ||
![]() |
974622b9be | ||
![]() |
8bee841d66 | ||
![]() |
9363d83945 | ||
![]() |
75affcbf30 | ||
![]() |
f5aeda1283 | ||
![]() |
3d75a4a281 | ||
![]() |
f9ed91934e | ||
![]() |
266b9e34b6 | ||
![]() |
047e867faa | ||
![]() |
9d08d6c672 | ||
![]() |
91c35aa381 | ||
![]() |
2d819f5d3c | ||
![]() |
3221740198 | ||
![]() |
8e85d0ddbd | ||
![]() |
bb4052d1d2 | ||
![]() |
1e759c64ed | ||
![]() |
b3c7f783ed | ||
![]() |
e0ae6aa2e0 | ||
![]() |
37fe5071c5 | ||
![]() |
ca716ec1dd | ||
![]() |
2137d8ef5d | ||
![]() |
8178fa6b62 | ||
![]() |
303a3cbbbe | ||
![]() |
9173b340c8 | ||
![]() |
818790650a | ||
![]() |
eb5041008d | ||
![]() |
152ab6dfd5 | ||
![]() |
192070c18e | ||
![]() |
f7f1a3e4b3 | ||
![]() |
9c8e9baa39 | ||
![]() |
217410e9a4 | ||
![]() |
e35d0cbcd6 | ||
![]() |
d9c40057e6 | ||
![]() |
86bcb8ca87 | ||
![]() |
cf4d94a7aa | ||
![]() |
df192bed4d | ||
![]() |
8d2371c14e | ||
![]() |
5ed669e0da | ||
![]() |
43dce69cce | ||
![]() |
4c53eb97d2 | ||
![]() |
b4b8593879 | ||
![]() |
46a91fde20 | ||
![]() |
8f826e6c2f | ||
![]() |
ebfea707c5 | ||
![]() |
8fc0175166 | ||
![]() |
dc43f56db1 | ||
![]() |
e5761f1e42 | ||
![]() |
8dd8252a46 | ||
![]() |
7ee4715af2 | ||
![]() |
1b3f931074 | ||
![]() |
30cde3e521 | ||
![]() |
6e9370458b | ||
![]() |
beae4502d4 | ||
![]() |
969395ccdb | ||
![]() |
3ee79c3937 | ||
![]() |
508c714932 | ||
![]() |
8f7582e1ec | ||
![]() |
bdbbe4adee | ||
![]() |
65478296cb | ||
![]() |
2342f7c5c6 | ||
![]() |
8266c1cff5 | ||
![]() |
c662c625a0 | ||
![]() |
c989648327 | ||
![]() |
a933ac1118 | ||
![]() |
b8afe7abcc | ||
![]() |
3ab02cc4ff | ||
![]() |
bedc3d0b50 | ||
![]() |
1398762e1d | ||
![]() |
e0a77da773 | ||
![]() |
c3f4cd3bf9 | ||
![]() |
f110255310 | ||
![]() |
155d2e65c2 | ||
![]() |
4d341e9876 | ||
![]() |
1761f41691 | ||
![]() |
00565c8717 | ||
![]() |
74a551ae09 | ||
![]() |
cb5b02d777 | ||
![]() |
6ca37a0d50 | ||
![]() |
a9cf301344 | ||
![]() |
0305a719db | ||
![]() |
4269e2242c | ||
![]() |
71e4e35de6 | ||
![]() |
3aeb3b8d67 | ||
![]() |
e1951bfbe0 | ||
![]() |
1299ec5f9c | ||
![]() |
bc53974d2a | ||
![]() |
3ed5426467 | ||
![]() |
29251c8c20 | ||
![]() |
08b1f97ccb | ||
![]() |
abc2991e1c | ||
![]() |
b69d0b8e23 | ||
![]() |
86d781b210 | ||
![]() |
4649391169 | ||
![]() |
f4992010ed | ||
![]() |
b376ee3b73 | ||
![]() |
27944cf7b5 | ||
![]() |
cab0b54567 | ||
![]() |
a50f8eba57 | ||
![]() |
814dc61414 | ||
![]() |
7abad9db01 | ||
![]() |
17777b111d | ||
![]() |
c7494a0bca | ||
![]() |
e23d6f92e5 | ||
![]() |
585091fefb | ||
![]() |
eabd88718d | ||
![]() |
072cd46809 | ||
![]() |
17676fa13b | ||
![]() |
8b510ed556 | ||
![]() |
3ac0b44446 | ||
![]() |
b1dd6f2c3b | ||
![]() |
f5ea94f63c | ||
![]() |
653befc737 | ||
![]() |
6fed439ec2 | ||
![]() |
5bd6fa5bc9 | ||
![]() |
75709e9f48 | ||
![]() |
b1b7f47f4c | ||
![]() |
e429a1f860 | ||
![]() |
6819597a79 | ||
![]() |
b34b385be5 | ||
![]() |
5acf59d16e | ||
![]() |
12d795b4e8 | ||
![]() |
f67bb3a9fc | ||
![]() |
173a55137f | ||
![]() |
c7726bb1b2 | ||
![]() |
4c2c91a2bd | ||
![]() |
b3a52c2466 | ||
![]() |
1ba53e2c49 | ||
![]() |
21e3425e76 | ||
![]() |
195270525c | ||
![]() |
70c8353042 | ||
![]() |
0c1dedb11c | ||
![]() |
12efb901a1 | ||
![]() |
4f969a0288 | ||
![]() |
adf4f1902d | ||
![]() |
36ea6a0a72 | ||
![]() |
f901b1fe4f | ||
![]() |
82991b9949 | ||
![]() |
a6a46b8d95 | ||
![]() |
5667d8dbb6 | ||
![]() |
602c33a1eb |
@@ -6,4 +6,6 @@ README.md
|
|||||||
ROADMAP.md
|
ROADMAP.md
|
||||||
build
|
build
|
||||||
.env
|
.env
|
||||||
data.db
|
data.db
|
||||||
|
app/node_modules
|
||||||
|
app/build
|
||||||
|
@@ -1,7 +1,3 @@
|
|||||||
ENV=production
|
|
||||||
DATABASE_URL=data.db
|
DATABASE_URL=data.db
|
||||||
DATABASE_TYPE=sqlite
|
DATABASE_TYPE=sqlite
|
||||||
ADMIN_SECRET=admin
|
CUSTOM_ACCESS_TOKEN_SCRIPT="function(user,tokenPayload){var data = tokenPayload;data.extra = {'x-extra-id': user.id};return data;}"
|
||||||
DISABLE_EMAIL_VERIFICATION=true
|
|
||||||
JWT_SECRET=random_string
|
|
||||||
JWT_TYPE=HS256
|
|
194
.github/CONTRIBUTING.md
vendored
@@ -10,7 +10,7 @@ We're so excited you're interested in helping with Authorizer! We are happy to h
|
|||||||
## Where to ask questions?
|
## Where to ask questions?
|
||||||
|
|
||||||
1. Check our [Github Issues](https://github.com/authorizerdev/authorizer/issues) to see if someone has already answered your question.
|
1. Check our [Github Issues](https://github.com/authorizerdev/authorizer/issues) to see if someone has already answered your question.
|
||||||
2. Join our community on [Discord](https://discord.gg/WDvCxwkX) and feel free to ask us your questions
|
2. Join our community on [Discord](https://discord.gg/Zv2D5h6kkK) and feel free to ask us your questions
|
||||||
|
|
||||||
As you gain experience with Authorizer, please help answer other people's questions! :pray:
|
As you gain experience with Authorizer, please help answer other people's questions! :pray:
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ As you gain experience with Authorizer, please help answer other people's questi
|
|||||||
You can get started by taking a look at our [Github issues](https://github.com/authorizerdev/authorizer/issues)
|
You can get started by taking a look at our [Github issues](https://github.com/authorizerdev/authorizer/issues)
|
||||||
If you find one that looks interesting and no one else is already working on it, comment on that issue and start contributing 🙂.
|
If you find one that looks interesting and no one else is already working on it, comment on that issue and start contributing 🙂.
|
||||||
|
|
||||||
Please ask as many questions as you need, either directly in the issue or on [Discord](https://discord.gg/WDvCxwkX). We're happy to help!:raised_hands:
|
Please ask as many questions as you need, either directly in the issue or on [Discord](https://discord.gg/Zv2D5h6kkK). We're happy to help!:raised_hands:
|
||||||
|
|
||||||
### Contributions that are ALWAYS welcome
|
### Contributions that are ALWAYS welcome
|
||||||
|
|
||||||
@@ -43,9 +43,189 @@ Please ask as many questions as you need, either directly in the issue or on [Di
|
|||||||
### Project Setup for Authorizer core
|
### Project Setup for Authorizer core
|
||||||
|
|
||||||
1. Fork the [authorizer](https://github.com/authorizerdev/authorizer) repository (**Skip this step if you have access to repo**)
|
1. Fork the [authorizer](https://github.com/authorizerdev/authorizer) repository (**Skip this step if you have access to repo**)
|
||||||
2. `git clone https://github.com/authorizerdev/authorizer.git`
|
2. Clone repo: `git clone https://github.com/authorizerdev/authorizer.git` or use the forked url from step 1
|
||||||
3. `cd authorizer`
|
3. Change directory to authorizer: `cd authorizer`
|
||||||
4. `cp .env.sample .env`. Check all the supported env [here](https://docs.authorizer.dev/core/env/)
|
5. Create Env file `cp .env.sample .env`. Check all the supported env [here](https://docs.authorizer.dev/core/env/)
|
||||||
5. Build the code `make clean && make`
|
6. Build Dashboard `make build-dashboard`
|
||||||
|
7. Build App `make build-app`
|
||||||
|
8. Build Server `make clean && make`
|
||||||
> Note: if you don't have [`make`](https://www.ibm.com/docs/en/aix/7.2?topic=concepts-make-command), you can `cd` into `server` dir and build using the `go build` command
|
> Note: if you don't have [`make`](https://www.ibm.com/docs/en/aix/7.2?topic=concepts-make-command), you can `cd` into `server` dir and build using the `go build` command
|
||||||
6. Run binary `./build/server`
|
9. Run binary `./build/server`
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Make sure you test before creating PR.
|
||||||
|
|
||||||
|
If you want to test for all the databases that authorizer supports you will have to run `mongodb` & `arangodb` instances locally.
|
||||||
|
|
||||||
|
Setup mongodb & arangodb using Docker
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run --name mongodb -d -p 27017:27017 mongo
|
||||||
|
|
||||||
|
// -e ARANGO_ROOT_PASSWORD=root
|
||||||
|
docker run --name arangodb -d -p 8529:8529 -e ARANGO_NO_AUTH=1 arangodb/arangodb:3.8.4
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: If you are not making any changes in db schema / db operations, you can disable those db tests [here](https://github.com/authorizerdev/authorizer/blob/main/server/__test__/resolvers_test.go#L14)
|
||||||
|
|
||||||
|
If you are adding new resolver,
|
||||||
|
|
||||||
|
1. create new resolver test file [here](https://github.com/authorizerdev/authorizer/tree/main/server/__test__)
|
||||||
|
Naming convention filename: `resolver_name_test.go` function name: `resolverNameTest(t *testing.T, s TestSetup)`
|
||||||
|
2. Add your tests [here](https://github.com/authorizerdev/authorizer/blob/main/server/__test__/resolvers_test.go#L38)
|
||||||
|
|
||||||
|
**Command to run tests:**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manual Testing:**
|
||||||
|
|
||||||
|
For manually testing using graphql playground, you can paste following queries and mutations in your playground and test it
|
||||||
|
|
||||||
|
```gql
|
||||||
|
mutation Signup {
|
||||||
|
signup(
|
||||||
|
params: {
|
||||||
|
email: "lakhan@yopmail.com"
|
||||||
|
password: "test"
|
||||||
|
confirm_password: "test"
|
||||||
|
given_name: "lakhan"
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
message
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
family_name
|
||||||
|
given_name
|
||||||
|
email
|
||||||
|
email_verified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation ResendEamil {
|
||||||
|
resend_verify_email(
|
||||||
|
params: { email: "lakhan@yopmail.com", identifier: "basic_auth_signup" }
|
||||||
|
) {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetVerifyRequests {
|
||||||
|
_verification_requests {
|
||||||
|
id
|
||||||
|
token
|
||||||
|
expires
|
||||||
|
identifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation VerifyEmail {
|
||||||
|
verify_email(params: { token: "" }) {
|
||||||
|
access_token
|
||||||
|
expires_at
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
given_name
|
||||||
|
email_verified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation Login {
|
||||||
|
login(params: { email: "lakhan@yopmail.com", password: "test" }) {
|
||||||
|
access_token
|
||||||
|
expires_at
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
family_name
|
||||||
|
given_name
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetSession {
|
||||||
|
session {
|
||||||
|
access_token
|
||||||
|
expires_at
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
given_name
|
||||||
|
family_name
|
||||||
|
email
|
||||||
|
email_verified
|
||||||
|
signup_methods
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation ForgotPassword {
|
||||||
|
forgot_password(params: { email: "lakhan@yopmail.com" }) {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation ResetPassword {
|
||||||
|
reset_password(
|
||||||
|
params: { token: "", password: "test", confirm_password: "test" }
|
||||||
|
) {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation UpdateProfile {
|
||||||
|
update_profile(params: { family_name: "samani" }) {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetUsers {
|
||||||
|
_users {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
email_verified
|
||||||
|
given_name
|
||||||
|
family_name
|
||||||
|
picture
|
||||||
|
signup_methods
|
||||||
|
phone_number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation MagicLinkLogin {
|
||||||
|
magic_link_login(params: { email: "test@yopmail.com" }) {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation Logout {
|
||||||
|
logout {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation UpdateUser {
|
||||||
|
_update_user(
|
||||||
|
params: {
|
||||||
|
id: "dafc9400-d603-4ade-997c-83fcd54bbd67"
|
||||||
|
roles: ["user", "admin"]
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
email
|
||||||
|
roles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation DeleteUser {
|
||||||
|
_delete_user(params: { email: "signup.test134523@yopmail.com" }) {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
69
.github/workflows/release.yaml
vendored
@@ -8,53 +8,64 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
- uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: '^1.17.3'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install build-essential wget zip gcc-mingw-w64 && \
|
sudo apt-get install build-essential wget zip gcc-mingw-w64 && \
|
||||||
sudo apt-get remove --auto-remove golang-go && \
|
echo "/usr/bin/x86_64-w64-mingw32-gcc" >> GITHUB_PATH && \
|
||||||
sudo rm -rf /usr/bin/go &&\
|
|
||||||
wget --progress=dot:mega https://golang.org/dl/go1.17.1.linux-amd64.tar.gz -O go-linux.tar.gz && \
|
|
||||||
sudo tar -zxf go-linux.tar.gz && \
|
|
||||||
sudo mv go /usr/bin/ && \
|
|
||||||
sudo mkdir -p /go/bin /go/src /go/pkg && \
|
|
||||||
export GO_HOME=/usr/bin/go && \
|
|
||||||
export GOPATH=/go && \
|
|
||||||
export PATH=${GOPATH}/bin:${GO_HOME}/bin/:$PATH && \
|
|
||||||
echo "/usr/bin/go/bin" >> $GITHUB_PATH
|
|
||||||
go version && \
|
|
||||||
wget --no-check-certificate --progress=dot:mega https://github.com/wangyoucao577/assets-uploader/releases/download/v0.3.0/github-assets-uploader-v0.3.0-linux-amd64.tar.gz -O github-assets-uploader.tar.gz && \
|
wget --no-check-certificate --progress=dot:mega https://github.com/wangyoucao577/assets-uploader/releases/download/v0.3.0/github-assets-uploader-v0.3.0-linux-amd64.tar.gz -O github-assets-uploader.tar.gz && \
|
||||||
tar -zxf github-assets-uploader.tar.gz && \
|
tar -zxf github-assets-uploader.tar.gz && \
|
||||||
sudo mv github-assets-uploader /usr/sbin/ && \
|
sudo mv github-assets-uploader /usr/sbin/ && \
|
||||||
sudo rm -f github-assets-uploader.tar.gz && \
|
sudo rm -f github-assets-uploader.tar.gz && \
|
||||||
github-assets-uploader -version
|
github-assets-uploader -version && \
|
||||||
|
make build-app && \
|
||||||
|
make build-dashboard
|
||||||
- name: Print Go paths
|
- name: Print Go paths
|
||||||
run: whereis go
|
run: whereis go
|
||||||
- name: Print Go Version
|
- name: Print Go Version
|
||||||
run: go version
|
run: go version
|
||||||
- name: Set VERSION env
|
- name: Set VERSION env
|
||||||
run: echo VERSION=$(basename ${GITHUB_REF}) >> ${GITHUB_ENV}
|
run: echo VERSION=$(basename ${GITHUB_REF}) >> ${GITHUB_ENV}
|
||||||
- name: Create build
|
- name: Copy .env file
|
||||||
run: make clean && make
|
run: mv .env.sample .env
|
||||||
- name: Package files for windows
|
- name: Package files for windows
|
||||||
run: |
|
run: |
|
||||||
make clean && CGO_ENABLED=1 GOOS=windows CC=/usr/bin/x86_64-w64-mingw32-gcc make
|
make clean && \
|
||||||
mv .env.sample .env && \
|
CGO_ENABLED=1 GOOS=windows CC=/usr/bin/x86_64-w64-mingw32-gcc make && \
|
||||||
zip -vr authorizer-${VERSION}-windows-amd64.zip .env app build templates
|
mv build/server build/server.exe && \
|
||||||
|
zip -vr authorizer-${VERSION}-windows-amd64.zip .env app/build build templates dashboard/build
|
||||||
- name: Package files for linux
|
- name: Package files for linux
|
||||||
run: |
|
run: |
|
||||||
make clean && CGO_ENABLED=1 make
|
make clean && \
|
||||||
mv .env.sample .env && \
|
CGO_ENABLED=1 make && \
|
||||||
tar cvfz authorizer-${VERSION}-linux-amd64.tar.gz .env app build templates
|
tar cvfz authorizer-${VERSION}-linux-amd64.tar.gz .env app/build build templates dashboard/build
|
||||||
- name: Upload assets
|
- name: Upload assets
|
||||||
run: |
|
run: |
|
||||||
github-assets-uploader -f authorizer-${VERSION}-windows-amd64.zip -mediatype application/zip -repo authorizerdev/authorizer -token ${{secrets.RELEASE_TOKEN}} -tag ${VERSION} && \
|
github-assets-uploader -f authorizer-${VERSION}-windows-amd64.zip -mediatype application/zip -repo authorizerdev/authorizer -token ${{secrets.RELEASE_TOKEN}} -tag ${VERSION} && \
|
||||||
github-assets-uploader -f authorizer-${VERSION}-linux-amd64.tar.gz -mediatype application/gzip -repo authorizerdev/authorizer -token ${{secrets.RELEASE_TOKEN}} -tag ${VERSION} && \
|
github-assets-uploader -f authorizer-${VERSION}-linux-amd64.tar.gz -mediatype application/gzip -repo authorizerdev/authorizer -token ${{secrets.RELEASE_TOKEN}} -tag ${VERSION}
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
# - uses: wangyoucao577/go-release-action@v1.20
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
# with:
|
id: meta
|
||||||
# github_token: ${{ secrets.RELEASE_TOKEN }}
|
uses: docker/metadata-action@v3
|
||||||
# goos: ${{ matrix.goos }}
|
with:
|
||||||
# goarch: ${{ matrix.goarch }}
|
images: lakhansamani/authorizer
|
||||||
# build_command: make clean && make
|
|
||||||
# md5sum: FALSE
|
- name: Build and push Docker image
|
||||||
# extra_files: .env.sample app build template
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||||
|
6
.gitignore
vendored
@@ -2,7 +2,13 @@ server/server
|
|||||||
server/.env
|
server/.env
|
||||||
data
|
data
|
||||||
app/node_modules
|
app/node_modules
|
||||||
|
app/build
|
||||||
|
dashboard/node_modules
|
||||||
|
dashboard/build
|
||||||
build
|
build
|
||||||
.env
|
.env
|
||||||
data.db
|
data.db
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
*.tar.gz
|
||||||
|
.vscode/
|
29
Dockerfile
@@ -1,20 +1,33 @@
|
|||||||
FROM golang:1.16-alpine as builder
|
FROM golang:1.17-alpine as go-builder
|
||||||
WORKDIR /app
|
WORKDIR /authorizer
|
||||||
COPY server server
|
COPY server server
|
||||||
COPY Makefile .
|
COPY Makefile .
|
||||||
|
|
||||||
ARG VERSION=0.1.0-beta.0
|
ARG VERSION="latest"
|
||||||
ENV VERSION="${VERSION}"
|
ENV VERSION="$VERSION"
|
||||||
|
|
||||||
|
RUN echo "$VERSION"
|
||||||
RUN apk add build-base &&\
|
RUN apk add build-base &&\
|
||||||
make clean && make && \
|
make clean && make && \
|
||||||
chmod 777 build/server
|
chmod 777 build/server
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM node:17-alpine3.12 as node-builder
|
||||||
RUN apk --no-cache add ca-certificates
|
WORKDIR /authorizer
|
||||||
WORKDIR /root/
|
|
||||||
COPY app app
|
COPY app app
|
||||||
|
COPY dashboard dashboard
|
||||||
|
COPY Makefile .
|
||||||
|
RUN apk add build-base &&\
|
||||||
|
make build-app && \
|
||||||
|
make build-dashboard
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
WORKDIR /root/
|
||||||
|
RUN mkdir app dashboard
|
||||||
|
COPY --from=node-builder /authorizer/app/build app/build
|
||||||
|
COPY --from=node-builder /authorizer/app/favicon_io app/favicon_io
|
||||||
|
COPY --from=node-builder /authorizer/dashboard/build dashboard/build
|
||||||
|
COPY --from=node-builder /authorizer/dashboard/favicon_io dashboard/favicon_io
|
||||||
|
COPY --from=go-builder /authorizer/build build
|
||||||
COPY templates templates
|
COPY templates templates
|
||||||
COPY --from=builder /app/build build
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
CMD [ "./build/server" ]
|
CMD [ "./build/server" ]
|
||||||
|
13
Makefile
@@ -2,6 +2,15 @@ DEFAULT_VERSION=0.1.0-local
|
|||||||
VERSION := $(or $(VERSION),$(DEFAULT_VERSION))
|
VERSION := $(or $(VERSION),$(DEFAULT_VERSION))
|
||||||
|
|
||||||
cmd:
|
cmd:
|
||||||
cd server && go build -ldflags "-w -X main.Version=$(VERSION)" -o '../build/server'
|
cd server && go build -ldflags "-w -X main.VERSION=$(VERSION)" -o '../build/server'
|
||||||
|
build-app:
|
||||||
|
cd app && npm i && npm run build
|
||||||
|
build-dashboard:
|
||||||
|
cd dashboard && npm i && npm run build
|
||||||
clean:
|
clean:
|
||||||
rm -rf build
|
rm -rf build
|
||||||
|
test:
|
||||||
|
cd server && go clean --testcache && go test -v ./test
|
||||||
|
generate:
|
||||||
|
cd server && go get github.com/99designs/gqlgen/cmd@v0.14.0 && go run github.com/99designs/gqlgen generate
|
||||||
|
|
146
README.md
@@ -1,21 +1,21 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://authorizer.dev">
|
<a href="https://authorizer.dev">
|
||||||
<img alt="Logo" src="https://github.com/authorizerdev/authorizer/blob/main/assets/logo.png" width="60" />
|
<img alt="Logo" src="https://authorizer.dev/images/logo.png" width="60" />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
Authorizer
|
Authorizer
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
**Authorizer** is an open-source authentication and authorization solution for your applications. Bring your database and have complete control over the user information. You can self-host authorizer instances and connect to any database (Currently supports [Postgres](https://www.postgresql.org/), [MySQL](https://www.mysql.com/), [SQLite](https://www.sqlite.org/index.html), [SQLServer](https://www.microsoft.com/en-us/sql-server/), [MongoDB](https://mongodb.com/), [ArangoDB](https://www.arangodb.com/)).
|
||||||
**Authorizer** is an open-source authentication and authorization solution for your applications. Bring your database and have complete control over the user information. You can self-host authorizer instances and connect to any SQL database.
|
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
- [Introduction](#introduction)
|
- [Introduction](#introduction)
|
||||||
- [Getting Started](#getting-started)
|
- [Getting Started](#getting-started)
|
||||||
- [Contributing](https://github.com/authorizerdev/authorizer/blob/main/.github/CONTRIBUTING.md)
|
- [Contributing](https://github.com/authorizerdev/authorizer/blob/main/.github/CONTRIBUTING.md)
|
||||||
- [Docs](http://docs.authorizer.dev/)
|
- [Docs](http://docs.authorizer.dev/)
|
||||||
- [Join Community](https://discord.gg/2fXUQN3E)
|
- [Join Community](https://discord.gg/Zv2D5h6kkK)
|
||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
|
|
||||||
@@ -28,16 +28,12 @@
|
|||||||
- ✅ Email verification
|
- ✅ Email verification
|
||||||
- ✅ APIs to update profile securely
|
- ✅ APIs to update profile securely
|
||||||
- ✅ Forgot password flow using email
|
- ✅ Forgot password flow using email
|
||||||
- ✅ Social logins (Google, Github, more coming soon)
|
- ✅ Social logins (Google, Github, Facebook, more coming soon)
|
||||||
|
- ✅ Role-based access management
|
||||||
## Project Status
|
- ✅ Password-less login with email and magic link
|
||||||
|
|
||||||
⚠️ **Authorizer is still an early beta! missing features and bugs are to be expected!** If you can stomach it, then bring authentication and authorization to your site today!
|
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- Password-less login with email and magic link
|
|
||||||
- Role-based access management system
|
|
||||||
- Support more JWT encryption algorithms (Currently supporting HS256)
|
- Support more JWT encryption algorithms (Currently supporting HS256)
|
||||||
- 2 Factor authentication
|
- 2 Factor authentication
|
||||||
- Back office (Admin dashboard to manage user)
|
- Back office (Admin dashboard to manage user)
|
||||||
@@ -65,20 +61,94 @@
|
|||||||
|
|
||||||
## Trying out Authorizer
|
## Trying out Authorizer
|
||||||
|
|
||||||
This guide helps you practice using Authorizer to evaluate it before you use it in a production environment. It includes instructions for installing the Authorizer server in standalone mode.
|
This guide helps you practice using Authorizer to evaluate it before you use it in a production environment. It includes instructions for installing the Authorizer server in local or standalone mode.
|
||||||
|
|
||||||
## Installing a simple instance of Authorizer
|
- [Install using source code](#install-using-source-code)
|
||||||
|
- [Install using binaries](#install-using-binaries)
|
||||||
|
- [Install instance on heroku](#install-instance-on-Heroku)
|
||||||
|
- [Install instance on railway.app](#install-instance-on-railway)
|
||||||
|
|
||||||
Deploy Authorizer using [heroku](https://github.com/authorizerdev/authorizer-heroku) and quickly play with it in 30seconds
|
## Install using source code
|
||||||
<br/><br/>
|
|
||||||
[](https://heroku.com/deploy?template=https://github.com/authorizerdev/authorizer-heroku)
|
### Prerequisites
|
||||||
|
|
||||||
|
- OS: Linux or macOS or windows
|
||||||
|
- Go: (Golang)(https://golang.org/dl/) >= v1.15
|
||||||
|
|
||||||
|
### Project Setup
|
||||||
|
|
||||||
|
1. Fork the [authorizer](https://github.com/authorizerdev/authorizer) repository (**Skip this step if you have access to repo**)
|
||||||
|
2. Clone repo: `git clone https://github.com/authorizerdev/authorizer.git` or use the forked url from step 1
|
||||||
|
3. Change directory to authorizer: `cd authorizer`
|
||||||
|
5. Create Env file `cp .env.sample .env`. Check all the supported env [here](https://docs.authorizer.dev/core/env/)
|
||||||
|
6. Build Dashboard `make build-dashboard`
|
||||||
|
7. Build App `make build-app`
|
||||||
|
8. Build Server `make clean && make`
|
||||||
|
> Note: if you don't have [`make`](https://www.ibm.com/docs/en/aix/7.2?topic=concepts-make-command), you can `cd` into `server` dir and build using the `go build` command
|
||||||
|
9. Run binary `./build/server`
|
||||||
|
|
||||||
|
## Install using binaries
|
||||||
|
|
||||||
|
Deploy / Try Authorizer using binaries. With each [Authorizer Release](https://github.com/authorizerdev/authorizer/releases)
|
||||||
|
binaries are baked with required deployment files and bundled. You can download a specific version of it for the following operating systems:
|
||||||
|
|
||||||
|
- Mac OSX
|
||||||
|
- Linux
|
||||||
|
|
||||||
|
### Step 1: Download and unzip bundle
|
||||||
|
|
||||||
|
- Download the Bundle for the specific OS from the [release page](https://github.com/authorizerdev/authorizer/releases)
|
||||||
|
|
||||||
|
> Note: For windows, we recommend running using docker image to run authorizer.
|
||||||
|
|
||||||
|
- Unzip using following command
|
||||||
|
|
||||||
|
- Mac / Linux
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tar -zxf AUTHORIZER_VERSION -c authorizer
|
||||||
|
```
|
||||||
|
|
||||||
|
- Change directory to `authorizer`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd authorizer
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Configure environment variables
|
||||||
|
|
||||||
|
Required environment variables are pre-configured in `.env` file. But based on the production requirements, please configure more environment variables. You can refer to [environment variables docs](/core/env) for more information.
|
||||||
|
|
||||||
|
### Step 3: Start Authorizer
|
||||||
|
|
||||||
|
- Run following command to start authorizer
|
||||||
|
|
||||||
|
- For Mac / Linux users
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./build/server
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: For mac users, you might have to give binary the permission to execute. Here is the command you can use to grant permission `xattr -d com.apple.quarantine build/server`
|
||||||
|
|
||||||
|
Deploy production ready Authorizer instance using one click deployment options available below
|
||||||
|
|
||||||
|
| **Infra provider** | **One-click link** | **Additional information** |
|
||||||
|
| :----------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------: |
|
||||||
|
| Railway.app | <a href="https://railway.app/new/template?template=https://github.com/authorizerdev/authorizer-railway&plugins=postgresql,redis"><img src="https://railway.app/button.svg" style="height: 44px" alt="Deploy on Railway"></a> | [docs](https://docs.authorizer.dev/deployment/railway) |
|
||||||
|
| Heroku | <a href="https://heroku.com/deploy?template=https://github.com/authorizerdev/authorizer-heroku"><img src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy to Heroku" style="height: 44px;"></a> | [docs](https://docs.authorizer.dev/deployment/heroku) |
|
||||||
|
| Render | [](https://render.com/deploy?repo=https://github.com/authorizerdev/authorizer-render) | [docs](https://docs.authorizer.dev/deployment/render) |
|
||||||
|
|
||||||
### Things to consider
|
### Things to consider
|
||||||
|
|
||||||
- For social logins, you will need respective social platform key and secret
|
- For social logins, you will need respective social platform key and secret
|
||||||
- For having verified users, you will need an SMTP server with an email address and password using which system can send emails. The system will send a verification link to an email address. Once an email is verified then, only able to access it.
|
- For having verified users, you will need an SMTP server with an email address and password using which system can send emails. The system will send a verification link to an email address. Once an email is verified then, only able to access it.
|
||||||
> Note: One can always disable the email verification to allow open sign up, which is not recommended for production as anyone can use anyone's email address 😅
|
> Note: One can always disable the email verification to allow open sign up, which is not recommended for production as anyone can use anyone's email address 😅
|
||||||
- For persisting user sessions, you will need Redis URL. If you do not configure a Redis server, sessions will be persisted until the instance is up or not restarted. For better response time on authorization requests/middleware, we recommend deploying Redis on the same infra/network as your authorizer server.
|
- For persisting user sessions, you will need Redis URL (not in case of railway.app). If you do not configure a Redis server, sessions will be persisted until the instance is up or not restarted. For better response time on authorization requests/middleware, we recommend deploying Redis on the same infra/network as your authorizer server.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Check the testing instructions [here](https://github.com/authorizerdev/authorizer/blob/main/.github/CONTRIBUTING.md#testing)
|
||||||
|
|
||||||
## Integrating into your website
|
## Integrating into your website
|
||||||
|
|
||||||
@@ -92,30 +162,36 @@ This example demonstrates how you can use [`@authorizerdev/authorizer-js`](/auth
|
|||||||
<script src="https://unpkg.com/@authorizerdev/authorizer-js/lib/authorizer.min.js"></script>
|
<script src="https://unpkg.com/@authorizerdev/authorizer-js/lib/authorizer.min.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const authorizerRef = new authorizerdev.Authorizer({
|
const authorizerRef = new authorizerdev.Authorizer({
|
||||||
authorizerURL: `AUTHORIZER_URL`,
|
authorizerURL: `AUTHORIZER_URL`,
|
||||||
redirectURL: window.location.origin,
|
redirectURL: window.location.origin,
|
||||||
});
|
});
|
||||||
|
|
||||||
// use the button selector as per your application
|
// use the button selector as per your application
|
||||||
const logoutBtn = document.getElementById("logout");
|
const logoutBtn = document.getElementById('logout');
|
||||||
logoutBtn.addEventListener("click", async function () {
|
logoutBtn.addEventListener('click', async function () {
|
||||||
await authorizerRef.logout();
|
await authorizerRef.logout();
|
||||||
window.location.href = "/";
|
window.location.href = '/';
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onLoad() {
|
async function onLoad() {
|
||||||
const res = await authorizerRef.fingertipLogin();
|
const res = await authorizerRef.browserLogin();
|
||||||
if (res && res.user) {
|
if (res && res.user) {
|
||||||
// you can use user information here, eg:
|
// you can use user information here, eg:
|
||||||
/**
|
/**
|
||||||
const userSection = document.getElementById('user');
|
const userSection = document.getElementById('user');
|
||||||
const logoutSection = document.getElementById('logout-section');
|
const logoutSection = document.getElementById('logout-section');
|
||||||
logoutSection.classList.toggle('hide');
|
logoutSection.classList.toggle('hide');
|
||||||
userSection.innerHTML = `Welcome, ${res.user.email}`;
|
userSection.innerHTML = `Welcome, ${res.user.email}`;
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onLoad();
|
onLoad();
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Support my work
|
||||||
|
|
||||||
|
<a href="https://www.buymeacoffee.com/lakhansamani" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
||||||
|
49
TODO.md
@@ -0,0 +1,49 @@
|
|||||||
|
# Task List
|
||||||
|
|
||||||
|
## Implement better way of handling jwt tokens
|
||||||
|
|
||||||
|
Check: https://hasura.io/blog/best-practices-of-using-jwt-with-graphql/#server-side-rendering-ssr
|
||||||
|
|
||||||
|
- [x] Set finger print in response cookie (https://github.com/hasura/jwt-guide/blob/60a7a86146d604fc48a799fffdee712be1c52cd0/lib/setFingerprintCookieAndSignJwt.ts#L8)
|
||||||
|
- [x] Save refresh token in session store
|
||||||
|
- [x] refresh token should be made more secure with the help of secure token rotation. Every time new token is requested new refresh token should be generated
|
||||||
|
- [x] Return jwt in response
|
||||||
|
- [x] To get session send finger print and refresh token [if they are valid -> a new access token is generated and sent to user]
|
||||||
|
- [x] Refresh token should be long living token (refresh token + finger print hash should be verified)
|
||||||
|
|
||||||
|
## Open ID compatible claims and schema
|
||||||
|
|
||||||
|
- [x] Rename `schema.graphqls` and re generate schema
|
||||||
|
- [x] Rename to snake case [files + schema]
|
||||||
|
- [x] Refactor db models
|
||||||
|
- [x] Check extra data in oauth profile and save accordingly
|
||||||
|
- [x] Update all the resolver to make them compatible with schema changes
|
||||||
|
- [x] Update JWT claims
|
||||||
|
- [x] Write integration tests for all resolvers
|
||||||
|
|
||||||
|
## Feature Multiple sessions
|
||||||
|
|
||||||
|
- Multiple sessions for users to login use hMset from redis for this
|
||||||
|
user_id access_token1 long_live_token1
|
||||||
|
user_id access_token2 long_live_token2
|
||||||
|
|
||||||
|
# Feature roles
|
||||||
|
|
||||||
|
For the first version we will only support setting roles master list via env
|
||||||
|
|
||||||
|
- [x] Support following ENV
|
||||||
|
- [x] `ROLES` -> comma separated list of role names
|
||||||
|
- [x] `DEFAULT_ROLE` -> default role to assign to users
|
||||||
|
- [x] Add roles input for signup
|
||||||
|
- [x] Add roles to update profile mutation
|
||||||
|
- [x] Add roles input for login
|
||||||
|
- [x] Return roles to user
|
||||||
|
- [x] Return roles in users list for super admin
|
||||||
|
- [x] Add roles to the JWT token generation
|
||||||
|
- [x] Validate token should also validate the role, if roles to validate again is present in request
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
|
||||||
|
- [x] Fix email template
|
||||||
|
- [x] Add support for organization name in .env
|
||||||
|
- [x] Add support for organization logo in .env
|
||||||
|
@@ -1,3 +1,14 @@
|
|||||||
# Authorizer APP
|
# Authorizer APP
|
||||||
|
|
||||||
App that can be used as login wall for your any application in combination with @authorizerdev/@authorizer.js
|
App that can be used as login wall for your any application in combination with @authorizerdev/@authorizer.js
|
||||||
|
|
||||||
|
### Getting started
|
||||||
|
|
||||||
|
**Setting up locally**
|
||||||
|
|
||||||
|
- `cd app`
|
||||||
|
- `npm start`
|
||||||
|
|
||||||
|
**Creating production build**
|
||||||
|
|
||||||
|
- `make build-app`
|
||||||
|
@@ -1,16 +0,0 @@
|
|||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
color: #374151;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*:before,
|
|
||||||
*:after {
|
|
||||||
box-sizing: inherit;
|
|
||||||
}
|
|
11
app/esbuild.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const __is_prod__ = process.env.NODE_ENV === 'production';
|
||||||
|
require('esbuild').build({
|
||||||
|
entryPoints: ['src/index.tsx'],
|
||||||
|
chunkNames: '[name]-[hash]',
|
||||||
|
bundle: true,
|
||||||
|
minify: __is_prod__,
|
||||||
|
outdir: 'build',
|
||||||
|
splitting: true,
|
||||||
|
format: 'esm',
|
||||||
|
watch: !__is_prod__,
|
||||||
|
});
|
BIN
app/favicon_io/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
app/favicon_io/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
app/favicon_io/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
app/favicon_io/favicon-16x16.png
Normal file
After Width: | Height: | Size: 528 B |
BIN
app/favicon_io/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
app/favicon_io/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
486
app/package-lock.json
generated
@@ -5,15 +5,17 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"name": "app",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@authorizerdev/authorizer-react": "^0.1.0-beta.8",
|
"@authorizerdev/authorizer-react": "latest",
|
||||||
"@types/react": "^17.0.15",
|
"@types/react": "^17.0.15",
|
||||||
"@types/react-dom": "^17.0.9",
|
"@types/react-dom": "^17.0.9",
|
||||||
"esbuild": "^0.12.17",
|
"esbuild": "^0.12.17",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-is": "^17.0.2",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"typescript": "^4.3.5"
|
"typescript": "^4.3.5"
|
||||||
},
|
},
|
||||||
@@ -22,9 +24,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@authorizerdev/authorizer-js": {
|
"node_modules/@authorizerdev/authorizer-js": {
|
||||||
"version": "0.1.0-beta.12",
|
"version": "0.4.0-beta.3",
|
||||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.1.0-beta.12.tgz",
|
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz",
|
||||||
"integrity": "sha512-vpZAWtnZz71q/Zn5qPbFqkId4Qe636jYrfTQPKl+PUW21UvJnpVcNuUUZM8VhNcIK+jVYnHLSWb9jJnuwZwrNg==",
|
"integrity": "sha512-OGZc6I6cnpi/WkSotkjVIc3LEzl8pFeiohr8+Db9xWd75/oTfOZqWRuIHTnTc1FC+6Sv2EjTJ9Aa6lrloWG+NQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-fetch": "^2.6.1"
|
"node-fetch": "^2.6.1"
|
||||||
},
|
},
|
||||||
@@ -33,11 +35,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@authorizerdev/authorizer-react": {
|
"node_modules/@authorizerdev/authorizer-react": {
|
||||||
"version": "0.1.0-beta.8",
|
"version": "0.9.0-beta.7",
|
||||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.1.0-beta.8.tgz",
|
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz",
|
||||||
"integrity": "sha512-5diOeAleZFA0uf/8IS3+D261VXoOBnN9kA4ZvfJa4M376br5CGESFRAyLRWT0iWskTLX9g3BfdHusUjVbBxfmg==",
|
"integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@authorizerdev/authorizer-js": "^0.1.0-beta.12",
|
"@authorizerdev/authorizer-js": "^0.4.0-beta.3",
|
||||||
"final-form": "^4.20.2",
|
"final-form": "^4.20.2",
|
||||||
"react-final-form": "^6.5.3",
|
"react-final-form": "^6.5.3",
|
||||||
"styled-components": "^5.3.0"
|
"styled-components": "^5.3.0"
|
||||||
@@ -50,22 +52,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
|
||||||
"integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==",
|
"integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/highlight": "^7.14.5"
|
"@babel/highlight": "^7.16.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator": {
|
"node_modules/@babel/generator": {
|
||||||
"version": "7.14.8",
|
"version": "7.16.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz",
|
||||||
"integrity": "sha512-cYDUpvIzhBVnMzRoY1fkSEhK/HmwEVwlyULYgn/tMQYd6Obag3ylCjONle3gdErfXBW61SVTlR9QR7uWlgeIkg==",
|
"integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.14.8",
|
"@babel/types": "^7.16.8",
|
||||||
"jsesc": "^2.5.1",
|
"jsesc": "^2.5.1",
|
||||||
"source-map": "^0.5.0"
|
"source-map": "^0.5.0"
|
||||||
},
|
},
|
||||||
@@ -74,87 +76,98 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-annotate-as-pure": {
|
"node_modules/@babel/helper-annotate-as-pure": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz",
|
||||||
"integrity": "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==",
|
"integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.14.5"
|
"@babel/types": "^7.16.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-environment-visitor": {
|
||||||
|
"version": "7.16.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz",
|
||||||
|
"integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.16.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-function-name": {
|
"node_modules/@babel/helper-function-name": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz",
|
||||||
"integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==",
|
"integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-get-function-arity": "^7.14.5",
|
"@babel/helper-get-function-arity": "^7.16.7",
|
||||||
"@babel/template": "^7.14.5",
|
"@babel/template": "^7.16.7",
|
||||||
"@babel/types": "^7.14.5"
|
"@babel/types": "^7.16.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-get-function-arity": {
|
"node_modules/@babel/helper-get-function-arity": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz",
|
||||||
"integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==",
|
"integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.14.5"
|
"@babel/types": "^7.16.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-hoist-variables": {
|
"node_modules/@babel/helper-hoist-variables": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz",
|
||||||
"integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==",
|
"integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.14.5"
|
"@babel/types": "^7.16.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-module-imports": {
|
"node_modules/@babel/helper-module-imports": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz",
|
||||||
"integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==",
|
"integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.14.5"
|
"@babel/types": "^7.16.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-split-export-declaration": {
|
"node_modules/@babel/helper-split-export-declaration": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz",
|
||||||
"integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==",
|
"integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.14.5"
|
"@babel/types": "^7.16.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.14.8",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
|
||||||
"integrity": "sha512-ZGy6/XQjllhYQrNw/3zfWRwZCTVSiBLZ9DHVZxn9n2gip/7ab8mv2TWlKPIBk26RwedCBoWdjLmn+t9na2Gcow==",
|
"integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/highlight": {
|
"node_modules/@babel/highlight": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz",
|
||||||
"integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==",
|
"integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.14.5",
|
"@babel/helper-validator-identifier": "^7.16.7",
|
||||||
"chalk": "^2.0.0",
|
"chalk": "^2.0.0",
|
||||||
"js-tokens": "^4.0.0"
|
"js-tokens": "^4.0.0"
|
||||||
},
|
},
|
||||||
@@ -163,9 +176,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.14.8",
|
"version": "7.16.12",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.12.tgz",
|
||||||
"integrity": "sha512-syoCQFOoo/fzkWDeM0dLEZi5xqurb5vuyzwIMNZRNun+N/9A4cUZeQaE7dTrB8jGaKuJRBtEOajtnmw0I5hvvA==",
|
"integrity": "sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
},
|
},
|
||||||
@@ -185,30 +198,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz",
|
||||||
"integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==",
|
"integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.14.5",
|
"@babel/code-frame": "^7.16.7",
|
||||||
"@babel/parser": "^7.14.5",
|
"@babel/parser": "^7.16.7",
|
||||||
"@babel/types": "^7.14.5"
|
"@babel/types": "^7.16.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/traverse": {
|
"node_modules/@babel/traverse": {
|
||||||
"version": "7.14.8",
|
"version": "7.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.10.tgz",
|
||||||
"integrity": "sha512-kexHhzCljJcFNn1KYAQ6A5wxMRzq9ebYpEDV4+WdNyr3i7O44tanbDOR/xjiG2F3sllan+LgwK+7OMk0EmydHg==",
|
"integrity": "sha512-yzuaYXoRJBGMlBhsMJoUW7G1UmSb/eXr/JHYM/MsOJgavJibLwASijW7oXBdw3NQ6T0bW7Ty5P/VarOs9cHmqw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.14.5",
|
"@babel/code-frame": "^7.16.7",
|
||||||
"@babel/generator": "^7.14.8",
|
"@babel/generator": "^7.16.8",
|
||||||
"@babel/helper-function-name": "^7.14.5",
|
"@babel/helper-environment-visitor": "^7.16.7",
|
||||||
"@babel/helper-hoist-variables": "^7.14.5",
|
"@babel/helper-function-name": "^7.16.7",
|
||||||
"@babel/helper-split-export-declaration": "^7.14.5",
|
"@babel/helper-hoist-variables": "^7.16.7",
|
||||||
"@babel/parser": "^7.14.8",
|
"@babel/helper-split-export-declaration": "^7.16.7",
|
||||||
"@babel/types": "^7.14.8",
|
"@babel/parser": "^7.16.10",
|
||||||
|
"@babel/types": "^7.16.8",
|
||||||
"debug": "^4.1.0",
|
"debug": "^4.1.0",
|
||||||
"globals": "^11.1.0"
|
"globals": "^11.1.0"
|
||||||
},
|
},
|
||||||
@@ -217,11 +231,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.14.8",
|
"version": "7.16.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.8.tgz",
|
||||||
"integrity": "sha512-iob4soQa7dZw8nodR/KlOQkPh9S4I8RwCxwRIFuiMRYjOzH/KJzdUfDgz6cGi5dDaclXF4P2PAhCdrBJNIg68Q==",
|
"integrity": "sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.14.8",
|
"@babel/helper-validator-identifier": "^7.16.7",
|
||||||
"to-fast-properties": "^2.0.0"
|
"to-fast-properties": "^2.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -318,12 +332,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/babel-plugin-styled-components": {
|
"node_modules/babel-plugin-styled-components": {
|
||||||
"version": "1.13.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.2.tgz",
|
||||||
"integrity": "sha512-Vb1R3d4g+MUfPQPVDMCGjm3cDocJEUTR7Xq7QS95JWWeksN1wdFRYpD2kulDgI3Huuaf1CZd+NK4KQmqUFh5dA==",
|
"integrity": "sha512-7eG5NE8rChnNTDxa6LQfynwgHTVOYYaHJbUYSlOhk8QBXIQiMBKq4gyfHBBKPrxUcVBXVJL61ihduCpCQbuNbw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-annotate-as-pure": "^7.0.0",
|
"@babel/helper-annotate-as-pure": "^7.16.0",
|
||||||
"@babel/helper-module-imports": "^7.0.0",
|
"@babel/helper-module-imports": "^7.16.0",
|
||||||
"babel-plugin-syntax-jsx": "^6.18.0",
|
"babel-plugin-syntax-jsx": "^6.18.0",
|
||||||
"lodash": "^4.17.11"
|
"lodash": "^4.17.11"
|
||||||
},
|
},
|
||||||
@@ -391,9 +405,9 @@
|
|||||||
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
|
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
|
||||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "2.1.2"
|
"ms": "2.1.2"
|
||||||
},
|
},
|
||||||
@@ -424,9 +438,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/final-form": {
|
"node_modules/final-form": {
|
||||||
"version": "4.20.2",
|
"version": "4.20.6",
|
||||||
"resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.6.tgz",
|
||||||
"integrity": "sha512-5i0IxqwjjPG1nUNCjWhqPCvQJJ2R+QwTwaAnjPmFnLbyjIHWuBPU8u+Ps4G3TcX2Sjno+O5xCZJzYcMJEzzfCQ==",
|
"integrity": "sha512-fCdwIj49KOaFfDRlXB57Eo+GghIMZQWrA9TakQI3C9uQxHwaFHXqZSNRlUdfnQmNNeySwGOaGPZCvjy58hyv4w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.10.0"
|
"@babel/runtime": "^7.10.0"
|
||||||
},
|
},
|
||||||
@@ -533,11 +547,22 @@
|
|||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
|
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "4.x || >=6.0.0"
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
@@ -557,9 +582,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-value-parser": {
|
"node_modules/postcss-value-parser": {
|
||||||
"version": "4.1.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
"integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ=="
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||||
},
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.7.2",
|
"version": "15.7.2",
|
||||||
@@ -602,26 +627,36 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-final-form": {
|
"node_modules/react-final-form": {
|
||||||
"version": "6.5.3",
|
"version": "6.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.7.tgz",
|
||||||
"integrity": "sha512-FCs6GC0AMWJl2p6YX7kM+a0AvuSLAZUgbVNtRBskOs4g984t/It0qGtx51O+9vgqnqk6JyoxmIzxKMq+7ch/vg==",
|
"integrity": "sha512-o7tvJXB+McGiXOILqIC8lnOcX4aLhIBiF/Xi9Qet35b7XOS8R7KL8HLRKTfnZWQJm6MCE15v1U0SFive0NcxyA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.1"
|
"@babel/runtime": "^7.15.4"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/final-form"
|
"url": "https://opencollective.com/final-form"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"final-form": "^4.20.0",
|
"final-form": "4.20.4",
|
||||||
"react": "^16.8.0 || ^17.0.0"
|
"react": "^16.8.0 || ^17.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-final-form/node_modules/@babel/runtime": {
|
||||||
|
"version": "7.16.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz",
|
||||||
|
"integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"regenerator-runtime": "^0.13.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
@@ -698,9 +733,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/styled-components": {
|
"node_modules/styled-components": {
|
||||||
"version": "5.3.0",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.3.tgz",
|
||||||
"integrity": "sha512-bPJKwZCHjJPf/hwTJl6TbkSZg/3evha+XPEizrZUGb535jLImwDUdjTNxXqjjaASt2M4qO4AVfoHJNe3XB/tpQ==",
|
"integrity": "sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-module-imports": "^7.0.0",
|
"@babel/helper-module-imports": "^7.0.0",
|
||||||
"@babel/traverse": "^7.4.5",
|
"@babel/traverse": "^7.4.5",
|
||||||
@@ -755,6 +790,11 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "4.3.5",
|
"version": "4.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
|
||||||
@@ -771,115 +811,137 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
||||||
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
|
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
|
||||||
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@authorizerdev/authorizer-js": {
|
"@authorizerdev/authorizer-js": {
|
||||||
"version": "0.1.0-beta.12",
|
"version": "0.4.0-beta.3",
|
||||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.1.0-beta.12.tgz",
|
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz",
|
||||||
"integrity": "sha512-vpZAWtnZz71q/Zn5qPbFqkId4Qe636jYrfTQPKl+PUW21UvJnpVcNuUUZM8VhNcIK+jVYnHLSWb9jJnuwZwrNg==",
|
"integrity": "sha512-OGZc6I6cnpi/WkSotkjVIc3LEzl8pFeiohr8+Db9xWd75/oTfOZqWRuIHTnTc1FC+6Sv2EjTJ9Aa6lrloWG+NQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"node-fetch": "^2.6.1"
|
"node-fetch": "^2.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@authorizerdev/authorizer-react": {
|
"@authorizerdev/authorizer-react": {
|
||||||
"version": "0.1.0-beta.8",
|
"version": "0.9.0-beta.7",
|
||||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.1.0-beta.8.tgz",
|
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz",
|
||||||
"integrity": "sha512-5diOeAleZFA0uf/8IS3+D261VXoOBnN9kA4ZvfJa4M376br5CGESFRAyLRWT0iWskTLX9g3BfdHusUjVbBxfmg==",
|
"integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@authorizerdev/authorizer-js": "^0.1.0-beta.12",
|
"@authorizerdev/authorizer-js": "^0.4.0-beta.3",
|
||||||
"final-form": "^4.20.2",
|
"final-form": "^4.20.2",
|
||||||
"react-final-form": "^6.5.3",
|
"react-final-form": "^6.5.3",
|
||||||
"styled-components": "^5.3.0"
|
"styled-components": "^5.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/code-frame": {
|
"@babel/code-frame": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
|
||||||
"integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==",
|
"integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/highlight": "^7.14.5"
|
"@babel/highlight": "^7.16.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/generator": {
|
"@babel/generator": {
|
||||||
"version": "7.14.8",
|
"version": "7.16.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz",
|
||||||
"integrity": "sha512-cYDUpvIzhBVnMzRoY1fkSEhK/HmwEVwlyULYgn/tMQYd6Obag3ylCjONle3gdErfXBW61SVTlR9QR7uWlgeIkg==",
|
"integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/types": "^7.14.8",
|
"@babel/types": "^7.16.8",
|
||||||
"jsesc": "^2.5.1",
|
"jsesc": "^2.5.1",
|
||||||
"source-map": "^0.5.0"
|
"source-map": "^0.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/helper-annotate-as-pure": {
|
"@babel/helper-annotate-as-pure": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz",
|
||||||
"integrity": "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==",
|
"integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/types": "^7.14.5"
|
"@babel/types": "^7.16.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@babel/helper-environment-visitor": {
|
||||||
|
"version": "7.16.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz",
|
||||||
|
"integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/types": "^7.16.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/helper-function-name": {
|
"@babel/helper-function-name": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz",
|
||||||
"integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==",
|
"integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/helper-get-function-arity": "^7.14.5",
|
"@babel/helper-get-function-arity": "^7.16.7",
|
||||||
"@babel/template": "^7.14.5",
|
"@babel/template": "^7.16.7",
|
||||||
"@babel/types": "^7.14.5"
|
"@babel/types": "^7.16.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/helper-get-function-arity": {
|
"@babel/helper-get-function-arity": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz",
|
||||||
"integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==",
|
"integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/types": "^7.14.5"
|
"@babel/types": "^7.16.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/helper-hoist-variables": {
|
"@babel/helper-hoist-variables": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz",
|
||||||
"integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==",
|
"integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/types": "^7.14.5"
|
"@babel/types": "^7.16.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/helper-module-imports": {
|
"@babel/helper-module-imports": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz",
|
||||||
"integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==",
|
"integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/types": "^7.14.5"
|
"@babel/types": "^7.16.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/helper-split-export-declaration": {
|
"@babel/helper-split-export-declaration": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz",
|
||||||
"integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==",
|
"integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/types": "^7.14.5"
|
"@babel/types": "^7.16.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/helper-validator-identifier": {
|
"@babel/helper-validator-identifier": {
|
||||||
"version": "7.14.8",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
|
||||||
"integrity": "sha512-ZGy6/XQjllhYQrNw/3zfWRwZCTVSiBLZ9DHVZxn9n2gip/7ab8mv2TWlKPIBk26RwedCBoWdjLmn+t9na2Gcow=="
|
"integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw=="
|
||||||
},
|
},
|
||||||
"@babel/highlight": {
|
"@babel/highlight": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz",
|
||||||
"integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==",
|
"integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/helper-validator-identifier": "^7.14.5",
|
"@babel/helper-validator-identifier": "^7.16.7",
|
||||||
"chalk": "^2.0.0",
|
"chalk": "^2.0.0",
|
||||||
"js-tokens": "^4.0.0"
|
"js-tokens": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/parser": {
|
"@babel/parser": {
|
||||||
"version": "7.14.8",
|
"version": "7.16.12",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.12.tgz",
|
||||||
"integrity": "sha512-syoCQFOoo/fzkWDeM0dLEZi5xqurb5vuyzwIMNZRNun+N/9A4cUZeQaE7dTrB8jGaKuJRBtEOajtnmw0I5hvvA=="
|
"integrity": "sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A=="
|
||||||
},
|
},
|
||||||
"@babel/runtime": {
|
"@babel/runtime": {
|
||||||
"version": "7.14.8",
|
"version": "7.14.8",
|
||||||
@@ -890,37 +952,38 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/template": {
|
"@babel/template": {
|
||||||
"version": "7.14.5",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz",
|
||||||
"integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==",
|
"integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/code-frame": "^7.14.5",
|
"@babel/code-frame": "^7.16.7",
|
||||||
"@babel/parser": "^7.14.5",
|
"@babel/parser": "^7.16.7",
|
||||||
"@babel/types": "^7.14.5"
|
"@babel/types": "^7.16.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/traverse": {
|
"@babel/traverse": {
|
||||||
"version": "7.14.8",
|
"version": "7.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.10.tgz",
|
||||||
"integrity": "sha512-kexHhzCljJcFNn1KYAQ6A5wxMRzq9ebYpEDV4+WdNyr3i7O44tanbDOR/xjiG2F3sllan+LgwK+7OMk0EmydHg==",
|
"integrity": "sha512-yzuaYXoRJBGMlBhsMJoUW7G1UmSb/eXr/JHYM/MsOJgavJibLwASijW7oXBdw3NQ6T0bW7Ty5P/VarOs9cHmqw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/code-frame": "^7.14.5",
|
"@babel/code-frame": "^7.16.7",
|
||||||
"@babel/generator": "^7.14.8",
|
"@babel/generator": "^7.16.8",
|
||||||
"@babel/helper-function-name": "^7.14.5",
|
"@babel/helper-environment-visitor": "^7.16.7",
|
||||||
"@babel/helper-hoist-variables": "^7.14.5",
|
"@babel/helper-function-name": "^7.16.7",
|
||||||
"@babel/helper-split-export-declaration": "^7.14.5",
|
"@babel/helper-hoist-variables": "^7.16.7",
|
||||||
"@babel/parser": "^7.14.8",
|
"@babel/helper-split-export-declaration": "^7.16.7",
|
||||||
"@babel/types": "^7.14.8",
|
"@babel/parser": "^7.16.10",
|
||||||
|
"@babel/types": "^7.16.8",
|
||||||
"debug": "^4.1.0",
|
"debug": "^4.1.0",
|
||||||
"globals": "^11.1.0"
|
"globals": "^11.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/types": {
|
"@babel/types": {
|
||||||
"version": "7.14.8",
|
"version": "7.16.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.8.tgz",
|
||||||
"integrity": "sha512-iob4soQa7dZw8nodR/KlOQkPh9S4I8RwCxwRIFuiMRYjOzH/KJzdUfDgz6cGi5dDaclXF4P2PAhCdrBJNIg68Q==",
|
"integrity": "sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/helper-validator-identifier": "^7.14.8",
|
"@babel/helper-validator-identifier": "^7.16.7",
|
||||||
"to-fast-properties": "^2.0.0"
|
"to-fast-properties": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1011,12 +1074,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-plugin-styled-components": {
|
"babel-plugin-styled-components": {
|
||||||
"version": "1.13.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.2.tgz",
|
||||||
"integrity": "sha512-Vb1R3d4g+MUfPQPVDMCGjm3cDocJEUTR7Xq7QS95JWWeksN1wdFRYpD2kulDgI3Huuaf1CZd+NK4KQmqUFh5dA==",
|
"integrity": "sha512-7eG5NE8rChnNTDxa6LQfynwgHTVOYYaHJbUYSlOhk8QBXIQiMBKq4gyfHBBKPrxUcVBXVJL61ihduCpCQbuNbw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/helper-annotate-as-pure": "^7.0.0",
|
"@babel/helper-annotate-as-pure": "^7.16.0",
|
||||||
"@babel/helper-module-imports": "^7.0.0",
|
"@babel/helper-module-imports": "^7.16.0",
|
||||||
"babel-plugin-syntax-jsx": "^6.18.0",
|
"babel-plugin-syntax-jsx": "^6.18.0",
|
||||||
"lodash": "^4.17.11"
|
"lodash": "^4.17.11"
|
||||||
}
|
}
|
||||||
@@ -1075,9 +1138,9 @@
|
|||||||
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
|
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
|
||||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"ms": "2.1.2"
|
"ms": "2.1.2"
|
||||||
}
|
}
|
||||||
@@ -1093,9 +1156,9 @@
|
|||||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||||
},
|
},
|
||||||
"final-form": {
|
"final-form": {
|
||||||
"version": "4.20.2",
|
"version": "4.20.6",
|
||||||
"resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.6.tgz",
|
||||||
"integrity": "sha512-5i0IxqwjjPG1nUNCjWhqPCvQJJ2R+QwTwaAnjPmFnLbyjIHWuBPU8u+Ps4G3TcX2Sjno+O5xCZJzYcMJEzzfCQ==",
|
"integrity": "sha512-fCdwIj49KOaFfDRlXB57Eo+GghIMZQWrA9TakQI3C9uQxHwaFHXqZSNRlUdfnQmNNeySwGOaGPZCvjy58hyv4w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.10.0"
|
"@babel/runtime": "^7.10.0"
|
||||||
}
|
}
|
||||||
@@ -1181,9 +1244,12 @@
|
|||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
"node-fetch": {
|
"node-fetch": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
|
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||||
|
"requires": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
@@ -1199,9 +1265,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"postcss-value-parser": {
|
"postcss-value-parser": {
|
||||||
"version": "4.1.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
"integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ=="
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||||
},
|
},
|
||||||
"prop-types": {
|
"prop-types": {
|
||||||
"version": "15.7.2",
|
"version": "15.7.2",
|
||||||
@@ -1240,18 +1306,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-final-form": {
|
"react-final-form": {
|
||||||
"version": "6.5.3",
|
"version": "6.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.7.tgz",
|
||||||
"integrity": "sha512-FCs6GC0AMWJl2p6YX7kM+a0AvuSLAZUgbVNtRBskOs4g984t/It0qGtx51O+9vgqnqk6JyoxmIzxKMq+7ch/vg==",
|
"integrity": "sha512-o7tvJXB+McGiXOILqIC8lnOcX4aLhIBiF/Xi9Qet35b7XOS8R7KL8HLRKTfnZWQJm6MCE15v1U0SFive0NcxyA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.12.1"
|
"@babel/runtime": "^7.15.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": {
|
||||||
|
"version": "7.16.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz",
|
||||||
|
"integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==",
|
||||||
|
"requires": {
|
||||||
|
"regenerator-runtime": "^0.13.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"react-router": {
|
"react-router": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
@@ -1321,9 +1396,9 @@
|
|||||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
|
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
|
||||||
},
|
},
|
||||||
"styled-components": {
|
"styled-components": {
|
||||||
"version": "5.3.0",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.3.tgz",
|
||||||
"integrity": "sha512-bPJKwZCHjJPf/hwTJl6TbkSZg/3evha+XPEizrZUGb535jLImwDUdjTNxXqjjaASt2M4qO4AVfoHJNe3XB/tpQ==",
|
"integrity": "sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/helper-module-imports": "^7.0.0",
|
"@babel/helper-module-imports": "^7.0.0",
|
||||||
"@babel/traverse": "^7.4.5",
|
"@babel/traverse": "^7.4.5",
|
||||||
@@ -1360,6 +1435,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||||
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
|
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
|
||||||
},
|
},
|
||||||
|
"tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
|
||||||
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "4.3.5",
|
"version": "4.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
|
||||||
@@ -1369,6 +1449,20 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
||||||
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
|
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
|
||||||
|
},
|
||||||
|
"webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
|
||||||
|
},
|
||||||
|
"whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
|
||||||
|
"requires": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,18 +4,20 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "esbuild src/index.tsx --bundle --minify --sourcemap --outfile=build/bundle.js"
|
"build": "rm -rf build && NODE_ENV=production node ./esbuild.config.js",
|
||||||
|
"start": "NODE_ENV=development node ./esbuild.config.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Lakhan Samani",
|
"author": "Lakhan Samani",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@authorizerdev/authorizer-react": "^0.1.0-beta.8",
|
"@authorizerdev/authorizer-react": "latest",
|
||||||
"@types/react": "^17.0.15",
|
"@types/react": "^17.0.15",
|
||||||
"@types/react-dom": "^17.0.9",
|
"@types/react-dom": "^17.0.9",
|
||||||
"esbuild": "^0.12.17",
|
"esbuild": "^0.12.17",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-is": "^17.0.2",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"typescript": "^4.3.5"
|
"typescript": "^4.3.5"
|
||||||
},
|
},
|
||||||
|
@@ -2,32 +2,69 @@ import React from 'react';
|
|||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { AuthorizerProvider } from '@authorizerdev/authorizer-react';
|
import { AuthorizerProvider } from '@authorizerdev/authorizer-react';
|
||||||
import Root from './Root';
|
import Root from './Root';
|
||||||
|
import { createRandomString } from './utils/common';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
// @ts-ignore
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
const globalState: Record<string, string> = window['__authorizer__'];
|
const state = searchParams.get('state') || createRandomString();
|
||||||
return (
|
const scope = searchParams.get('scope')
|
||||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
? searchParams.get('scope')?.toString().split(' ')
|
||||||
<div
|
: `openid profile email`;
|
||||||
style={{
|
|
||||||
width: 400,
|
const urlProps: Record<string, any> = {
|
||||||
margin: `10px auto`,
|
state,
|
||||||
border: `1px solid #D1D5DB`,
|
scope,
|
||||||
padding: `25px 20px`,
|
};
|
||||||
borderRadius: 5,
|
|
||||||
}}
|
const redirectURL =
|
||||||
>
|
searchParams.get('redirect_uri') || searchParams.get('redirectURL');
|
||||||
<BrowserRouter>
|
if (redirectURL) {
|
||||||
<AuthorizerProvider
|
urlProps.redirectURL = redirectURL;
|
||||||
config={{
|
} else {
|
||||||
authorizerURL: globalState.authorizerURL,
|
urlProps.redirectURL = window.location.origin;
|
||||||
redirectURL: globalState.redirectURL,
|
}
|
||||||
}}
|
const globalState: Record<string, string> = {
|
||||||
>
|
// @ts-ignore
|
||||||
<Root />
|
...window['__authorizer__'],
|
||||||
</AuthorizerProvider>
|
...urlProps,
|
||||||
</BrowserRouter>
|
};
|
||||||
</div>
|
|
||||||
</div>
|
return (
|
||||||
);
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginTop: 20,
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`${globalState.organizationLogo}`}
|
||||||
|
alt="logo"
|
||||||
|
style={{ height: 60, width: 60, objectFit: 'cover' }}
|
||||||
|
/>
|
||||||
|
<h1>{globalState.organizationName}</h1>
|
||||||
|
</div>
|
||||||
|
<div className="container">
|
||||||
|
<BrowserRouter>
|
||||||
|
<AuthorizerProvider
|
||||||
|
config={{
|
||||||
|
authorizerURL: window.location.origin,
|
||||||
|
redirectURL: globalState.redirectURL,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Root globalState={globalState} />
|
||||||
|
</AuthorizerProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,35 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, lazy, Suspense } from 'react';
|
||||||
import { Switch, Route } from 'react-router-dom';
|
import { Switch, Route } from 'react-router-dom';
|
||||||
import { useAuthorizer } from '@authorizerdev/authorizer-react';
|
import { useAuthorizer } from '@authorizerdev/authorizer-react';
|
||||||
import Dashboard from './pages/dashboard';
|
|
||||||
import Login from './pages/login';
|
|
||||||
import ResetPassword from './pages/rest-password';
|
|
||||||
|
|
||||||
export default function Root() {
|
const ResetPassword = lazy(() => import('./pages/rest-password'));
|
||||||
|
const Login = lazy(() => import('./pages/login'));
|
||||||
|
const Dashboard = lazy(() => import('./pages/dashboard'));
|
||||||
|
|
||||||
|
export default function Root({
|
||||||
|
globalState,
|
||||||
|
}: {
|
||||||
|
globalState: Record<string, string>;
|
||||||
|
}) {
|
||||||
const { token, loading, config } = useAuthorizer();
|
const { token, loading, config } = useAuthorizer();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token) {
|
if (token) {
|
||||||
const url = new URL(config.redirectURL);
|
let redirectURL = config.redirectURL || '/app';
|
||||||
|
let params = `access_token=${token.access_token}&id_token=${token.id_token}&expires_in=${token.expires_in}&state=${globalState.state}`;
|
||||||
|
if (token.refresh_token) {
|
||||||
|
params += `&refresh_token=${token.refresh_token}`;
|
||||||
|
}
|
||||||
|
const url = new URL(redirectURL);
|
||||||
|
if (redirectURL.includes('?')) {
|
||||||
|
redirectURL = `${redirectURL}&${params}`;
|
||||||
|
} else {
|
||||||
|
redirectURL = `${redirectURL}?${params}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (url.origin !== window.location.origin) {
|
if (url.origin !== window.location.origin) {
|
||||||
window.location.href = config.redirectURL;
|
sessionStorage.removeItem('authorizer_state');
|
||||||
|
window.location.replace(redirectURL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return () => {};
|
return () => {};
|
||||||
@@ -24,22 +41,26 @@ export default function Root() {
|
|||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Suspense fallback={<></>}>
|
||||||
<Route path="/app" exact>
|
<Switch>
|
||||||
<Dashboard />
|
<Route path="/app" exact>
|
||||||
</Route>
|
<Dashboard />
|
||||||
</Switch>
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Suspense fallback={<></>}>
|
||||||
<Route path="/app" exact>
|
<Switch>
|
||||||
<Login />
|
<Route path="/app" exact>
|
||||||
</Route>
|
<Login />
|
||||||
<Route path="/app/reset-password">
|
</Route>
|
||||||
<ResetPassword />
|
<Route path="/app/reset-password">
|
||||||
</Route>
|
<ResetPassword />
|
||||||
</Switch>
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
30
app/src/index.css
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
body {
|
||||||
|
margin: 10;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*:before,
|
||||||
|
*:after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
box-sizing: content-box;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
padding: 25px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.getElementById('root'));
|
ReactDOM.render(<App />, document.getElementById('root'));
|
||||||
|
@@ -15,7 +15,7 @@ export default function Dashboard() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Hey 👋,</h1>
|
<h1>Hey 👋,</h1>
|
||||||
<p>Thank you for joining authorizer demo app.</p>
|
<p>Thank you for using authorizer.</p>
|
||||||
<p>
|
<p>
|
||||||
Your email address is{' '}
|
Your email address is{' '}
|
||||||
<a href={`mailto:${user?.email}`} style={{ color: '#3B82F6' }}>
|
<a href={`mailto:${user?.email}`} style={{ color: '#3B82F6' }}>
|
||||||
|
@@ -2,9 +2,9 @@ import React, { Fragment } from 'react';
|
|||||||
import { Authorizer } from '@authorizerdev/authorizer-react';
|
import { Authorizer } from '@authorizerdev/authorizer-react';
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Authorizer />
|
<Authorizer />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -2,11 +2,11 @@ import React, { Fragment } from 'react';
|
|||||||
import { AuthorizerResetPassword } from '@authorizerdev/authorizer-react';
|
import { AuthorizerResetPassword } from '@authorizerdev/authorizer-react';
|
||||||
|
|
||||||
export default function ResetPassword() {
|
export default function ResetPassword() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<h1 style={{ textAlign: 'center' }}>Reset Password</h1>
|
<h1 style={{ textAlign: 'center' }}>Reset Password</h1>
|
||||||
<br />
|
<br />
|
||||||
<AuthorizerResetPassword />
|
<AuthorizerResetPassword />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
22
app/src/utils/common.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export const getCrypto = () => {
|
||||||
|
//ie 11.x uses msCrypto
|
||||||
|
return (window.crypto || (window as any).msCrypto) as Crypto;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createRandomString = () => {
|
||||||
|
const charset =
|
||||||
|
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_~.';
|
||||||
|
let random = '';
|
||||||
|
const randomValues = Array.from(
|
||||||
|
getCrypto().getRandomValues(new Uint8Array(43))
|
||||||
|
);
|
||||||
|
randomValues.forEach((v) => (random += charset[v % charset.length]));
|
||||||
|
return random;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createQueryParams = (params: any) => {
|
||||||
|
return Object.keys(params)
|
||||||
|
.filter((k) => typeof params[k] !== 'undefined')
|
||||||
|
.map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
|
||||||
|
.join('&');
|
||||||
|
};
|
12
dashboard/README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Authorizer dashboard
|
||||||
|
|
||||||
|
### Getting started
|
||||||
|
|
||||||
|
**Setting up locally**
|
||||||
|
|
||||||
|
- `cd dashboard`
|
||||||
|
- `npm start`
|
||||||
|
|
||||||
|
**Creating production build**
|
||||||
|
|
||||||
|
- `make build-dashboard`
|
12
dashboard/esbuild.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const __is_prod__ = process.env.NODE_ENV === 'production';
|
||||||
|
require('esbuild').build({
|
||||||
|
entryPoints: ['src/index.tsx'],
|
||||||
|
chunkNames: '[name]-[hash]',
|
||||||
|
bundle: true,
|
||||||
|
minify: __is_prod__,
|
||||||
|
outdir: 'build',
|
||||||
|
splitting: true,
|
||||||
|
format: 'esm',
|
||||||
|
watch: !__is_prod__,
|
||||||
|
logLevel: 'info',
|
||||||
|
});
|
BIN
dashboard/favicon_io/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
dashboard/favicon_io/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
dashboard/favicon_io/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
dashboard/favicon_io/favicon-16x16.png
Normal file
After Width: | Height: | Size: 528 B |
BIN
dashboard/favicon_io/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
dashboard/favicon_io/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
3992
dashboard/package-lock.json
generated
Normal file
32
dashboard/package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "dashboard",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "rm -rf build && NODE_ENV=production node ./esbuild.config.js",
|
||||||
|
"start": "NODE_ENV=development node ./esbuild.config.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Lakhan Samani",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@chakra-ui/react": "^1.7.3",
|
||||||
|
"@emotion/react": "^11.7.1",
|
||||||
|
"@emotion/styled": "^11.6.0",
|
||||||
|
"@types/react": "^17.0.38",
|
||||||
|
"@types/react-dom": "^17.0.11",
|
||||||
|
"@types/react-router-dom": "^5.3.2",
|
||||||
|
"dayjs": "^1.10.7",
|
||||||
|
"esbuild": "^0.14.9",
|
||||||
|
"framer-motion": "^5.5.5",
|
||||||
|
"graphql": "^16.2.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-icons": "^4.3.1",
|
||||||
|
"react-router-dom": "^6.2.1",
|
||||||
|
"typescript": "^4.5.4",
|
||||||
|
"urql": "^2.0.6"
|
||||||
|
}
|
||||||
|
}
|
46
dashboard/src/App.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { ChakraProvider, extendTheme } from '@chakra-ui/react';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import { createClient, Provider } from 'urql';
|
||||||
|
import { AppRoutes } from './routes';
|
||||||
|
import { AuthContextProvider } from './contexts/AuthContext';
|
||||||
|
|
||||||
|
const queryClient = createClient({
|
||||||
|
url: '/graphql',
|
||||||
|
fetchOptions: () => {
|
||||||
|
return {
|
||||||
|
credentials: 'include',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
requestPolicy: 'network-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const theme = extendTheme({
|
||||||
|
styles: {
|
||||||
|
global: {
|
||||||
|
'html, body, #root': {
|
||||||
|
fontFamily: 'Avenir, Helvetica, Arial, sans-serif',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
blue: {
|
||||||
|
500: 'rgb(59,130,246)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<ChakraProvider theme={theme}>
|
||||||
|
<Provider value={queryClient}>
|
||||||
|
<BrowserRouter basename="/dashboard">
|
||||||
|
<AuthContextProvider>
|
||||||
|
<AppRoutes />
|
||||||
|
</AuthContextProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
</ChakraProvider>
|
||||||
|
);
|
||||||
|
}
|
112
dashboard/src/components/DeleteUserModal.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Flex,
|
||||||
|
MenuItem,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
useDisclosure,
|
||||||
|
Text,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useClient } from 'urql';
|
||||||
|
import { FaRegTrashAlt } from 'react-icons/fa';
|
||||||
|
import { DeleteUser } from '../graphql/mutation';
|
||||||
|
import { capitalizeFirstLetter } from '../utils';
|
||||||
|
|
||||||
|
interface userDataTypes {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteUserModal = ({
|
||||||
|
user,
|
||||||
|
updateUserList,
|
||||||
|
}: {
|
||||||
|
user: userDataTypes;
|
||||||
|
updateUserList: Function;
|
||||||
|
}) => {
|
||||||
|
const client = useClient();
|
||||||
|
const toast = useToast();
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const [userData, setUserData] = React.useState<userDataTypes>({
|
||||||
|
id: '',
|
||||||
|
email: '',
|
||||||
|
});
|
||||||
|
React.useEffect(() => {
|
||||||
|
setUserData(user);
|
||||||
|
}, []);
|
||||||
|
const deleteHandler = async () => {
|
||||||
|
const res = await client
|
||||||
|
.mutation(DeleteUser, { params: { email: userData.email } })
|
||||||
|
.toPromise();
|
||||||
|
if (res.error) {
|
||||||
|
toast({
|
||||||
|
title: capitalizeFirstLetter(res.error.message),
|
||||||
|
isClosable: true,
|
||||||
|
status: 'error',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
} else if (res.data?._delete_user) {
|
||||||
|
toast({
|
||||||
|
title: capitalizeFirstLetter(res.data?._delete_user.message),
|
||||||
|
isClosable: true,
|
||||||
|
status: 'success',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
updateUserList();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={onOpen}>Delete User</MenuItem>
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>Delete User</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<Text fontSize="md">Are you sure?</Text>
|
||||||
|
<Flex
|
||||||
|
padding="5%"
|
||||||
|
marginTop="5%"
|
||||||
|
marginBottom="2%"
|
||||||
|
border="1px solid #ff7875"
|
||||||
|
borderRadius="5px"
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm">
|
||||||
|
User <b>{user.email}</b> will be deleted permanently!
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FaRegTrashAlt />}
|
||||||
|
colorScheme="red"
|
||||||
|
variant="solid"
|
||||||
|
onClick={deleteHandler}
|
||||||
|
isDisabled={false}
|
||||||
|
>
|
||||||
|
<Center h="100%" pt="5%">
|
||||||
|
Delete
|
||||||
|
</Center>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteUserModal;
|
250
dashboard/src/components/EditUserModal.tsx
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Flex,
|
||||||
|
MenuItem,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
Stack,
|
||||||
|
useDisclosure,
|
||||||
|
Text,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useClient } from 'urql';
|
||||||
|
import { FaSave } from 'react-icons/fa';
|
||||||
|
import InputField from './InputField';
|
||||||
|
import {
|
||||||
|
ArrayInputType,
|
||||||
|
DateInputType,
|
||||||
|
SelectInputType,
|
||||||
|
TextInputType,
|
||||||
|
} from '../constants';
|
||||||
|
import { getObjectDiff } from '../utils';
|
||||||
|
import { UpdateUser } from '../graphql/mutation';
|
||||||
|
|
||||||
|
const GenderTypes = {
|
||||||
|
Undisclosed: null,
|
||||||
|
Male: 'Male',
|
||||||
|
Female: 'Female',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface userDataTypes {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
given_name: string;
|
||||||
|
family_name: string;
|
||||||
|
middle_name: string;
|
||||||
|
nickname: string;
|
||||||
|
gender: string;
|
||||||
|
birthdate: string;
|
||||||
|
phone_number: string;
|
||||||
|
picture: string;
|
||||||
|
roles: [string] | [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditUserModal = ({
|
||||||
|
user,
|
||||||
|
updateUserList,
|
||||||
|
}: {
|
||||||
|
user: userDataTypes;
|
||||||
|
updateUserList: Function;
|
||||||
|
}) => {
|
||||||
|
const client = useClient();
|
||||||
|
const toast = useToast();
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const [userData, setUserData] = React.useState<userDataTypes>({
|
||||||
|
id: '',
|
||||||
|
email: '',
|
||||||
|
given_name: '',
|
||||||
|
family_name: '',
|
||||||
|
middle_name: '',
|
||||||
|
nickname: '',
|
||||||
|
gender: '',
|
||||||
|
birthdate: '',
|
||||||
|
phone_number: '',
|
||||||
|
picture: '',
|
||||||
|
roles: [],
|
||||||
|
});
|
||||||
|
React.useEffect(() => {
|
||||||
|
setUserData(user);
|
||||||
|
}, []);
|
||||||
|
const saveHandler = async () => {
|
||||||
|
const diff = getObjectDiff(user, userData);
|
||||||
|
const updatedUserData = diff.reduce(
|
||||||
|
(acc: any, property: string) => ({
|
||||||
|
...acc,
|
||||||
|
// @ts-ignore
|
||||||
|
[property]: userData[property],
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
const res = await client
|
||||||
|
.mutation(UpdateUser, { params: { ...updatedUserData, id: userData.id } })
|
||||||
|
.toPromise();
|
||||||
|
if (res.error) {
|
||||||
|
toast({
|
||||||
|
title: 'User data update failed',
|
||||||
|
isClosable: true,
|
||||||
|
status: 'error',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
} else if (res.data?._update_user?.id) {
|
||||||
|
toast({
|
||||||
|
title: 'User data update successful',
|
||||||
|
isClosable: true,
|
||||||
|
status: 'success',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
updateUserList();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={onOpen}>Edit User Details</MenuItem>
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>Edit User Details</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<Stack>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Given Name:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={TextInputType.GIVEN_NAME}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Middle Name:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={TextInputType.MIDDLE_NAME}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Family Name:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={TextInputType.FAMILY_NAME}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Birth Date:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={DateInputType.BIRTHDATE}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Nickname:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={TextInputType.NICKNAME}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Gender:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={SelectInputType.GENDER}
|
||||||
|
value={userData.gender}
|
||||||
|
options={GenderTypes}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Phone Number:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={TextInputType.PHONE_NUMBER}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Picture:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={TextInputType.PICTURE}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Roles:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={ArrayInputType.USER_ROLES}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FaSave />}
|
||||||
|
colorScheme="blue"
|
||||||
|
variant="solid"
|
||||||
|
onClick={saveHandler}
|
||||||
|
isDisabled={false}
|
||||||
|
>
|
||||||
|
<Center h="100%" pt="5%">
|
||||||
|
Save
|
||||||
|
</Center>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditUserModal;
|
335
dashboard/src/components/InputField.tsx
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Input,
|
||||||
|
Center,
|
||||||
|
InputGroup,
|
||||||
|
InputRightElement,
|
||||||
|
Tag,
|
||||||
|
TagLabel,
|
||||||
|
TagRightIcon,
|
||||||
|
Select,
|
||||||
|
Textarea,
|
||||||
|
Switch,
|
||||||
|
Code,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
FaRegClone,
|
||||||
|
FaRegEye,
|
||||||
|
FaRegEyeSlash,
|
||||||
|
FaPlus,
|
||||||
|
FaTimes,
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
import {
|
||||||
|
ArrayInputOperations,
|
||||||
|
ArrayInputType,
|
||||||
|
SelectInputType,
|
||||||
|
HiddenInputType,
|
||||||
|
TextInputType,
|
||||||
|
TextAreaInputType,
|
||||||
|
SwitchInputType,
|
||||||
|
DateInputType,
|
||||||
|
} from '../constants';
|
||||||
|
import { copyTextToClipboard } from '../utils';
|
||||||
|
|
||||||
|
const InputField = ({
|
||||||
|
inputType,
|
||||||
|
variables,
|
||||||
|
setVariables,
|
||||||
|
fieldVisibility,
|
||||||
|
setFieldVisibility,
|
||||||
|
...downshiftProps
|
||||||
|
}: any) => {
|
||||||
|
const props = {
|
||||||
|
size: 'sm',
|
||||||
|
...downshiftProps,
|
||||||
|
};
|
||||||
|
const [inputFieldVisibility, setInputFieldVisibility] = React.useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({
|
||||||
|
ROLES: false,
|
||||||
|
DEFAULT_ROLES: false,
|
||||||
|
PROTECTED_ROLES: false,
|
||||||
|
ALLOWED_ORIGINS: false,
|
||||||
|
roles: false,
|
||||||
|
});
|
||||||
|
const [inputData, setInputData] = React.useState<Record<string, string>>({
|
||||||
|
ROLES: '',
|
||||||
|
DEFAULT_ROLES: '',
|
||||||
|
PROTECTED_ROLES: '',
|
||||||
|
ALLOWED_ORIGINS: '',
|
||||||
|
roles: '',
|
||||||
|
});
|
||||||
|
const updateInputHandler = (
|
||||||
|
type: string,
|
||||||
|
operation: any,
|
||||||
|
role: string = ''
|
||||||
|
) => {
|
||||||
|
if (operation === ArrayInputOperations.APPEND) {
|
||||||
|
if (inputData[type] !== '') {
|
||||||
|
setVariables({
|
||||||
|
...variables,
|
||||||
|
[type]: [...variables[type], inputData[type]],
|
||||||
|
});
|
||||||
|
setInputData({ ...inputData, [type]: '' });
|
||||||
|
}
|
||||||
|
setInputFieldVisibility({ ...inputFieldVisibility, [type]: false });
|
||||||
|
}
|
||||||
|
if (operation === ArrayInputOperations.REMOVE) {
|
||||||
|
let updatedEnvVars = variables[type].filter(
|
||||||
|
(item: string) => item !== role
|
||||||
|
);
|
||||||
|
setVariables({
|
||||||
|
...variables,
|
||||||
|
[type]: updatedEnvVars,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (Object.values(TextInputType).includes(inputType)) {
|
||||||
|
return (
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input
|
||||||
|
{...props}
|
||||||
|
value={variables[inputType] ? variables[inputType] : ''}
|
||||||
|
onChange={(
|
||||||
|
event: Event & {
|
||||||
|
target: HTMLInputElement;
|
||||||
|
}
|
||||||
|
) =>
|
||||||
|
setVariables({
|
||||||
|
...variables,
|
||||||
|
[inputType]: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputRightElement
|
||||||
|
children={<FaRegClone color="#bfbfbf" />}
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => copyTextToClipboard(variables[inputType])}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Object.values(HiddenInputType).includes(inputType)) {
|
||||||
|
return (
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input
|
||||||
|
{...props}
|
||||||
|
value={variables[inputType]}
|
||||||
|
onChange={(
|
||||||
|
event: Event & {
|
||||||
|
target: HTMLInputElement;
|
||||||
|
}
|
||||||
|
) =>
|
||||||
|
setVariables({
|
||||||
|
...variables,
|
||||||
|
[inputType]: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type={!fieldVisibility[inputType] ? 'password' : 'text'}
|
||||||
|
/>
|
||||||
|
<InputRightElement
|
||||||
|
right="15px"
|
||||||
|
children={
|
||||||
|
<Flex>
|
||||||
|
{fieldVisibility[inputType] ? (
|
||||||
|
<Center
|
||||||
|
w="25px"
|
||||||
|
margin="0 1.5%"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFieldVisibility({
|
||||||
|
...fieldVisibility,
|
||||||
|
[inputType]: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaRegEyeSlash color="#bfbfbf" />
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<Center
|
||||||
|
w="25px"
|
||||||
|
margin="0 1.5%"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFieldVisibility({
|
||||||
|
...fieldVisibility,
|
||||||
|
[inputType]: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaRegEye color="#bfbfbf" />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
<Center
|
||||||
|
w="25px"
|
||||||
|
margin="0 1.5%"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => copyTextToClipboard(variables[inputType])}
|
||||||
|
>
|
||||||
|
<FaRegClone color="#bfbfbf" />
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Object.values(ArrayInputType).includes(inputType)) {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
border="1px solid #e2e8f0"
|
||||||
|
w="100%"
|
||||||
|
paddingTop="0.5%"
|
||||||
|
overflowX="scroll"
|
||||||
|
overflowY="hidden"
|
||||||
|
justifyContent="start"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
{variables[inputType].map((role: string, index: number) => (
|
||||||
|
<Box key={index} margin="0.5%" role="group">
|
||||||
|
<Tag
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme="gray"
|
||||||
|
minW="fit-content"
|
||||||
|
>
|
||||||
|
<TagLabel cursor="default">{role}</TagLabel>
|
||||||
|
<TagRightIcon
|
||||||
|
boxSize="12px"
|
||||||
|
as={FaTimes}
|
||||||
|
display="none"
|
||||||
|
cursor="pointer"
|
||||||
|
_groupHover={{ display: 'block' }}
|
||||||
|
onClick={() =>
|
||||||
|
updateInputHandler(
|
||||||
|
inputType,
|
||||||
|
ArrayInputOperations.REMOVE,
|
||||||
|
role
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tag>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
{inputFieldVisibility[inputType] ? (
|
||||||
|
<Box ml="1%" mb="0.75%">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
size="xs"
|
||||||
|
minW="150px"
|
||||||
|
placeholder="add a new value"
|
||||||
|
value={inputData[inputType]}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
setInputData({ ...inputData, [inputType]: e.target.value });
|
||||||
|
}}
|
||||||
|
onBlur={() =>
|
||||||
|
updateInputHandler(inputType, ArrayInputOperations.APPEND)
|
||||||
|
}
|
||||||
|
onKeyPress={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
updateInputHandler(inputType, ArrayInputOperations.APPEND);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
marginLeft="0.5%"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setInputFieldVisibility({
|
||||||
|
...inputFieldVisibility,
|
||||||
|
[inputType]: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Tag
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme="gray"
|
||||||
|
minW="fit-content"
|
||||||
|
>
|
||||||
|
<FaPlus />
|
||||||
|
</Tag>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Object.values(SelectInputType).includes(inputType)) {
|
||||||
|
const { options, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
size="sm"
|
||||||
|
{...rest}
|
||||||
|
value={variables[inputType] ? variables[inputType] : ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setVariables({ ...variables, [inputType]: e.target.value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{Object.entries(options).map(([key, value]: any) => (
|
||||||
|
<option value={value} key={key}>
|
||||||
|
{key}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Object.values(TextAreaInputType).includes(inputType)) {
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
{...props}
|
||||||
|
size="lg"
|
||||||
|
fontSize={14}
|
||||||
|
value={variables[inputType] ? variables[inputType] : ''}
|
||||||
|
onChange={(
|
||||||
|
event: Event & {
|
||||||
|
target: HTMLInputElement;
|
||||||
|
}
|
||||||
|
) =>
|
||||||
|
setVariables({
|
||||||
|
...variables,
|
||||||
|
[inputType]: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Object.values(SwitchInputType).includes(inputType)) {
|
||||||
|
return (
|
||||||
|
<Flex w="25%" justifyContent="space-between">
|
||||||
|
<Code h="75%">Off</Code>
|
||||||
|
<Switch
|
||||||
|
size="md"
|
||||||
|
isChecked={variables[inputType]}
|
||||||
|
onChange={() => {
|
||||||
|
setVariables({
|
||||||
|
...variables,
|
||||||
|
[inputType]: !variables[inputType],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Code h="75%">On</Code>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Object.values(DateInputType).includes(inputType)) {
|
||||||
|
return (
|
||||||
|
<Flex border="1px solid #e2e8f0" w="100%" h="33px" padding="1%">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
style={{ width: '100%', paddingLeft: '2.5%' }}
|
||||||
|
value={variables[inputType] ? variables[inputType] : ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setVariables({ ...variables, [inputType]: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputField;
|
233
dashboard/src/components/Menu.tsx
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
Box,
|
||||||
|
CloseButton,
|
||||||
|
Flex,
|
||||||
|
Image,
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
Icon,
|
||||||
|
useColorModeValue,
|
||||||
|
Link,
|
||||||
|
Text,
|
||||||
|
BoxProps,
|
||||||
|
FlexProps,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
FiHome,
|
||||||
|
FiCode,
|
||||||
|
FiSettings,
|
||||||
|
FiMenu,
|
||||||
|
FiUser,
|
||||||
|
FiUsers,
|
||||||
|
FiChevronDown,
|
||||||
|
} from 'react-icons/fi';
|
||||||
|
import { IconType } from 'react-icons';
|
||||||
|
import { ReactText } from 'react';
|
||||||
|
import { useMutation, useQuery } from 'urql';
|
||||||
|
import { NavLink, useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
import { useAuthContext } from '../contexts/AuthContext';
|
||||||
|
import { AdminLogout } from '../graphql/mutation';
|
||||||
|
import { MetaQuery } from '../graphql/queries';
|
||||||
|
|
||||||
|
interface LinkItemProps {
|
||||||
|
name: string;
|
||||||
|
icon: IconType;
|
||||||
|
route: string;
|
||||||
|
}
|
||||||
|
const LinkItems: Array<LinkItemProps> = [
|
||||||
|
// { name: 'Home', icon: FiHome, route: '/' },
|
||||||
|
{ name: 'Environment Variables', icon: FiSettings, route: '/' },
|
||||||
|
{ name: 'Users', icon: FiUsers, route: '/users' },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface SidebarProps extends BoxProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sidebar = ({ onClose, ...rest }: SidebarProps) => {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const [{ fetching, data }] = useQuery({ query: MetaQuery });
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
transition="3s ease"
|
||||||
|
bg={useColorModeValue('white', 'gray.900')}
|
||||||
|
borderRight="1px"
|
||||||
|
borderRightColor={useColorModeValue('gray.200', 'gray.700')}
|
||||||
|
w={{ base: 'full', md: 60 }}
|
||||||
|
pos="fixed"
|
||||||
|
h="full"
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<Flex h="20" alignItems="center" mx="8" justifyContent="space-between">
|
||||||
|
<NavLink to="/">
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<Image
|
||||||
|
src="https://authorizer.dev/images/logo.png"
|
||||||
|
alt="logo"
|
||||||
|
height="36px"
|
||||||
|
/>
|
||||||
|
<Text fontSize="large" ml="2" letterSpacing="3">
|
||||||
|
AUTHORIZER
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</NavLink>
|
||||||
|
<CloseButton display={{ base: 'flex', md: 'none' }} onClick={onClose} />
|
||||||
|
</Flex>
|
||||||
|
{LinkItems.map((link) => (
|
||||||
|
<NavLink key={link.name} to={link.route}>
|
||||||
|
<NavItem
|
||||||
|
icon={link.icon}
|
||||||
|
color={pathname === link.route ? 'blue.500' : ''}
|
||||||
|
>
|
||||||
|
{link.name}
|
||||||
|
</NavItem>
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/playground"
|
||||||
|
target="_blank"
|
||||||
|
style={{
|
||||||
|
textDecoration: 'none',
|
||||||
|
}}
|
||||||
|
_focus={{ _boxShadow: 'none' }}
|
||||||
|
>
|
||||||
|
<NavItem icon={FiCode}>API Playground</NavItem>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{data?.meta?.version && (
|
||||||
|
<Text
|
||||||
|
color="gray.600"
|
||||||
|
fontSize="sm"
|
||||||
|
textAlign="center"
|
||||||
|
position="absolute"
|
||||||
|
bottom="5"
|
||||||
|
left="7"
|
||||||
|
>
|
||||||
|
Current Version: {data.meta.version}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NavItemProps extends FlexProps {
|
||||||
|
icon: IconType;
|
||||||
|
children: ReactText;
|
||||||
|
}
|
||||||
|
export const NavItem = ({ icon, children, ...rest }: NavItemProps) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
p="3"
|
||||||
|
mx="3"
|
||||||
|
borderRadius="md"
|
||||||
|
role="group"
|
||||||
|
cursor="pointer"
|
||||||
|
_hover={{
|
||||||
|
bg: 'blue.500',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{icon && (
|
||||||
|
<Icon
|
||||||
|
mr="4"
|
||||||
|
fontSize="16"
|
||||||
|
_groupHover={{
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
as={icon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MobileProps extends FlexProps {
|
||||||
|
onOpen: () => void;
|
||||||
|
}
|
||||||
|
export const MobileNav = ({ onOpen, ...rest }: MobileProps) => {
|
||||||
|
const [_, logout] = useMutation(AdminLogout);
|
||||||
|
const { setIsLoggedIn } = useAuthContext();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await logout();
|
||||||
|
setIsLoggedIn(false);
|
||||||
|
navigate('/', { replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
ml={{ base: 0, md: 60 }}
|
||||||
|
px={{ base: 4, md: 4 }}
|
||||||
|
height="20"
|
||||||
|
position="fixed"
|
||||||
|
right="0"
|
||||||
|
left="0"
|
||||||
|
alignItems="center"
|
||||||
|
bg={useColorModeValue('white', 'gray.900')}
|
||||||
|
borderBottomWidth="1px"
|
||||||
|
borderBottomColor={useColorModeValue('gray.200', 'gray.700')}
|
||||||
|
justifyContent={{ base: 'space-between', md: 'flex-end' }}
|
||||||
|
zIndex={99}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
display={{ base: 'flex', md: 'none' }}
|
||||||
|
onClick={onOpen}
|
||||||
|
variant="outline"
|
||||||
|
aria-label="open menu"
|
||||||
|
icon={<FiMenu />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Image
|
||||||
|
src="https://authorizer.dev/images/logo.png"
|
||||||
|
alt="logo"
|
||||||
|
height="36px"
|
||||||
|
display={{ base: 'flex', md: 'none' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HStack spacing={{ base: '0', md: '6' }}>
|
||||||
|
<Flex alignItems={'center'}>
|
||||||
|
<Menu>
|
||||||
|
<MenuButton
|
||||||
|
py={2}
|
||||||
|
transition="all 0.3s"
|
||||||
|
_focus={{ boxShadow: 'none' }}
|
||||||
|
>
|
||||||
|
<HStack>
|
||||||
|
<FiUser />
|
||||||
|
<VStack
|
||||||
|
display={{ base: 'none', md: 'flex' }}
|
||||||
|
alignItems="flex-start"
|
||||||
|
spacing="1px"
|
||||||
|
ml="2"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm">Admin</Text>
|
||||||
|
</VStack>
|
||||||
|
<Box display={{ base: 'none', md: 'flex' }}>
|
||||||
|
<FiChevronDown />
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList
|
||||||
|
bg={useColorModeValue('white', 'gray.900')}
|
||||||
|
borderColor={useColorModeValue('gray.200', 'gray.700')}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleLogout}>Sign out</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</Flex>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
90
dashboard/src/constants.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
export const LOGO_URL =
|
||||||
|
'https://user-images.githubusercontent.com/6964334/147834043-fc384cab-e7ca-40f8-9663-38fc25fd5f3a.png';
|
||||||
|
|
||||||
|
export const TextInputType = {
|
||||||
|
CLIENT_ID: 'CLIENT_ID',
|
||||||
|
GOOGLE_CLIENT_ID: 'GOOGLE_CLIENT_ID',
|
||||||
|
GITHUB_CLIENT_ID: 'GITHUB_CLIENT_ID',
|
||||||
|
FACEBOOK_CLIENT_ID: 'FACEBOOK_CLIENT_ID',
|
||||||
|
JWT_ROLE_CLAIM: 'JWT_ROLE_CLAIM',
|
||||||
|
REDIS_URL: 'REDIS_URL',
|
||||||
|
SMTP_HOST: 'SMTP_HOST',
|
||||||
|
SMTP_PORT: 'SMTP_PORT',
|
||||||
|
SMTP_USERNAME: 'SMTP_USERNAME',
|
||||||
|
SENDER_EMAIL: 'SENDER_EMAIL',
|
||||||
|
ORGANIZATION_NAME: 'ORGANIZATION_NAME',
|
||||||
|
ORGANIZATION_LOGO: 'ORGANIZATION_LOGO',
|
||||||
|
DATABASE_NAME: 'DATABASE_NAME',
|
||||||
|
DATABASE_TYPE: 'DATABASE_TYPE',
|
||||||
|
DATABASE_URL: 'DATABASE_URL',
|
||||||
|
GIVEN_NAME: 'given_name',
|
||||||
|
MIDDLE_NAME: 'middle_name',
|
||||||
|
FAMILY_NAME: 'family_name',
|
||||||
|
NICKNAME: 'nickname',
|
||||||
|
PHONE_NUMBER: 'phone_number',
|
||||||
|
PICTURE: 'picture',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HiddenInputType = {
|
||||||
|
CLIENT_SECRET: 'CLIENT_SECRET',
|
||||||
|
GOOGLE_CLIENT_SECRET: 'GOOGLE_CLIENT_SECRET',
|
||||||
|
GITHUB_CLIENT_SECRET: 'GITHUB_CLIENT_SECRET',
|
||||||
|
FACEBOOK_CLIENT_SECRET: 'FACEBOOK_CLIENT_SECRET',
|
||||||
|
JWT_SECRET: 'JWT_SECRET',
|
||||||
|
SMTP_PASSWORD: 'SMTP_PASSWORD',
|
||||||
|
ADMIN_SECRET: 'ADMIN_SECRET',
|
||||||
|
OLD_ADMIN_SECRET: 'OLD_ADMIN_SECRET',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ArrayInputType = {
|
||||||
|
ROLES: 'ROLES',
|
||||||
|
DEFAULT_ROLES: 'DEFAULT_ROLES',
|
||||||
|
PROTECTED_ROLES: 'PROTECTED_ROLES',
|
||||||
|
ALLOWED_ORIGINS: 'ALLOWED_ORIGINS',
|
||||||
|
USER_ROLES: 'roles',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectInputType = {
|
||||||
|
JWT_TYPE: 'JWT_TYPE',
|
||||||
|
GENDER: 'gender',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TextAreaInputType = {
|
||||||
|
CUSTOM_ACCESS_TOKEN_SCRIPT: 'CUSTOM_ACCESS_TOKEN_SCRIPT',
|
||||||
|
JWT_PRIVATE_KEY: 'JWT_PRIVATE_KEY',
|
||||||
|
JWT_PUBLIC_KEY: 'JWT_PUBLIC_KEY',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SwitchInputType = {
|
||||||
|
DISABLE_LOGIN_PAGE: 'DISABLE_LOGIN_PAGE',
|
||||||
|
DISABLE_MAGIC_LINK_LOGIN: 'DISABLE_MAGIC_LINK_LOGIN',
|
||||||
|
DISABLE_EMAIL_VERIFICATION: 'DISABLE_EMAIL_VERIFICATION',
|
||||||
|
DISABLE_BASIC_AUTHENTICATION: 'DISABLE_BASIC_AUTHENTICATION',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DateInputType = {
|
||||||
|
BIRTHDATE: 'birthdate',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ArrayInputOperations = {
|
||||||
|
APPEND: 'APPEND',
|
||||||
|
REMOVE: 'REMOVE',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HMACEncryptionType = {
|
||||||
|
HS256: 'HS256',
|
||||||
|
HS384: 'HS384',
|
||||||
|
HS512: 'HS512',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RSAEncryptionType = {
|
||||||
|
RS256: 'RS256',
|
||||||
|
RS384: 'RS384',
|
||||||
|
RS512: 'RS512',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ECDSAEncryptionType = {
|
||||||
|
ES256: 'ES256',
|
||||||
|
ES384: 'ES384',
|
||||||
|
ES512: 'ES512',
|
||||||
|
};
|
48
dashboard/src/contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import React, { createContext, useState, useContext, useEffect } from 'react';
|
||||||
|
import { Center, Spinner } from '@chakra-ui/react';
|
||||||
|
import { useQuery } from 'urql';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { AdminSessionQuery } from '../graphql/queries';
|
||||||
|
import { hasAdminSecret } from '../utils';
|
||||||
|
|
||||||
|
const AuthContext = createContext({
|
||||||
|
isLoggedIn: false,
|
||||||
|
setIsLoggedIn: (data: boolean) => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AuthContextProvider = ({ children }: { children: any }) => {
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [{ fetching, data, error }] = useQuery({
|
||||||
|
query: AdminSessionQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fetching && !error) {
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
if (pathname === '/login' || pathname === 'signup') {
|
||||||
|
navigate('/', { replace: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [fetching, error]);
|
||||||
|
|
||||||
|
if (fetching) {
|
||||||
|
return (
|
||||||
|
<Center h="100%">
|
||||||
|
<Spinner />
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={{ isLoggedIn, setIsLoggedIn }}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuthContext = () => useContext(AuthContext);
|
47
dashboard/src/graphql/mutation/index.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
export const AdminSignup = `
|
||||||
|
mutation adminSignup($secret: String!) {
|
||||||
|
_admin_signup (params: {admin_secret: $secret}) {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const AdminLogin = `
|
||||||
|
mutation adminLogin($secret: String!){
|
||||||
|
_admin_login(params: { admin_secret: $secret }) {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const AdminLogout = `
|
||||||
|
mutation adminLogout {
|
||||||
|
_admin_logout {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UpdateEnvVariables = `
|
||||||
|
mutation updateEnvVariables($params: UpdateEnvInput!) {
|
||||||
|
_update_env(params: $params) {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UpdateUser = `
|
||||||
|
mutation updateUser($params: UpdateUserInput!) {
|
||||||
|
_update_user(params: $params) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const DeleteUser = `
|
||||||
|
mutation deleteUser($params: DeleteUserInput!) {
|
||||||
|
_delete_user(params: $params) {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
86
dashboard/src/graphql/queries/index.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
export const MetaQuery = `
|
||||||
|
query MetaQuery {
|
||||||
|
meta {
|
||||||
|
version
|
||||||
|
client_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const AdminSessionQuery = `
|
||||||
|
query {
|
||||||
|
_admin_session{
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EnvVariablesQuery = `
|
||||||
|
query {
|
||||||
|
_env{
|
||||||
|
CLIENT_ID,
|
||||||
|
CLIENT_SECRET,
|
||||||
|
GOOGLE_CLIENT_ID,
|
||||||
|
GOOGLE_CLIENT_SECRET,
|
||||||
|
GITHUB_CLIENT_ID,
|
||||||
|
GITHUB_CLIENT_SECRET,
|
||||||
|
FACEBOOK_CLIENT_ID,
|
||||||
|
FACEBOOK_CLIENT_SECRET,
|
||||||
|
ROLES,
|
||||||
|
DEFAULT_ROLES,
|
||||||
|
PROTECTED_ROLES,
|
||||||
|
JWT_TYPE,
|
||||||
|
JWT_SECRET,
|
||||||
|
JWT_ROLE_CLAIM,
|
||||||
|
JWT_PRIVATE_KEY,
|
||||||
|
JWT_PUBLIC_KEY,
|
||||||
|
REDIS_URL,
|
||||||
|
SMTP_HOST,
|
||||||
|
SMTP_PORT,
|
||||||
|
SMTP_USERNAME,
|
||||||
|
SMTP_PASSWORD,
|
||||||
|
SENDER_EMAIL,
|
||||||
|
ALLOWED_ORIGINS,
|
||||||
|
ORGANIZATION_NAME,
|
||||||
|
ORGANIZATION_LOGO,
|
||||||
|
ADMIN_SECRET,
|
||||||
|
DISABLE_LOGIN_PAGE,
|
||||||
|
DISABLE_MAGIC_LINK_LOGIN,
|
||||||
|
DISABLE_EMAIL_VERIFICATION,
|
||||||
|
DISABLE_BASIC_AUTHENTICATION,
|
||||||
|
CUSTOM_ACCESS_TOKEN_SCRIPT,
|
||||||
|
DATABASE_NAME,
|
||||||
|
DATABASE_TYPE,
|
||||||
|
DATABASE_URL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UserDetailsQuery = `
|
||||||
|
query($params: PaginatedInput) {
|
||||||
|
_users(params: $params) {
|
||||||
|
pagination {
|
||||||
|
limit
|
||||||
|
page
|
||||||
|
offset
|
||||||
|
total
|
||||||
|
}
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
email_verified
|
||||||
|
given_name
|
||||||
|
family_name
|
||||||
|
middle_name
|
||||||
|
nickname
|
||||||
|
gender
|
||||||
|
birthdate
|
||||||
|
phone_number
|
||||||
|
picture
|
||||||
|
signup_methods
|
||||||
|
roles
|
||||||
|
created_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
5
dashboard/src/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.render(<App />, document.getElementById('root'));
|
42
dashboard/src/layouts/AuthLayout.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Box, Flex, Image, Text, Spinner } from '@chakra-ui/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { useQuery } from 'urql';
|
||||||
|
import { MetaQuery } from '../graphql/queries';
|
||||||
|
|
||||||
|
export function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const [{ fetching, data }] = useQuery({ query: MetaQuery });
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
flexWrap="wrap"
|
||||||
|
h="100%"
|
||||||
|
bg="gray.100"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<Image
|
||||||
|
src="https://authorizer.dev/images/logo.png"
|
||||||
|
alt="logo"
|
||||||
|
height="50"
|
||||||
|
/>
|
||||||
|
<Text fontSize="x-large" ml="3" letterSpacing="3">
|
||||||
|
AUTHORIZER
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{fetching ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Box p="6" m="5" rounded="5" bg="white" w="500px" shadow="xl">
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
<Text color="gray.600" fontSize="sm">
|
||||||
|
Current Version: {data.meta.version}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
39
dashboard/src/layouts/DashboardLayout.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Drawer,
|
||||||
|
DrawerContent,
|
||||||
|
useDisclosure,
|
||||||
|
useColorModeValue,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Sidebar, MobileNav } from '../components/Menu';
|
||||||
|
|
||||||
|
export function DashboardLayout({ children }: { children: ReactNode }) {
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
return (
|
||||||
|
<Box minH="100vh" bg={useColorModeValue('gray.100', 'gray.900')}>
|
||||||
|
<Sidebar
|
||||||
|
onClose={() => onClose}
|
||||||
|
display={{ base: 'none', md: 'block' }}
|
||||||
|
/>
|
||||||
|
<Drawer
|
||||||
|
autoFocus={false}
|
||||||
|
isOpen={isOpen}
|
||||||
|
placement="left"
|
||||||
|
onClose={onClose}
|
||||||
|
returnFocusOnClose={false}
|
||||||
|
onOverlayClick={onClose}
|
||||||
|
size="full"
|
||||||
|
>
|
||||||
|
<DrawerContent>
|
||||||
|
<Sidebar onClose={onClose} />
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
{/* mobilenav */}
|
||||||
|
<MobileNav onOpen={onOpen} />
|
||||||
|
<Box ml={{ base: 0, md: 60 }} p="4" pt="24">
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
128
dashboard/src/pages/Auth.tsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
useToast,
|
||||||
|
VStack,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useMutation } from 'urql';
|
||||||
|
|
||||||
|
import { AuthLayout } from '../layouts/AuthLayout';
|
||||||
|
import { AdminLogin, AdminSignup } from '../graphql/mutation';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuthContext } from '../contexts/AuthContext';
|
||||||
|
import { capitalizeFirstLetter, hasAdminSecret } from '../utils';
|
||||||
|
|
||||||
|
export default function Auth() {
|
||||||
|
const [loginResult, login] = useMutation(AdminLogin);
|
||||||
|
const [signUpResult, signup] = useMutation(AdminSignup);
|
||||||
|
const { setIsLoggedIn } = useAuthContext();
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const isLogin = hasAdminSecret();
|
||||||
|
|
||||||
|
const handleSubmit = (e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formValues = [...e.target.elements].reduce((agg: any, elem: any) => {
|
||||||
|
if (elem.id) {
|
||||||
|
return {
|
||||||
|
...agg,
|
||||||
|
[elem.id]: elem.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return agg;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
(isLogin ? login : signup)({
|
||||||
|
secret: formValues['admin-secret'],
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.data) {
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
navigate('/', { replace: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const errors = isLogin ? loginResult.error : signUpResult.error;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (errors?.graphQLErrors) {
|
||||||
|
(errors?.graphQLErrors || []).map((error: any) => {
|
||||||
|
toast({
|
||||||
|
title: capitalizeFirstLetter(error.message),
|
||||||
|
isClosable: true,
|
||||||
|
status: 'error',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [errors]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthLayout>
|
||||||
|
<Text
|
||||||
|
fontSize="large"
|
||||||
|
textAlign="center"
|
||||||
|
color="gray.600"
|
||||||
|
fontWeight="bold"
|
||||||
|
mb="2"
|
||||||
|
>
|
||||||
|
Hello Admin 👋 <br />
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="large" textAlign="center" color="gray.500" mb="8">
|
||||||
|
Welcome to Admin Dashboard
|
||||||
|
</Text>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<VStack spacing="5" justify="space-between">
|
||||||
|
<FormControl isRequired>
|
||||||
|
<FormLabel htmlFor="admin-username">Username</FormLabel>
|
||||||
|
<Input
|
||||||
|
size="lg"
|
||||||
|
id="admin-username"
|
||||||
|
placeholder="Username"
|
||||||
|
disabled
|
||||||
|
value="admin"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl isRequired>
|
||||||
|
<FormLabel htmlFor="admin-secret">Password</FormLabel>
|
||||||
|
<Input
|
||||||
|
size="lg"
|
||||||
|
id="admin-secret"
|
||||||
|
placeholder="Admin secret"
|
||||||
|
type="password"
|
||||||
|
minLength={!isLogin ? 6 : 1}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
isLoading={signUpResult.fetching || loginResult.fetching}
|
||||||
|
colorScheme="blue"
|
||||||
|
size="lg"
|
||||||
|
w="100%"
|
||||||
|
d="block"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{isLogin ? 'Login' : 'Sign up'}
|
||||||
|
</Button>
|
||||||
|
{isLogin ? (
|
||||||
|
<Text color="gray.600" fontSize="sm">
|
||||||
|
<b>Note:</b> In case if you have forgot your admin secret, you can
|
||||||
|
reset it by updating <code>ADMIN_SECRET</code> environment
|
||||||
|
variable. For more information, please refer to the{' '}
|
||||||
|
<a href="https://docs.authorizer.dev/core/env/">documentation</a>.
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text color="gray.600" fontSize="sm">
|
||||||
|
<b>Note:</b> Configure the password to start using your dashboard.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</form>
|
||||||
|
</AuthLayout>
|
||||||
|
);
|
||||||
|
}
|
843
dashboard/src/pages/Environment.tsx
Normal file
@@ -0,0 +1,843 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
Stack,
|
||||||
|
Center,
|
||||||
|
Text,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputRightElement,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useClient } from 'urql';
|
||||||
|
import {
|
||||||
|
FaGoogle,
|
||||||
|
FaGithub,
|
||||||
|
FaFacebookF,
|
||||||
|
FaSave,
|
||||||
|
FaRegEyeSlash,
|
||||||
|
FaRegEye,
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import InputField from '../components/InputField';
|
||||||
|
import { EnvVariablesQuery } from '../graphql/queries';
|
||||||
|
import {
|
||||||
|
ArrayInputType,
|
||||||
|
SelectInputType,
|
||||||
|
HiddenInputType,
|
||||||
|
TextInputType,
|
||||||
|
TextAreaInputType,
|
||||||
|
SwitchInputType,
|
||||||
|
HMACEncryptionType,
|
||||||
|
RSAEncryptionType,
|
||||||
|
ECDSAEncryptionType,
|
||||||
|
} from '../constants';
|
||||||
|
import { UpdateEnvVariables } from '../graphql/mutation';
|
||||||
|
import { getObjectDiff, capitalizeFirstLetter } from '../utils';
|
||||||
|
|
||||||
|
interface envVarTypes {
|
||||||
|
GOOGLE_CLIENT_ID: string;
|
||||||
|
GOOGLE_CLIENT_SECRET: string;
|
||||||
|
GITHUB_CLIENT_ID: string;
|
||||||
|
GITHUB_CLIENT_SECRET: string;
|
||||||
|
FACEBOOK_CLIENT_ID: string;
|
||||||
|
FACEBOOK_CLIENT_SECRET: string;
|
||||||
|
ROLES: [string] | [];
|
||||||
|
DEFAULT_ROLES: [string] | [];
|
||||||
|
PROTECTED_ROLES: [string] | [];
|
||||||
|
JWT_TYPE: string;
|
||||||
|
JWT_SECRET: string;
|
||||||
|
JWT_ROLE_CLAIM: string;
|
||||||
|
JWT_PRIVATE_KEY: string;
|
||||||
|
JWT_PUBLIC_KEY: string;
|
||||||
|
REDIS_URL: string;
|
||||||
|
SMTP_HOST: string;
|
||||||
|
SMTP_PORT: string;
|
||||||
|
SMTP_USERNAME: string;
|
||||||
|
SMTP_PASSWORD: string;
|
||||||
|
SENDER_EMAIL: string;
|
||||||
|
ALLOWED_ORIGINS: [string] | [];
|
||||||
|
ORGANIZATION_NAME: string;
|
||||||
|
ORGANIZATION_LOGO: string;
|
||||||
|
CUSTOM_ACCESS_TOKEN_SCRIPT: string;
|
||||||
|
ADMIN_SECRET: string;
|
||||||
|
DISABLE_LOGIN_PAGE: boolean;
|
||||||
|
DISABLE_MAGIC_LINK_LOGIN: boolean;
|
||||||
|
DISABLE_EMAIL_VERIFICATION: boolean;
|
||||||
|
DISABLE_BASIC_AUTHENTICATION: boolean;
|
||||||
|
OLD_ADMIN_SECRET: string;
|
||||||
|
DATABASE_NAME: string;
|
||||||
|
DATABASE_TYPE: string;
|
||||||
|
DATABASE_URL: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Environment() {
|
||||||
|
const client = useClient();
|
||||||
|
const toast = useToast();
|
||||||
|
const [adminSecret, setAdminSecret] = React.useState<
|
||||||
|
Record<string, string | boolean>
|
||||||
|
>({
|
||||||
|
value: '',
|
||||||
|
disableInputField: true,
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = React.useState<boolean>(true);
|
||||||
|
const [envVariables, setEnvVariables] = React.useState<envVarTypes>({
|
||||||
|
GOOGLE_CLIENT_ID: '',
|
||||||
|
GOOGLE_CLIENT_SECRET: '',
|
||||||
|
GITHUB_CLIENT_ID: '',
|
||||||
|
GITHUB_CLIENT_SECRET: '',
|
||||||
|
FACEBOOK_CLIENT_ID: '',
|
||||||
|
FACEBOOK_CLIENT_SECRET: '',
|
||||||
|
ROLES: [],
|
||||||
|
DEFAULT_ROLES: [],
|
||||||
|
PROTECTED_ROLES: [],
|
||||||
|
JWT_TYPE: '',
|
||||||
|
JWT_SECRET: '',
|
||||||
|
JWT_ROLE_CLAIM: '',
|
||||||
|
JWT_PRIVATE_KEY: '',
|
||||||
|
JWT_PUBLIC_KEY: '',
|
||||||
|
REDIS_URL: '',
|
||||||
|
SMTP_HOST: '',
|
||||||
|
SMTP_PORT: '',
|
||||||
|
SMTP_USERNAME: '',
|
||||||
|
SMTP_PASSWORD: '',
|
||||||
|
SENDER_EMAIL: '',
|
||||||
|
ALLOWED_ORIGINS: [],
|
||||||
|
ORGANIZATION_NAME: '',
|
||||||
|
ORGANIZATION_LOGO: '',
|
||||||
|
CUSTOM_ACCESS_TOKEN_SCRIPT: '',
|
||||||
|
ADMIN_SECRET: '',
|
||||||
|
DISABLE_LOGIN_PAGE: false,
|
||||||
|
DISABLE_MAGIC_LINK_LOGIN: false,
|
||||||
|
DISABLE_EMAIL_VERIFICATION: false,
|
||||||
|
DISABLE_BASIC_AUTHENTICATION: false,
|
||||||
|
OLD_ADMIN_SECRET: '',
|
||||||
|
DATABASE_NAME: '',
|
||||||
|
DATABASE_TYPE: '',
|
||||||
|
DATABASE_URL: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [fieldVisibility, setFieldVisibility] = React.useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({
|
||||||
|
GOOGLE_CLIENT_SECRET: false,
|
||||||
|
GITHUB_CLIENT_SECRET: false,
|
||||||
|
FACEBOOK_CLIENT_SECRET: false,
|
||||||
|
JWT_SECRET: false,
|
||||||
|
SMTP_PASSWORD: false,
|
||||||
|
ADMIN_SECRET: false,
|
||||||
|
OLD_ADMIN_SECRET: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
async function getData() {
|
||||||
|
const {
|
||||||
|
data: { _env: envData },
|
||||||
|
} = await client.query(EnvVariablesQuery).toPromise();
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
|
setLoading(false);
|
||||||
|
setEnvVariables({
|
||||||
|
...envData,
|
||||||
|
OLD_ADMIN_SECRET: envData.ADMIN_SECRET,
|
||||||
|
ADMIN_SECRET: '',
|
||||||
|
});
|
||||||
|
setAdminSecret({
|
||||||
|
value: '',
|
||||||
|
disableInputField: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getData();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const validateAdminSecretHandler = (event: any) => {
|
||||||
|
if (envVariables.OLD_ADMIN_SECRET === event.target.value) {
|
||||||
|
setAdminSecret({
|
||||||
|
...adminSecret,
|
||||||
|
value: event.target.value,
|
||||||
|
disableInputField: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setAdminSecret({
|
||||||
|
...adminSecret,
|
||||||
|
value: event.target.value,
|
||||||
|
disableInputField: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (envVariables.ADMIN_SECRET !== '') {
|
||||||
|
setEnvVariables({ ...envVariables, ADMIN_SECRET: '' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveHandler = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const {
|
||||||
|
data: { _env: envData },
|
||||||
|
} = await client.query(EnvVariablesQuery).toPromise();
|
||||||
|
const diff = getObjectDiff(envVariables, envData);
|
||||||
|
const updatedEnvVariables = diff.reduce(
|
||||||
|
(acc: any, property: string) => ({
|
||||||
|
...acc,
|
||||||
|
// @ts-ignore
|
||||||
|
[property]: envVariables[property],
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
updatedEnvVariables[HiddenInputType.ADMIN_SECRET] === '' ||
|
||||||
|
updatedEnvVariables[HiddenInputType.OLD_ADMIN_SECRET] !==
|
||||||
|
envData.ADMIN_SECRET
|
||||||
|
) {
|
||||||
|
delete updatedEnvVariables.OLD_ADMIN_SECRET;
|
||||||
|
delete updatedEnvVariables.ADMIN_SECRET;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete updatedEnvVariables.DATABASE_URL;
|
||||||
|
delete updatedEnvVariables.DATABASE_TYPE;
|
||||||
|
delete updatedEnvVariables.DATABASE_NAME;
|
||||||
|
|
||||||
|
const res = await client
|
||||||
|
.mutation(UpdateEnvVariables, { params: updatedEnvVariables })
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
toast({
|
||||||
|
title: capitalizeFirstLetter(res.error.message),
|
||||||
|
isClosable: true,
|
||||||
|
status: 'error',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAdminSecret({
|
||||||
|
value: '',
|
||||||
|
disableInputField: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: `Successfully updated ${
|
||||||
|
Object.keys(updatedEnvVariables).length
|
||||||
|
} variables`,
|
||||||
|
isClosable: true,
|
||||||
|
status: 'success',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box m="5" py="5" px="10" bg="white" rounded="md">
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Your instance information
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Client ID</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={() => {}}
|
||||||
|
inputType={TextInputType.CLIENT_ID}
|
||||||
|
placeholder="Client ID"
|
||||||
|
isDisabled={true}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Client Secret</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
fieldVisibility={fieldVisibility}
|
||||||
|
setFieldVisibility={setFieldVisibility}
|
||||||
|
inputType={HiddenInputType.CLIENT_SECRET}
|
||||||
|
placeholder="Client Secret"
|
||||||
|
isDisabled={true}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Social Media Logins
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Center
|
||||||
|
w="50px"
|
||||||
|
marginRight="1.5%"
|
||||||
|
border="1px solid #e2e8f0"
|
||||||
|
borderRadius="5px"
|
||||||
|
>
|
||||||
|
<FaGoogle style={{ color: '#8c8c8c' }} />
|
||||||
|
</Center>
|
||||||
|
<Center w="45%" marginRight="1.5%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.GOOGLE_CLIENT_ID}
|
||||||
|
placeholder="Google Client ID"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
<Center w="45%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
fieldVisibility={fieldVisibility}
|
||||||
|
setFieldVisibility={setFieldVisibility}
|
||||||
|
inputType={HiddenInputType.GOOGLE_CLIENT_SECRET}
|
||||||
|
placeholder="Google Secret"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Center
|
||||||
|
w="50px"
|
||||||
|
marginRight="1.5%"
|
||||||
|
border="1px solid #e2e8f0"
|
||||||
|
borderRadius="5px"
|
||||||
|
>
|
||||||
|
<FaGithub style={{ color: '#8c8c8c' }} />
|
||||||
|
</Center>
|
||||||
|
<Center w="45%" marginRight="1.5%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.GITHUB_CLIENT_ID}
|
||||||
|
placeholder="Github Client ID"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
<Center w="45%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
fieldVisibility={fieldVisibility}
|
||||||
|
setFieldVisibility={setFieldVisibility}
|
||||||
|
inputType={HiddenInputType.GITHUB_CLIENT_SECRET}
|
||||||
|
placeholder="Github Secret"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Center
|
||||||
|
w="50px"
|
||||||
|
marginRight="1.5%"
|
||||||
|
border="1px solid #e2e8f0"
|
||||||
|
borderRadius="5px"
|
||||||
|
>
|
||||||
|
<FaFacebookF style={{ color: '#8c8c8c' }} />
|
||||||
|
</Center>
|
||||||
|
<Center w="45%" marginRight="1.5%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.FACEBOOK_CLIENT_ID}
|
||||||
|
placeholder="Facebook Client ID"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
<Center w="45%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
fieldVisibility={fieldVisibility}
|
||||||
|
setFieldVisibility={setFieldVisibility}
|
||||||
|
inputType={HiddenInputType.FACEBOOK_CLIENT_SECRET}
|
||||||
|
placeholder="Facebook Secret"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Roles
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Roles:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={ArrayInputType.ROLES}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Default Roles:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={ArrayInputType.DEFAULT_ROLES}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Protected Roles:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={ArrayInputType.PROTECTED_ROLES}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
JWT (JSON Web Tokens) Configurations
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">JWT Type:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={SelectInputType.JWT_TYPE}
|
||||||
|
value={SelectInputType.JWT_TYPE}
|
||||||
|
options={{
|
||||||
|
...HMACEncryptionType,
|
||||||
|
...RSAEncryptionType,
|
||||||
|
...ECDSAEncryptionType,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
{Object.values(HMACEncryptionType).includes(envVariables.JWT_TYPE) ? (
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">JWT Secret</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
fieldVisibility={fieldVisibility}
|
||||||
|
setFieldVisibility={setFieldVisibility}
|
||||||
|
inputType={HiddenInputType.JWT_SECRET}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Public Key</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextAreaInputType.JWT_PUBLIC_KEY}
|
||||||
|
placeholder="Add public key here"
|
||||||
|
minH="25vh"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Private Key</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextAreaInputType.JWT_PRIVATE_KEY}
|
||||||
|
placeholder="Add private key here"
|
||||||
|
minH="25vh"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">JWT Role Claim:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.JWT_ROLE_CLAIM}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Session Storage
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Redis URL:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.REDIS_URL}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Email Configurations
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">SMTP Host:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.SMTP_HOST}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">SMTP Port:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.SMTP_PORT}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">SMTP Username:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.SMTP_USERNAME}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">SMTP Password:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
fieldVisibility={fieldVisibility}
|
||||||
|
setFieldVisibility={setFieldVisibility}
|
||||||
|
inputType={HiddenInputType.SMTP_PASSWORD}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">From Email:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.SENDER_EMAIL}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
White Listing
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Allowed Origins:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={ArrayInputType.ALLOWED_ORIGINS}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Organization Information
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Organization Name:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.ORGANIZATION_NAME}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Organization Logo:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.ORGANIZATION_LOGO}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Custom Access Token Scripts
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Center w="100%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextAreaInputType.CUSTOM_ACCESS_TOKEN_SCRIPT}
|
||||||
|
placeholder="Add script here"
|
||||||
|
minH="25vh"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Disable Features
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Disable Login Page:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex justifyContent="start" w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={SwitchInputType.DISABLE_LOGIN_PAGE}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Disable Email Verification:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex justifyContent="start" w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={SwitchInputType.DISABLE_EMAIL_VERIFICATION}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Disable Magic Login Link:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex justifyContent="start" w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={SwitchInputType.DISABLE_MAGIC_LINK_LOGIN}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Disable Basic Authentication:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex justifyContent="start" w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={SwitchInputType.DISABLE_BASIC_AUTHENTICATION}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Danger
|
||||||
|
</Text>
|
||||||
|
<Stack
|
||||||
|
spacing={6}
|
||||||
|
padding="0 5%"
|
||||||
|
marginTop="3%"
|
||||||
|
border="1px solid #ff7875"
|
||||||
|
borderRadius="5px"
|
||||||
|
>
|
||||||
|
<Stack spacing={6} padding="3% 0">
|
||||||
|
<Text fontStyle="italic" fontSize="sm" color="gray.600">
|
||||||
|
Note: Database related environment variables cannot be updated from
|
||||||
|
dashboard :(
|
||||||
|
</Text>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">DataBase Name:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.DATABASE_NAME}
|
||||||
|
isDisabled={true}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">DataBase Type:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.DATABASE_TYPE}
|
||||||
|
isDisabled={true}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">DataBase URL:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.DATABASE_URL}
|
||||||
|
isDisabled={true}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Flex marginTop="3%">
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Old Admin Secret:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input
|
||||||
|
size="sm"
|
||||||
|
placeholder="Enter Old Admin Secret"
|
||||||
|
value={adminSecret.value as string}
|
||||||
|
onChange={(event: any) => validateAdminSecretHandler(event)}
|
||||||
|
type={
|
||||||
|
!fieldVisibility[HiddenInputType.OLD_ADMIN_SECRET]
|
||||||
|
? 'password'
|
||||||
|
: 'text'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputRightElement
|
||||||
|
right="5px"
|
||||||
|
children={
|
||||||
|
<Flex>
|
||||||
|
{fieldVisibility[HiddenInputType.OLD_ADMIN_SECRET] ? (
|
||||||
|
<Center
|
||||||
|
w="25px"
|
||||||
|
margin="0 1.5%"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFieldVisibility({
|
||||||
|
...fieldVisibility,
|
||||||
|
[HiddenInputType.OLD_ADMIN_SECRET]: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaRegEyeSlash color="#bfbfbf" />
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<Center
|
||||||
|
w="25px"
|
||||||
|
margin="0 1.5%"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFieldVisibility({
|
||||||
|
...fieldVisibility,
|
||||||
|
[HiddenInputType.OLD_ADMIN_SECRET]: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaRegEye color="#bfbfbf" />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex paddingBottom="3%">
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">New Admin Secret:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={HiddenInputType.ADMIN_SECRET}
|
||||||
|
fieldVisibility={fieldVisibility}
|
||||||
|
setFieldVisibility={setFieldVisibility}
|
||||||
|
isDisabled={adminSecret.disableInputField}
|
||||||
|
placeholder="Enter New Admin Secret"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="5%" marginBottom="2%" />
|
||||||
|
<Stack spacing={6} padding="1% 0">
|
||||||
|
<Flex justifyContent="end" alignItems="center">
|
||||||
|
<Button
|
||||||
|
leftIcon={<FaSave />}
|
||||||
|
colorScheme="blue"
|
||||||
|
variant="solid"
|
||||||
|
onClick={saveHandler}
|
||||||
|
isDisabled={loading}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
18
dashboard/src/pages/Home.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Text } from '@chakra-ui/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text fontSize="2xl" fontWeight="bold">
|
||||||
|
Hi there 👋 <br />
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text fontSize="xl" color="gray.700">
|
||||||
|
Welcome to Authorizer Administrative Dashboard! <br />
|
||||||
|
Please use this dashboard to configure your environment variables or
|
||||||
|
have look at your users
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
400
dashboard/src/pages/Users.tsx
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useClient } from 'urql';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
IconButton,
|
||||||
|
NumberDecrementStepper,
|
||||||
|
NumberIncrementStepper,
|
||||||
|
NumberInput,
|
||||||
|
NumberInputField,
|
||||||
|
NumberInputStepper,
|
||||||
|
Select,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
Text,
|
||||||
|
TableCaption,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tooltip,
|
||||||
|
Tr,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
useToast,
|
||||||
|
Spinner,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
FaAngleLeft,
|
||||||
|
FaAngleRight,
|
||||||
|
FaAngleDoubleLeft,
|
||||||
|
FaAngleDoubleRight,
|
||||||
|
FaExclamationCircle,
|
||||||
|
FaAngleDown,
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
import { UserDetailsQuery } from '../graphql/queries';
|
||||||
|
import { UpdateUser } from '../graphql/mutation';
|
||||||
|
import EditUserModal from '../components/EditUserModal';
|
||||||
|
import DeleteUserModal from '../components/DeleteUserModal';
|
||||||
|
|
||||||
|
interface paginationPropTypes {
|
||||||
|
limit: number;
|
||||||
|
page: number;
|
||||||
|
offset: number;
|
||||||
|
total: number;
|
||||||
|
maxPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface userDataTypes {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
email_verified: boolean;
|
||||||
|
given_name: string;
|
||||||
|
family_name: string;
|
||||||
|
middle_name: string;
|
||||||
|
nickname: string;
|
||||||
|
gender: string;
|
||||||
|
birthdate: string;
|
||||||
|
phone_number: string;
|
||||||
|
picture: string;
|
||||||
|
signup_methods: string;
|
||||||
|
roles: [string];
|
||||||
|
created_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMaxPages = (pagination: paginationPropTypes) => {
|
||||||
|
const { limit, total } = pagination;
|
||||||
|
if (total > 1) {
|
||||||
|
return total % limit === 0
|
||||||
|
? total / limit
|
||||||
|
: parseInt(`${total / limit}`) + 1;
|
||||||
|
} else return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLimits = (pagination: paginationPropTypes) => {
|
||||||
|
const { total } = pagination;
|
||||||
|
const limits = [5];
|
||||||
|
if (total > 10) {
|
||||||
|
for (let i = 10; i <= total && limits.length <= 10; i += 5) {
|
||||||
|
limits.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return limits;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Users() {
|
||||||
|
const client = useClient();
|
||||||
|
const toast = useToast();
|
||||||
|
const [paginationProps, setPaginationProps] =
|
||||||
|
React.useState<paginationPropTypes>({
|
||||||
|
limit: 5,
|
||||||
|
page: 1,
|
||||||
|
offset: 0,
|
||||||
|
total: 0,
|
||||||
|
maxPages: 1,
|
||||||
|
});
|
||||||
|
const [userList, setUserList] = React.useState<userDataTypes[]>([]);
|
||||||
|
const [loading, setLoading] = React.useState<boolean>(false);
|
||||||
|
const updateUserList = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const { data } = await client
|
||||||
|
.query(UserDetailsQuery, {
|
||||||
|
params: {
|
||||||
|
pagination: {
|
||||||
|
limit: paginationProps.limit,
|
||||||
|
page: paginationProps.page,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.toPromise();
|
||||||
|
if (data?._users) {
|
||||||
|
const { pagination, users } = data._users;
|
||||||
|
const maxPages = getMaxPages(pagination);
|
||||||
|
if (users && users.length > 0) {
|
||||||
|
setPaginationProps({ ...paginationProps, ...pagination, maxPages });
|
||||||
|
setUserList(users);
|
||||||
|
} else {
|
||||||
|
if (paginationProps.page !== 1) {
|
||||||
|
setPaginationProps({
|
||||||
|
...paginationProps,
|
||||||
|
...pagination,
|
||||||
|
maxPages,
|
||||||
|
page: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
React.useEffect(() => {
|
||||||
|
updateUserList();
|
||||||
|
}, []);
|
||||||
|
React.useEffect(() => {
|
||||||
|
updateUserList();
|
||||||
|
}, [paginationProps.page, paginationProps.limit]);
|
||||||
|
|
||||||
|
const paginationHandler = (value: Record<string, number>) => {
|
||||||
|
setPaginationProps({ ...paginationProps, ...value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const userVerificationHandler = async (user: userDataTypes) => {
|
||||||
|
const { id, email } = user;
|
||||||
|
const res = await client
|
||||||
|
.mutation(UpdateUser, {
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
email_verified: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.toPromise();
|
||||||
|
if (res.error) {
|
||||||
|
toast({
|
||||||
|
title: 'User verification failed',
|
||||||
|
isClosable: true,
|
||||||
|
status: 'error',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
} else if (res.data?._update_user?.id) {
|
||||||
|
toast({
|
||||||
|
title: 'User verification successful',
|
||||||
|
isClosable: true,
|
||||||
|
status: 'success',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateUserList();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box m="5" py="5" px="10" bg="white" rounded="md">
|
||||||
|
<Flex margin="2% 0" justifyContent="space-between" alignItems="center">
|
||||||
|
<Text fontSize="md" fontWeight="bold">
|
||||||
|
Users
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
{!loading ? (
|
||||||
|
userList.length > 0 ? (
|
||||||
|
<Table variant="simple">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Email</Th>
|
||||||
|
<Th>Created At</Th>
|
||||||
|
<Th>Signup Methods</Th>
|
||||||
|
<Th>Roles</Th>
|
||||||
|
<Th>Verified</Th>
|
||||||
|
<Th>Actions</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{userList.map((user: userDataTypes) => {
|
||||||
|
const { email_verified, created_at, ...rest }: any = user;
|
||||||
|
return (
|
||||||
|
<Tr key={user.id} style={{ fontSize: 14 }}>
|
||||||
|
<Td>{user.email}</Td>
|
||||||
|
<Td>
|
||||||
|
{dayjs(user.created_at * 1000).format('MMM DD, YYYY')}
|
||||||
|
</Td>
|
||||||
|
<Td>{user.signup_methods}</Td>
|
||||||
|
<Td>{user.roles.join(', ')}</Td>
|
||||||
|
<Td>
|
||||||
|
<Tag
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme={user.email_verified ? 'green' : 'yellow'}
|
||||||
|
>
|
||||||
|
{user.email_verified.toString()}
|
||||||
|
</Tag>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Menu>
|
||||||
|
<MenuButton as={Button} variant="unstyled" size="sm">
|
||||||
|
<Flex
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm" fontWeight="light">
|
||||||
|
Menu
|
||||||
|
</Text>
|
||||||
|
<FaAngleDown style={{ marginLeft: 10 }} />
|
||||||
|
</Flex>
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList>
|
||||||
|
{!user.email_verified && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => userVerificationHandler(user)}
|
||||||
|
>
|
||||||
|
Verify User
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<EditUserModal
|
||||||
|
user={rest}
|
||||||
|
updateUserList={updateUserList}
|
||||||
|
/>
|
||||||
|
<DeleteUserModal
|
||||||
|
user={rest}
|
||||||
|
updateUserList={updateUserList}
|
||||||
|
/>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tbody>
|
||||||
|
{(paginationProps.maxPages > 1 || paginationProps.total >= 5) && (
|
||||||
|
<TableCaption>
|
||||||
|
<Flex
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
m="2% 0"
|
||||||
|
>
|
||||||
|
<Flex flex="1">
|
||||||
|
<Tooltip label="First Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={paginationProps.page <= 1}
|
||||||
|
mr={4}
|
||||||
|
icon={<FaAngleDoubleLeft />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Previous Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: paginationProps.page - 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={paginationProps.page <= 1}
|
||||||
|
icon={<FaAngleLeft />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
flex="8"
|
||||||
|
justifyContent="space-evenly"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Text mr={8}>
|
||||||
|
Page{' '}
|
||||||
|
<Text fontWeight="bold" as="span">
|
||||||
|
{paginationProps.page}
|
||||||
|
</Text>{' '}
|
||||||
|
of{' '}
|
||||||
|
<Text fontWeight="bold" as="span">
|
||||||
|
{paginationProps.maxPages}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<Text flexShrink="0">Go to page:</Text>{' '}
|
||||||
|
<NumberInput
|
||||||
|
ml={2}
|
||||||
|
mr={8}
|
||||||
|
w={28}
|
||||||
|
min={1}
|
||||||
|
max={paginationProps.maxPages}
|
||||||
|
onChange={(value) =>
|
||||||
|
paginationHandler({
|
||||||
|
page: parseInt(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value={paginationProps.page}
|
||||||
|
>
|
||||||
|
<NumberInputField />
|
||||||
|
<NumberInputStepper>
|
||||||
|
<NumberIncrementStepper />
|
||||||
|
<NumberDecrementStepper />
|
||||||
|
</NumberInputStepper>
|
||||||
|
</NumberInput>
|
||||||
|
</Flex>
|
||||||
|
<Select
|
||||||
|
w={32}
|
||||||
|
value={paginationProps.limit}
|
||||||
|
onChange={(e) =>
|
||||||
|
paginationHandler({
|
||||||
|
page: 1,
|
||||||
|
limit: parseInt(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{getLimits(paginationProps).map((pageSize) => (
|
||||||
|
<option key={pageSize} value={pageSize}>
|
||||||
|
Show {pageSize}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Flex>
|
||||||
|
<Flex flex="1">
|
||||||
|
<Tooltip label="Next Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: paginationProps.page + 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={
|
||||||
|
paginationProps.page >= paginationProps.maxPages
|
||||||
|
}
|
||||||
|
icon={<FaAngleRight />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Last Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: paginationProps.maxPages,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={
|
||||||
|
paginationProps.page >= paginationProps.maxPages
|
||||||
|
}
|
||||||
|
ml={4}
|
||||||
|
icon={<FaAngleDoubleRight />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</TableCaption>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
) : (
|
||||||
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
minH="25vh"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Center w="50px" marginRight="1.5%">
|
||||||
|
<FaExclamationCircle style={{ color: '#f0f0f0', fontSize: 70 }} />
|
||||||
|
</Center>
|
||||||
|
<Text
|
||||||
|
fontSize="2xl"
|
||||||
|
paddingRight="1%"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="#d9d9d9"
|
||||||
|
>
|
||||||
|
No Data
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Center minH="25vh">
|
||||||
|
<Spinner />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
43
dashboard/src/routes/index.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React, { lazy, Suspense } from 'react';
|
||||||
|
import { Outlet, Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useAuthContext } from '../contexts/AuthContext';
|
||||||
|
import { DashboardLayout } from '../layouts/DashboardLayout';
|
||||||
|
|
||||||
|
const Auth = lazy(() => import('../pages/Auth'));
|
||||||
|
const Environment = lazy(() => import('../pages/Environment'));
|
||||||
|
const Home = lazy(() => import('../pages/Home'));
|
||||||
|
const Users = lazy(() => import('../pages/Users'));
|
||||||
|
|
||||||
|
export const AppRoutes = () => {
|
||||||
|
const { isLoggedIn } = useAuthContext();
|
||||||
|
|
||||||
|
if (isLoggedIn) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<></>}>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
element={
|
||||||
|
<DashboardLayout>
|
||||||
|
<Outlet />
|
||||||
|
</DashboardLayout>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Route path="/" element={<Environment />} />
|
||||||
|
<Route path="users" element={<Users />} />
|
||||||
|
<Route path="environment" element={<Environment />} />
|
||||||
|
<Route path="*" element={<Home />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<></>}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Auth />} />
|
||||||
|
<Route path="*" element={<Auth />} />
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
};
|
66
dashboard/src/utils/index.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export const hasAdminSecret = () => {
|
||||||
|
return (<any>window)['__authorizer__'].isOnboardingCompleted === true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const capitalizeFirstLetter = (data: string): string =>
|
||||||
|
data.charAt(0).toUpperCase() + data.slice(1);
|
||||||
|
|
||||||
|
const fallbackCopyTextToClipboard = (text: string) => {
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
|
||||||
|
textArea.value = text;
|
||||||
|
textArea.style.top = '0';
|
||||||
|
textArea.style.left = '0';
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const successful = document.execCommand('copy');
|
||||||
|
const msg = successful ? 'successful' : 'unsuccessful';
|
||||||
|
console.log('Fallback: Copying text command was ' + msg);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fallback: Oops, unable to copy', err);
|
||||||
|
}
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const copyTextToClipboard = (text: string) => {
|
||||||
|
if (!navigator.clipboard) {
|
||||||
|
fallbackCopyTextToClipboard(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(text).then(
|
||||||
|
() => {
|
||||||
|
console.log('Async: Copying to clipboard was successful!');
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.error('Async: Could not copy text: ', err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getObjectDiff = (obj1: any, obj2: any) => {
|
||||||
|
const diff = Object.keys(obj1).reduce((result, key) => {
|
||||||
|
if (!obj2.hasOwnProperty(key)) {
|
||||||
|
result.push(key);
|
||||||
|
} else if (
|
||||||
|
_.isEqual(obj1[key], obj2[key]) ||
|
||||||
|
(obj1[key] === null && obj2[key] === '') ||
|
||||||
|
(obj1[key] &&
|
||||||
|
Array.isArray(obj1[key]) &&
|
||||||
|
obj1[key].length === 0 &&
|
||||||
|
obj2[key] === null)
|
||||||
|
) {
|
||||||
|
const resultKeyIndex = result.indexOf(key);
|
||||||
|
result.splice(resultKeyIndex, 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, Object.keys(obj2));
|
||||||
|
|
||||||
|
return diff;
|
||||||
|
};
|
73
dashboard/tsconfig.json
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||||
|
|
||||||
|
/* Basic Options */
|
||||||
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
|
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */,
|
||||||
|
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
|
||||||
|
// "lib": ["es2018", "dom"], /* Specify library files to be included in the compilation. */
|
||||||
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
|
"jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */,
|
||||||
|
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
|
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
|
// "outDir": "./", /* Redirect output structure to the directory. */
|
||||||
|
"rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
|
||||||
|
// "composite": true, /* Enable project compilation */
|
||||||
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
|
// "removeComments": true, /* Do not emit comments to output. */
|
||||||
|
// "noEmit": true, /* Do not emit outputs. */
|
||||||
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
"strict": true /* Enable all strict type-checking options. */,
|
||||||
|
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||||
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
|
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
|
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
|
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
|
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
|
/* Additional Checks */
|
||||||
|
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
||||||
|
|
||||||
|
/* Module Resolution Options */
|
||||||
|
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
|
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
|
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||||
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
|
||||||
|
/* Source Map Options */
|
||||||
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||||
|
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||||
|
|
||||||
|
/* Experimental Options */
|
||||||
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
|
||||||
|
/* Advanced Options */
|
||||||
|
"skipLibCheck": true /* Skip type checking of declaration files. */,
|
||||||
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
|
||||||
|
"lib": ["esnext", "dom"]
|
||||||
|
}
|
||||||
|
}
|
18
scripts/build-mac.sh
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
VERSION="$1"
|
||||||
|
make clean && make build-app && CGO_ENABLED=1 VERSION=${VERSION} make
|
||||||
|
FILE_NAME=authorizer-${VERSION}-darwin-amd64.tar.gz
|
||||||
|
tar cvfz ${FILE_NAME} .env app/build build templates dashboard/build
|
||||||
|
AUTH="Authorization: token $GITHUB_TOKEN"
|
||||||
|
RELASE_INFO=$(curl -sH "$AUTH" https://api.github.com/repos/authorizerdev/authorizer/releases/tags/${VERSION})
|
||||||
|
echo $RELASE_INFO
|
||||||
|
|
||||||
|
eval $(echo "$RELASE_INFO" | grep -m 1 "id.:" | grep -w id | tr : = | tr -cd '[[:alnum:]]=')
|
||||||
|
[ "$id" ] || { echo "Error: Failed to get release id for tag: $VERSION"; echo "$RELASE_INFO" | awk 'length($0)<100' >&2; exit 1; }
|
||||||
|
echo $id
|
||||||
|
GH_ASSET="https://uploads.github.com/repos/authorizerdev/authorizer/releases/$id/assets?name=$(basename $FILE_NAME)"
|
||||||
|
|
||||||
|
echo $GH_ASSET
|
||||||
|
|
||||||
|
curl -H $AUTH -H "Content-Type: $(file -b --mime-type $FILE_NAME)" --data-binary @$FILE_NAME $GH_ASSET
|
||||||
|
|
||||||
|
curl "$GITHUB_OAUTH_BASIC" --data-binary @"$FILE_NAME" -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/octet-stream" $GH_ASSET
|
@@ -1,35 +0,0 @@
|
|||||||
package constants
|
|
||||||
|
|
||||||
var (
|
|
||||||
ADMIN_SECRET = ""
|
|
||||||
ENV = ""
|
|
||||||
VERSION = ""
|
|
||||||
DATABASE_TYPE = ""
|
|
||||||
DATABASE_URL = ""
|
|
||||||
SMTP_HOST = ""
|
|
||||||
SMTP_PORT = ""
|
|
||||||
SENDER_EMAIL = ""
|
|
||||||
SENDER_PASSWORD = ""
|
|
||||||
JWT_TYPE = ""
|
|
||||||
JWT_SECRET = ""
|
|
||||||
ALLOWED_ORIGINS = []string{}
|
|
||||||
ALLOWED_CALLBACK_URLS = []string{}
|
|
||||||
AUTHORIZER_URL = ""
|
|
||||||
PORT = "8080"
|
|
||||||
REDIS_URL = ""
|
|
||||||
IS_PROD = false
|
|
||||||
COOKIE_NAME = ""
|
|
||||||
RESET_PASSWORD_URL = ""
|
|
||||||
DISABLE_EMAIL_VERIFICATION = "false"
|
|
||||||
DISABLE_BASIC_AUTHENTICATION = "false"
|
|
||||||
|
|
||||||
// OAuth login
|
|
||||||
GOOGLE_CLIENT_ID = ""
|
|
||||||
GOOGLE_CLIENT_SECRET = ""
|
|
||||||
GITHUB_CLIENT_ID = ""
|
|
||||||
GITHUB_CLIENT_SECRET = ""
|
|
||||||
FACEBOOK_CLIENT_ID = ""
|
|
||||||
FACEBOOK_CLIENT_SECRET = ""
|
|
||||||
TWITTER_CLIENT_ID = ""
|
|
||||||
TWITTER_CLIENT_SECRET = ""
|
|
||||||
)
|
|
20
server/constants/db_types.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DbTypePostgres is the postgres database type
|
||||||
|
DbTypePostgres = "postgres"
|
||||||
|
// DbTypeSqlite is the sqlite database type
|
||||||
|
DbTypeSqlite = "sqlite"
|
||||||
|
// DbTypeMysql is the mysql database type
|
||||||
|
DbTypeMysql = "mysql"
|
||||||
|
// DbTypeSqlserver is the sqlserver database type
|
||||||
|
DbTypeSqlserver = "sqlserver"
|
||||||
|
// DbTypeArangodb is the arangodb database type
|
||||||
|
DbTypeArangodb = "arangodb"
|
||||||
|
// DbTypeMongodb is the mongodb database type
|
||||||
|
DbTypeMongodb = "mongodb"
|
||||||
|
// DbTypeYugabyte is the yugabyte database type
|
||||||
|
DbTypeYugabyte = "yugabyte"
|
||||||
|
// DbTypeMariaDB is the mariadb database type
|
||||||
|
DbTypeMariaDB = "mariadb"
|
||||||
|
)
|
108
server/constants/env.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Envstore identifier
|
||||||
|
// StringStore string store identifier
|
||||||
|
StringStoreIdentifier = "stringStore"
|
||||||
|
// BoolStore bool store identifier
|
||||||
|
BoolStoreIdentifier = "boolStore"
|
||||||
|
// SliceStore slice store identifier
|
||||||
|
SliceStoreIdentifier = "sliceStore"
|
||||||
|
|
||||||
|
// EnvKeyEnv key for env variable ENV
|
||||||
|
EnvKeyEnv = "ENV"
|
||||||
|
// EnvKeyEnvPath key for cli arg variable ENV_PATH
|
||||||
|
EnvKeyEnvPath = "ENV_PATH"
|
||||||
|
// EnvKeyVersion key for build arg version
|
||||||
|
EnvKeyVersion = "VERSION"
|
||||||
|
// EnvKeyAuthorizerURL key for env variable AUTHORIZER_URL
|
||||||
|
// TODO: remove support AUTHORIZER_URL env
|
||||||
|
EnvKeyAuthorizerURL = "AUTHORIZER_URL"
|
||||||
|
// EnvKeyPort key for env variable PORT
|
||||||
|
EnvKeyPort = "PORT"
|
||||||
|
|
||||||
|
// EnvKeyAdminSecret key for env variable ADMIN_SECRET
|
||||||
|
EnvKeyAdminSecret = "ADMIN_SECRET"
|
||||||
|
// EnvKeyDatabaseType key for env variable DATABASE_TYPE
|
||||||
|
EnvKeyDatabaseType = "DATABASE_TYPE"
|
||||||
|
// EnvKeyDatabaseURL key for env variable DATABASE_URL
|
||||||
|
EnvKeyDatabaseURL = "DATABASE_URL"
|
||||||
|
// EnvKeyDatabaseName key for env variable DATABASE_NAME
|
||||||
|
EnvKeyDatabaseName = "DATABASE_NAME"
|
||||||
|
// EnvKeySmtpHost key for env variable SMTP_HOST
|
||||||
|
EnvKeySmtpHost = "SMTP_HOST"
|
||||||
|
// EnvKeySmtpPort key for env variable SMTP_PORT
|
||||||
|
EnvKeySmtpPort = "SMTP_PORT"
|
||||||
|
// EnvKeySmtpUsername key for env variable SMTP_USERNAME
|
||||||
|
EnvKeySmtpUsername = "SMTP_USERNAME"
|
||||||
|
// EnvKeySmtpPassword key for env variable SMTP_PASSWORD
|
||||||
|
EnvKeySmtpPassword = "SMTP_PASSWORD"
|
||||||
|
// EnvKeySenderEmail key for env variable SENDER_EMAIL
|
||||||
|
EnvKeySenderEmail = "SENDER_EMAIL"
|
||||||
|
// EnvKeyJwtType key for env variable JWT_TYPE
|
||||||
|
EnvKeyJwtType = "JWT_TYPE"
|
||||||
|
// EnvKeyJwtSecret key for env variable JWT_SECRET
|
||||||
|
EnvKeyJwtSecret = "JWT_SECRET"
|
||||||
|
// EnvKeyJwtPrivateKey key for env variable JWT_PRIVATE_KEY
|
||||||
|
EnvKeyJwtPrivateKey = "JWT_PRIVATE_KEY"
|
||||||
|
// EnvKeyJwtPublicKey key for env variable JWT_PUBLIC_KEY
|
||||||
|
EnvKeyJwtPublicKey = "JWT_PUBLIC_KEY"
|
||||||
|
// EnvKeyAllowedOrigins key for env variable ALLOWED_ORIGINS
|
||||||
|
EnvKeyAllowedOrigins = "ALLOWED_ORIGINS"
|
||||||
|
// EnvKeyAppURL key for env variable APP_URL
|
||||||
|
EnvKeyAppURL = "APP_URL"
|
||||||
|
// EnvKeyRedisURL key for env variable REDIS_URL
|
||||||
|
EnvKeyRedisURL = "REDIS_URL"
|
||||||
|
// EnvKeyCookieName key for env variable COOKIE_NAME
|
||||||
|
EnvKeyCookieName = "COOKIE_NAME"
|
||||||
|
// EnvKeyAdminCookieName key for env variable ADMIN_COOKIE_NAME
|
||||||
|
EnvKeyAdminCookieName = "ADMIN_COOKIE_NAME"
|
||||||
|
// EnvKeyResetPasswordURL key for env variable RESET_PASSWORD_URL
|
||||||
|
EnvKeyResetPasswordURL = "RESET_PASSWORD_URL"
|
||||||
|
// EnvKeyDisableEmailVerification key for env variable DISABLE_EMAIL_VERIFICATION
|
||||||
|
EnvKeyDisableEmailVerification = "DISABLE_EMAIL_VERIFICATION"
|
||||||
|
// EnvKeyDisableBasicAuthentication key for env variable DISABLE_BASIC_AUTH
|
||||||
|
EnvKeyDisableBasicAuthentication = "DISABLE_BASIC_AUTHENTICATION"
|
||||||
|
// EnvKeyDisableMagicLinkLogin key for env variable DISABLE_MAGIC_LINK_LOGIN
|
||||||
|
EnvKeyDisableMagicLinkLogin = "DISABLE_MAGIC_LINK_LOGIN"
|
||||||
|
// EnvKeyDisableLoginPage key for env variable DISABLE_LOGIN_PAGE
|
||||||
|
EnvKeyDisableLoginPage = "DISABLE_LOGIN_PAGE"
|
||||||
|
// EnvKeyRoles key for env variable ROLES
|
||||||
|
EnvKeyRoles = "ROLES"
|
||||||
|
// EnvKeyProtectedRoles key for env variable PROTECTED_ROLES
|
||||||
|
EnvKeyProtectedRoles = "PROTECTED_ROLES"
|
||||||
|
// EnvKeyDefaultRoles key for env variable DEFAULT_ROLES
|
||||||
|
EnvKeyDefaultRoles = "DEFAULT_ROLES"
|
||||||
|
// EnvKeyJwtRoleClaim key for env variable JWT_ROLE_CLAIM
|
||||||
|
EnvKeyJwtRoleClaim = "JWT_ROLE_CLAIM"
|
||||||
|
// EnvKeyGoogleClientID key for env variable GOOGLE_CLIENT_ID
|
||||||
|
EnvKeyGoogleClientID = "GOOGLE_CLIENT_ID"
|
||||||
|
// EnvKeyGoogleClientSecret key for env variable GOOGLE_CLIENT_SECRET
|
||||||
|
EnvKeyGoogleClientSecret = "GOOGLE_CLIENT_SECRET"
|
||||||
|
// EnvKeyGithubClientID key for env variable GITHUB_CLIENT_ID
|
||||||
|
EnvKeyGithubClientID = "GITHUB_CLIENT_ID"
|
||||||
|
// EnvKeyGithubClientSecret key for env variable GITHUB_CLIENT_SECRET
|
||||||
|
EnvKeyGithubClientSecret = "GITHUB_CLIENT_SECRET"
|
||||||
|
// EnvKeyFacebookClientID key for env variable FACEBOOK_CLIENT_ID
|
||||||
|
EnvKeyFacebookClientID = "FACEBOOK_CLIENT_ID"
|
||||||
|
// EnvKeyFacebookClientSecret key for env variable FACEBOOK_CLIENT_SECRET
|
||||||
|
EnvKeyFacebookClientSecret = "FACEBOOK_CLIENT_SECRET"
|
||||||
|
// EnvKeyOrganizationName key for env variable ORGANIZATION_NAME
|
||||||
|
EnvKeyOrganizationName = "ORGANIZATION_NAME"
|
||||||
|
// EnvKeyOrganizationLogo key for env variable ORGANIZATION_LOGO
|
||||||
|
EnvKeyOrganizationLogo = "ORGANIZATION_LOGO"
|
||||||
|
// EnvKeyCustomAccessTokenScript key for env variable CUSTOM_ACCESS_TOKEN_SCRIPT
|
||||||
|
EnvKeyCustomAccessTokenScript = "CUSTOM_ACCESS_TOKEN_SCRIPT"
|
||||||
|
|
||||||
|
// Not Exposed Keys
|
||||||
|
// EnvKeyClientID key for env variable CLIENT_ID
|
||||||
|
EnvKeyClientID = "CLIENT_ID"
|
||||||
|
// EnvKeyClientSecret key for env variable CLIENT_SECRET
|
||||||
|
EnvKeyClientSecret = "CLIENT_SECRET"
|
||||||
|
// EnvKeyEncryptionKey key for env variable ENCRYPTION_KEY
|
||||||
|
EnvKeyEncryptionKey = "ENCRYPTION_KEY"
|
||||||
|
// EnvKeyJWK key for env variable JWK
|
||||||
|
EnvKeyJWK = "JWK"
|
||||||
|
// EnvKeyIsProd key for env variable IS_PROD
|
||||||
|
EnvKeyIsProd = "IS_PROD"
|
||||||
|
)
|
@@ -1,7 +1,8 @@
|
|||||||
package constants
|
package constants
|
||||||
|
|
||||||
var (
|
const (
|
||||||
// Ref: https://github.com/qor/auth/blob/master/providers/google/google.go
|
// Ref: https://github.com/qor/auth/blob/master/providers/google/google.go
|
||||||
|
// deprecated and not used. instead we follow open id approach for google login
|
||||||
GoogleUserInfoURL = "https://www.googleapis.com/oauth2/v3/userinfo"
|
GoogleUserInfoURL = "https://www.googleapis.com/oauth2/v3/userinfo"
|
||||||
// Ref: https://github.com/qor/auth/blob/master/providers/facebook/facebook.go#L18
|
// Ref: https://github.com/qor/auth/blob/master/providers/facebook/facebook.go#L18
|
||||||
FacebookUserInfoURL = "https://graph.facebook.com/me?fields=id,first_name,last_name,name,email,picture&access_token="
|
FacebookUserInfoURL = "https://graph.facebook.com/me?fields=id,first_name,last_name,name,email,picture&access_token="
|
4
server/constants/pagination.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
// DefaultLimit is the default limit for pagination
|
||||||
|
var DefaultLimit = 10
|
14
server/constants/signup_methods.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SignupMethodBasicAuth is the basic_auth signup method
|
||||||
|
SignupMethodBasicAuth = "basic_auth"
|
||||||
|
// SignupMethodMagicLinkLogin is the magic_link_login signup method
|
||||||
|
SignupMethodMagicLinkLogin = "magic_link_login"
|
||||||
|
// SignupMethodGoogle is the google signup method
|
||||||
|
SignupMethodGoogle = "google"
|
||||||
|
// SignupMethodGithub is the github signup method
|
||||||
|
SignupMethodGithub = "github"
|
||||||
|
// SignupMethodFacebook is the facebook signup method
|
||||||
|
SignupMethodFacebook = "facebook"
|
||||||
|
)
|
10
server/constants/token_types.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TokenTypeRefreshToken is the refresh_token token type
|
||||||
|
TokenTypeRefreshToken = "refresh_token"
|
||||||
|
// TokenTypeAccessToken is the access_token token type
|
||||||
|
TokenTypeAccessToken = "access_token"
|
||||||
|
// TokenTypeIdentityToken is the identity_token token type
|
||||||
|
TokenTypeIdentityToken = "id_token"
|
||||||
|
)
|
12
server/constants/verification_types.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
const (
|
||||||
|
// VerificationTypeBasicAuthSignup is the basic_auth_signup verification type
|
||||||
|
VerificationTypeBasicAuthSignup = "basic_auth_signup"
|
||||||
|
// VerificationTypeMagicLinkLogin is the magic_link_login verification type
|
||||||
|
VerificationTypeMagicLinkLogin = "magic_link_login"
|
||||||
|
// VerificationTypeUpdateEmail is the update_email verification type
|
||||||
|
VerificationTypeUpdateEmail = "update_email"
|
||||||
|
// VerificationTypeForgotPassword is the forgot_password verification type
|
||||||
|
VerificationTypeForgotPassword = "forgot_password"
|
||||||
|
)
|
46
server/cookie/admin_cookie.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package cookie
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
|
"github.com/authorizerdev/authorizer/server/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetAdminCookie sets the admin cookie in the response
|
||||||
|
func SetAdminCookie(gc *gin.Context, token string) {
|
||||||
|
secure := true
|
||||||
|
httpOnly := true
|
||||||
|
hostname := utils.GetHost(gc)
|
||||||
|
host, _ := utils.GetHostParts(hostname)
|
||||||
|
|
||||||
|
gc.SetCookie(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), token, 3600, "/", host, secure, httpOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAdminCookie gets the admin cookie from the request
|
||||||
|
func GetAdminCookie(gc *gin.Context) (string, error) {
|
||||||
|
cookie, err := gc.Request.Cookie(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// cookie escapes special characters like $
|
||||||
|
// hence we need to unescape before comparing
|
||||||
|
decodedValue, err := url.QueryUnescape(cookie.Value)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return decodedValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAdminCookie sets the response cookie to empty
|
||||||
|
func DeleteAdminCookie(gc *gin.Context) {
|
||||||
|
secure := true
|
||||||
|
httpOnly := true
|
||||||
|
hostname := utils.GetHost(gc)
|
||||||
|
host, _ := utils.GetHostParts(hostname)
|
||||||
|
|
||||||
|
gc.SetCookie(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), "", -1, "/", host, secure, httpOnly)
|
||||||
|
}
|
65
server/cookie/cookie.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package cookie
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
|
"github.com/authorizerdev/authorizer/server/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetSession sets the session cookie in the response
|
||||||
|
func SetSession(gc *gin.Context, sessionID string) {
|
||||||
|
secure := true
|
||||||
|
httpOnly := true
|
||||||
|
hostname := utils.GetHost(gc)
|
||||||
|
host, _ := utils.GetHostParts(hostname)
|
||||||
|
domain := utils.GetDomainName(hostname)
|
||||||
|
if domain != "localhost" {
|
||||||
|
domain = "." + domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO allow configuring from dashboard
|
||||||
|
year := 60 * 60 * 24 * 365
|
||||||
|
|
||||||
|
gc.SetSameSite(http.SameSiteNoneMode)
|
||||||
|
gc.SetCookie(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName)+"_session", sessionID, year, "/", host, secure, httpOnly)
|
||||||
|
gc.SetCookie(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName)+"_session_domain", sessionID, year, "/", domain, secure, httpOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSession sets session cookies to expire
|
||||||
|
func DeleteSession(gc *gin.Context) {
|
||||||
|
secure := true
|
||||||
|
httpOnly := true
|
||||||
|
hostname := utils.GetHost(gc)
|
||||||
|
host, _ := utils.GetHostParts(hostname)
|
||||||
|
domain := utils.GetDomainName(hostname)
|
||||||
|
if domain != "localhost" {
|
||||||
|
domain = "." + domain
|
||||||
|
}
|
||||||
|
|
||||||
|
gc.SetSameSite(http.SameSiteNoneMode)
|
||||||
|
gc.SetCookie(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName)+"_session", "", -1, "/", host, secure, httpOnly)
|
||||||
|
gc.SetCookie(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName)+"_session_domain", "", -1, "/", domain, secure, httpOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSession gets the session cookie from context
|
||||||
|
func GetSession(gc *gin.Context) (string, error) {
|
||||||
|
var cookie *http.Cookie
|
||||||
|
var err error
|
||||||
|
cookie, err = gc.Request.Cookie(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName) + "_session")
|
||||||
|
if err != nil {
|
||||||
|
cookie, err = gc.Request.Cookie(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName) + "_session_domain")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedValue, err := url.PathUnescape(cookie.Value)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return decodedValue, nil
|
||||||
|
}
|
108
server/crypto/aes.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
var bytes = []byte{35, 46, 57, 24, 85, 35, 24, 74, 87, 35, 88, 98, 66, 32, 14, 0o5}
|
||||||
|
|
||||||
|
// EncryptAES method is to encrypt or hide any classified text
|
||||||
|
func EncryptAES(text string) (string, error) {
|
||||||
|
key := []byte(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyEncryptionKey))
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
plainText := []byte(text)
|
||||||
|
cfb := cipher.NewCFBEncrypter(block, bytes)
|
||||||
|
cipherText := make([]byte, len(plainText))
|
||||||
|
cfb.XORKeyStream(cipherText, plainText)
|
||||||
|
return EncryptB64(string(cipherText)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptAES method is to extract back the encrypted text
|
||||||
|
func DecryptAES(text string) (string, error) {
|
||||||
|
key := []byte(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyEncryptionKey))
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
cipherText, err := DecryptB64(text)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
cfb := cipher.NewCFBDecrypter(block, bytes)
|
||||||
|
plainText := make([]byte, len(cipherText))
|
||||||
|
cfb.XORKeyStream(plainText, []byte(cipherText))
|
||||||
|
return string(plainText), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptAESEnv encrypts data using AES algorithm
|
||||||
|
// kept for the backward compatibility of env data encryption
|
||||||
|
func EncryptAESEnv(text []byte) ([]byte, error) {
|
||||||
|
key := []byte(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyEncryptionKey))
|
||||||
|
c, err := aes.NewCipher(key)
|
||||||
|
var res []byte
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// gcm or Galois/Counter Mode, is a mode of operation
|
||||||
|
// for symmetric key cryptographic block ciphers
|
||||||
|
// - https://en.wikipedia.org/wiki/Galois/Counter_Mode
|
||||||
|
gcm, err := cipher.NewGCM(c)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates a new byte array the size of the nonce
|
||||||
|
// which must be passed to Seal
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
// populates our nonce with a cryptographically secure
|
||||||
|
// random sequence
|
||||||
|
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// here we encrypt our text using the Seal function
|
||||||
|
// Seal encrypts and authenticates plaintext, authenticates the
|
||||||
|
// additional data and appends the result to dst, returning the updated
|
||||||
|
// slice. The nonce must be NonceSize() bytes long and unique for all
|
||||||
|
// time, for a given key.
|
||||||
|
return gcm.Seal(nonce, nonce, text, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptAES decrypts data using AES algorithm
|
||||||
|
// Kept for the backward compatibility of env data decryption
|
||||||
|
func DecryptAESEnv(ciphertext []byte) ([]byte, error) {
|
||||||
|
key := []byte(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyEncryptionKey))
|
||||||
|
c, err := aes.NewCipher(key)
|
||||||
|
var res []byte
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(c)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonceSize := gcm.NonceSize()
|
||||||
|
if len(ciphertext) < nonceSize {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
||||||
|
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext, nil
|
||||||
|
}
|
17
server/crypto/b64.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import "encoding/base64"
|
||||||
|
|
||||||
|
// EncryptB64 encrypts data into base64 string
|
||||||
|
func EncryptB64(text string) string {
|
||||||
|
return base64.StdEncoding.EncodeToString([]byte(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptB64 decrypts from base64 string to readable string
|
||||||
|
func DecryptB64(s string) (string, error) {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
114
server/crypto/common.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetPubJWK returns JWK for given keys
|
||||||
|
func GetPubJWK(algo, keyID string, publicKey interface{}) (string, error) {
|
||||||
|
jwk := &jose.JSONWebKeySet{
|
||||||
|
Keys: []jose.JSONWebKey{
|
||||||
|
{
|
||||||
|
Algorithm: algo,
|
||||||
|
Key: publicKey,
|
||||||
|
Use: "sig",
|
||||||
|
KeyID: keyID,
|
||||||
|
Certificates: []*x509.Certificate{},
|
||||||
|
CertificateThumbprintSHA1: []uint8{},
|
||||||
|
CertificateThumbprintSHA256: []uint8{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
jwkPublicKey, err := jwk.Keys[0].MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(jwkPublicKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateJWKBasedOnEnv generates JWK based on env
|
||||||
|
// make sure clientID, jwtType, jwtSecret / public & private key pair is set
|
||||||
|
// this is called while initializing app / when env is updated
|
||||||
|
func GenerateJWKBasedOnEnv() (string, error) {
|
||||||
|
jwk := ""
|
||||||
|
algo := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtType)
|
||||||
|
clientID := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
// check if jwt secret is provided
|
||||||
|
if IsHMACA(algo) {
|
||||||
|
jwk, err = GetPubJWK(algo, clientID, []byte(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtSecret)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsRSA(algo) {
|
||||||
|
publicKeyInstance, err := ParseRsaPublicKeyFromPemStr(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtPublicKey))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
jwk, err = GetPubJWK(algo, clientID, publicKeyInstance)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsECDSA(algo) {
|
||||||
|
publicKeyInstance, err := ParseEcdsaPublicKeyFromPemStr(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtPublicKey))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
jwk, err = GetPubJWK(algo, clientID, publicKeyInstance)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jwk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptEnvData is used to encrypt the env data
|
||||||
|
func EncryptEnvData(data envstore.Store) (string, error) {
|
||||||
|
jsonBytes, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
storeData := envstore.EnvStoreObj.GetEnvStoreClone()
|
||||||
|
|
||||||
|
err = json.Unmarshal(jsonBytes, &storeData)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := json.Marshal(storeData)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedConfig, err := EncryptAESEnv(configData)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return EncryptB64(string(encryptedConfig)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptPassword is used for encrypting password
|
||||||
|
func EncryptPassword(password string) (string, error) {
|
||||||
|
pw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(pw), nil
|
||||||
|
}
|
154
server/crypto/ecdsa.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewECDSAKey to generate new ECDSA Key if env is not set
|
||||||
|
// returns key instance, private key string, public key string, jwk string, error
|
||||||
|
func NewECDSAKey(algo, keyID string) (*ecdsa.PrivateKey, string, string, string, error) {
|
||||||
|
var curve elliptic.Curve
|
||||||
|
switch algo {
|
||||||
|
case "ES256":
|
||||||
|
curve = elliptic.P256()
|
||||||
|
case "ES384":
|
||||||
|
curve = elliptic.P384()
|
||||||
|
case "ES512":
|
||||||
|
curve = elliptic.P521()
|
||||||
|
default:
|
||||||
|
return nil, "", "", "", errors.New("Invalid algo")
|
||||||
|
}
|
||||||
|
key, err := ecdsa.GenerateKey(curve, rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey, publicKey, err := AsECDSAStr(key, &key.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
jwkPublicKey, err := GetPubJWK(algo, keyID, &key.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, privateKey, publicKey, string(jwkPublicKey), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsECDSA checks if given string is valid ECDSA algo
|
||||||
|
func IsECDSA(algo string) bool {
|
||||||
|
switch algo {
|
||||||
|
case "ES256", "ES384", "ES512":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportEcdsaPrivateKeyAsPemStr to get ECDSA private key as pem string
|
||||||
|
func ExportEcdsaPrivateKeyAsPemStr(privkey *ecdsa.PrivateKey) (string, error) {
|
||||||
|
privkeyBytes, err := x509.MarshalECPrivateKey(privkey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
privkeyPem := pem.EncodeToMemory(
|
||||||
|
&pem.Block{
|
||||||
|
Type: "ECDSA PRIVATE KEY",
|
||||||
|
Bytes: privkeyBytes,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return string(privkeyPem), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportEcdsaPublicKeyAsPemStr to get ECDSA public key as pem string
|
||||||
|
func ExportEcdsaPublicKeyAsPemStr(pubkey *ecdsa.PublicKey) (string, error) {
|
||||||
|
pubkeyBytes, err := x509.MarshalPKIXPublicKey(pubkey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
pubkeyPem := pem.EncodeToMemory(
|
||||||
|
&pem.Block{
|
||||||
|
Type: "ECDSA PUBLIC KEY",
|
||||||
|
Bytes: pubkeyBytes,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return string(pubkeyPem), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseEcdsaPrivateKeyFromPemStr to parse ECDSA private key from pem string
|
||||||
|
func ParseEcdsaPrivateKeyFromPemStr(privPEM string) (*ecdsa.PrivateKey, error) {
|
||||||
|
block, _ := pem.Decode([]byte(privPEM))
|
||||||
|
if block == nil {
|
||||||
|
return nil, errors.New("failed to parse PEM block containing the key")
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, err := x509.ParseECPrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return priv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseEcdsaPublicKeyFromPemStr to parse ECDSA public key from pem string
|
||||||
|
func ParseEcdsaPublicKeyFromPemStr(pubPEM string) (*ecdsa.PublicKey, error) {
|
||||||
|
block, _ := pem.Decode([]byte(pubPEM))
|
||||||
|
if block == nil {
|
||||||
|
return nil, errors.New("failed to parse PEM block containing the key")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch pub := pub.(type) {
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
return pub, nil
|
||||||
|
default:
|
||||||
|
break // fall through
|
||||||
|
}
|
||||||
|
return nil, errors.New("Key type is not ECDSA")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsECDSAStr returns private, public key string or error
|
||||||
|
func AsECDSAStr(privateKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey) (string, string, error) {
|
||||||
|
// Export the keys to pem string
|
||||||
|
privPem, err := ExportEcdsaPrivateKeyAsPemStr(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
pubPem, err := ExportEcdsaPublicKeyAsPemStr(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import the keys from pem string
|
||||||
|
privParsed, err := ParseEcdsaPrivateKeyFromPemStr(privPem)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
pubParsed, err := ParseEcdsaPublicKeyFromPemStr(pubPem)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the newly imported keys
|
||||||
|
privParsedPem, err := ExportEcdsaPrivateKeyAsPemStr(privParsed)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
pubParsedPem, err := ExportEcdsaPublicKeyAsPemStr(pubParsed)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return privParsedPem, pubParsedPem, nil
|
||||||
|
}
|
26
server/crypto/hmac.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHMAC key returns new key that can be used to ecnrypt data using HMAC algo
|
||||||
|
// returns key, string, error
|
||||||
|
func NewHMACKey(algo, keyID string) (string, string, error) {
|
||||||
|
key := uuid.New().String()
|
||||||
|
jwkPublicKey, err := GetPubJWK(algo, keyID, []byte(key))
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return key, string(jwkPublicKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsHMACValid checks if given string is valid HMCA algo
|
||||||
|
func IsHMACA(algo string) bool {
|
||||||
|
switch algo {
|
||||||
|
case "HS256", "HS384", "HS512":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
118
server/crypto/rsa.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRSAKey to generate new RSA Key if env is not set
|
||||||
|
// returns key instance, private key string, public key string, jwk string, error
|
||||||
|
func NewRSAKey(algo, keyID string) (*rsa.PrivateKey, string, string, string, error) {
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey, publicKey, err := AsRSAStr(key, &key.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
jwkPublicKey, err := GetPubJWK(algo, keyID, &key.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, privateKey, publicKey, string(jwkPublicKey), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRSA checks if given string is valid RSA algo
|
||||||
|
func IsRSA(algo string) bool {
|
||||||
|
switch algo {
|
||||||
|
case "RS256", "RS384", "RS512":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportRsaPrivateKeyAsPemStr to get RSA private key as pem string
|
||||||
|
func ExportRsaPrivateKeyAsPemStr(privkey *rsa.PrivateKey) string {
|
||||||
|
privkeyBytes := x509.MarshalPKCS1PrivateKey(privkey)
|
||||||
|
privkeyPem := pem.EncodeToMemory(
|
||||||
|
&pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: privkeyBytes,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return string(privkeyPem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportRsaPublicKeyAsPemStr to get RSA public key as pem string
|
||||||
|
func ExportRsaPublicKeyAsPemStr(pubkey *rsa.PublicKey) string {
|
||||||
|
pubkeyBytes := x509.MarshalPKCS1PublicKey(pubkey)
|
||||||
|
pubkeyPem := pem.EncodeToMemory(
|
||||||
|
&pem.Block{
|
||||||
|
Type: "RSA PUBLIC KEY",
|
||||||
|
Bytes: pubkeyBytes,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return string(pubkeyPem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseRsaPrivateKeyFromPemStr to parse RSA private key from pem string
|
||||||
|
func ParseRsaPrivateKeyFromPemStr(privPEM string) (*rsa.PrivateKey, error) {
|
||||||
|
block, _ := pem.Decode([]byte(privPEM))
|
||||||
|
if block == nil {
|
||||||
|
return nil, errors.New("failed to parse PEM block containing the key")
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return priv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseRsaPublicKeyFromPemStr to parse RSA public key from pem string
|
||||||
|
func ParseRsaPublicKeyFromPemStr(pubPEM string) (*rsa.PublicKey, error) {
|
||||||
|
block, _ := pem.Decode([]byte(pubPEM))
|
||||||
|
if block == nil {
|
||||||
|
return nil, errors.New("failed to parse PEM block containing the key")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, err := x509.ParsePKCS1PublicKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsRSAStr returns private, public key string or error
|
||||||
|
func AsRSAStr(privateKey *rsa.PrivateKey, publickKey *rsa.PublicKey) (string, string, error) {
|
||||||
|
// Export the keys to pem string
|
||||||
|
privPem := ExportRsaPrivateKeyAsPemStr(privateKey)
|
||||||
|
pubPem := ExportRsaPublicKeyAsPemStr(publickKey)
|
||||||
|
|
||||||
|
// Import the keys from pem string
|
||||||
|
privParsed, err := ParseRsaPrivateKeyFromPemStr(privPem)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
pubParsed, err := ParseRsaPublicKeyFromPemStr(pubPem)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the newly imported keys
|
||||||
|
privParsedPem := ExportRsaPrivateKeyAsPemStr(privParsed)
|
||||||
|
pubParsedPem := ExportRsaPublicKeyAsPemStr(pubParsed)
|
||||||
|
|
||||||
|
return privParsedPem, pubParsedPem, nil
|
||||||
|
}
|
@@ -1,60 +1,44 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/authorizerdev/authorizer/server/constants"
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
"github.com/authorizerdev/authorizer/server/enum"
|
"github.com/authorizerdev/authorizer/server/db/providers"
|
||||||
"gorm.io/driver/mysql"
|
"github.com/authorizerdev/authorizer/server/db/providers/arangodb"
|
||||||
"gorm.io/driver/postgres"
|
"github.com/authorizerdev/authorizer/server/db/providers/mongodb"
|
||||||
"gorm.io/driver/sqlite"
|
"github.com/authorizerdev/authorizer/server/db/providers/sql"
|
||||||
"gorm.io/gorm"
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
"gorm.io/gorm/schema"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager interface {
|
// Provider returns the current database provider
|
||||||
SaveUser(user User) (User, error)
|
var Provider providers.Provider
|
||||||
UpdateUser(user User) (User, error)
|
|
||||||
GetUsers() ([]User, error)
|
|
||||||
GetUserByEmail(email string) (User, error)
|
|
||||||
UpdateVerificationTime(verifiedAt int64, id uint) error
|
|
||||||
AddVerification(verification VerificationRequest) (VerificationRequest, error)
|
|
||||||
GetVerificationByToken(token string) (VerificationRequest, error)
|
|
||||||
DeleteToken(email string) error
|
|
||||||
GetVerificationRequests() ([]VerificationRequest, error)
|
|
||||||
GetVerificationByEmail(email string) (VerificationRequest, error)
|
|
||||||
DeleteUser(email string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type manager struct {
|
func InitDB() error {
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
var Mgr Manager
|
|
||||||
|
|
||||||
func InitDB() {
|
|
||||||
var db *gorm.DB
|
|
||||||
var err error
|
var err error
|
||||||
ormConfig := &gorm.Config{
|
|
||||||
NamingStrategy: schema.NamingStrategy{
|
isSQL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) != constants.DbTypeArangodb && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) != constants.DbTypeMongodb
|
||||||
TablePrefix: "authorizer_",
|
isArangoDB := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) == constants.DbTypeArangodb
|
||||||
},
|
isMongoDB := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) == constants.DbTypeMongodb
|
||||||
}
|
|
||||||
if constants.DATABASE_TYPE == enum.Postgres.String() {
|
if isSQL {
|
||||||
db, err = gorm.Open(postgres.Open(constants.DATABASE_URL), ormConfig)
|
Provider, err = sql.NewProvider()
|
||||||
}
|
if err != nil {
|
||||||
if constants.DATABASE_TYPE == enum.Mysql.String() {
|
return err
|
||||||
db, err = gorm.Open(mysql.Open(constants.DATABASE_URL), ormConfig)
|
}
|
||||||
}
|
|
||||||
if constants.DATABASE_TYPE == enum.Sqlite.String() {
|
|
||||||
db, err = gorm.Open(sqlite.Open(constants.DATABASE_URL), ormConfig)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if isArangoDB {
|
||||||
log.Fatal("Failed to init db:", err)
|
Provider, err = arangodb.NewProvider()
|
||||||
} else {
|
if err != nil {
|
||||||
db.AutoMigrate(&User{}, &VerificationRequest{})
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Mgr = &manager{db: db}
|
if isMongoDB {
|
||||||
|
Provider, err = mongodb.NewProvider()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
11
server/db/models/env.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// Env model for db
|
||||||
|
type Env struct {
|
||||||
|
Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
|
||||||
|
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
|
||||||
|
EnvData string `gorm:"type:text" json:"env" bson:"env"`
|
||||||
|
Hash string `gorm:"type:text" json:"hash" bson:"hash"`
|
||||||
|
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
||||||
|
CreatedAt int64 `json:"created_at" bson:"created_at"`
|
||||||
|
}
|
21
server/db/models/model.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// Collections / Tables available for authorizer in the database
|
||||||
|
type CollectionList struct {
|
||||||
|
User string
|
||||||
|
VerificationRequest string
|
||||||
|
Session string
|
||||||
|
Env string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Prefix for table name / collection names
|
||||||
|
Prefix = "authorizer_"
|
||||||
|
// Collections / Tables available for authorizer in the database (used for dbs other than gorm)
|
||||||
|
Collections = CollectionList{
|
||||||
|
User: Prefix + "users",
|
||||||
|
VerificationRequest: Prefix + "verification_requests",
|
||||||
|
Session: Prefix + "sessions",
|
||||||
|
Env: Prefix + "env",
|
||||||
|
}
|
||||||
|
)
|
13
server/db/models/session.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// Session model for db
|
||||||
|
type Session struct {
|
||||||
|
Key string `json:"_key,omitempty" bson:"_key,omitempty"` // for arangodb
|
||||||
|
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
|
||||||
|
UserID string `gorm:"type:char(36),index:" json:"user_id" bson:"user_id"`
|
||||||
|
User User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" bson:"-"`
|
||||||
|
UserAgent string `json:"user_agent" bson:"user_agent"`
|
||||||
|
IP string `json:"ip" bson:"ip"`
|
||||||
|
CreatedAt int64 `json:"created_at" bson:"created_at"`
|
||||||
|
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
||||||
|
}
|
54
server/db/models/user.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User model for db
|
||||||
|
type User struct {
|
||||||
|
Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
|
||||||
|
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
|
||||||
|
|
||||||
|
Email string `gorm:"unique" json:"email" bson:"email"`
|
||||||
|
EmailVerifiedAt *int64 `json:"email_verified_at" bson:"email_verified_at"`
|
||||||
|
Password *string `gorm:"type:text" json:"password" bson:"password"`
|
||||||
|
SignupMethods string `json:"signup_methods" bson:"signup_methods"`
|
||||||
|
GivenName *string `json:"given_name" bson:"given_name"`
|
||||||
|
FamilyName *string `json:"family_name" bson:"family_name"`
|
||||||
|
MiddleName *string `json:"middle_name" bson:"middle_name"`
|
||||||
|
Nickname *string `json:"nickname" bson:"nickname"`
|
||||||
|
Gender *string `json:"gender" bson:"gender"`
|
||||||
|
Birthdate *string `json:"birthdate" bson:"birthdate"`
|
||||||
|
PhoneNumber *string `gorm:"unique" json:"phone_number" bson:"phone_number"`
|
||||||
|
PhoneNumberVerifiedAt *int64 `json:"phone_number_verified_at" bson:"phone_number_verified_at"`
|
||||||
|
Picture *string `gorm:"type:text" json:"picture" bson:"picture"`
|
||||||
|
Roles string `json:"roles" bson:"roles"`
|
||||||
|
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
||||||
|
CreatedAt int64 `json:"created_at" bson:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) AsAPIUser() *model.User {
|
||||||
|
isEmailVerified := user.EmailVerifiedAt != nil
|
||||||
|
isPhoneVerified := user.PhoneNumberVerifiedAt != nil
|
||||||
|
return &model.User{
|
||||||
|
ID: user.ID,
|
||||||
|
Email: user.Email,
|
||||||
|
EmailVerified: isEmailVerified,
|
||||||
|
SignupMethods: user.SignupMethods,
|
||||||
|
GivenName: user.GivenName,
|
||||||
|
FamilyName: user.FamilyName,
|
||||||
|
MiddleName: user.MiddleName,
|
||||||
|
Nickname: user.Nickname,
|
||||||
|
PreferredUsername: &user.Email,
|
||||||
|
Gender: user.Gender,
|
||||||
|
Birthdate: user.Birthdate,
|
||||||
|
PhoneNumber: user.PhoneNumber,
|
||||||
|
PhoneNumberVerified: &isPhoneVerified,
|
||||||
|
Picture: user.Picture,
|
||||||
|
Roles: strings.Split(user.Roles, ","),
|
||||||
|
CreatedAt: &user.CreatedAt,
|
||||||
|
UpdatedAt: &user.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
31
server/db/models/verification_requests.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
|
|
||||||
|
// VerificationRequest model for db
|
||||||
|
type VerificationRequest struct {
|
||||||
|
Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
|
||||||
|
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
|
||||||
|
Token string `gorm:"type:text" json:"token" bson:"token"`
|
||||||
|
Identifier string `gorm:"uniqueIndex:idx_email_identifier" json:"identifier" bson:"identifier"`
|
||||||
|
ExpiresAt int64 `json:"expires_at" bson:"expires_at"`
|
||||||
|
CreatedAt int64 `json:"created_at" bson:"created_at"`
|
||||||
|
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
||||||
|
Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"`
|
||||||
|
Nonce string `gorm:"type:text" json:"nonce" bson:"nonce"`
|
||||||
|
RedirectURI string `gorm:"type:text" json:"redirect_uri" bson:"redirect_uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest {
|
||||||
|
return &model.VerificationRequest{
|
||||||
|
ID: v.ID,
|
||||||
|
Token: &v.Token,
|
||||||
|
Identifier: &v.Identifier,
|
||||||
|
Expires: &v.ExpiresAt,
|
||||||
|
CreatedAt: &v.CreatedAt,
|
||||||
|
UpdatedAt: &v.UpdatedAt,
|
||||||
|
Email: &v.Email,
|
||||||
|
Nonce: &v.Nonce,
|
||||||
|
RedirectURI: &v.RedirectURI,
|
||||||
|
}
|
||||||
|
}
|
123
server/db/providers/arangodb/arangodb.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package arangodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/arangodb/go-driver"
|
||||||
|
arangoDriver "github.com/arangodb/go-driver"
|
||||||
|
"github.com/arangodb/go-driver/http"
|
||||||
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type provider struct {
|
||||||
|
db arangoDriver.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
// for this we need arangodb instance up and running
|
||||||
|
// for local testing we can use dockerized version of it
|
||||||
|
// docker run -p 8529:8529 -e ARANGO_ROOT_PASSWORD=root arangodb/arangodb:3.8.4
|
||||||
|
|
||||||
|
// NewProvider to initialize arangodb connection
|
||||||
|
func NewProvider() (*provider, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
conn, err := http.NewConnection(http.ConnectionConfig{
|
||||||
|
Endpoints: []string{envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseURL)},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
arangoClient, err := arangoDriver.NewClient(arangoDriver.ClientConfig{
|
||||||
|
Connection: conn,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var arangodb driver.Database
|
||||||
|
|
||||||
|
arangodb_exists, err := arangoClient.DatabaseExists(nil, envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseName))
|
||||||
|
|
||||||
|
if arangodb_exists {
|
||||||
|
log.Println(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseName) + " db exists already")
|
||||||
|
arangodb, err = arangoClient.Database(nil, envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
arangodb, err = arangoClient.CreateDatabase(nil, envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseName), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userCollectionExists, err := arangodb.CollectionExists(ctx, models.Collections.User)
|
||||||
|
if userCollectionExists {
|
||||||
|
log.Println(models.Collections.User + " collection exists already")
|
||||||
|
} else {
|
||||||
|
_, err = arangodb.CreateCollection(ctx, models.Collections.User, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error creating collection("+models.Collections.User+"):", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userCollection, _ := arangodb.Collection(nil, models.Collections.User)
|
||||||
|
userCollection.EnsureHashIndex(ctx, []string{"email"}, &arangoDriver.EnsureHashIndexOptions{
|
||||||
|
Unique: true,
|
||||||
|
Sparse: true,
|
||||||
|
})
|
||||||
|
userCollection.EnsureHashIndex(ctx, []string{"phone_number"}, &arangoDriver.EnsureHashIndexOptions{
|
||||||
|
Unique: true,
|
||||||
|
Sparse: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
verificationRequestCollectionExists, err := arangodb.CollectionExists(ctx, models.Collections.VerificationRequest)
|
||||||
|
if verificationRequestCollectionExists {
|
||||||
|
log.Println(models.Collections.VerificationRequest + " collection exists already")
|
||||||
|
} else {
|
||||||
|
_, err = arangodb.CreateCollection(ctx, models.Collections.VerificationRequest, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error creating collection("+models.Collections.VerificationRequest+"):", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verificationRequestCollection, _ := arangodb.Collection(nil, models.Collections.VerificationRequest)
|
||||||
|
verificationRequestCollection.EnsureHashIndex(ctx, []string{"email", "identifier"}, &arangoDriver.EnsureHashIndexOptions{
|
||||||
|
Unique: true,
|
||||||
|
Sparse: true,
|
||||||
|
})
|
||||||
|
verificationRequestCollection.EnsureHashIndex(ctx, []string{"token"}, &arangoDriver.EnsureHashIndexOptions{
|
||||||
|
Sparse: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
sessionCollectionExists, err := arangodb.CollectionExists(ctx, models.Collections.Session)
|
||||||
|
if sessionCollectionExists {
|
||||||
|
log.Println(models.Collections.Session + " collection exists already")
|
||||||
|
} else {
|
||||||
|
_, err = arangodb.CreateCollection(ctx, models.Collections.Session, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error creating collection("+models.Collections.Session+"):", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionCollection, _ := arangodb.Collection(nil, models.Collections.Session)
|
||||||
|
sessionCollection.EnsureHashIndex(ctx, []string{"user_id"}, &arangoDriver.EnsureHashIndexOptions{
|
||||||
|
Sparse: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
configCollectionExists, err := arangodb.CollectionExists(ctx, models.Collections.Env)
|
||||||
|
if configCollectionExists {
|
||||||
|
log.Println(models.Collections.Env + " collection exists already")
|
||||||
|
} else {
|
||||||
|
_, err = arangodb.CreateCollection(ctx, models.Collections.Env, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error creating collection("+models.Collections.Env+"):", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &provider{
|
||||||
|
db: arangodb,
|
||||||
|
}, err
|
||||||
|
}
|
73
server/db/providers/arangodb/env.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package arangodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
arangoDriver "github.com/arangodb/go-driver"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddEnv to save environment information in database
|
||||||
|
func (p *provider) AddEnv(env models.Env) (models.Env, error) {
|
||||||
|
if env.ID == "" {
|
||||||
|
env.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
env.CreatedAt = time.Now().Unix()
|
||||||
|
env.UpdatedAt = time.Now().Unix()
|
||||||
|
configCollection, _ := p.db.Collection(nil, models.Collections.Env)
|
||||||
|
meta, err := configCollection.CreateDocument(arangoDriver.WithOverwrite(nil), env)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error adding config:", err)
|
||||||
|
return env, err
|
||||||
|
}
|
||||||
|
env.Key = meta.Key
|
||||||
|
env.ID = meta.ID.String()
|
||||||
|
return env, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEnv to update environment information in database
|
||||||
|
func (p *provider) UpdateEnv(env models.Env) (models.Env, error) {
|
||||||
|
env.UpdatedAt = time.Now().Unix()
|
||||||
|
collection, _ := p.db.Collection(nil, models.Collections.Env)
|
||||||
|
meta, err := collection.UpdateDocument(nil, env.Key, env)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error updating config:", err)
|
||||||
|
return env, err
|
||||||
|
}
|
||||||
|
|
||||||
|
env.Key = meta.Key
|
||||||
|
env.ID = meta.ID.String()
|
||||||
|
return env, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnv to get environment information from database
|
||||||
|
func (p *provider) GetEnv() (models.Env, error) {
|
||||||
|
var env models.Env
|
||||||
|
query := fmt.Sprintf("FOR d in %s RETURN d", models.Collections.Env)
|
||||||
|
|
||||||
|
cursor, err := p.db.Query(nil, query, nil)
|
||||||
|
if err != nil {
|
||||||
|
return env, err
|
||||||
|
}
|
||||||
|
defer cursor.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
if !cursor.HasMore() {
|
||||||
|
if env.Key == "" {
|
||||||
|
return env, fmt.Errorf("config not found")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_, err := cursor.ReadDocument(nil, &env)
|
||||||
|
if err != nil {
|
||||||
|
return env, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return env, nil
|
||||||
|
}
|
41
server/db/providers/arangodb/session.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package arangodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddSession to save session information in database
|
||||||
|
func (p *provider) AddSession(session models.Session) error {
|
||||||
|
if session.ID == "" {
|
||||||
|
session.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
session.CreatedAt = time.Now().Unix()
|
||||||
|
session.UpdatedAt = time.Now().Unix()
|
||||||
|
sessionCollection, _ := p.db.Collection(nil, models.Collections.Session)
|
||||||
|
_, err := sessionCollection.CreateDocument(nil, session)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(`error saving session`, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSession to delete session information from database
|
||||||
|
func (p *provider) DeleteSession(userId string) error {
|
||||||
|
query := fmt.Sprintf(`FOR d IN %s FILTER d.user_id == @userId REMOVE { _key: d._key } IN %s`, models.Collections.Session, models.Collections.Session)
|
||||||
|
bindVars := map[string]interface{}{
|
||||||
|
"userId": userId,
|
||||||
|
}
|
||||||
|
cursor, err := p.db.Query(nil, query, bindVars)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cursor.Close()
|
||||||
|
return nil
|
||||||
|
}
|
167
server/db/providers/arangodb/user.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package arangodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/arangodb/go-driver"
|
||||||
|
arangoDriver "github.com/arangodb/go-driver"
|
||||||
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddUser to save user information in database
|
||||||
|
func (p *provider) AddUser(user models.User) (models.User, error) {
|
||||||
|
if user.ID == "" {
|
||||||
|
user.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Roles == "" {
|
||||||
|
user.Roles = strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
user.CreatedAt = time.Now().Unix()
|
||||||
|
user.UpdatedAt = time.Now().Unix()
|
||||||
|
userCollection, _ := p.db.Collection(nil, models.Collections.User)
|
||||||
|
meta, err := userCollection.CreateDocument(arangoDriver.WithOverwrite(nil), user)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error adding user:", err)
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
user.Key = meta.Key
|
||||||
|
user.ID = meta.ID.String()
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser to update user information in database
|
||||||
|
func (p *provider) UpdateUser(user models.User) (models.User, error) {
|
||||||
|
user.UpdatedAt = time.Now().Unix()
|
||||||
|
collection, _ := p.db.Collection(nil, models.Collections.User)
|
||||||
|
meta, err := collection.UpdateDocument(nil, user.Key, user)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error updating user:", err)
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Key = meta.Key
|
||||||
|
user.ID = meta.ID.String()
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser to delete user information from database
|
||||||
|
func (p *provider) DeleteUser(user models.User) error {
|
||||||
|
collection, _ := p.db.Collection(nil, models.Collections.User)
|
||||||
|
_, err := collection.RemoveDocument(nil, user.Key)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(`error deleting user:`, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsers to get list of users from database
|
||||||
|
func (p *provider) ListUsers(pagination model.Pagination) (*model.Users, error) {
|
||||||
|
var users []*model.User
|
||||||
|
ctx := driver.WithQueryFullCount(context.Background())
|
||||||
|
|
||||||
|
query := fmt.Sprintf("FOR d in %s SORT d.created_at DESC LIMIT %d, %d RETURN d", models.Collections.User, pagination.Offset, pagination.Limit)
|
||||||
|
|
||||||
|
cursor, err := p.db.Query(ctx, query, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cursor.Close()
|
||||||
|
|
||||||
|
paginationClone := pagination
|
||||||
|
paginationClone.Total = cursor.Statistics().FullCount()
|
||||||
|
|
||||||
|
for {
|
||||||
|
var user models.User
|
||||||
|
meta, err := cursor.ReadDocument(nil, &user)
|
||||||
|
|
||||||
|
if arangoDriver.IsNoMoreDocuments(err) {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if meta.Key != "" {
|
||||||
|
users = append(users, user.AsAPIUser())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Users{
|
||||||
|
Pagination: &paginationClone,
|
||||||
|
Users: users,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByEmail to get user information from database using email address
|
||||||
|
func (p *provider) GetUserByEmail(email string) (models.User, error) {
|
||||||
|
var user models.User
|
||||||
|
|
||||||
|
query := fmt.Sprintf("FOR d in %s FILTER d.email == @email RETURN d", models.Collections.User)
|
||||||
|
bindVars := map[string]interface{}{
|
||||||
|
"email": email,
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor, err := p.db.Query(nil, query, bindVars)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
defer cursor.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
if !cursor.HasMore() {
|
||||||
|
if user.Key == "" {
|
||||||
|
return user, fmt.Errorf("user not found")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_, err := cursor.ReadDocument(nil, &user)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByID to get user information from database using user ID
|
||||||
|
func (p *provider) GetUserByID(id string) (models.User, error) {
|
||||||
|
var user models.User
|
||||||
|
|
||||||
|
query := fmt.Sprintf("FOR d in %s FILTER d._id == @id LIMIT 1 RETURN d", models.Collections.User)
|
||||||
|
bindVars := map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor, err := p.db.Query(nil, query, bindVars)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
defer cursor.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
if !cursor.HasMore() {
|
||||||
|
if user.Key == "" {
|
||||||
|
return user, fmt.Errorf("user not found")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_, err := cursor.ReadDocument(nil, &user)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
143
server/db/providers/arangodb/verification_requests.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package arangodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/arangodb/go-driver"
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddVerification to save verification request in database
|
||||||
|
func (p *provider) AddVerificationRequest(verificationRequest models.VerificationRequest) (models.VerificationRequest, error) {
|
||||||
|
if verificationRequest.ID == "" {
|
||||||
|
verificationRequest.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
verificationRequest.CreatedAt = time.Now().Unix()
|
||||||
|
verificationRequest.UpdatedAt = time.Now().Unix()
|
||||||
|
verificationRequestRequestCollection, _ := p.db.Collection(nil, models.Collections.VerificationRequest)
|
||||||
|
meta, err := verificationRequestRequestCollection.CreateDocument(nil, verificationRequest)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error saving verificationRequest record:", err)
|
||||||
|
return verificationRequest, err
|
||||||
|
}
|
||||||
|
verificationRequest.Key = meta.Key
|
||||||
|
verificationRequest.ID = meta.ID.String()
|
||||||
|
|
||||||
|
return verificationRequest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVerificationRequestByToken to get verification request from database using token
|
||||||
|
func (p *provider) GetVerificationRequestByToken(token string) (models.VerificationRequest, error) {
|
||||||
|
var verificationRequest models.VerificationRequest
|
||||||
|
query := fmt.Sprintf("FOR d in %s FILTER d.token == @token LIMIT 1 RETURN d", models.Collections.VerificationRequest)
|
||||||
|
bindVars := map[string]interface{}{
|
||||||
|
"token": token,
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor, err := p.db.Query(nil, query, bindVars)
|
||||||
|
if err != nil {
|
||||||
|
return verificationRequest, err
|
||||||
|
}
|
||||||
|
defer cursor.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
if !cursor.HasMore() {
|
||||||
|
if verificationRequest.Key == "" {
|
||||||
|
return verificationRequest, fmt.Errorf("verification request not found")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_, err := cursor.ReadDocument(nil, &verificationRequest)
|
||||||
|
if err != nil {
|
||||||
|
return verificationRequest, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return verificationRequest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVerificationRequestByEmail to get verification request by email from database
|
||||||
|
func (p *provider) GetVerificationRequestByEmail(email string, identifier string) (models.VerificationRequest, error) {
|
||||||
|
var verificationRequest models.VerificationRequest
|
||||||
|
|
||||||
|
query := fmt.Sprintf("FOR d in %s FILTER d.email == @email FILTER d.identifier == @identifier LIMIT 1 RETURN d", models.Collections.VerificationRequest)
|
||||||
|
bindVars := map[string]interface{}{
|
||||||
|
"email": email,
|
||||||
|
"identifier": identifier,
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor, err := p.db.Query(nil, query, bindVars)
|
||||||
|
if err != nil {
|
||||||
|
return verificationRequest, err
|
||||||
|
}
|
||||||
|
defer cursor.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
if !cursor.HasMore() {
|
||||||
|
if verificationRequest.Key == "" {
|
||||||
|
return verificationRequest, fmt.Errorf("verification request not found")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_, err := cursor.ReadDocument(nil, &verificationRequest)
|
||||||
|
if err != nil {
|
||||||
|
return verificationRequest, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return verificationRequest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVerificationRequests to get list of verification requests from database
|
||||||
|
func (p *provider) ListVerificationRequests(pagination model.Pagination) (*model.VerificationRequests, error) {
|
||||||
|
var verificationRequests []*model.VerificationRequest
|
||||||
|
ctx := driver.WithQueryFullCount(context.Background())
|
||||||
|
query := fmt.Sprintf("FOR d in %s SORT d.created_at DESC LIMIT %d, %d RETURN d", models.Collections.VerificationRequest, pagination.Offset, pagination.Limit)
|
||||||
|
|
||||||
|
cursor, err := p.db.Query(ctx, query, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cursor.Close()
|
||||||
|
|
||||||
|
paginationClone := pagination
|
||||||
|
paginationClone.Total = cursor.Statistics().FullCount()
|
||||||
|
|
||||||
|
for {
|
||||||
|
var verificationRequest models.VerificationRequest
|
||||||
|
meta, err := cursor.ReadDocument(nil, &verificationRequest)
|
||||||
|
|
||||||
|
if driver.IsNoMoreDocuments(err) {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if meta.Key != "" {
|
||||||
|
verificationRequests = append(verificationRequests, verificationRequest.AsAPIVerificationRequest())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.VerificationRequests{
|
||||||
|
VerificationRequests: verificationRequests,
|
||||||
|
Pagination: &paginationClone,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteVerificationRequest to delete verification request from database
|
||||||
|
func (p *provider) DeleteVerificationRequest(verificationRequest models.VerificationRequest) error {
|
||||||
|
collection, _ := p.db.Collection(nil, models.Collections.VerificationRequest)
|
||||||
|
_, err := collection.RemoveDocument(nil, verificationRequest.Key)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(`error deleting verification request:`, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
66
server/db/providers/mongodb/env.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddEnv to save environment information in database
|
||||||
|
func (p *provider) AddEnv(env models.Env) (models.Env, error) {
|
||||||
|
if env.ID == "" {
|
||||||
|
env.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
env.CreatedAt = time.Now().Unix()
|
||||||
|
env.UpdatedAt = time.Now().Unix()
|
||||||
|
env.Key = env.ID
|
||||||
|
configCollection := p.db.Collection(models.Collections.Env, options.Collection())
|
||||||
|
_, err := configCollection.InsertOne(nil, env)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error adding config:", err)
|
||||||
|
return env, err
|
||||||
|
}
|
||||||
|
return env, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEnv to update environment information in database
|
||||||
|
func (p *provider) UpdateEnv(env models.Env) (models.Env, error) {
|
||||||
|
env.UpdatedAt = time.Now().Unix()
|
||||||
|
configCollection := p.db.Collection(models.Collections.Env, options.Collection())
|
||||||
|
_, err := configCollection.UpdateOne(nil, bson.M{"_id": bson.M{"$eq": env.ID}}, bson.M{"$set": env}, options.MergeUpdateOptions())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error updating config:", err)
|
||||||
|
return env, err
|
||||||
|
}
|
||||||
|
return env, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnv to get environment information from database
|
||||||
|
func (p *provider) GetEnv() (models.Env, error) {
|
||||||
|
var env models.Env
|
||||||
|
configCollection := p.db.Collection(models.Collections.Env, options.Collection())
|
||||||
|
cursor, err := configCollection.Find(nil, bson.M{}, options.Find())
|
||||||
|
if err != nil {
|
||||||
|
return env, err
|
||||||
|
}
|
||||||
|
defer cursor.Close(nil)
|
||||||
|
|
||||||
|
for cursor.Next(nil) {
|
||||||
|
err := cursor.Decode(&env)
|
||||||
|
if err != nil {
|
||||||
|
return env, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if env.ID == "" {
|
||||||
|
return env, fmt.Errorf("config not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return env, nil
|
||||||
|
}
|
88
server/db/providers/mongodb/mongodb.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||||
|
)
|
||||||
|
|
||||||
|
type provider struct {
|
||||||
|
db *mongo.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProvider to initialize mongodb connection
|
||||||
|
func NewProvider() (*provider, error) {
|
||||||
|
mongodbOptions := options.Client().ApplyURI(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseURL))
|
||||||
|
maxWait := time.Duration(5 * time.Second)
|
||||||
|
mongodbOptions.ConnectTimeout = &maxWait
|
||||||
|
mongoClient, err := mongo.NewClient(mongodbOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
err = mongoClient.Connect(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mongoClient.Ping(ctx, readpref.Primary())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mongodb := mongoClient.Database(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseName), options.Database())
|
||||||
|
|
||||||
|
mongodb.CreateCollection(ctx, models.Collections.User, options.CreateCollection())
|
||||||
|
userCollection := mongodb.Collection(models.Collections.User, options.Collection())
|
||||||
|
userCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||||
|
mongo.IndexModel{
|
||||||
|
Keys: bson.M{"email": 1},
|
||||||
|
Options: options.Index().SetUnique(true).SetSparse(true),
|
||||||
|
},
|
||||||
|
}, options.CreateIndexes())
|
||||||
|
userCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||||
|
mongo.IndexModel{
|
||||||
|
Keys: bson.M{"phone_number": 1},
|
||||||
|
Options: options.Index().SetUnique(true).SetSparse(true).SetPartialFilterExpression(map[string]interface{}{
|
||||||
|
"phone_number": map[string]string{"$type": "string"},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}, options.CreateIndexes())
|
||||||
|
|
||||||
|
mongodb.CreateCollection(ctx, models.Collections.VerificationRequest, options.CreateCollection())
|
||||||
|
verificationRequestCollection := mongodb.Collection(models.Collections.VerificationRequest, options.Collection())
|
||||||
|
verificationRequestCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||||
|
mongo.IndexModel{
|
||||||
|
Keys: bson.M{"email": 1, "identifier": 1},
|
||||||
|
Options: options.Index().SetUnique(true).SetSparse(true),
|
||||||
|
},
|
||||||
|
}, options.CreateIndexes())
|
||||||
|
verificationRequestCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||||
|
mongo.IndexModel{
|
||||||
|
Keys: bson.M{"token": 1},
|
||||||
|
Options: options.Index().SetSparse(true),
|
||||||
|
},
|
||||||
|
}, options.CreateIndexes())
|
||||||
|
|
||||||
|
mongodb.CreateCollection(ctx, models.Collections.Session, options.CreateCollection())
|
||||||
|
sessionCollection := mongodb.Collection(models.Collections.Session, options.Collection())
|
||||||
|
sessionCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||||
|
mongo.IndexModel{
|
||||||
|
Keys: bson.M{"user_id": 1},
|
||||||
|
Options: options.Index().SetSparse(true),
|
||||||
|
},
|
||||||
|
}, options.CreateIndexes())
|
||||||
|
|
||||||
|
mongodb.CreateCollection(ctx, models.Collections.Env, options.CreateCollection())
|
||||||
|
|
||||||
|
return &provider{
|
||||||
|
db: mongodb,
|
||||||
|
}, nil
|
||||||
|
}
|
40
server/db/providers/mongodb/session.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddSession to save session information in database
|
||||||
|
func (p *provider) AddSession(session models.Session) error {
|
||||||
|
if session.ID == "" {
|
||||||
|
session.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Key = session.ID
|
||||||
|
session.CreatedAt = time.Now().Unix()
|
||||||
|
session.UpdatedAt = time.Now().Unix()
|
||||||
|
sessionCollection := p.db.Collection(models.Collections.Session, options.Collection())
|
||||||
|
_, err := sessionCollection.InsertOne(nil, session)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(`error saving session`, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSession to delete session information from database
|
||||||
|
func (p *provider) DeleteSession(userId string) error {
|
||||||
|
sessionCollection := p.db.Collection(models.Collections.Session, options.Collection())
|
||||||
|
_, err := sessionCollection.DeleteMany(nil, bson.M{"user_id": userId}, options.Delete())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error deleting session:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
128
server/db/providers/mongodb/user.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddUser to save user information in database
|
||||||
|
func (p *provider) AddUser(user models.User) (models.User, error) {
|
||||||
|
if user.ID == "" {
|
||||||
|
user.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Roles == "" {
|
||||||
|
user.Roles = strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",")
|
||||||
|
}
|
||||||
|
user.CreatedAt = time.Now().Unix()
|
||||||
|
user.UpdatedAt = time.Now().Unix()
|
||||||
|
user.Key = user.ID
|
||||||
|
userCollection := p.db.Collection(models.Collections.User, options.Collection())
|
||||||
|
_, err := userCollection.InsertOne(nil, user)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error adding user:", err)
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser to update user information in database
|
||||||
|
func (p *provider) UpdateUser(user models.User) (models.User, error) {
|
||||||
|
user.UpdatedAt = time.Now().Unix()
|
||||||
|
userCollection := p.db.Collection(models.Collections.User, options.Collection())
|
||||||
|
_, err := userCollection.UpdateOne(nil, bson.M{"_id": bson.M{"$eq": user.ID}}, bson.M{"$set": user}, options.MergeUpdateOptions())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error updating user:", err)
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser to delete user information from database
|
||||||
|
func (p *provider) DeleteUser(user models.User) error {
|
||||||
|
userCollection := p.db.Collection(models.Collections.User, options.Collection())
|
||||||
|
_, err := userCollection.DeleteOne(nil, bson.M{"_id": user.ID}, options.Delete())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error deleting user:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsers to get list of users from database
|
||||||
|
func (p *provider) ListUsers(pagination model.Pagination) (*model.Users, error) {
|
||||||
|
var users []*model.User
|
||||||
|
opts := options.Find()
|
||||||
|
opts.SetLimit(pagination.Limit)
|
||||||
|
opts.SetSkip(pagination.Offset)
|
||||||
|
opts.SetSort(bson.M{"created_at": -1})
|
||||||
|
|
||||||
|
paginationClone := pagination
|
||||||
|
// TODO add pagination total
|
||||||
|
|
||||||
|
userCollection := p.db.Collection(models.Collections.User, options.Collection())
|
||||||
|
count, err := userCollection.CountDocuments(nil, bson.M{}, options.Count())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error getting total users:", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
paginationClone.Total = count
|
||||||
|
|
||||||
|
cursor, err := userCollection.Find(nil, bson.M{}, opts)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error getting users:", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cursor.Close(nil)
|
||||||
|
|
||||||
|
for cursor.Next(nil) {
|
||||||
|
var user models.User
|
||||||
|
err := cursor.Decode(&user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
users = append(users, user.AsAPIUser())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Users{
|
||||||
|
Pagination: &paginationClone,
|
||||||
|
Users: users,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByEmail to get user information from database using email address
|
||||||
|
func (p *provider) GetUserByEmail(email string) (models.User, error) {
|
||||||
|
var user models.User
|
||||||
|
userCollection := p.db.Collection(models.Collections.User, options.Collection())
|
||||||
|
err := userCollection.FindOne(nil, bson.M{"email": email}).Decode(&user)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByID to get user information from database using user ID
|
||||||
|
func (p *provider) GetUserByID(id string) (models.User, error) {
|
||||||
|
var user models.User
|
||||||
|
|
||||||
|
userCollection := p.db.Collection(models.Collections.User, options.Collection())
|
||||||
|
err := userCollection.FindOne(nil, bson.M{"_id": id}).Decode(&user)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
106
server/db/providers/mongodb/verification_requests.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddVerification to save verification request in database
|
||||||
|
func (p *provider) AddVerificationRequest(verificationRequest models.VerificationRequest) (models.VerificationRequest, error) {
|
||||||
|
if verificationRequest.ID == "" {
|
||||||
|
verificationRequest.ID = uuid.New().String()
|
||||||
|
|
||||||
|
verificationRequest.CreatedAt = time.Now().Unix()
|
||||||
|
verificationRequest.UpdatedAt = time.Now().Unix()
|
||||||
|
verificationRequest.Key = verificationRequest.ID
|
||||||
|
verificationRequestCollection := p.db.Collection(models.Collections.VerificationRequest, options.Collection())
|
||||||
|
_, err := verificationRequestCollection.InsertOne(nil, verificationRequest)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error saving verification record:", err)
|
||||||
|
return verificationRequest, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return verificationRequest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVerificationRequestByToken to get verification request from database using token
|
||||||
|
func (p *provider) GetVerificationRequestByToken(token string) (models.VerificationRequest, error) {
|
||||||
|
var verificationRequest models.VerificationRequest
|
||||||
|
|
||||||
|
verificationRequestCollection := p.db.Collection(models.Collections.VerificationRequest, options.Collection())
|
||||||
|
err := verificationRequestCollection.FindOne(nil, bson.M{"token": token}).Decode(&verificationRequest)
|
||||||
|
if err != nil {
|
||||||
|
return verificationRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return verificationRequest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVerificationRequestByEmail to get verification request by email from database
|
||||||
|
func (p *provider) GetVerificationRequestByEmail(email string, identifier string) (models.VerificationRequest, error) {
|
||||||
|
var verificationRequest models.VerificationRequest
|
||||||
|
|
||||||
|
verificationRequestCollection := p.db.Collection(models.Collections.VerificationRequest, options.Collection())
|
||||||
|
err := verificationRequestCollection.FindOne(nil, bson.M{"email": email, "identifier": identifier}).Decode(&verificationRequest)
|
||||||
|
if err != nil {
|
||||||
|
return verificationRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return verificationRequest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVerificationRequests to get list of verification requests from database
|
||||||
|
func (p *provider) ListVerificationRequests(pagination model.Pagination) (*model.VerificationRequests, error) {
|
||||||
|
var verificationRequests []*model.VerificationRequest
|
||||||
|
|
||||||
|
opts := options.Find()
|
||||||
|
opts.SetLimit(pagination.Limit)
|
||||||
|
opts.SetSkip(pagination.Offset)
|
||||||
|
opts.SetSort(bson.M{"created_at": -1})
|
||||||
|
|
||||||
|
verificationRequestCollection := p.db.Collection(models.Collections.VerificationRequest, options.Collection())
|
||||||
|
|
||||||
|
verificationRequestCollectionCount, err := verificationRequestCollection.CountDocuments(nil, bson.M{})
|
||||||
|
paginationClone := pagination
|
||||||
|
paginationClone.Total = verificationRequestCollectionCount
|
||||||
|
|
||||||
|
cursor, err := verificationRequestCollection.Find(nil, bson.M{}, opts)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error getting verification requests:", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cursor.Close(nil)
|
||||||
|
|
||||||
|
for cursor.Next(nil) {
|
||||||
|
var verificationRequest models.VerificationRequest
|
||||||
|
err := cursor.Decode(&verificationRequest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
verificationRequests = append(verificationRequests, verificationRequest.AsAPIVerificationRequest())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.VerificationRequests{
|
||||||
|
VerificationRequests: verificationRequests,
|
||||||
|
Pagination: &paginationClone,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteVerificationRequest to delete verification request from database
|
||||||
|
func (p *provider) DeleteVerificationRequest(verificationRequest models.VerificationRequest) error {
|
||||||
|
verificationRequestCollection := p.db.Collection(models.Collections.VerificationRequest, options.Collection())
|
||||||
|
_, err := verificationRequestCollection.DeleteOne(nil, bson.M{"_id": verificationRequest.ID}, options.Delete())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error deleting verification request::", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
44
server/db/providers/providers.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
// AddUser to save user information in database
|
||||||
|
AddUser(user models.User) (models.User, error)
|
||||||
|
// UpdateUser to update user information in database
|
||||||
|
UpdateUser(user models.User) (models.User, error)
|
||||||
|
// DeleteUser to delete user information from database
|
||||||
|
DeleteUser(user models.User) error
|
||||||
|
// ListUsers to get list of users from database
|
||||||
|
ListUsers(pagination model.Pagination) (*model.Users, error)
|
||||||
|
// GetUserByEmail to get user information from database using email address
|
||||||
|
GetUserByEmail(email string) (models.User, error)
|
||||||
|
// GetUserByID to get user information from database using user ID
|
||||||
|
GetUserByID(id string) (models.User, error)
|
||||||
|
|
||||||
|
// AddVerification to save verification request in database
|
||||||
|
AddVerificationRequest(verificationRequest models.VerificationRequest) (models.VerificationRequest, error)
|
||||||
|
// GetVerificationRequestByToken to get verification request from database using token
|
||||||
|
GetVerificationRequestByToken(token string) (models.VerificationRequest, error)
|
||||||
|
// GetVerificationRequestByEmail to get verification request by email from database
|
||||||
|
GetVerificationRequestByEmail(email string, identifier string) (models.VerificationRequest, error)
|
||||||
|
// ListVerificationRequests to get list of verification requests from database
|
||||||
|
ListVerificationRequests(pagination model.Pagination) (*model.VerificationRequests, error)
|
||||||
|
// DeleteVerificationRequest to delete verification request from database
|
||||||
|
DeleteVerificationRequest(verificationRequest models.VerificationRequest) error
|
||||||
|
|
||||||
|
// AddSession to save session information in database
|
||||||
|
AddSession(session models.Session) error
|
||||||
|
// DeleteSession to delete session information from database
|
||||||
|
DeleteSession(userId string) error
|
||||||
|
|
||||||
|
// AddEnv to save environment information in database
|
||||||
|
AddEnv(env models.Env) (models.Env, error)
|
||||||
|
// UpdateEnv to update environment information in database
|
||||||
|
UpdateEnv(env models.Env) (models.Env, error)
|
||||||
|
// GetEnv to get environment information from database
|
||||||
|
GetEnv() (models.Env, error)
|
||||||
|
}
|
51
server/db/providers/sql/env.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddEnv to save environment information in database
|
||||||
|
func (p *provider) AddEnv(env models.Env) (models.Env, error) {
|
||||||
|
if env.ID == "" {
|
||||||
|
env.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
env.Key = env.ID
|
||||||
|
env.CreatedAt = time.Now().Unix()
|
||||||
|
env.UpdatedAt = time.Now().Unix()
|
||||||
|
|
||||||
|
result := p.db.Create(&env)
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Println("error adding config:", result.Error)
|
||||||
|
return env, result.Error
|
||||||
|
}
|
||||||
|
return env, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEnv to update environment information in database
|
||||||
|
func (p *provider) UpdateEnv(env models.Env) (models.Env, error) {
|
||||||
|
env.UpdatedAt = time.Now().Unix()
|
||||||
|
result := p.db.Save(&env)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Println("error updating config:", result.Error)
|
||||||
|
return env, result.Error
|
||||||
|
}
|
||||||
|
return env, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnv to get environment information from database
|
||||||
|
func (p *provider) GetEnv() (models.Env, error) {
|
||||||
|
var env models.Env
|
||||||
|
result := p.db.First(&env)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return env, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return env, nil
|
||||||
|
}
|
41
server/db/providers/sql/session.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddSession to save session information in database
|
||||||
|
func (p *provider) AddSession(session models.Session) error {
|
||||||
|
if session.ID == "" {
|
||||||
|
session.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Key = session.ID
|
||||||
|
session.CreatedAt = time.Now().Unix()
|
||||||
|
session.UpdatedAt = time.Now().Unix()
|
||||||
|
res := p.db.Clauses(
|
||||||
|
clause.OnConflict{
|
||||||
|
DoNothing: true,
|
||||||
|
}).Create(&session)
|
||||||
|
if res.Error != nil {
|
||||||
|
log.Println(`error saving session`, res.Error)
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSession to delete session information from database
|
||||||
|
func (p *provider) DeleteSession(userId string) error {
|
||||||
|
result := p.db.Where("user_id = ?", userId).Delete(&models.Session{})
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Println(`error deleting session:`, result.Error)
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
66
server/db/providers/sql/sql.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/driver/sqlserver"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
"gorm.io/gorm/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type provider struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProvider returns a new SQL provider
|
||||||
|
func NewProvider() (*provider, error) {
|
||||||
|
var sqlDB *gorm.DB
|
||||||
|
var err error
|
||||||
|
customLogger := logger.New(
|
||||||
|
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
|
||||||
|
logger.Config{
|
||||||
|
SlowThreshold: time.Second, // Slow SQL threshold
|
||||||
|
LogLevel: logger.Silent, // Log level
|
||||||
|
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
|
||||||
|
Colorful: false, // Disable color
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ormConfig := &gorm.Config{
|
||||||
|
Logger: customLogger,
|
||||||
|
NamingStrategy: schema.NamingStrategy{
|
||||||
|
TablePrefix: models.Prefix,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
switch envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) {
|
||||||
|
case constants.DbTypePostgres, constants.DbTypeYugabyte:
|
||||||
|
sqlDB, err = gorm.Open(postgres.Open(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseURL)), ormConfig)
|
||||||
|
case constants.DbTypeSqlite:
|
||||||
|
sqlDB, err = gorm.Open(sqlite.Open(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseURL)), ormConfig)
|
||||||
|
case constants.DbTypeMysql, constants.DbTypeMariaDB:
|
||||||
|
sqlDB, err = gorm.Open(mysql.Open(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseURL)), ormConfig)
|
||||||
|
case constants.DbTypeSqlserver:
|
||||||
|
sqlDB, err = gorm.Open(sqlserver.Open(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseURL)), ormConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sqlDB.AutoMigrate(&models.User{}, &models.VerificationRequest{}, &models.Session{}, &models.Env{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &provider{
|
||||||
|
db: sqlDB,
|
||||||
|
}, nil
|
||||||
|
}
|
121
server/db/providers/sql/user.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddUser to save user information in database
|
||||||
|
func (p *provider) AddUser(user models.User) (models.User, error) {
|
||||||
|
if user.ID == "" {
|
||||||
|
user.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Roles == "" {
|
||||||
|
user.Roles = strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
user.CreatedAt = time.Now().Unix()
|
||||||
|
user.UpdatedAt = time.Now().Unix()
|
||||||
|
user.Key = user.ID
|
||||||
|
result := p.db.Clauses(
|
||||||
|
clause.OnConflict{
|
||||||
|
UpdateAll: true,
|
||||||
|
Columns: []clause.Column{{Name: "email"}},
|
||||||
|
}).Create(&user)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Println("error adding user:", result.Error)
|
||||||
|
return user, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser to update user information in database
|
||||||
|
func (p *provider) UpdateUser(user models.User) (models.User, error) {
|
||||||
|
user.UpdatedAt = time.Now().Unix()
|
||||||
|
|
||||||
|
result := p.db.Save(&user)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Println("error updating user:", result.Error)
|
||||||
|
return user, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser to delete user information from database
|
||||||
|
func (p *provider) DeleteUser(user models.User) error {
|
||||||
|
result := p.db.Delete(&user)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Println(`error deleting user:`, result.Error)
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsers to get list of users from database
|
||||||
|
func (p *provider) ListUsers(pagination model.Pagination) (*model.Users, error) {
|
||||||
|
var users []models.User
|
||||||
|
result := p.db.Limit(int(pagination.Limit)).Offset(int(pagination.Offset)).Order("created_at DESC").Find(&users)
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Println("error getting users:", result.Error)
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
responseUsers := []*model.User{}
|
||||||
|
for _, user := range users {
|
||||||
|
responseUsers = append(responseUsers, user.AsAPIUser())
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
totalRes := p.db.Model(&models.User{}).Count(&total)
|
||||||
|
if totalRes.Error != nil {
|
||||||
|
return nil, totalRes.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
paginationClone := pagination
|
||||||
|
paginationClone.Total = total
|
||||||
|
|
||||||
|
return &model.Users{
|
||||||
|
Pagination: &paginationClone,
|
||||||
|
Users: responseUsers,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByEmail to get user information from database using email address
|
||||||
|
func (p *provider) GetUserByEmail(email string) (models.User, error) {
|
||||||
|
var user models.User
|
||||||
|
result := p.db.Where("email = ?", email).First(&user)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return user, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByID to get user information from database using user ID
|
||||||
|
func (p *provider) GetUserByID(id string) (models.User, error) {
|
||||||
|
var user models.User
|
||||||
|
|
||||||
|
result := p.db.Where("id = ?", id).First(&user)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return user, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
102
server/db/providers/sql/verification_requests.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddVerification to save verification request in database
|
||||||
|
func (p *provider) AddVerificationRequest(verificationRequest models.VerificationRequest) (models.VerificationRequest, error) {
|
||||||
|
if verificationRequest.ID == "" {
|
||||||
|
verificationRequest.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
verificationRequest.Key = verificationRequest.ID
|
||||||
|
verificationRequest.CreatedAt = time.Now().Unix()
|
||||||
|
verificationRequest.UpdatedAt = time.Now().Unix()
|
||||||
|
result := p.db.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "email"}, {Name: "identifier"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at", "nonce", "redirect_uri"}),
|
||||||
|
}).Create(&verificationRequest)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Println(`error saving verification request record`, result.Error)
|
||||||
|
return verificationRequest, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return verificationRequest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVerificationRequestByToken to get verification request from database using token
|
||||||
|
func (p *provider) GetVerificationRequestByToken(token string) (models.VerificationRequest, error) {
|
||||||
|
var verificationRequest models.VerificationRequest
|
||||||
|
result := p.db.Where("token = ?", token).First(&verificationRequest)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Println(`error getting verification request:`, result.Error)
|
||||||
|
return verificationRequest, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return verificationRequest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVerificationRequestByEmail to get verification request by email from database
|
||||||
|
func (p *provider) GetVerificationRequestByEmail(email string, identifier string) (models.VerificationRequest, error) {
|
||||||
|
var verificationRequest models.VerificationRequest
|
||||||
|
|
||||||
|
result := p.db.Where("email = ? AND identifier = ?", email, identifier).First(&verificationRequest)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Println(`error getting verification token:`, result.Error)
|
||||||
|
return verificationRequest, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return verificationRequest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVerificationRequests to get list of verification requests from database
|
||||||
|
func (p *provider) ListVerificationRequests(pagination model.Pagination) (*model.VerificationRequests, error) {
|
||||||
|
var verificationRequests []models.VerificationRequest
|
||||||
|
|
||||||
|
result := p.db.Limit(int(pagination.Limit)).Offset(int(pagination.Offset)).Order("created_at DESC").Find(&verificationRequests)
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Println("error getting verification requests:", result.Error)
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
responseVerificationRequests := []*model.VerificationRequest{}
|
||||||
|
for _, v := range verificationRequests {
|
||||||
|
responseVerificationRequests = append(responseVerificationRequests, v.AsAPIVerificationRequest())
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
totalRes := p.db.Model(&models.VerificationRequest{}).Count(&total)
|
||||||
|
if totalRes.Error != nil {
|
||||||
|
return nil, totalRes.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
paginationClone := pagination
|
||||||
|
paginationClone.Total = total
|
||||||
|
|
||||||
|
return &model.VerificationRequests{
|
||||||
|
VerificationRequests: responseVerificationRequests,
|
||||||
|
Pagination: &paginationClone,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteVerificationRequest to delete verification request from database
|
||||||
|
func (p *provider) DeleteVerificationRequest(verificationRequest models.VerificationRequest) error {
|
||||||
|
result := p.db.Delete(&verificationRequest)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Println(`error deleting verification request:`, result.Error)
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@@ -1,97 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
ID uint `gorm:"primaryKey"`
|
|
||||||
FirstName string
|
|
||||||
LastName string
|
|
||||||
Email string `gorm:"unique"`
|
|
||||||
Password string
|
|
||||||
SignupMethod string
|
|
||||||
EmailVerifiedAt int64
|
|
||||||
CreatedAt int64 `gorm:"autoCreateTime"`
|
|
||||||
UpdatedAt int64 `gorm:"autoUpdateTime"`
|
|
||||||
Image string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveUser function to add user even with email conflict
|
|
||||||
func (mgr *manager) SaveUser(user User) (User, error) {
|
|
||||||
result := mgr.db.Clauses(
|
|
||||||
clause.OnConflict{
|
|
||||||
UpdateAll: true,
|
|
||||||
Columns: []clause.Column{{Name: "email"}},
|
|
||||||
}).Create(&user)
|
|
||||||
|
|
||||||
if result.Error != nil {
|
|
||||||
log.Println(result.Error)
|
|
||||||
return user, result.Error
|
|
||||||
}
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateUser function to update user with ID conflict
|
|
||||||
func (mgr *manager) UpdateUser(user User) (User, error) {
|
|
||||||
result := mgr.db.Clauses(
|
|
||||||
clause.OnConflict{
|
|
||||||
UpdateAll: true,
|
|
||||||
Columns: []clause.Column{{Name: "id"}},
|
|
||||||
}).Create(&user)
|
|
||||||
|
|
||||||
if result.Error != nil {
|
|
||||||
log.Println(result.Error)
|
|
||||||
return user, result.Error
|
|
||||||
}
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUsers function to get all users
|
|
||||||
func (mgr *manager) GetUsers() ([]User, error) {
|
|
||||||
var users []User
|
|
||||||
result := mgr.db.Find(&users)
|
|
||||||
if result.Error != nil {
|
|
||||||
log.Println(result.Error)
|
|
||||||
return users, result.Error
|
|
||||||
}
|
|
||||||
return users, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mgr *manager) GetUserByEmail(email string) (User, error) {
|
|
||||||
var user User
|
|
||||||
result := mgr.db.Where("email = ?", email).First(&user)
|
|
||||||
|
|
||||||
if result.Error != nil {
|
|
||||||
return user, result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mgr *manager) UpdateVerificationTime(verifiedAt int64, id uint) error {
|
|
||||||
user := &User{
|
|
||||||
ID: id,
|
|
||||||
}
|
|
||||||
result := mgr.db.Model(&user).Where("id = ?", id).Update("email_verified_at", verifiedAt)
|
|
||||||
|
|
||||||
if result.Error != nil {
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mgr *manager) DeleteUser(email string) error {
|
|
||||||
var user User
|
|
||||||
result := mgr.db.Where("email = ?", email).Delete(&user)
|
|
||||||
|
|
||||||
if result.Error != nil {
|
|
||||||
log.Println(`Error deleting user:`, result.Error)
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@@ -1,77 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
|
||||||
|
|
||||||
type VerificationRequest struct {
|
|
||||||
ID uint `gorm:"primaryKey"`
|
|
||||||
Token string `gorm:"index"`
|
|
||||||
Identifier string
|
|
||||||
ExpiresAt int64
|
|
||||||
CreatedAt int64 `gorm:"autoCreateTime"`
|
|
||||||
UpdatedAt int64 `gorm:"autoUpdateTime"`
|
|
||||||
Email string `gorm:"unique"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddVerification function to add verification record
|
|
||||||
func (mgr *manager) AddVerification(verification VerificationRequest) (VerificationRequest, error) {
|
|
||||||
result := mgr.db.Clauses(clause.OnConflict{
|
|
||||||
Columns: []clause.Column{{Name: "email"}},
|
|
||||||
DoUpdates: clause.AssignmentColumns([]string{"token", "identifier", "expires_at"}),
|
|
||||||
}).Create(&verification)
|
|
||||||
if result.Error != nil {
|
|
||||||
log.Println(`Error saving verification record`, result.Error)
|
|
||||||
return verification, result.Error
|
|
||||||
}
|
|
||||||
return verification, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mgr *manager) GetVerificationByToken(token string) (VerificationRequest, error) {
|
|
||||||
var verification VerificationRequest
|
|
||||||
result := mgr.db.Where("token = ?", token).First(&verification)
|
|
||||||
|
|
||||||
if result.Error != nil {
|
|
||||||
log.Println(`Error getting verification token:`, result.Error)
|
|
||||||
return verification, result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
return verification, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mgr *manager) GetVerificationByEmail(email string) (VerificationRequest, error) {
|
|
||||||
var verification VerificationRequest
|
|
||||||
result := mgr.db.Where("email = ?", email).First(&verification)
|
|
||||||
|
|
||||||
if result.Error != nil {
|
|
||||||
log.Println(`Error getting verification token:`, result.Error)
|
|
||||||
return verification, result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
return verification, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mgr *manager) DeleteToken(email string) error {
|
|
||||||
var verification VerificationRequest
|
|
||||||
result := mgr.db.Where("email = ?", email).Delete(&verification)
|
|
||||||
|
|
||||||
if result.Error != nil {
|
|
||||||
log.Println(`Error deleting token:`, result.Error)
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUsers function to get all users
|
|
||||||
func (mgr *manager) GetVerificationRequests() ([]VerificationRequest, error) {
|
|
||||||
var verificationRequests []VerificationRequest
|
|
||||||
result := mgr.db.Find(&verificationRequests)
|
|
||||||
if result.Error != nil {
|
|
||||||
log.Println(result.Error)
|
|
||||||
return verificationRequests, result.Error
|
|
||||||
}
|
|
||||||
return verificationRequests, nil
|
|
||||||
}
|
|