Compare commits
185 Commits
0.1.0-beta
...
0.14.0-bet
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 |
@@ -7,3 +7,5 @@ ROADMAP.md
|
||||
build
|
||||
.env
|
||||
data.db
|
||||
app/node_modules
|
||||
app/build
|
||||
|
11
.env.sample
@@ -1,12 +1,3 @@
|
||||
ENV=production
|
||||
DATABASE_URL=data.db
|
||||
DATABASE_TYPE=sqlite
|
||||
ADMIN_SECRET=admin
|
||||
DISABLE_EMAIL_VERIFICATION=true
|
||||
JWT_SECRET=random_string
|
||||
JWT_TYPE=HS256
|
||||
ROLES=user
|
||||
DEFAULT_ROLES=user
|
||||
PROTECTED_ROLES=admin
|
||||
JWT_ROLE_CLAIM=role
|
||||
CUSTOM_ACCESS_TOKEN_SCRIPT=function(user,tokenPayload){var data = tokenPayload;data.extra = {'x-extra-id': user.id};return data;}
|
||||
CUSTOM_ACCESS_TOKEN_SCRIPT="function(user,tokenPayload){var data = tokenPayload;data.extra = {'x-extra-id': user.id};return data;}"
|
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?
|
||||
|
||||
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:
|
||||
|
||||
@@ -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)
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
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`
|
||||
3. `cd authorizer`
|
||||
4. `cp .env.sample .env`. Check all the supported env [here](https://docs.authorizer.dev/core/env/)
|
||||
5. Build the code `make clean && make`
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
27
.github/workflows/release.yaml
vendored
@@ -8,26 +8,23 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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
|
||||
run: |
|
||||
sudo apt-get install build-essential wget zip gcc-mingw-w64 && \
|
||||
sudo apt-get remove --auto-remove golang-go && \
|
||||
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
|
||||
echo "/usr/bin/x86_64-w64-mingw32-gcc" >> GITHUB_PATH
|
||||
go version && \
|
||||
echo "/usr/bin/x86_64-w64-mingw32-gcc" >> GITHUB_PATH && \
|
||||
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 && \
|
||||
sudo mv github-assets-uploader /usr/sbin/ && \
|
||||
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
|
||||
run: whereis go
|
||||
- name: Print Go Version
|
||||
@@ -41,12 +38,12 @@ jobs:
|
||||
make clean && \
|
||||
CGO_ENABLED=1 GOOS=windows CC=/usr/bin/x86_64-w64-mingw32-gcc make && \
|
||||
mv build/server build/server.exe && \
|
||||
zip -vr authorizer-${VERSION}-windows-amd64.zip .env app/build build templates
|
||||
zip -vr authorizer-${VERSION}-windows-amd64.zip .env app/build build templates dashboard/build
|
||||
- name: Package files for linux
|
||||
run: |
|
||||
make clean && \
|
||||
CGO_ENABLED=1 make && \
|
||||
tar cvfz authorizer-${VERSION}-linux-amd64.tar.gz .env app/build build templates
|
||||
tar cvfz authorizer-${VERSION}-linux-amd64.tar.gz .env app/build build templates dashboard/build
|
||||
- name: Upload assets
|
||||
run: |
|
||||
github-assets-uploader -f authorizer-${VERSION}-windows-amd64.zip -mediatype application/zip -repo authorizerdev/authorizer -token ${{secrets.RELEASE_TOKEN}} -tag ${VERSION} && \
|
||||
|
4
.gitignore
vendored
@@ -2,9 +2,13 @@ server/server
|
||||
server/.env
|
||||
data
|
||||
app/node_modules
|
||||
app/build
|
||||
dashboard/node_modules
|
||||
dashboard/build
|
||||
build
|
||||
.env
|
||||
data.db
|
||||
.DS_Store
|
||||
.env.local
|
||||
*.tar.gz
|
||||
.vscode/
|
26
Dockerfile
@@ -1,5 +1,5 @@
|
||||
FROM golang:1.16-alpine as builder
|
||||
WORKDIR /app
|
||||
FROM golang:1.17-alpine as go-builder
|
||||
WORKDIR /authorizer
|
||||
COPY server server
|
||||
COPY Makefile .
|
||||
|
||||
@@ -7,15 +7,27 @@ ARG VERSION="latest"
|
||||
ENV VERSION="$VERSION"
|
||||
|
||||
RUN echo "$VERSION"
|
||||
RUN apk add build-base nodejs &&\
|
||||
RUN apk add build-base &&\
|
||||
make clean && make && \
|
||||
chmod 777 build/server
|
||||
|
||||
FROM alpine:latest
|
||||
RUN apk --no-cache add ca-certificates
|
||||
WORKDIR /root/
|
||||
FROM node:17-alpine3.12 as node-builder
|
||||
WORKDIR /authorizer
|
||||
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 --from=builder /app/build build
|
||||
EXPOSE 8080
|
||||
CMD [ "./build/server" ]
|
||||
|
11
Makefile
@@ -2,6 +2,15 @@ DEFAULT_VERSION=0.1.0-local
|
||||
VERSION := $(or $(VERSION),$(DEFAULT_VERSION))
|
||||
|
||||
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:
|
||||
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
|
||||
|
45
README.md
@@ -1,13 +1,13 @@
|
||||
<p align="center">
|
||||
<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>
|
||||
</p>
|
||||
<h1 align="center">
|
||||
Authorizer
|
||||
</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 SQL database.
|
||||
**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/)).
|
||||
|
||||
## Table of contents
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
- [Getting Started](#getting-started)
|
||||
- [Contributing](https://github.com/authorizerdev/authorizer/blob/main/.github/CONTRIBUTING.md)
|
||||
- [Docs](http://docs.authorizer.dev/)
|
||||
- [Join Community](https://discord.gg/2fXUQN3E)
|
||||
- [Join Community](https://discord.gg/Zv2D5h6kkK)
|
||||
|
||||
# Introduction
|
||||
|
||||
@@ -32,10 +32,6 @@
|
||||
- ✅ Role-based access management
|
||||
- ✅ Password-less login with email and magic link
|
||||
|
||||
## Project Status
|
||||
|
||||
⚠️ **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
|
||||
|
||||
- Support more JWT encryption algorithms (Currently supporting HS256)
|
||||
@@ -70,6 +66,7 @@ This guide helps you practice using Authorizer to evaluate it before you use it
|
||||
- [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)
|
||||
|
||||
## Install using source code
|
||||
|
||||
@@ -81,12 +78,14 @@ This guide helps you practice using Authorizer to evaluate it before you use it
|
||||
### Project Setup
|
||||
|
||||
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`
|
||||
3. `cd authorizer`
|
||||
4. `cp .env.sample .env`. Check all the supported env [here](https://docs.authorizer.dev/core/env/)
|
||||
5. Build the code `make clean && make`
|
||||
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
|
||||
6. Run binary `./build/server`
|
||||
9. Run binary `./build/server`
|
||||
|
||||
## Install using binaries
|
||||
|
||||
@@ -132,18 +131,24 @@ Required environment variables are pre-configured in `.env` file. But based on t
|
||||
|
||||
> 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`
|
||||
|
||||
## Install instance on Heroku
|
||||
Deploy production ready Authorizer instance using one click deployment options available below
|
||||
|
||||
Deploy Authorizer using [heroku](https://github.com/authorizerdev/authorizer-heroku) and quickly play with it in 30seconds
|
||||
<br/><br/>
|
||||
[](https://heroku.com/deploy?template=https://github.com/authorizerdev/authorizer-heroku)
|
||||
| **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
|
||||
|
||||
- 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.
|
||||
> 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
|
||||
|
||||
@@ -184,3 +189,9 @@ This example demonstrates how you can use [`@authorizerdev/authorizer-js`](/auth
|
||||
onLoad();
|
||||
</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>
|
||||
|
21
TODO.md
@@ -1,5 +1,26 @@
|
||||
# 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
|
||||
|
@@ -1,3 +1,14 @@
|
||||
# Authorizer APP
|
||||
|
||||
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 |
442
app/package-lock.json
generated
@@ -5,15 +5,17 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "app",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@authorizerdev/authorizer-react": "^0.1.0-beta.19",
|
||||
"@authorizerdev/authorizer-react": "0.9.0-beta.2",
|
||||
"@types/react": "^17.0.15",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"esbuild": "^0.12.17",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-is": "^17.0.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
@@ -22,9 +24,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@authorizerdev/authorizer-js": {
|
||||
"version": "0.1.0-beta.22",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.1.0-beta.22.tgz",
|
||||
"integrity": "sha512-fHyZDL49eEsbxxIb2xxW6iLM+/N60m5e2NeVVlMFZjZFX9eUetdFatCOrPTuukGul7l8wO6YofTnwqOLOVjKrA==",
|
||||
"version": "0.4.0-beta.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.0.tgz",
|
||||
"integrity": "sha512-wNh5ROldNqdbOXFPDlq1tObzPZyEQkbnOvSEwvnDfPYb9/BsJ3naj3/ayz4J2R5k2+Eyuk0LK64XYdkfIW0HYA==",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.1"
|
||||
},
|
||||
@@ -33,11 +35,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@authorizerdev/authorizer-react": {
|
||||
"version": "0.1.0-beta.19",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.1.0-beta.19.tgz",
|
||||
"integrity": "sha512-ScAG3Auu0KirxuxpQ25JGtYCDmiC/3DM/ngnGO+XBaW+bWUARifH2HPkOnddUxkKPAfrKyxoa/l730W8mxhy+A==",
|
||||
"version": "0.9.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.2.tgz",
|
||||
"integrity": "sha512-clngw7MdFzvnns9rgrg9fHRH4p3K+HGGMju6qhdjDF+4vPruSu6HwBi1hRvVxLi1q7CZ25CEE7CfA7Vfq7H3Bw==",
|
||||
"dependencies": {
|
||||
"@authorizerdev/authorizer-js": "^0.1.0-beta.22",
|
||||
"@authorizerdev/authorizer-js": "^0.4.0-beta.0",
|
||||
"final-form": "^4.20.2",
|
||||
"react-final-form": "^6.5.3",
|
||||
"styled-components": "^5.3.0"
|
||||
@@ -50,22 +52,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
|
||||
"integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
|
||||
"integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
|
||||
"dependencies": {
|
||||
"@babel/highlight": "^7.14.5"
|
||||
"@babel/highlight": "^7.16.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.8.tgz",
|
||||
"integrity": "sha512-cYDUpvIzhBVnMzRoY1fkSEhK/HmwEVwlyULYgn/tMQYd6Obag3ylCjONle3gdErfXBW61SVTlR9QR7uWlgeIkg==",
|
||||
"version": "7.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz",
|
||||
"integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.14.8",
|
||||
"@babel/types": "^7.16.8",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
},
|
||||
@@ -74,87 +76,98 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-annotate-as-pure": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz",
|
||||
"integrity": "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz",
|
||||
"integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==",
|
||||
"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": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-function-name": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz",
|
||||
"integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz",
|
||||
"integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==",
|
||||
"dependencies": {
|
||||
"@babel/helper-get-function-arity": "^7.14.5",
|
||||
"@babel/template": "^7.14.5",
|
||||
"@babel/types": "^7.14.5"
|
||||
"@babel/helper-get-function-arity": "^7.16.7",
|
||||
"@babel/template": "^7.16.7",
|
||||
"@babel/types": "^7.16.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-get-function-arity": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz",
|
||||
"integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz",
|
||||
"integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.14.5"
|
||||
"@babel/types": "^7.16.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-hoist-variables": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz",
|
||||
"integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz",
|
||||
"integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.14.5"
|
||||
"@babel/types": "^7.16.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz",
|
||||
"integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz",
|
||||
"integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.14.5"
|
||||
"@babel/types": "^7.16.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-split-export-declaration": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz",
|
||||
"integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz",
|
||||
"integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.14.5"
|
||||
"@babel/types": "^7.16.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.8.tgz",
|
||||
"integrity": "sha512-ZGy6/XQjllhYQrNw/3zfWRwZCTVSiBLZ9DHVZxn9n2gip/7ab8mv2TWlKPIBk26RwedCBoWdjLmn+t9na2Gcow==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
|
||||
"integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz",
|
||||
"integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==",
|
||||
"version": "7.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz",
|
||||
"integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.14.5",
|
||||
"@babel/helper-validator-identifier": "^7.16.7",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
},
|
||||
@@ -163,9 +176,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.8.tgz",
|
||||
"integrity": "sha512-syoCQFOoo/fzkWDeM0dLEZi5xqurb5vuyzwIMNZRNun+N/9A4cUZeQaE7dTrB8jGaKuJRBtEOajtnmw0I5hvvA==",
|
||||
"version": "7.16.12",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.12.tgz",
|
||||
"integrity": "sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A==",
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
@@ -185,30 +198,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz",
|
||||
"integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz",
|
||||
"integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.14.5",
|
||||
"@babel/parser": "^7.14.5",
|
||||
"@babel/types": "^7.14.5"
|
||||
"@babel/code-frame": "^7.16.7",
|
||||
"@babel/parser": "^7.16.7",
|
||||
"@babel/types": "^7.16.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.8.tgz",
|
||||
"integrity": "sha512-kexHhzCljJcFNn1KYAQ6A5wxMRzq9ebYpEDV4+WdNyr3i7O44tanbDOR/xjiG2F3sllan+LgwK+7OMk0EmydHg==",
|
||||
"version": "7.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.10.tgz",
|
||||
"integrity": "sha512-yzuaYXoRJBGMlBhsMJoUW7G1UmSb/eXr/JHYM/MsOJgavJibLwASijW7oXBdw3NQ6T0bW7Ty5P/VarOs9cHmqw==",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.14.5",
|
||||
"@babel/generator": "^7.14.8",
|
||||
"@babel/helper-function-name": "^7.14.5",
|
||||
"@babel/helper-hoist-variables": "^7.14.5",
|
||||
"@babel/helper-split-export-declaration": "^7.14.5",
|
||||
"@babel/parser": "^7.14.8",
|
||||
"@babel/types": "^7.14.8",
|
||||
"@babel/code-frame": "^7.16.7",
|
||||
"@babel/generator": "^7.16.8",
|
||||
"@babel/helper-environment-visitor": "^7.16.7",
|
||||
"@babel/helper-function-name": "^7.16.7",
|
||||
"@babel/helper-hoist-variables": "^7.16.7",
|
||||
"@babel/helper-split-export-declaration": "^7.16.7",
|
||||
"@babel/parser": "^7.16.10",
|
||||
"@babel/types": "^7.16.8",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
@@ -217,11 +231,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.8.tgz",
|
||||
"integrity": "sha512-iob4soQa7dZw8nodR/KlOQkPh9S4I8RwCxwRIFuiMRYjOzH/KJzdUfDgz6cGi5dDaclXF4P2PAhCdrBJNIg68Q==",
|
||||
"version": "7.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.8.tgz",
|
||||
"integrity": "sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg==",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.14.8",
|
||||
"@babel/helper-validator-identifier": "^7.16.7",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -318,12 +332,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-styled-components": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.13.2.tgz",
|
||||
"integrity": "sha512-Vb1R3d4g+MUfPQPVDMCGjm3cDocJEUTR7Xq7QS95JWWeksN1wdFRYpD2kulDgI3Huuaf1CZd+NK4KQmqUFh5dA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.2.tgz",
|
||||
"integrity": "sha512-7eG5NE8rChnNTDxa6LQfynwgHTVOYYaHJbUYSlOhk8QBXIQiMBKq4gyfHBBKPrxUcVBXVJL61ihduCpCQbuNbw==",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.0.0",
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/helper-annotate-as-pure": "^7.16.0",
|
||||
"@babel/helper-module-imports": "^7.16.0",
|
||||
"babel-plugin-syntax-jsx": "^6.18.0",
|
||||
"lodash": "^4.17.11"
|
||||
},
|
||||
@@ -391,9 +405,9 @@
|
||||
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
|
||||
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
@@ -424,9 +438,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/final-form": {
|
||||
"version": "4.20.2",
|
||||
"resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.2.tgz",
|
||||
"integrity": "sha512-5i0IxqwjjPG1nUNCjWhqPCvQJJ2R+QwTwaAnjPmFnLbyjIHWuBPU8u+Ps4G3TcX2Sjno+O5xCZJzYcMJEzzfCQ==",
|
||||
"version": "4.20.6",
|
||||
"resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.6.tgz",
|
||||
"integrity": "sha512-fCdwIj49KOaFfDRlXB57Eo+GghIMZQWrA9TakQI3C9uQxHwaFHXqZSNRlUdfnQmNNeySwGOaGPZCvjy58hyv4w==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.10.0"
|
||||
},
|
||||
@@ -533,14 +547,22 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz",
|
||||
"integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==",
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
@@ -560,9 +582,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-value-parser": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
|
||||
"integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ=="
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.7.2",
|
||||
@@ -605,26 +627,36 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-final-form": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.3.tgz",
|
||||
"integrity": "sha512-FCs6GC0AMWJl2p6YX7kM+a0AvuSLAZUgbVNtRBskOs4g984t/It0qGtx51O+9vgqnqk6JyoxmIzxKMq+7ch/vg==",
|
||||
"version": "6.5.7",
|
||||
"resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.7.tgz",
|
||||
"integrity": "sha512-o7tvJXB+McGiXOILqIC8lnOcX4aLhIBiF/Xi9Qet35b7XOS8R7KL8HLRKTfnZWQJm6MCE15v1U0SFive0NcxyA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.1"
|
||||
"@babel/runtime": "^7.15.4"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/final-form"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"final-form": "^4.20.0",
|
||||
"final-form": "4.20.4",
|
||||
"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": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"peer": true
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "5.2.0",
|
||||
@@ -701,9 +733,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/styled-components": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.0.tgz",
|
||||
"integrity": "sha512-bPJKwZCHjJPf/hwTJl6TbkSZg/3evha+XPEizrZUGb535jLImwDUdjTNxXqjjaASt2M4qO4AVfoHJNe3XB/tpQ==",
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.3.tgz",
|
||||
"integrity": "sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw==",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/traverse": "^7.4.5",
|
||||
@@ -797,111 +829,119 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@authorizerdev/authorizer-js": {
|
||||
"version": "0.1.0-beta.22",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.1.0-beta.22.tgz",
|
||||
"integrity": "sha512-fHyZDL49eEsbxxIb2xxW6iLM+/N60m5e2NeVVlMFZjZFX9eUetdFatCOrPTuukGul7l8wO6YofTnwqOLOVjKrA==",
|
||||
"version": "0.4.0-beta.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.0.tgz",
|
||||
"integrity": "sha512-wNh5ROldNqdbOXFPDlq1tObzPZyEQkbnOvSEwvnDfPYb9/BsJ3naj3/ayz4J2R5k2+Eyuk0LK64XYdkfIW0HYA==",
|
||||
"requires": {
|
||||
"node-fetch": "^2.6.1"
|
||||
}
|
||||
},
|
||||
"@authorizerdev/authorizer-react": {
|
||||
"version": "0.1.0-beta.19",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.1.0-beta.19.tgz",
|
||||
"integrity": "sha512-ScAG3Auu0KirxuxpQ25JGtYCDmiC/3DM/ngnGO+XBaW+bWUARifH2HPkOnddUxkKPAfrKyxoa/l730W8mxhy+A==",
|
||||
"version": "0.9.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.2.tgz",
|
||||
"integrity": "sha512-clngw7MdFzvnns9rgrg9fHRH4p3K+HGGMju6qhdjDF+4vPruSu6HwBi1hRvVxLi1q7CZ25CEE7CfA7Vfq7H3Bw==",
|
||||
"requires": {
|
||||
"@authorizerdev/authorizer-js": "^0.1.0-beta.22",
|
||||
"@authorizerdev/authorizer-js": "^0.4.0-beta.0",
|
||||
"final-form": "^4.20.2",
|
||||
"react-final-form": "^6.5.3",
|
||||
"styled-components": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
|
||||
"integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
|
||||
"integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
|
||||
"requires": {
|
||||
"@babel/highlight": "^7.14.5"
|
||||
"@babel/highlight": "^7.16.7"
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.8.tgz",
|
||||
"integrity": "sha512-cYDUpvIzhBVnMzRoY1fkSEhK/HmwEVwlyULYgn/tMQYd6Obag3ylCjONle3gdErfXBW61SVTlR9QR7uWlgeIkg==",
|
||||
"version": "7.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz",
|
||||
"integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.8",
|
||||
"@babel/types": "^7.16.8",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-annotate-as-pure": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz",
|
||||
"integrity": "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz",
|
||||
"integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==",
|
||||
"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": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz",
|
||||
"integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz",
|
||||
"integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==",
|
||||
"requires": {
|
||||
"@babel/helper-get-function-arity": "^7.14.5",
|
||||
"@babel/template": "^7.14.5",
|
||||
"@babel/types": "^7.14.5"
|
||||
"@babel/helper-get-function-arity": "^7.16.7",
|
||||
"@babel/template": "^7.16.7",
|
||||
"@babel/types": "^7.16.7"
|
||||
}
|
||||
},
|
||||
"@babel/helper-get-function-arity": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz",
|
||||
"integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz",
|
||||
"integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.5"
|
||||
"@babel/types": "^7.16.7"
|
||||
}
|
||||
},
|
||||
"@babel/helper-hoist-variables": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz",
|
||||
"integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz",
|
||||
"integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.5"
|
||||
"@babel/types": "^7.16.7"
|
||||
}
|
||||
},
|
||||
"@babel/helper-module-imports": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz",
|
||||
"integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz",
|
||||
"integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.5"
|
||||
"@babel/types": "^7.16.7"
|
||||
}
|
||||
},
|
||||
"@babel/helper-split-export-declaration": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz",
|
||||
"integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz",
|
||||
"integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.5"
|
||||
"@babel/types": "^7.16.7"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.8.tgz",
|
||||
"integrity": "sha512-ZGy6/XQjllhYQrNw/3zfWRwZCTVSiBLZ9DHVZxn9n2gip/7ab8mv2TWlKPIBk26RwedCBoWdjLmn+t9na2Gcow=="
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
|
||||
"integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw=="
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz",
|
||||
"integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==",
|
||||
"version": "7.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz",
|
||||
"integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.5",
|
||||
"@babel/helper-validator-identifier": "^7.16.7",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.8.tgz",
|
||||
"integrity": "sha512-syoCQFOoo/fzkWDeM0dLEZi5xqurb5vuyzwIMNZRNun+N/9A4cUZeQaE7dTrB8jGaKuJRBtEOajtnmw0I5hvvA=="
|
||||
"version": "7.16.12",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.12.tgz",
|
||||
"integrity": "sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A=="
|
||||
},
|
||||
"@babel/runtime": {
|
||||
"version": "7.14.8",
|
||||
@@ -912,37 +952,38 @@
|
||||
}
|
||||
},
|
||||
"@babel/template": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz",
|
||||
"integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==",
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz",
|
||||
"integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==",
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.14.5",
|
||||
"@babel/parser": "^7.14.5",
|
||||
"@babel/types": "^7.14.5"
|
||||
"@babel/code-frame": "^7.16.7",
|
||||
"@babel/parser": "^7.16.7",
|
||||
"@babel/types": "^7.16.7"
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.8.tgz",
|
||||
"integrity": "sha512-kexHhzCljJcFNn1KYAQ6A5wxMRzq9ebYpEDV4+WdNyr3i7O44tanbDOR/xjiG2F3sllan+LgwK+7OMk0EmydHg==",
|
||||
"version": "7.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.10.tgz",
|
||||
"integrity": "sha512-yzuaYXoRJBGMlBhsMJoUW7G1UmSb/eXr/JHYM/MsOJgavJibLwASijW7oXBdw3NQ6T0bW7Ty5P/VarOs9cHmqw==",
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.14.5",
|
||||
"@babel/generator": "^7.14.8",
|
||||
"@babel/helper-function-name": "^7.14.5",
|
||||
"@babel/helper-hoist-variables": "^7.14.5",
|
||||
"@babel/helper-split-export-declaration": "^7.14.5",
|
||||
"@babel/parser": "^7.14.8",
|
||||
"@babel/types": "^7.14.8",
|
||||
"@babel/code-frame": "^7.16.7",
|
||||
"@babel/generator": "^7.16.8",
|
||||
"@babel/helper-environment-visitor": "^7.16.7",
|
||||
"@babel/helper-function-name": "^7.16.7",
|
||||
"@babel/helper-hoist-variables": "^7.16.7",
|
||||
"@babel/helper-split-export-declaration": "^7.16.7",
|
||||
"@babel/parser": "^7.16.10",
|
||||
"@babel/types": "^7.16.8",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.8.tgz",
|
||||
"integrity": "sha512-iob4soQa7dZw8nodR/KlOQkPh9S4I8RwCxwRIFuiMRYjOzH/KJzdUfDgz6cGi5dDaclXF4P2PAhCdrBJNIg68Q==",
|
||||
"version": "7.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.8.tgz",
|
||||
"integrity": "sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.8",
|
||||
"@babel/helper-validator-identifier": "^7.16.7",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
@@ -1033,12 +1074,12 @@
|
||||
}
|
||||
},
|
||||
"babel-plugin-styled-components": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.13.2.tgz",
|
||||
"integrity": "sha512-Vb1R3d4g+MUfPQPVDMCGjm3cDocJEUTR7Xq7QS95JWWeksN1wdFRYpD2kulDgI3Huuaf1CZd+NK4KQmqUFh5dA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.2.tgz",
|
||||
"integrity": "sha512-7eG5NE8rChnNTDxa6LQfynwgHTVOYYaHJbUYSlOhk8QBXIQiMBKq4gyfHBBKPrxUcVBXVJL61ihduCpCQbuNbw==",
|
||||
"requires": {
|
||||
"@babel/helper-annotate-as-pure": "^7.0.0",
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/helper-annotate-as-pure": "^7.16.0",
|
||||
"@babel/helper-module-imports": "^7.16.0",
|
||||
"babel-plugin-syntax-jsx": "^6.18.0",
|
||||
"lodash": "^4.17.11"
|
||||
}
|
||||
@@ -1097,9 +1138,9 @@
|
||||
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
|
||||
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
@@ -1115,9 +1156,9 @@
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||
},
|
||||
"final-form": {
|
||||
"version": "4.20.2",
|
||||
"resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.2.tgz",
|
||||
"integrity": "sha512-5i0IxqwjjPG1nUNCjWhqPCvQJJ2R+QwTwaAnjPmFnLbyjIHWuBPU8u+Ps4G3TcX2Sjno+O5xCZJzYcMJEzzfCQ==",
|
||||
"version": "4.20.6",
|
||||
"resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.6.tgz",
|
||||
"integrity": "sha512-fCdwIj49KOaFfDRlXB57Eo+GghIMZQWrA9TakQI3C9uQxHwaFHXqZSNRlUdfnQmNNeySwGOaGPZCvjy58hyv4w==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.10.0"
|
||||
}
|
||||
@@ -1203,9 +1244,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz",
|
||||
"integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==",
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"requires": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
}
|
||||
@@ -1224,9 +1265,9 @@
|
||||
}
|
||||
},
|
||||
"postcss-value-parser": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
|
||||
"integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ=="
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.7.2",
|
||||
@@ -1265,18 +1306,27 @@
|
||||
}
|
||||
},
|
||||
"react-final-form": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.3.tgz",
|
||||
"integrity": "sha512-FCs6GC0AMWJl2p6YX7kM+a0AvuSLAZUgbVNtRBskOs4g984t/It0qGtx51O+9vgqnqk6JyoxmIzxKMq+7ch/vg==",
|
||||
"version": "6.5.7",
|
||||
"resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.7.tgz",
|
||||
"integrity": "sha512-o7tvJXB+McGiXOILqIC8lnOcX4aLhIBiF/Xi9Qet35b7XOS8R7KL8HLRKTfnZWQJm6MCE15v1U0SFive0NcxyA==",
|
||||
"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": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"peer": true
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||
},
|
||||
"react-router": {
|
||||
"version": "5.2.0",
|
||||
@@ -1346,9 +1396,9 @@
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
|
||||
},
|
||||
"styled-components": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.0.tgz",
|
||||
"integrity": "sha512-bPJKwZCHjJPf/hwTJl6TbkSZg/3evha+XPEizrZUGb535jLImwDUdjTNxXqjjaASt2M4qO4AVfoHJNe3XB/tpQ==",
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.3.tgz",
|
||||
"integrity": "sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw==",
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/traverse": "^7.4.5",
|
||||
|
@@ -4,18 +4,20 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"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": [],
|
||||
"author": "Lakhan Samani",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@authorizerdev/authorizer-react": "^0.1.0-beta.19",
|
||||
"@authorizerdev/authorizer-react": "0.9.0-beta.2",
|
||||
"@types/react": "^17.0.15",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"esbuild": "^0.12.17",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-is": "^17.0.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
|
@@ -30,19 +30,11 @@ export default function App() {
|
||||
/>
|
||||
<h1>{globalState.organizationName}</h1>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 400,
|
||||
margin: `10px auto`,
|
||||
border: `1px solid #D1D5DB`,
|
||||
padding: `25px 20px`,
|
||||
borderRadius: 5,
|
||||
}}
|
||||
>
|
||||
<div className="container">
|
||||
<BrowserRouter>
|
||||
<AuthorizerProvider
|
||||
config={{
|
||||
authorizerURL: globalState.authorizerURL,
|
||||
authorizerURL: window.location.origin,
|
||||
redirectURL: globalState.redirectURL,
|
||||
}}
|
||||
>
|
||||
|
@@ -1,18 +1,29 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, lazy, Suspense } from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import { useAuthorizer } from '@authorizerdev/authorizer-react';
|
||||
import Dashboard from './pages/dashboard';
|
||||
import Login from './pages/login';
|
||||
import ResetPassword from './pages/rest-password';
|
||||
|
||||
const ResetPassword = lazy(() => import('./pages/rest-password'));
|
||||
const Login = lazy(() => import('./pages/login'));
|
||||
const Dashboard = lazy(() => import('./pages/dashboard'));
|
||||
|
||||
export default function Root() {
|
||||
const { token, loading, config } = useAuthorizer();
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
const url = new URL(config.redirectURL || '/app');
|
||||
console.log({ token });
|
||||
let redirectURL = config.redirectURL || '/app';
|
||||
const params = `access_token=${token.access_token}&id_token=${token.id_token}&expires_in=${token.expires_in}&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) {
|
||||
window.location.href = config.redirectURL || '/app';
|
||||
sessionStorage.removeItem('authorizer_state');
|
||||
window.location.replace(redirectURL);
|
||||
}
|
||||
}
|
||||
return () => {};
|
||||
@@ -24,15 +35,18 @@ export default function Root() {
|
||||
|
||||
if (token) {
|
||||
return (
|
||||
<Suspense fallback={<></>}>
|
||||
<Switch>
|
||||
<Route path="/app" exact>
|
||||
<Dashboard />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<></>}>
|
||||
<Switch>
|
||||
<Route path="/app" exact>
|
||||
<Login />
|
||||
@@ -41,5 +55,6 @@ export default function Root() {
|
||||
<ResetPassword />
|
||||
</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 ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
|
@@ -15,7 +15,7 @@ export default function Dashboard() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hey 👋,</h1>
|
||||
<p>Thank you for joining authorizer demo app.</p>
|
||||
<p>Thank you for using authorizer.</p>
|
||||
<p>
|
||||
Your email address is{' '}
|
||||
<a href={`mailto:${user?.email}`} style={{ color: '#3B82F6' }}>
|
||||
|
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;
|
218
dashboard/src/components/Menu.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
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 } from 'urql';
|
||||
import { NavLink, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthContext } from '../contexts/AuthContext';
|
||||
import { AdminLogout } from '../graphql/mutation';
|
||||
|
||||
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();
|
||||
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>
|
||||
</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
|
||||
}
|
||||
}
|
||||
`;
|
77
dashboard/src/graphql/queries/index.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
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'));
|
31
dashboard/src/layouts/AuthLayout.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Box, Center, Flex, Image, Text } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { LOGO_URL } from '../constants';
|
||||
|
||||
export function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
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>
|
||||
|
||||
<Box p="6" m="5" rounded="5" bg="white" w="500px" shadow="xl">
|
||||
{children}
|
||||
</Box>
|
||||
</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>
|
||||
);
|
||||
}
|
129
dashboard/src/pages/Auth.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
useToast,
|
||||
VStack,
|
||||
Text,
|
||||
Divider,
|
||||
} 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"]
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
VERSION="$1"
|
||||
make clean && CGO_ENABLED=1 VERSION=${VERSION} make
|
||||
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
|
||||
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
|
||||
|
@@ -1,45 +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{}
|
||||
AUTHORIZER_URL = ""
|
||||
PORT = "8080"
|
||||
REDIS_URL = ""
|
||||
IS_PROD = false
|
||||
COOKIE_NAME = ""
|
||||
RESET_PASSWORD_URL = ""
|
||||
DISABLE_EMAIL_VERIFICATION = "false"
|
||||
DISABLE_BASIC_AUTHENTICATION = "false"
|
||||
DISABLE_MAGIC_LOGIN = "false"
|
||||
|
||||
// ROLES
|
||||
ROLES = []string{}
|
||||
PROTECTED_ROLES = []string{}
|
||||
DEFAULT_ROLES = []string{}
|
||||
JWT_ROLE_CLAIM = "role"
|
||||
|
||||
// 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 = ""
|
||||
|
||||
// Org envs
|
||||
ORGANIZATION_NAME = "Authorizer"
|
||||
ORGANIZATION_LOGO = "https://authorizer.dev/images/logo.png"
|
||||
)
|
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
|
||||
|
||||
var (
|
||||
const (
|
||||
// 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"
|
||||
// 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="
|
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,64 +1,44 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/enum"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
"github.com/authorizerdev/authorizer/server/db/providers"
|
||||
"github.com/authorizerdev/authorizer/server/db/providers/arangodb"
|
||||
"github.com/authorizerdev/authorizer/server/db/providers/mongodb"
|
||||
"github.com/authorizerdev/authorizer/server/db/providers/sql"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
)
|
||||
|
||||
type Manager interface {
|
||||
SaveUser(user User) (User, error)
|
||||
UpdateUser(user User) (User, error)
|
||||
GetUsers() ([]User, error)
|
||||
GetUserByEmail(email string) (User, error)
|
||||
GetUserByID(email string) (User, error)
|
||||
UpdateVerificationTime(verifiedAt int64, id uuid.UUID) 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
|
||||
SaveRoles(roles []Role) error
|
||||
SaveSession(session Session) error
|
||||
}
|
||||
// Provider returns the current database provider
|
||||
var Provider providers.Provider
|
||||
|
||||
type manager struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
var Mgr Manager
|
||||
|
||||
func InitDB() {
|
||||
var db *gorm.DB
|
||||
func InitDB() error {
|
||||
var err error
|
||||
ormConfig := &gorm.Config{
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: "authorizer_",
|
||||
},
|
||||
}
|
||||
if constants.DATABASE_TYPE == enum.Postgres.String() {
|
||||
db, err = gorm.Open(postgres.Open(constants.DATABASE_URL), ormConfig)
|
||||
}
|
||||
if constants.DATABASE_TYPE == enum.Mysql.String() {
|
||||
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)
|
||||
}
|
||||
|
||||
isSQL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) != constants.DbTypeArangodb && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) != constants.DbTypeMongodb
|
||||
isArangoDB := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) == constants.DbTypeArangodb
|
||||
isMongoDB := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) == constants.DbTypeMongodb
|
||||
|
||||
if isSQL {
|
||||
Provider, err = sql.NewProvider()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to init db:", err)
|
||||
} else {
|
||||
db.AutoMigrate(&User{}, &VerificationRequest{}, &Role{}, &Session{})
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
Mgr = &manager{db: db}
|
||||
if isArangoDB {
|
||||
Provider, err = arangodb.NewProvider()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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:char(36)" 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"}),
|
||||
}).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,34 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type Role struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;"`
|
||||
Role string `gorm:"unique"`
|
||||
}
|
||||
|
||||
func (r *Role) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
r.ID = uuid.New()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SaveRoles function to save roles
|
||||
func (mgr *manager) SaveRoles(roles []Role) error {
|
||||
res := mgr.db.Clauses(
|
||||
clause.OnConflict{
|
||||
DoNothing: true,
|
||||
}).Create(&roles)
|
||||
if res.Error != nil {
|
||||
log.Println(`Error saving roles`)
|
||||
return res.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@@ -1,39 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;"`
|
||||
UserID uuid.UUID `gorm:"type:uuid;"`
|
||||
User User
|
||||
UserAgent string
|
||||
IP string
|
||||
CreatedAt int64 `gorm:"autoCreateTime"`
|
||||
UpdatedAt int64 `gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
func (r *Session) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
r.ID = uuid.New()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SaveSession function to save user sessiosn
|
||||
func (mgr *manager) SaveSession(session Session) error {
|
||||
res := mgr.db.Clauses(
|
||||
clause.OnConflict{
|
||||
DoNothing: true,
|
||||
}).Create(&session)
|
||||
if res.Error != nil {
|
||||
log.Println(`Error saving session`, res.Error)
|
||||
return res.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@@ -1,119 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;"`
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string `gorm:"unique"`
|
||||
Password string
|
||||
SignupMethod string
|
||||
EmailVerifiedAt int64
|
||||
CreatedAt int64 `gorm:"autoCreateTime"`
|
||||
UpdatedAt int64 `gorm:"autoUpdateTime"`
|
||||
Image string
|
||||
Roles string
|
||||
}
|
||||
|
||||
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
u.ID = uuid.New()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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) {
|
||||
user.UpdatedAt = time.Now().Unix()
|
||||
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
|
||||
}
|
||||
|
||||
// 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) GetUserByID(id string) (User, error) {
|
||||
var user User
|
||||
result := mgr.db.Where("id = ?", id).First(&user)
|
||||
|
||||
if result.Error != nil {
|
||||
return user, result.Error
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (mgr *manager) UpdateVerificationTime(verifiedAt int64, id uuid.UUID) 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,86 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type VerificationRequest struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;"`
|
||||
Token string `gorm:"index"`
|
||||
Identifier string
|
||||
ExpiresAt int64
|
||||
CreatedAt int64 `gorm:"autoCreateTime"`
|
||||
UpdatedAt int64 `gorm:"autoUpdateTime"`
|
||||
Email string `gorm:"unique"`
|
||||
}
|
||||
|
||||
func (v *VerificationRequest) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
v.ID = uuid.New()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
@@ -2,89 +2,52 @@ package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"mime/quotedprintable"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"strconv"
|
||||
"text/template"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
gomail "gopkg.in/mail.v2"
|
||||
)
|
||||
|
||||
/**
|
||||
Using: https://github.com/tangingw/go_smtp/blob/master/send_mail.go
|
||||
For gmail add instruction to enable less security
|
||||
// https://myaccount.google.com/u/0/lesssecureapps
|
||||
// https://www.google.com/settings/security/lesssecureapps
|
||||
// https://stackoverflow.com/questions/19877246/nodemailer-with-gmail-and-nodejs
|
||||
**/
|
||||
|
||||
// TODO -> try using gomail.v2
|
||||
|
||||
type Sender struct {
|
||||
User string
|
||||
Password string
|
||||
}
|
||||
|
||||
func NewSender() Sender {
|
||||
return Sender{User: constants.SENDER_EMAIL, Password: constants.SENDER_PASSWORD}
|
||||
}
|
||||
|
||||
func (sender Sender) SendMail(Dest []string, Subject, bodyMessage string) error {
|
||||
msg := "From: " + sender.User + "\n" +
|
||||
"To: " + strings.Join(Dest, ",") + "\n" +
|
||||
"Subject: " + Subject + "\n" + bodyMessage
|
||||
|
||||
err := smtp.SendMail(constants.SMTP_HOST+":"+constants.SMTP_PORT,
|
||||
smtp.PlainAuth("", sender.User, sender.Password, constants.SMTP_HOST),
|
||||
sender.User, Dest, []byte(msg))
|
||||
// addEmailTemplate is used to add html template in email body
|
||||
func addEmailTemplate(a string, b map[string]interface{}, templateName string) string {
|
||||
tmpl, err := template.New(templateName).Parse(a)
|
||||
if err != nil {
|
||||
output, _ := json.Marshal(b)
|
||||
return string(output)
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
err = tmpl.Execute(buf, b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s := buf.String()
|
||||
return s
|
||||
}
|
||||
|
||||
// SendMail function to send mail
|
||||
func SendMail(to []string, Subject, bodyMessage string) error {
|
||||
// dont trigger email sending in case of test
|
||||
if envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyEnv) == "test" {
|
||||
return nil
|
||||
}
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeySenderEmail))
|
||||
m.SetHeader("To", to...)
|
||||
m.SetHeader("Subject", Subject)
|
||||
m.SetBody("text/html", bodyMessage)
|
||||
port, _ := strconv.Atoi(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeySmtpPort))
|
||||
d := gomail.NewDialer(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeySmtpHost), port, envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeySmtpUsername), envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeySmtpPassword))
|
||||
if envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyEnv) == "development" {
|
||||
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
if err := d.DialAndSend(m); err != nil {
|
||||
log.Printf("smtp error: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sender Sender) WriteEmail(dest []string, contentType, subject, bodyMessage string) string {
|
||||
header := make(map[string]string)
|
||||
header["From"] = sender.User
|
||||
|
||||
receipient := ""
|
||||
|
||||
for _, user := range dest {
|
||||
receipient = receipient + user
|
||||
}
|
||||
|
||||
header["To"] = receipient
|
||||
header["Subject"] = subject
|
||||
header["MIME-Version"] = "1.0"
|
||||
header["Content-Type"] = fmt.Sprintf("%s; charset=\"utf-8\"", contentType)
|
||||
header["Content-Transfer-Encoding"] = "quoted-printable"
|
||||
header["Content-Disposition"] = "inline"
|
||||
|
||||
message := ""
|
||||
|
||||
for key, value := range header {
|
||||
message += fmt.Sprintf("%s: %s\r\n", key, value)
|
||||
}
|
||||
|
||||
var encodedMessage bytes.Buffer
|
||||
|
||||
finalMessage := quotedprintable.NewWriter(&encodedMessage)
|
||||
finalMessage.Write([]byte(bodyMessage))
|
||||
finalMessage.Close()
|
||||
|
||||
message += "\r\n" + encodedMessage.String()
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
func (sender *Sender) WriteHTMLEmail(dest []string, subject, bodyMessage string) string {
|
||||
return sender.WriteEmail(dest, "text/html", subject, bodyMessage)
|
||||
}
|
||||
|
||||
func (sender *Sender) WritePlainEmail(dest []string, subject, bodyMessage string) string {
|
||||
return sender.WriteEmail(dest, "text/plain", subject, bodyMessage)
|
||||
}
|
||||
|