Compare commits
228 Commits
fix/organi
...
0.25.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
360dd3c3bd | ||
![]() |
c6add0cca6 | ||
![]() |
7ac6252aac | ||
![]() |
5d2d1c342b | ||
![]() |
6da0a85936 | ||
![]() |
116972d725 | ||
![]() |
d1e1e287db | ||
![]() |
a7f04f8754 | ||
![]() |
69b56c9912 | ||
![]() |
98015708a2 | ||
![]() |
1b5a7b8fb0 | ||
![]() |
8b9bcdfdbe | ||
![]() |
ba429da05f | ||
![]() |
7c7bb42003 | ||
![]() |
eeff88c853 | ||
![]() |
cf8762b7a0 | ||
![]() |
c61c3024ec | ||
![]() |
7e3bd6a721 | ||
![]() |
1146468a03 | ||
![]() |
268b22ffb2 | ||
![]() |
43359f1dba | ||
![]() |
1941cf4299 | ||
![]() |
7b13034081 | ||
![]() |
7c16900618 | ||
![]() |
d722fe258d | ||
![]() |
99dc5ee572 | ||
![]() |
8bee421d0a | ||
![]() |
714b79e4ab | ||
![]() |
d886d780b4 | ||
![]() |
d7bb10fd21 | ||
![]() |
f5515bec28 | ||
![]() |
b35d86fd40 | ||
![]() |
a638f02014 | ||
![]() |
2c4bc9adb6 | ||
![]() |
5884802e60 | ||
![]() |
241f977b2a | ||
![]() |
ed855a274a | ||
![]() |
049ea64475 | ||
![]() |
5e4f34c889 | ||
![]() |
ab717d956a | ||
![]() |
6209c4d506 | ||
![]() |
2bc4c74930 | ||
![]() |
1efa419cdf | ||
![]() |
4ceb6db4ba | ||
![]() |
9edc8d0fb5 | ||
![]() |
da0fcb109b | ||
![]() |
3e51a7bd01 | ||
![]() |
28bed69b2e | ||
![]() |
0433d64737 | ||
![]() |
773213e5a4 | ||
![]() |
de44c40de5 | ||
![]() |
a7fa988bf0 | ||
![]() |
538a2d0b59 | ||
![]() |
f519f0eb0e | ||
![]() |
d5ad4a6e55 | ||
![]() |
d9b49ca932 | ||
![]() |
7c5aab7bf3 | ||
![]() |
c783e101d5 | ||
![]() |
ebccfb18cd | ||
![]() |
b7aeff57af | ||
![]() |
075c287f34 | ||
![]() |
4778827545 | ||
![]() |
39c2c364d9 | ||
![]() |
961f2271c1 | ||
![]() |
aaf0831793 | ||
![]() |
27cb41c54c | ||
![]() |
718b2d535f | ||
![]() |
ed6a1ceccc | ||
![]() |
fd52d6e5d3 | ||
![]() |
325aa88368 | ||
![]() |
75e44ff698 | ||
![]() |
d5f1c5a5eb | ||
![]() |
39947f1753 | ||
![]() |
4fa9f79c3f | ||
![]() |
fe73c2f6f8 | ||
![]() |
4a3e3633ea | ||
![]() |
dbbe36f6b5 | ||
![]() |
819dd57377 | ||
![]() |
044b025ba2 | ||
![]() |
41b5f00b83 | ||
![]() |
3c31b7fdc7 | ||
![]() |
7e91c6ca28 | ||
![]() |
b1b43a41ca | ||
![]() |
f969495178 | ||
![]() |
3c4c128931 | ||
![]() |
003cec4f48 | ||
![]() |
3e488155dc | ||
![]() |
4f4a3a91e1 | ||
![]() |
a3d9783aef | ||
![]() |
7d77396657 | ||
![]() |
7a18fc6312 | ||
![]() |
90e2709eeb | ||
![]() |
4c4743ac24 | ||
![]() |
b2541c8e9a | ||
![]() |
1f3dec6ea6 | ||
![]() |
a6b743465f | ||
![]() |
f356b4728d | ||
![]() |
ec4ef97766 | ||
![]() |
47d67bf3cd | ||
![]() |
0c54da1168 | ||
![]() |
d6f60ce464 | ||
![]() |
3aa888b14e | ||
![]() |
30be32a10b | ||
![]() |
69d781d6cf | ||
![]() |
e4d9c60971 | ||
![]() |
96edb43b67 | ||
![]() |
21fef67c7d | ||
![]() |
9f09823c8b | ||
![]() |
1a64149da7 | ||
![]() |
99b846811a | ||
![]() |
df7837f44d | ||
![]() |
d709f53c47 | ||
![]() |
a257b77501 | ||
![]() |
2213619ed5 | ||
![]() |
f65ea72944 | ||
![]() |
32f8c99a71 | ||
![]() |
8ec52a90f1 | ||
![]() |
2498958295 | ||
![]() |
2913fa0603 | ||
![]() |
e126bfddad | ||
![]() |
83001b859c | ||
![]() |
74a8024131 | ||
![]() |
5e6ee8d9b0 | ||
![]() |
3e7150f872 | ||
![]() |
9a19552f72 | ||
![]() |
ab01ff249d | ||
![]() |
1b387f7564 | ||
![]() |
8e79ab77b2 | ||
![]() |
2bf6b8f91d | ||
![]() |
776c0fba8b | ||
![]() |
dd64aa2e79 | ||
![]() |
157b13baa7 | ||
![]() |
d1e284116d | ||
![]() |
2f9725d8e1 | ||
![]() |
ee7aea7bee | ||
![]() |
5d73df0040 | ||
![]() |
60cd317e67 | ||
![]() |
f5bdc8db39 | ||
![]() |
9eca697a91 | ||
![]() |
7136ee924d | ||
![]() |
fd9eb7c733 | ||
![]() |
917eaeb2ed | ||
![]() |
3bb90acc9e | ||
![]() |
a69b8e290c | ||
![]() |
674eeeea4e | ||
![]() |
8c2bf6ee0d | ||
![]() |
57bc091499 | ||
![]() |
128a2a8f75 | ||
![]() |
7b09a8817c | ||
![]() |
1d61840c6d | ||
![]() |
4b25e8941c | ||
![]() |
136eda15bf | ||
![]() |
eea6349318 | ||
![]() |
513b5d2948 | ||
![]() |
e61dc2f08a | ||
![]() |
07552bc0b1 | ||
![]() |
0787a3b494 | ||
![]() |
2946428ab8 | ||
![]() |
5c7d32ec16 | ||
![]() |
f0f2e0b6c8 | ||
![]() |
5399ea8f32 | ||
![]() |
4830a7e9ac | ||
![]() |
df1c56bb1c | ||
![]() |
b68d9ce661 | ||
![]() |
145091dce1 | ||
![]() |
ad46210112 | ||
![]() |
4e19f73845 | ||
![]() |
332269ecf9 | ||
![]() |
dfa96f09a0 | ||
![]() |
5bf26f7385 | ||
![]() |
1b269dc6db | ||
![]() |
ce9a115a14 | ||
![]() |
f2f4c72aa6 | ||
![]() |
9970eb16c9 | ||
![]() |
23e53286bd | ||
![]() |
47acff05e2 | ||
![]() |
5572928619 | ||
![]() |
85b4cd6339 | ||
![]() |
f0d38ab260 | ||
![]() |
1276af43ef | ||
![]() |
66d42fc2bc | ||
![]() |
1f058f954d | ||
![]() |
8259fb515c | ||
![]() |
6c2a4c3256 | ||
![]() |
51532657d7 | ||
![]() |
fd56fac353 | ||
![]() |
260f533b41 | ||
![]() |
c09ca3b810 | ||
![]() |
f07fb50eff | ||
![]() |
ea62a20c80 | ||
![]() |
2bb0ded20e | ||
![]() |
fec43f55f2 | ||
![]() |
271e901398 | ||
![]() |
c6f01ce839 | ||
![]() |
2dd404c02c | ||
![]() |
2f29bbcee4 | ||
![]() |
63a8c82535 | ||
![]() |
1f81e45e79 | ||
![]() |
40dcf67de9 | ||
![]() |
32d8b7c038 | ||
![]() |
2a91f3e7d8 | ||
![]() |
36d9861517 | ||
![]() |
d577a21a9a | ||
![]() |
115607cb6b | ||
![]() |
adb969ec04 | ||
![]() |
4e48320cf1 | ||
![]() |
cfe035e96b | ||
![]() |
34a91f3195 | ||
![]() |
ea14cc1743 | ||
![]() |
9d7f5fd9db | ||
![]() |
0520056e43 | ||
![]() |
1821e27692 | ||
![]() |
388530a69c | ||
![]() |
1e32d790b3 | ||
![]() |
681ffc65f1 | ||
![]() |
6331ec7b7a | ||
![]() |
25c9ce03bd | ||
![]() |
ac416bfc7b | ||
![]() |
0049e1380b | ||
![]() |
9bd185a9c6 | ||
![]() |
82bc38923c | ||
![]() |
8df8010b22 | ||
![]() |
b42cc1549a | ||
![]() |
4bc9059b0f | ||
![]() |
87b1cac979 | ||
![]() |
7f18a3f634 | ||
![]() |
0511e737ae | ||
![]() |
003d88fb6c |
@@ -9,3 +9,4 @@ build
|
||||
data.db
|
||||
app/node_modules
|
||||
app/build
|
||||
certs/
|
||||
|
14
.env.sample
@@ -1,16 +1,4 @@
|
||||
ENV=production
|
||||
DATABASE_URL=data.db
|
||||
DATABASE_TYPE=sqlite
|
||||
ADMIN_SECRET=admin
|
||||
JWT_SECRET=random_string
|
||||
SENDER_EMAIL=info@authorizer.dev
|
||||
SMTP_USERNAME=username
|
||||
SMTP_PASSWORD=password
|
||||
SMTP_HOST=smtp.mailtrap.io
|
||||
SMTP_PORT=2525
|
||||
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;}"
|
9
.env.test
Normal file
@@ -0,0 +1,9 @@
|
||||
ENV=test
|
||||
DATABASE_URL=test.db
|
||||
DATABASE_TYPE=sqlite
|
||||
CUSTOM_ACCESS_TOKEN_SCRIPT="function(user,tokenPayload){var data = tokenPayload;data.extra = {'x-extra-id': user.id};return data;}"
|
||||
SMTP_HOST=smtp.mailtrap.io
|
||||
SMTP_PORT=2525
|
||||
SMTP_USERNAME=test
|
||||
SMTP_PASSWORD=test
|
||||
SENDER_EMAIL="info@authorizer.dev"
|
12
.github/CONTRIBUTING.md
vendored
@@ -43,12 +43,14 @@ 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
|
||||
|
||||
|
15
.github/workflows/release.yaml
vendored
@@ -1,4 +1,19 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
logLevel:
|
||||
description: 'Log level'
|
||||
required: true
|
||||
default: 'warning'
|
||||
type: choice
|
||||
options:
|
||||
- info
|
||||
- warning
|
||||
- debug
|
||||
tags:
|
||||
description: 'Tags'
|
||||
required: false
|
||||
type: boolean
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
|
7
.gitignore
vendored
@@ -8,6 +8,11 @@ dashboard/build
|
||||
build
|
||||
.env
|
||||
data.db
|
||||
test.db
|
||||
.DS_Store
|
||||
.env.local
|
||||
*.tar.gz
|
||||
*.tar.gz
|
||||
.vscode/
|
||||
.yalc
|
||||
yalc.lock
|
||||
certs/
|
@@ -24,7 +24,9 @@ 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
|
||||
EXPOSE 8080
|
||||
|
5
Makefile
@@ -10,4 +10,7 @@ build-dashboard:
|
||||
clean:
|
||||
rm -rf build
|
||||
test:
|
||||
cd server && go clean --testcache && go test -v ./test
|
||||
rm -rf server/test/test.db && rm -rf test.db && cd server && go clean --testcache && go test -p 1 -v ./test
|
||||
generate:
|
||||
cd server && go get github.com/99designs/gqlgen/cmd@v0.14.0 && go run github.com/99designs/gqlgen generate
|
||||
|
97
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 database (Currently supports [Postgres](https://www.postgresql.org/), [MySQL](https://www.mysql.com/), [SQLite](https://www.sqlite.org/index.html), [SQLServer](https://www.microsoft.com/en-us/sql-server/), [MongoDB](https://mongodb.com/),[ArangoDB](https://www.arangodb.com/)).
|
||||
**Authorizer** is an open-source authentication and authorization solution for your applications. Bring your database and have complete control over the user information. You can self-host authorizer instances and connect to any 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
|
||||
|
||||
@@ -26,18 +26,16 @@
|
||||
- ✅ Sign-in / Sign-up with email ID and password
|
||||
- ✅ Secure session management
|
||||
- ✅ Email verification
|
||||
- ✅ OAuth2 and OpenID compatible APIs
|
||||
- ✅ APIs to update profile securely
|
||||
- ✅ Forgot password flow using email
|
||||
- ✅ Social logins (Google, Github, Facebook, more coming soon)
|
||||
- ✅ Role-based access management
|
||||
- ✅ Password-less login with email and magic link
|
||||
- ✅ Password-less login with magic link login
|
||||
|
||||
## Roadmap
|
||||
|
||||
- Support more JWT encryption algorithms (Currently supporting HS256)
|
||||
- 2 Factor authentication
|
||||
- Back office (Admin dashboard to manage user)
|
||||
- Support more database
|
||||
- VueJS SDK
|
||||
- Svelte SDK
|
||||
- React Native SDK
|
||||
@@ -59,33 +57,42 @@
|
||||
|
||||
# Getting Started
|
||||
|
||||
## Trying out Authorizer
|
||||
## Step 1: Get Authorizer Instance
|
||||
|
||||
### Deploy Production Ready Instance
|
||||
|
||||
Deploy production ready Authorizer instance using one click deployment options available below
|
||||
|
||||
| **Infra provider** | **One-click link** | **Additional information** |
|
||||
| :----------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------: |
|
||||
| Railway.app | <a href="https://railway.app/new/template?template=https://github.com/authorizerdev/authorizer-railway&plugins=postgresql,redis"><img src="https://railway.app/button.svg" style="height: 44px" alt="Deploy on Railway"></a> | [docs](https://docs.authorizer.dev/deployment/railway) |
|
||||
| Heroku | <a href="https://heroku.com/deploy?template=https://github.com/authorizerdev/authorizer-heroku"><img src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy to Heroku" style="height: 44px;"></a> | [docs](https://docs.authorizer.dev/deployment/heroku) |
|
||||
| Render | [](https://render.com/deploy?repo=https://github.com/authorizerdev/authorizer-render) | [docs](https://docs.authorizer.dev/deployment/render) |
|
||||
|
||||
### Deploy Authorizer Using Source Code
|
||||
|
||||
This guide helps you practice using Authorizer to evaluate it before you use it in a production environment. It includes instructions for installing the Authorizer server in local or standalone mode.
|
||||
|
||||
- [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
|
||||
|
||||
## Install using source code
|
||||
|
||||
### Prerequisites
|
||||
#### Prerequisites
|
||||
|
||||
- OS: Linux or macOS or windows
|
||||
- Go: (Golang)(https://golang.org/dl/) >= v1.15
|
||||
|
||||
### Project Setup
|
||||
#### 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`
|
||||
4. Create Env file `cp .env.sample .env`. Check all the supported env [here](https://docs.authorizer.dev/core/env/)
|
||||
5. Build Dashboard `make build-dashboard`
|
||||
6. Build App `make build-app`
|
||||
7. 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`
|
||||
8. Run binary `./build/server`
|
||||
|
||||
## Install using binaries
|
||||
### Deploy Authorizer using binaries
|
||||
|
||||
Deploy / Try Authorizer using binaries. With each [Authorizer Release](https://github.com/authorizerdev/authorizer/releases)
|
||||
binaries are baked with required deployment files and bundled. You can download a specific version of it for the following operating systems:
|
||||
@@ -93,7 +100,7 @@ binaries are baked with required deployment files and bundled. You can download
|
||||
- Mac OSX
|
||||
- Linux
|
||||
|
||||
### Step 1: Download and unzip bundle
|
||||
#### Download and unzip bundle
|
||||
|
||||
- Download the Bundle for the specific OS from the [release page](https://github.com/authorizerdev/authorizer/releases)
|
||||
|
||||
@@ -113,11 +120,7 @@ binaries are baked with required deployment files and bundled. You can download
|
||||
cd authorizer
|
||||
```
|
||||
|
||||
### Step 2: Configure environment variables
|
||||
|
||||
Required environment variables are pre-configured in `.env` file. But based on the production requirements, please configure more environment variables. You can refer to [environment variables docs](/core/env) for more information.
|
||||
|
||||
### Step 3: Start Authorizer
|
||||
#### Step 3: Start Authorizer
|
||||
|
||||
- Run following command to start authorizer
|
||||
|
||||
@@ -129,25 +132,20 @@ 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
|
||||
## Step 2: Setup Instance
|
||||
|
||||
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)
|
||||
- Open authorizer instance endpoint in browser
|
||||
- Sign up as an admin with a secure password
|
||||
- Configure environment variables from authorizer dashboard. Check env [docs](/core/env) for more information
|
||||
|
||||
# Install instance on railway
|
||||
|
||||
Deploy production ready Authorizer instance using [railway.app](https://github.com/authorizerdev/authorizer-railway) with postgres and redis for free and build with it in 30seconds
|
||||
<br/>
|
||||
|
||||
[](https://railway.app/new/template?template=https%3A%2F%2Fgithub.com%2Fauthorizerdev%2Fauthorizer-railway&plugins=postgresql%2Credis&envs=ENV%2CDATABASE_TYPE%2CADMIN_SECRET%2CCOOKIE_NAME%2CJWT_ROLE_CLAIM%2CJWT_TYPE%2CJWT_SECRET%2CFACEBOOK_CLIENT_ID%2CFACEBOOK_CLIENT_SECRET%2CGOOGLE_CLIENT_ID%2CGOOGLE_CLIENT_SECRET%2CGITHUB_CLIENT_ID%2CGITHUB_CLIENT_SECRET%2CALLOWED_ORIGINS%2CROLES%2CPROTECTED_ROLES%2CDEFAULT_ROLES&optionalEnvs=FACEBOOK_CLIENT_ID%2CFACEBOOK_CLIENT_SECRET%2CGOOGLE_CLIENT_ID%2CGOOGLE_CLIENT_SECRET%2CGITHUB_CLIENT_ID%2CGITHUB_CLIENT_SECRET%2CALLOWED_ORIGINS%2CROLES%2CPROTECTED_ROLES%2CDEFAULT_ROLES&ENVDesc=Deployment+environment&DATABASE_TYPEDesc=With+railway+we+are+deploying+postgres+db&ADMIN_SECRETDesc=Secret+to+access+the+admin+apis&COOKIE_NAMEDesc=Name+of+http+only+cookie+that+will+be+used+as+session&FACEBOOK_CLIENT_IDDesc=Facebook+client+ID+for+facebook+login&FACEBOOK_CLIENT_SECRETDesc=Facebook+client+secret+for+facebook+login&GOOGLE_CLIENT_IDDesc=Google+client+ID+for+google+login&GOOGLE_CLIENT_SECRETDesc=Google+client+secret+for+google+login&GITHUB_CLIENT_IDDesc=Github+client+ID+for+github+login&GITHUB_CLIENT_SECRETDesc=Github+client+secret+for+github+login&ALLOWED_ORIGINSDesc=Whitelist+the+URL+for+which+this+instance+of+authorizer+is+allowed&ROLESDesc=Comma+separated+list+of+roles+that+platform+supports.+Default+role+is+user&PROTECTED_ROLESDesc=Comma+separated+list+of+protected+roles+for+which+sign-up+is+disabled&DEFAULT_ROLESDesc=Default+role+that+should+be+assigned+to+user.+It+should+be+one+from+the+list+of+%60ROLES%60+env.+Default+role+is+user&JWT_ROLE_CLAIMDesc=JWT+key+to+be+used+to+validate+the+role+field.&JWT_TYPEDesc=JWT+encryption+type&JWT_SECRETDesc=Random+string+that+will+be+used+for+encrypting+the+JWT+token&ENVDefault=PRODUCTION&DATABASE_TYPEDefault=postgres&COOKIE_NAMEDefault=authorizer&JWT_TYPEDefault=HS256&JWT_ROLE_CLAIMDefault=role)
|
||||
> Note: `DATABASE_URL`, `DATABASE_TYPE` and `DATABASE_NAME` are only configurable via platform envs
|
||||
|
||||
### 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 (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.
|
||||
- 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
|
||||
|
||||
@@ -166,8 +164,9 @@ This example demonstrates how you can use [`@authorizerdev/authorizer-js`](/auth
|
||||
|
||||
<script type="text/javascript">
|
||||
const authorizerRef = new authorizerdev.Authorizer({
|
||||
authorizerURL: `AUTHORIZER_URL`,
|
||||
authorizerURL: `YOUR_AUTHORIZER_INSTANCE_URL`,
|
||||
redirectURL: window.location.origin,
|
||||
clientID: 'YOUR_CLIENT_ID', // obtain your client id from authorizer dashboard
|
||||
});
|
||||
|
||||
// use the button selector as per your application
|
||||
@@ -178,15 +177,19 @@ This example demonstrates how you can use [`@authorizerdev/authorizer-js`](/auth
|
||||
});
|
||||
|
||||
async function onLoad() {
|
||||
const res = await authorizerRef.browserLogin();
|
||||
if (res && res.user) {
|
||||
const res = await authorizerRef.authorize({
|
||||
response_type: 'code',
|
||||
use_refresh_token: false,
|
||||
});
|
||||
if (res && res.access_token) {
|
||||
// you can use user information here, eg:
|
||||
/**
|
||||
const userSection = document.getElementById('user');
|
||||
const logoutSection = document.getElementById('logout-section');
|
||||
logoutSection.classList.toggle('hide');
|
||||
userSection.innerHTML = `Welcome, ${res.user.email}`;
|
||||
*/
|
||||
const user = await authorizerRef.getProfile({
|
||||
Authorization: `Bearer ${res.access_token}`,
|
||||
});
|
||||
const userSection = document.getElementById('user');
|
||||
const logoutSection = document.getElementById('logout-section');
|
||||
logoutSection.classList.toggle('hide');
|
||||
userSection.innerHTML = `Welcome, ${user.email}`;
|
||||
}
|
||||
}
|
||||
onLoad();
|
||||
|
11
TODO.md
@@ -1,5 +1,16 @@
|
||||
# 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
|
||||
|
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 |
928
app/package-lock.json
generated
@@ -1,23 +1,870 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "app",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@authorizerdev/authorizer-react": "^0.17.0",
|
||||
"@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",
|
||||
"styled-components": "^5.3.0",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/styled-components": "^5.1.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@authorizerdev/authorizer-js": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.10.0.tgz",
|
||||
"integrity": "sha512-REM8FLD/Ej9gzA2zDGDAke6QFss33ubePlTDmLDmIYUuQmpHFlO5mCCS6nVsKkN7F/Bcwkmp+eUNQjkdGCaKLg==",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@authorizerdev/authorizer-react": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.17.0.tgz",
|
||||
"integrity": "sha512-7WcNCU7hDFkVfFb8LcJXFwWiLYd8aY78z1AbNPxCa2Cw5G85PaRkzjKybP6h01ITVOHO6M03lLwPj8p6Sr6fEg==",
|
||||
"dependencies": {
|
||||
"@authorizerdev/authorizer-js": "^0.10.0",
|
||||
"final-form": "^4.20.2",
|
||||
"react-final-form": "^6.5.3",
|
||||
"styled-components": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"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.16.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"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.16.8",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-annotate-as-pure": {
|
||||
"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.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.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.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.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.16.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-hoist-variables": {
|
||||
"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.16.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"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.16.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-split-export-declaration": {
|
||||
"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.16.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"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.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.16.7",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz",
|
||||
"integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"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.16.7",
|
||||
"@babel/parser": "^7.16.7",
|
||||
"@babel/types": "^7.16.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"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.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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"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.16.7",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/is-prop-valid": {
|
||||
"version": "0.8.8",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
|
||||
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
|
||||
"dependencies": {
|
||||
"@emotion/memoize": "0.7.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/memoize": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
|
||||
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
|
||||
},
|
||||
"node_modules/@emotion/stylis": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
|
||||
"integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
|
||||
},
|
||||
"node_modules/@emotion/unitless": {
|
||||
"version": "0.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
|
||||
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
|
||||
},
|
||||
"node_modules/@types/history": {
|
||||
"version": "4.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz",
|
||||
"integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/hoist-non-react-statics": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
||||
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
|
||||
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ=="
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "17.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.15.tgz",
|
||||
"integrity": "sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw==",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "17.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz",
|
||||
"integrity": "sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-router": {
|
||||
"version": "5.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.16.tgz",
|
||||
"integrity": "sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/history": "*",
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-router-dom": {
|
||||
"version": "5.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.8.tgz",
|
||||
"integrity": "sha512-03xHyncBzG0PmDmf8pf3rehtjY0NpUj7TIN46FrT5n1ZWHPZvXz32gUyNboJ+xsL8cpg8bQVLcllptcQHvocrw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/history": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-router": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/scheduler": {
|
||||
"version": "0.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
|
||||
},
|
||||
"node_modules/@types/styled-components": {
|
||||
"version": "5.1.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.25.tgz",
|
||||
"integrity": "sha512-fgwl+0Pa8pdkwXRoVPP9JbqF0Ivo9llnmsm+7TCI330kbPIFd9qv1Lrhr37shf4tnxCOSu+/IgqM7uJXLWZZNQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/hoist-non-react-statics": "*",
|
||||
"@types/react": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-styled-components": {
|
||||
"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.16.0",
|
||||
"@babel/helper-module-imports": "^7.16.0",
|
||||
"babel-plugin-syntax-jsx": "^6.18.0",
|
||||
"lodash": "^4.17.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"styled-components": ">= 2"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-syntax-jsx": {
|
||||
"version": "6.18.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
|
||||
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
|
||||
},
|
||||
"node_modules/camelize": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
|
||||
"integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dependencies": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
},
|
||||
"node_modules/css-color-keywords": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||
"integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/css-to-react-native": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz",
|
||||
"integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==",
|
||||
"dependencies": {
|
||||
"camelize": "^1.0.0",
|
||||
"css-color-keywords": "^1.0.0",
|
||||
"postcss-value-parser": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
|
||||
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.12.17",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.17.tgz",
|
||||
"integrity": "sha512-GshKJyVYUnlSXIZj/NheC2O0Kblh42CS7P1wJyTbbIHevTG4jYMS9NNw8EOd8dDWD0dzydYHS01MpZoUcQXB4g==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/final-form": {
|
||||
"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"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/final-form"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/history": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"loose-envify": "^1.2.0",
|
||||
"resolve-pathname": "^3.0.0",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0",
|
||||
"value-equal": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"dependencies": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hoist-non-react-statics/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mini-create-react-context": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
|
||||
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.1",
|
||||
"tiny-warning": "^1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prop-types": "^15.0.0",
|
||||
"react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"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": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
|
||||
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
|
||||
"dependencies": {
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-value-parser": {
|
||||
"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",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
|
||||
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
|
||||
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"scheduler": "^0.20.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "17.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-final-form": {
|
||||
"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.15.4"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/final-form"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"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=="
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
|
||||
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"history": "^4.9.0",
|
||||
"hoist-non-react-statics": "^3.1.0",
|
||||
"loose-envify": "^1.3.1",
|
||||
"mini-create-react-context": "^0.4.0",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-is": "^16.6.0",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=15"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
|
||||
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"history": "^4.9.0",
|
||||
"loose-envify": "^1.3.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-router": "5.2.0",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=15"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.9",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
|
||||
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
|
||||
},
|
||||
"node_modules/resolve-pathname": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
|
||||
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
|
||||
"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/shallowequal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/styled-components": {
|
||||
"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",
|
||||
"@emotion/is-prop-valid": "^0.8.8",
|
||||
"@emotion/stylis": "^0.8.4",
|
||||
"@emotion/unitless": "^0.7.4",
|
||||
"babel-plugin-styled-components": ">= 1.12.0",
|
||||
"css-to-react-native": "^3.0.0",
|
||||
"hoist-non-react-statics": "^3.0.0",
|
||||
"shallowequal": "^1.1.0",
|
||||
"supports-color": "^5.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/styled-components"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0",
|
||||
"react-dom": ">= 16.8.0",
|
||||
"react-is": ">= 16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
|
||||
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
|
||||
},
|
||||
"node_modules/tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
|
||||
},
|
||||
"node_modules/to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
|
||||
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/value-equal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
||||
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@authorizerdev/authorizer-js": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.2.0.tgz",
|
||||
"integrity": "sha512-T2gurEYEP1T56j9IwDIWP1PsELoWcU7TBl5G0UsMqFQhKo7T6p2hM634Z7PS1yLegU3aihuvHGI0C0n4xSo0VQ==",
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.10.0.tgz",
|
||||
"integrity": "sha512-REM8FLD/Ej9gzA2zDGDAke6QFss33ubePlTDmLDmIYUuQmpHFlO5mCCS6nVsKkN7F/Bcwkmp+eUNQjkdGCaKLg==",
|
||||
"requires": {
|
||||
"node-fetch": "^2.6.1"
|
||||
}
|
||||
},
|
||||
"@authorizerdev/authorizer-react": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.4.0.tgz",
|
||||
"integrity": "sha512-ydE7xrP3cqeTtU923bK0+OIB1fnL0VHnbThcNa41n89XUPV3VBhZ23gxMg90nqon8JFE5g2TNz7+/qBCOQ7aZw==",
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.17.0.tgz",
|
||||
"integrity": "sha512-7WcNCU7hDFkVfFb8LcJXFwWiLYd8aY78z1AbNPxCa2Cw5G85PaRkzjKybP6h01ITVOHO6M03lLwPj8p6Sr6fEg==",
|
||||
"requires": {
|
||||
"@authorizerdev/authorizer-js": "^0.2.0",
|
||||
"@authorizerdev/authorizer-js": "^0.10.0",
|
||||
"final-form": "^4.20.2",
|
||||
"react-final-form": "^6.5.3",
|
||||
"styled-components": "^5.3.0"
|
||||
@@ -32,11 +879,11 @@
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.7.tgz",
|
||||
"integrity": "sha512-/ST3Sg8MLGY5HVYmrjOgL60ENux/HfO/CsUh7y4MalThufhE/Ff/6EibFDHi4jiDCaWfJKoqbE6oTh21c5hrRg==",
|
||||
"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.16.7",
|
||||
"@babel/types": "^7.16.8",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
@@ -105,9 +952,9 @@
|
||||
"integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw=="
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.7.tgz",
|
||||
"integrity": "sha512-aKpPMfLvGO3Q97V0qhw/V2SWNWlwfJknuwAunU7wZLSfrM4xTBvg7E5opUVi1kJTBKihE38CPg4nBiqX83PWYw==",
|
||||
"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.16.7",
|
||||
"chalk": "^2.0.0",
|
||||
@@ -115,9 +962,9 @@
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.7.tgz",
|
||||
"integrity": "sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA=="
|
||||
"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",
|
||||
@@ -138,26 +985,26 @@
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.7.tgz",
|
||||
"integrity": "sha512-8KWJPIb8c2VvY8AJrydh6+fVRo2ODx1wYBU2398xJVq0JomuLBZmVQzLPBblJgHIGYG4znCpUZUZ0Pt2vdmVYQ==",
|
||||
"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.16.7",
|
||||
"@babel/generator": "^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.7",
|
||||
"@babel/types": "^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.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.7.tgz",
|
||||
"integrity": "sha512-E8HuV7FO9qLpx6OtoGfUQ2cjIYnbFwvZWYBS+87EwtdMvmUPJSwykpovFB+8insbpF0uJcpr8KMUi64XZntZcg==",
|
||||
"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.16.7",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
@@ -192,6 +1039,16 @@
|
||||
"integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/hoist-non-react-statics": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
||||
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"@types/prop-types": {
|
||||
"version": "15.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
|
||||
@@ -241,6 +1098,17 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
|
||||
},
|
||||
"@types/styled-components": {
|
||||
"version": "5.1.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.25.tgz",
|
||||
"integrity": "sha512-fgwl+0Pa8pdkwXRoVPP9JbqF0Ivo9llnmsm+7TCI330kbPIFd9qv1Lrhr37shf4tnxCOSu+/IgqM7uJXLWZZNQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/hoist-non-react-statics": "*",
|
||||
"@types/react": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
@@ -420,9 +1288,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
|
||||
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
|
||||
"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"
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@
|
||||
"author": "Lakhan Samani",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@authorizerdev/authorizer-react": "latest",
|
||||
"@authorizerdev/authorizer-react": "^0.17.0",
|
||||
"@types/react": "^17.0.15",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"esbuild": "^0.12.17",
|
||||
@@ -19,9 +19,11 @@
|
||||
"react-dom": "^17.0.2",
|
||||
"react-is": "^17.0.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"typescript": "^4.3.5"
|
||||
"typescript": "^4.3.5",
|
||||
"styled-components": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-router-dom": "^5.1.8"
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/styled-components": "^5.1.11"
|
||||
}
|
||||
}
|
||||
|
@@ -2,10 +2,33 @@ import React from 'react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { AuthorizerProvider } from '@authorizerdev/authorizer-react';
|
||||
import Root from './Root';
|
||||
import { createRandomString } from './utils/common';
|
||||
|
||||
export default function App() {
|
||||
// @ts-ignore
|
||||
const globalState: Record<string, string> = window['__authorizer__'];
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const state = searchParams.get('state') || createRandomString();
|
||||
const scope = searchParams.get('scope')
|
||||
? searchParams.get('scope')?.toString().split(' ')
|
||||
: `openid profile email`;
|
||||
|
||||
const urlProps: Record<string, any> = {
|
||||
state,
|
||||
scope,
|
||||
};
|
||||
|
||||
const redirectURL =
|
||||
searchParams.get('redirect_uri') || searchParams.get('redirectURL');
|
||||
if (redirectURL) {
|
||||
urlProps.redirectURL = redirectURL;
|
||||
} else {
|
||||
urlProps.redirectURL = window.location.origin + '/app';
|
||||
}
|
||||
const globalState: Record<string, string> = {
|
||||
// @ts-ignore
|
||||
...window['__authorizer__'],
|
||||
...urlProps,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -30,15 +53,7 @@ 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={{
|
||||
@@ -46,7 +61,7 @@ export default function App() {
|
||||
redirectURL: globalState.redirectURL,
|
||||
}}
|
||||
>
|
||||
<Root />
|
||||
<Root globalState={globalState} />
|
||||
</AuthorizerProvider>
|
||||
</BrowserRouter>
|
||||
</div>
|
||||
|
@@ -1,19 +1,76 @@
|
||||
import React, { useEffect, lazy, Suspense } from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import { useAuthorizer } from '@authorizerdev/authorizer-react';
|
||||
import styled, { ThemeProvider } from 'styled-components';
|
||||
import SetupPassword from './pages/setup-password';
|
||||
import { hasWindow, createRandomString } from './utils/common';
|
||||
import { theme } from './theme';
|
||||
|
||||
const ResetPassword = lazy(() => import('./pages/rest-password'));
|
||||
const Login = lazy(() => import('./pages/login'));
|
||||
const Dashboard = lazy(() => import('./pages/dashboard'));
|
||||
const SignUp = lazy(() => import('./pages/signup'));
|
||||
|
||||
export default function Root() {
|
||||
const Wrapper = styled.div`
|
||||
font-family: ${(props) => props.theme.fonts.fontStack};
|
||||
color: ${(props) => props.theme.colors.textColor};
|
||||
font-size: ${(props) => props.theme.fonts.mediumText};
|
||||
box-sizing: border-box;
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Root({
|
||||
globalState,
|
||||
}: {
|
||||
globalState: Record<string, string>;
|
||||
}) {
|
||||
const { token, loading, config } = useAuthorizer();
|
||||
|
||||
const searchParams = new URLSearchParams(
|
||||
hasWindow() ? window.location.search : ``
|
||||
);
|
||||
const state = searchParams.get('state') || createRandomString();
|
||||
const scope = searchParams.get('scope')
|
||||
? searchParams.get('scope')?.toString().split(' ')
|
||||
: ['openid', 'profile', 'email'];
|
||||
|
||||
const urlProps: Record<string, any> = {
|
||||
state,
|
||||
scope,
|
||||
};
|
||||
|
||||
const redirectURL =
|
||||
searchParams.get('redirect_uri') || searchParams.get('redirectURL');
|
||||
if (redirectURL) {
|
||||
urlProps.redirectURL = redirectURL;
|
||||
} else {
|
||||
urlProps.redirectURL = hasWindow() ? window.location.origin : redirectURL;
|
||||
}
|
||||
|
||||
urlProps.redirect_uri = urlProps.redirectURL;
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
const url = new URL(config.redirectURL || '/app');
|
||||
let redirectURL = config.redirectURL || '/app';
|
||||
let params = `access_token=${token.access_token}&id_token=${token.id_token}&expires_in=${token.expires_in}&state=${globalState.state}`;
|
||||
if (token.refresh_token) {
|
||||
params += `&refresh_token=${token.refresh_token}`;
|
||||
}
|
||||
const url = new URL(redirectURL);
|
||||
if (redirectURL.includes('?')) {
|
||||
redirectURL = `${redirectURL}&${params}`;
|
||||
} else {
|
||||
redirectURL = `${redirectURL}?${params}`;
|
||||
}
|
||||
|
||||
if (url.origin !== window.location.origin) {
|
||||
window.location.href = config.redirectURL || '/app';
|
||||
sessionStorage.removeItem('authorizer_state');
|
||||
window.location.replace(redirectURL);
|
||||
}
|
||||
}
|
||||
return () => {};
|
||||
@@ -37,14 +94,24 @@ export default function Root() {
|
||||
|
||||
return (
|
||||
<Suspense fallback={<></>}>
|
||||
<Switch>
|
||||
<Route path="/app" exact>
|
||||
<Login />
|
||||
</Route>
|
||||
<Route path="/app/reset-password">
|
||||
<ResetPassword />
|
||||
</Route>
|
||||
</Switch>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Wrapper>
|
||||
<Switch>
|
||||
<Route path="/app" exact>
|
||||
<Login urlProps={urlProps} />
|
||||
</Route>
|
||||
<Route path="/app/signup" exact>
|
||||
<SignUp urlProps={urlProps} />
|
||||
</Route>
|
||||
<Route path="/app/reset-password">
|
||||
<ResetPassword />
|
||||
</Route>
|
||||
<Route path="/app/setup-password">
|
||||
<SetupPassword />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Wrapper>
|
||||
</ThemeProvider>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
body {
|
||||
margin: 0;
|
||||
margin: 10;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
@@ -14,3 +14,17 @@ body {
|
||||
*: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;
|
||||
}
|
||||
}
|
||||
|
@@ -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' }}>
|
||||
|
@@ -1,10 +1,84 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { Authorizer } from '@authorizerdev/authorizer-react';
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import {
|
||||
AuthorizerBasicAuthLogin,
|
||||
AuthorizerForgotPassword,
|
||||
AuthorizerMagicLinkLogin,
|
||||
AuthorizerSocialLogin,
|
||||
useAuthorizer,
|
||||
} from '@authorizerdev/authorizer-react';
|
||||
import styled from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function Login() {
|
||||
const enum VIEW_TYPES {
|
||||
LOGIN = 'login',
|
||||
FORGOT_PASSWORD = 'forgot-password',
|
||||
}
|
||||
|
||||
const Footer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
`;
|
||||
|
||||
const FooterContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
`;
|
||||
|
||||
export default function Login({ urlProps }: { urlProps: Record<string, any> }) {
|
||||
const { config } = useAuthorizer();
|
||||
const [view, setView] = useState<VIEW_TYPES>(VIEW_TYPES.LOGIN);
|
||||
return (
|
||||
<Fragment>
|
||||
<Authorizer />
|
||||
{view === VIEW_TYPES.LOGIN && (
|
||||
<Fragment>
|
||||
<h1 style={{ textAlign: 'center' }}>Login</h1>
|
||||
<br />
|
||||
<AuthorizerSocialLogin urlProps={urlProps} />
|
||||
{config.is_basic_authentication_enabled &&
|
||||
!config.is_magic_link_login_enabled && (
|
||||
<AuthorizerBasicAuthLogin urlProps={urlProps} />
|
||||
)}
|
||||
{config.is_magic_link_login_enabled && (
|
||||
<AuthorizerMagicLinkLogin urlProps={urlProps} />
|
||||
)}
|
||||
<Footer>
|
||||
<Link
|
||||
to="#"
|
||||
onClick={() => setView(VIEW_TYPES.FORGOT_PASSWORD)}
|
||||
style={{ marginBottom: 10 }}
|
||||
>
|
||||
Forgot Password?
|
||||
</Link>
|
||||
</Footer>
|
||||
</Fragment>
|
||||
)}
|
||||
{view === VIEW_TYPES.FORGOT_PASSWORD && (
|
||||
<Fragment>
|
||||
<h1 style={{ textAlign: 'center' }}>Forgot Password</h1>
|
||||
<AuthorizerForgotPassword urlProps={urlProps} />
|
||||
<Footer>
|
||||
<Link
|
||||
to="#"
|
||||
onClick={() => setView(VIEW_TYPES.LOGIN)}
|
||||
style={{ marginBottom: 10 }}
|
||||
>
|
||||
Back
|
||||
</Link>
|
||||
</Footer>
|
||||
</Fragment>
|
||||
)}
|
||||
{config.is_basic_authentication_enabled &&
|
||||
!config.is_magic_link_login_enabled &&
|
||||
config.is_sign_up_enabled && (
|
||||
<FooterContent>
|
||||
Don't have an account? <Link to="/app/signup"> Sign Up</Link>
|
||||
</FooterContent>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
12
app/src/pages/setup-password.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { AuthorizerResetPassword } from '@authorizerdev/authorizer-react';
|
||||
|
||||
export default function SetupPassword() {
|
||||
return (
|
||||
<Fragment>
|
||||
<h1 style={{ textAlign: 'center' }}>Setup new Password</h1>
|
||||
<br />
|
||||
<AuthorizerResetPassword />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
28
app/src/pages/signup.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { AuthorizerSignup } from '@authorizerdev/authorizer-react';
|
||||
import styled from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const FooterContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
`;
|
||||
|
||||
export default function SignUp({
|
||||
urlProps,
|
||||
}: {
|
||||
urlProps: Record<string, any>;
|
||||
}) {
|
||||
return (
|
||||
<Fragment>
|
||||
<h1 style={{ textAlign: 'center' }}>Sign Up</h1>
|
||||
<br />
|
||||
<AuthorizerSignup urlProps={urlProps} />
|
||||
<FooterContent>
|
||||
Already have an account? <Link to="/app"> Login</Link>
|
||||
</FooterContent>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
28
app/src/theme.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// colors: https://tailwindcss.com/docs/customizing-colors
|
||||
|
||||
export const theme = {
|
||||
colors: {
|
||||
primary: '#3B82F6',
|
||||
primaryDisabled: '#60A5FA',
|
||||
gray: '#D1D5DB',
|
||||
danger: '#DC2626',
|
||||
success: '#10B981',
|
||||
textColor: '#374151',
|
||||
},
|
||||
fonts: {
|
||||
// typography
|
||||
fontStack: '-apple-system, system-ui, sans-serif',
|
||||
|
||||
// font sizes
|
||||
largeText: '18px',
|
||||
mediumText: '14px',
|
||||
smallText: '12px',
|
||||
tinyText: '10px',
|
||||
},
|
||||
|
||||
radius: {
|
||||
card: '5px',
|
||||
button: '5px',
|
||||
input: '5px',
|
||||
},
|
||||
};
|
24
app/src/utils/common.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export const getCrypto = () => {
|
||||
//ie 11.x uses msCrypto
|
||||
return (window.crypto || (window as any).msCrypto) as Crypto;
|
||||
};
|
||||
|
||||
export const createRandomString = () => {
|
||||
const charset =
|
||||
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_~.';
|
||||
let random = '';
|
||||
const randomValues = Array.from(
|
||||
getCrypto().getRandomValues(new Uint8Array(43))
|
||||
);
|
||||
randomValues.forEach((v) => (random += charset[v % charset.length]));
|
||||
return random;
|
||||
};
|
||||
|
||||
export const createQueryParams = (params: any) => {
|
||||
return Object.keys(params)
|
||||
.filter((k) => typeof params[k] !== 'undefined')
|
||||
.map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
|
||||
.join('&');
|
||||
};
|
||||
|
||||
export const hasWindow = (): boolean => typeof window !== 'undefined';
|
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 |
2395
dashboard/package-lock.json
generated
@@ -12,16 +12,21 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^1.7.3",
|
||||
"@emotion/core": "^11.0.0",
|
||||
"@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",
|
||||
"focus-visible": "^5.2.0",
|
||||
"framer-motion": "^5.5.5",
|
||||
"graphql": "^16.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dropzone": "^12.0.4",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"typescript": "^4.5.4",
|
||||
|
1
dashboard/public/sample.csv
Normal file
@@ -0,0 +1 @@
|
||||
foo@bar.com,test@authorizer.dev
|
|
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import { ChakraProvider, extendTheme } from '@chakra-ui/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { createClient, Provider } from 'urql';
|
||||
@@ -10,16 +11,20 @@ const queryClient = createClient({
|
||||
fetchOptions: () => {
|
||||
return {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'x-authorizer-url': window.location.origin,
|
||||
},
|
||||
};
|
||||
},
|
||||
requestPolicy: 'network-only',
|
||||
});
|
||||
|
||||
const theme = extendTheme({
|
||||
styles: {
|
||||
global: {
|
||||
'html, body, #root': {
|
||||
fontFamily: 'Avenir, Helvetica, Arial, sans-serif',
|
||||
height: '100%',
|
||||
outline: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -32,14 +37,16 @@ const theme = extendTheme({
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ChakraProvider theme={theme}>
|
||||
<Provider value={queryClient}>
|
||||
<BrowserRouter basename="/dashboard">
|
||||
<AuthContextProvider>
|
||||
<AppRoutes />
|
||||
</AuthContextProvider>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</ChakraProvider>
|
||||
<Fragment>
|
||||
<ChakraProvider theme={theme}>
|
||||
<Provider value={queryClient}>
|
||||
<BrowserRouter basename="/dashboard">
|
||||
<AuthContextProvider>
|
||||
<AppRoutes />
|
||||
</AuthContextProvider>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</ChakraProvider>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
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;
|
65
dashboard/src/components/EnvComponents/AccessToken.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
import { Flex, Stack, Text, useMediaQuery } from "@chakra-ui/react";
|
||||
import InputField from "../../components/InputField";
|
||||
import { TextInputType, TextAreaInputType } from "../../constants";
|
||||
|
||||
const AccessToken = ({ variables, setVariables }: any) => {
|
||||
const [isNotSmallerScreen] = useMediaQuery("(min-width:600px)");
|
||||
return (
|
||||
<div>
|
||||
{" "}
|
||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold" mb={5}>
|
||||
Access Token
|
||||
</Text>
|
||||
<Stack spacing={6} padding="2% 0%">
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex
|
||||
w={isNotSmallerScreen ? "30%" : "50%"}
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text fontSize="sm">Access Token Expiry Time:</Text>
|
||||
</Flex>
|
||||
<Flex
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "3"}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextInputType.ACCESS_TOKEN_EXPIRY_TIME}
|
||||
placeholder="0h15m0s"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex
|
||||
w={isNotSmallerScreen ? "30%" : "60%"}
|
||||
justifyContent="start"
|
||||
direction="column"
|
||||
>
|
||||
<Text fontSize="sm">Custom Scripts:</Text>
|
||||
<Text fontSize="xs" color="blackAlpha.500">
|
||||
(Used to add custom fields in ID token)
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "3"}
|
||||
>
|
||||
<InputField
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextAreaInputType.CUSTOM_ACCESS_TOKEN_SCRIPT}
|
||||
placeholder="Add script here"
|
||||
minH="25vh"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessToken;
|
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { Flex, Stack, Center, Text, useMediaQuery } from '@chakra-ui/react';
|
||||
|
||||
import InputField from '../../components/InputField';
|
||||
import { TextInputType } from '../../constants';
|
||||
|
||||
const DatabaseCredentials = ({ variables, setVariables }: any) => {
|
||||
const [isNotSmallerScreen] = useMediaQuery('(min-width:600px)');
|
||||
return (
|
||||
<div>
|
||||
{' '}
|
||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||
Database Credentials
|
||||
</Text>
|
||||
<Stack spacing={6} padding="3% 0">
|
||||
<Text fontStyle="italic" fontSize="sm" color="blackAlpha.500" mt={3}>
|
||||
Note: Database related environment variables cannot be updated from
|
||||
dashboard. Please use .env file or OS environment variables to update
|
||||
it.
|
||||
</Text>
|
||||
<Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
|
||||
<Flex
|
||||
w={isNotSmallerScreen ? '30%' : '40%'}
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text fontSize="sm">DataBase Name:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? '70%' : '100%'}
|
||||
mt={isNotSmallerScreen ? '0' : '3'}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextInputType.DATABASE_NAME}
|
||||
isDisabled={true}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
<Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
|
||||
<Flex
|
||||
w={isNotSmallerScreen ? '30%' : '40%'}
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text fontSize="sm">DataBase Type:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? '70%' : '100%'}
|
||||
mt={isNotSmallerScreen ? '0' : '3'}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextInputType.DATABASE_TYPE}
|
||||
isDisabled={true}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
<Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
|
||||
<Flex
|
||||
w={isNotSmallerScreen ? '30%' : '40%'}
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text fontSize="sm">DataBase URL:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? '70%' : '100%'}
|
||||
mt={isNotSmallerScreen ? '0' : '3'}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextInputType.DATABASE_URL}
|
||||
isDisabled={true}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatabaseCredentials;
|
@@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
import { Flex, Stack, Center, Text, useMediaQuery } from "@chakra-ui/react";
|
||||
import InputField from "../../components/InputField";
|
||||
import { ArrayInputType} from "../../constants";
|
||||
|
||||
const DomainWhiteListing = ({ variables, setVariables }: any) => {
|
||||
const [isNotSmallerScreen] = useMediaQuery("(min-width:600px)");
|
||||
return (
|
||||
<div>
|
||||
{" "}
|
||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold" mb={5}>
|
||||
Domain White Listing
|
||||
</Text>
|
||||
<Stack spacing={6} padding="2% 0%">
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Allowed Origins:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "3"}
|
||||
>
|
||||
<InputField
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={ArrayInputType.ALLOWED_ORIGINS}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomainWhiteListing;
|
114
dashboard/src/components/EnvComponents/EmailConfiguration.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React from "react";
|
||||
import { Flex, Stack, Center, Text, useMediaQuery } from "@chakra-ui/react";
|
||||
import InputField from "../../components/InputField";
|
||||
import { TextInputType, HiddenInputType} from "../../constants";
|
||||
const EmailConfigurations = ({
|
||||
variables,
|
||||
setVariables,
|
||||
fieldVisibility,
|
||||
setFieldVisibility,
|
||||
}: any) => {
|
||||
const [isNotSmallerScreen] = useMediaQuery("(min-width:600px)");
|
||||
return (
|
||||
<div>
|
||||
{" "}
|
||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold" mb={5}>
|
||||
Email Configurations
|
||||
</Text>
|
||||
<Stack spacing={6} padding="2% 0%">
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">SMTP Host:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "3"}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextInputType.SMTP_HOST}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">SMTP Port:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "3"}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextInputType.SMTP_PORT}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex
|
||||
w={isNotSmallerScreen ? "30%" : "40%"}
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text fontSize="sm">SMTP Username:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "3"}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextInputType.SMTP_USERNAME}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex
|
||||
w={isNotSmallerScreen ? "30%" : "40%"}
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text fontSize="sm">SMTP Password:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "3"}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
fieldVisibility={fieldVisibility}
|
||||
setFieldVisibility={setFieldVisibility}
|
||||
inputType={HiddenInputType.SMTP_PASSWORD}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">From Email:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "3"}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextInputType.SENDER_EMAIL}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailConfigurations;
|
79
dashboard/src/components/EnvComponents/Features.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { Flex, Stack, Text } from '@chakra-ui/react';
|
||||
import InputField from '../InputField';
|
||||
import { SwitchInputType } from '../../constants';
|
||||
|
||||
const Features = ({ variables, setVariables }: any) => {
|
||||
return (
|
||||
<div>
|
||||
{' '}
|
||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold" mb={5}>
|
||||
Disable Features
|
||||
</Text>
|
||||
<Stack spacing={6} padding="2% 0%">
|
||||
<Flex>
|
||||
<Flex w="100%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Disable Login Page:</Text>
|
||||
</Flex>
|
||||
<Flex justifyContent="start">
|
||||
<InputField
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={SwitchInputType.DISABLE_LOGIN_PAGE}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Flex w="100%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Disable Email Verification:</Text>
|
||||
</Flex>
|
||||
<Flex justifyContent="start">
|
||||
<InputField
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={SwitchInputType.DISABLE_EMAIL_VERIFICATION}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Flex w="100%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Disable Magic Login Link:</Text>
|
||||
</Flex>
|
||||
<Flex justifyContent="start">
|
||||
<InputField
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={SwitchInputType.DISABLE_MAGIC_LINK_LOGIN}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Flex w="100%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Disable Basic Authentication:</Text>
|
||||
</Flex>
|
||||
<Flex justifyContent="start">
|
||||
<InputField
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={SwitchInputType.DISABLE_BASIC_AUTHENTICATION}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Flex w="100%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Disable Sign Up:</Text>
|
||||
</Flex>
|
||||
<Flex justifyContent="start" mb={3}>
|
||||
<InputField
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={SwitchInputType.DISABLE_SIGN_UP}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Features;
|
154
dashboard/src/components/EnvComponents/JWTConfiguration.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import React from "react";
|
||||
import { Flex, Stack, Center, Text, useMediaQuery } from "@chakra-ui/react";
|
||||
import {
|
||||
HiddenInputType,
|
||||
TextInputType,
|
||||
TextAreaInputType,
|
||||
} from "../../constants";
|
||||
import GenerateKeysModal from "../GenerateKeysModal";
|
||||
import InputField from "../InputField";
|
||||
|
||||
const JSTConfigurations = ({
|
||||
variables,
|
||||
setVariables,
|
||||
fieldVisibility,
|
||||
setFieldVisibility,
|
||||
SelectInputType,
|
||||
getData,
|
||||
HMACEncryptionType,
|
||||
RSAEncryptionType,
|
||||
ECDSAEncryptionType,
|
||||
}: any) => {
|
||||
const [isNotSmallerScreen] = useMediaQuery("(min-width:600px)");
|
||||
|
||||
return (
|
||||
<div>
|
||||
{" "}
|
||||
<Flex
|
||||
borderRadius={5}
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
paddingTop="2%"
|
||||
>
|
||||
<Text
|
||||
fontSize={isNotSmallerScreen ? "md" : "sm"}
|
||||
fontWeight="bold"
|
||||
mb={5}
|
||||
>
|
||||
JWT (JSON Web Tokens) Configurations
|
||||
</Text>
|
||||
<Flex mb={7}>
|
||||
<GenerateKeysModal jwtType={variables.JWT_TYPE} getData={getData} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Stack spacing={6} padding="2% 0%">
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">JWT Type:</Text>
|
||||
</Flex>
|
||||
<Flex
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "2"}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={SelectInputType}
|
||||
value={SelectInputType}
|
||||
options={{
|
||||
...HMACEncryptionType,
|
||||
...RSAEncryptionType,
|
||||
...ECDSAEncryptionType,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{Object.values(HMACEncryptionType).includes(variables.JWT_TYPE) ? (
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">JWT Secret</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "2"}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
fieldVisibility={fieldVisibility}
|
||||
setFieldVisibility={setFieldVisibility}
|
||||
inputType={HiddenInputType.JWT_SECRET}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
) : (
|
||||
<>
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Public Key</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "2"}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextAreaInputType.JWT_PUBLIC_KEY}
|
||||
placeholder="Add public key here"
|
||||
minH="25vh"
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Private Key</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "2"}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextAreaInputType.JWT_PRIVATE_KEY}
|
||||
placeholder="Add private key here"
|
||||
minH="25vh"
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex
|
||||
w={isNotSmallerScreen ? "30%" : "40%"}
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text fontSize="sm" orientation="vertical">
|
||||
JWT Role Claim:
|
||||
</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "2"}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextInputType.JWT_ROLE_CLAIM}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JSTConfigurations;
|
191
dashboard/src/components/EnvComponents/OAuthConfig.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import React from 'react';
|
||||
import InputField from '../InputField';
|
||||
import {
|
||||
Flex,
|
||||
Stack,
|
||||
Center,
|
||||
Text,
|
||||
Box,
|
||||
Divider,
|
||||
useMediaQuery,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaGoogle, FaGithub, FaFacebookF } from 'react-icons/fa';
|
||||
import { TextInputType, HiddenInputType } from '../../constants';
|
||||
|
||||
const OAuthConfig = ({
|
||||
envVariables,
|
||||
setVariables,
|
||||
fieldVisibility,
|
||||
setFieldVisibility,
|
||||
}: any) => {
|
||||
const [isNotSmallerScreen] = useMediaQuery('(min-width:667px)');
|
||||
return (
|
||||
<div>
|
||||
<Box>
|
||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold" mb={6}>
|
||||
Authorizer Config
|
||||
</Text>
|
||||
<Stack spacing={6} padding="2% 0%">
|
||||
<Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Client ID</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? '70%' : '100%'}
|
||||
mt={isNotSmallerScreen ? '0' : '3'}
|
||||
>
|
||||
<InputField
|
||||
variables={envVariables}
|
||||
setVariables={() => {}}
|
||||
inputType={TextInputType.CLIENT_ID}
|
||||
placeholder="Client ID"
|
||||
readOnly={true}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
<Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Client Secret</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? '70%' : '100%'}
|
||||
mt={isNotSmallerScreen ? '0' : '3'}
|
||||
>
|
||||
<InputField
|
||||
variables={envVariables}
|
||||
setVariables={setVariables}
|
||||
fieldVisibility={fieldVisibility}
|
||||
setFieldVisibility={setFieldVisibility}
|
||||
inputType={HiddenInputType.CLIENT_SECRET}
|
||||
placeholder="Client Secret"
|
||||
readOnly={true}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
</Stack>
|
||||
<Divider mt={5} mb={2} color="blackAlpha.700" />
|
||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold" mb={4}>
|
||||
Social Media Logins
|
||||
</Text>
|
||||
<Stack spacing={6} padding="2% 0%">
|
||||
<Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? '55px' : '35px'}
|
||||
h="35px"
|
||||
marginRight="1.5%"
|
||||
border="1px solid #ff3e30"
|
||||
borderRadius="5px"
|
||||
>
|
||||
<FaGoogle style={{ color: '#ff3e30' }} />
|
||||
</Center>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? '70%' : '100%'}
|
||||
mt={isNotSmallerScreen ? '0' : '3'}
|
||||
marginRight="1.5%"
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={envVariables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextInputType.GOOGLE_CLIENT_ID}
|
||||
placeholder="Google Client ID"
|
||||
/>
|
||||
</Center>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? '70%' : '100%'}
|
||||
mt={isNotSmallerScreen ? '0' : '3'}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={envVariables}
|
||||
setVariables={setVariables}
|
||||
fieldVisibility={fieldVisibility}
|
||||
setFieldVisibility={setFieldVisibility}
|
||||
inputType={HiddenInputType.GOOGLE_CLIENT_SECRET}
|
||||
placeholder="Google Secret"
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
<Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? '55px' : '35px'}
|
||||
h="35px"
|
||||
marginRight="1.5%"
|
||||
border="1px solid #171515"
|
||||
borderRadius="5px"
|
||||
>
|
||||
<FaGithub style={{ color: '#171515' }} />
|
||||
</Center>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? '70%' : '100%'}
|
||||
mt={isNotSmallerScreen ? '0' : '3'}
|
||||
marginRight="1.5%"
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={envVariables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextInputType.GITHUB_CLIENT_ID}
|
||||
placeholder="Github Client ID"
|
||||
/>
|
||||
</Center>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? '70%' : '100%'}
|
||||
mt={isNotSmallerScreen ? '0' : '3'}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={envVariables}
|
||||
setVariables={setVariables}
|
||||
fieldVisibility={fieldVisibility}
|
||||
setFieldVisibility={setFieldVisibility}
|
||||
inputType={HiddenInputType.GITHUB_CLIENT_SECRET}
|
||||
placeholder="Github Secret"
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
<Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? '55px' : '35px'}
|
||||
h="35px"
|
||||
marginRight="1.5%"
|
||||
border="1px solid #3b5998"
|
||||
borderRadius="5px"
|
||||
>
|
||||
<FaFacebookF style={{ color: '#3b5998' }} />
|
||||
</Center>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? '70%' : '100%'}
|
||||
mt={isNotSmallerScreen ? '0' : '3'}
|
||||
marginRight="1.5%"
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={envVariables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextInputType.FACEBOOK_CLIENT_ID}
|
||||
placeholder="Facebook Client ID"
|
||||
/>
|
||||
</Center>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? '70%' : '100%'}
|
||||
mt={isNotSmallerScreen ? '0' : '3'}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={envVariables}
|
||||
setVariables={setVariables}
|
||||
fieldVisibility={fieldVisibility}
|
||||
setFieldVisibility={setFieldVisibility}
|
||||
inputType={HiddenInputType.FACEBOOK_CLIENT_SECRET}
|
||||
placeholder="Facebook Secret"
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuthConfig;
|
60
dashboard/src/components/EnvComponents/OrganizationInfo.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
import { Flex, Stack, Center, Text, useMediaQuery } from "@chakra-ui/react";
|
||||
import InputField from "../InputField";
|
||||
import { TextInputType } from "../../constants";
|
||||
|
||||
const OrganizationInfo = ({ variables, setVariables }: any) => {
|
||||
const [isNotSmallerScreen] = useMediaQuery("(min-width:600px)");
|
||||
return (
|
||||
<div>
|
||||
{" "}
|
||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold" mb={5}>
|
||||
Organization Information
|
||||
</Text>
|
||||
<Stack spacing={6} padding="2% 0%">
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex
|
||||
w={isNotSmallerScreen ? "30%" : "40%"}
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text fontSize="sm">Organization Name:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "3"}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextInputType.ORGANIZATION_NAME}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex
|
||||
w={isNotSmallerScreen ? "30%" : "40%"}
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text fontSize="sm">Organization Logo:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "3"}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={TextInputType.ORGANIZATION_LOGO}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrganizationInfo;
|
67
dashboard/src/components/EnvComponents/Roles.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from "react";
|
||||
import { Flex, Stack, Center, Text, useMediaQuery } from "@chakra-ui/react";
|
||||
import { ArrayInputType } from "../../constants";
|
||||
import InputField from "../InputField";
|
||||
|
||||
const Roles = ({ variables, setVariables }: any) => {
|
||||
const [isNotSmallerScreen] = useMediaQuery("(min-width:600px)");
|
||||
return (
|
||||
<div>
|
||||
{" "}
|
||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold" mb={5}>
|
||||
Roles
|
||||
</Text>
|
||||
<Stack spacing={6} padding="2% 0%">
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Roles:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "2"}
|
||||
overflow="hidden"
|
||||
>
|
||||
<InputField
|
||||
borderRadius={7}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={ArrayInputType.ROLES}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Default Roles:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "2"}
|
||||
>
|
||||
<InputField
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={ArrayInputType.DEFAULT_ROLES}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
<Flex direction={isNotSmallerScreen ? "row" : "column"}>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Protected Roles:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "2"}
|
||||
>
|
||||
<InputField
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={ArrayInputType.PROTECTED_ROLES}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Roles;
|
138
dashboard/src/components/EnvComponents/SecurityAdminSecret.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Flex,
|
||||
Stack,
|
||||
Center,
|
||||
Text,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
useMediaQuery,
|
||||
} from "@chakra-ui/react";
|
||||
import { FaRegEyeSlash, FaRegEye } from "react-icons/fa";
|
||||
import InputField from "../InputField";
|
||||
import { HiddenInputType } from "../../constants";
|
||||
const SecurityAdminSecret = ({
|
||||
variables,
|
||||
setVariables,
|
||||
fieldVisibility,
|
||||
setFieldVisibility,
|
||||
validateAdminSecretHandler,
|
||||
adminSecret,
|
||||
}: any) => {
|
||||
const [isNotSmallerScreen] = useMediaQuery("(min-width:600px)");
|
||||
return (
|
||||
<div>
|
||||
{" "}
|
||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||
Security (Admin Secret)
|
||||
</Text>
|
||||
<Stack
|
||||
spacing={6}
|
||||
padding="0 5%"
|
||||
marginTop="3%"
|
||||
border="1px solid #ff7875"
|
||||
borderRadius="5px"
|
||||
>
|
||||
<Flex
|
||||
marginTop={isNotSmallerScreen ? "3%" : "5%"}
|
||||
direction={isNotSmallerScreen ? "row" : "column"}
|
||||
>
|
||||
<Flex
|
||||
mt={3}
|
||||
w={isNotSmallerScreen ? "30%" : "40%"}
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text fontSize="sm">Old Admin Secret:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "3"}
|
||||
>
|
||||
<InputGroup size="sm">
|
||||
<Input
|
||||
borderRadius={5}
|
||||
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%"
|
||||
direction={isNotSmallerScreen ? "row" : "column"}
|
||||
>
|
||||
<Flex
|
||||
w={isNotSmallerScreen ? "30%" : "50%"}
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text fontSize="sm">New Admin Secret:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? "70%" : "100%"}
|
||||
mt={isNotSmallerScreen ? "0" : "3"}
|
||||
>
|
||||
<InputField
|
||||
borderRadius={5}
|
||||
mb={3}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={HiddenInputType.ADMIN_SECRET}
|
||||
fieldVisibility={fieldVisibility}
|
||||
setFieldVisibility={setFieldVisibility}
|
||||
isDisabled={adminSecret.disableInputField}
|
||||
placeholder="Enter New Admin Secret"
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecurityAdminSecret;
|
42
dashboard/src/components/EnvComponents/SessionStorage.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { Flex, Stack, Center, Text, useMediaQuery } from '@chakra-ui/react';
|
||||
import InputField from '../InputField';
|
||||
|
||||
const SessionStorage = ({ variables, setVariables, RedisURL }: any) => {
|
||||
const [isNotSmallerScreen] = useMediaQuery('(min-width:600px)');
|
||||
return (
|
||||
<div>
|
||||
{' '}
|
||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold" mb={5}>
|
||||
Session Storage
|
||||
</Text>
|
||||
<Text fontStyle="italic" fontSize="sm" color="blackAlpha.500" mt={3}>
|
||||
Note: Redis related environment variables cannot be updated from
|
||||
dashboard. Please use .env file or OS environment variables to update
|
||||
it.
|
||||
</Text>
|
||||
<Stack spacing={6} padding="2% 0%">
|
||||
<Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Redis URL:</Text>
|
||||
</Flex>
|
||||
<Center
|
||||
w={isNotSmallerScreen ? '70%' : '100%'}
|
||||
mt={isNotSmallerScreen ? '0' : '3'}
|
||||
>
|
||||
<InputField
|
||||
disabled
|
||||
borderRadius={5}
|
||||
variables={variables}
|
||||
setVariables={setVariables}
|
||||
inputType={RedisURL}
|
||||
placeholder="Redis URL"
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionStorage;
|
247
dashboard/src/components/GenerateKeysModal.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Flex,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
useDisclosure,
|
||||
Text,
|
||||
useToast,
|
||||
Input,
|
||||
Spinner,
|
||||
} from '@chakra-ui/react';
|
||||
import { useClient } from 'urql';
|
||||
import { FaSave } from 'react-icons/fa';
|
||||
import {
|
||||
ECDSAEncryptionType,
|
||||
HMACEncryptionType,
|
||||
RSAEncryptionType,
|
||||
SelectInputType,
|
||||
TextAreaInputType,
|
||||
} from '../constants';
|
||||
import InputField from './InputField';
|
||||
import { GenerateKeys, UpdateEnvVariables } from '../graphql/mutation';
|
||||
|
||||
interface propTypes {
|
||||
jwtType: string;
|
||||
getData: Function;
|
||||
}
|
||||
|
||||
interface stateVarTypes {
|
||||
JWT_TYPE: string;
|
||||
JWT_SECRET: string;
|
||||
JWT_PRIVATE_KEY: string;
|
||||
JWT_PUBLIC_KEY: string;
|
||||
}
|
||||
|
||||
const initState: stateVarTypes = {
|
||||
JWT_TYPE: '',
|
||||
JWT_SECRET: '',
|
||||
JWT_PRIVATE_KEY: '',
|
||||
JWT_PUBLIC_KEY: '',
|
||||
};
|
||||
|
||||
const GenerateKeysModal = ({ jwtType, getData }: propTypes) => {
|
||||
const client = useClient();
|
||||
const toast = useToast();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [stateVariables, setStateVariables] = React.useState<stateVarTypes>({
|
||||
...initState,
|
||||
});
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
setStateVariables({ ...initState, JWT_TYPE: jwtType });
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const fetchKeys = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await client
|
||||
.mutation(GenerateKeys, { params: { type: stateVariables.JWT_TYPE } })
|
||||
.toPromise();
|
||||
if (res?.error) {
|
||||
toast({
|
||||
title: 'Error occurred generating jwt keys',
|
||||
isClosable: true,
|
||||
status: 'error',
|
||||
position: 'bottom-right',
|
||||
});
|
||||
closeHandler();
|
||||
} else {
|
||||
setStateVariables({
|
||||
...stateVariables,
|
||||
JWT_SECRET: res?.data?._generate_jwt_keys?.secret || '',
|
||||
JWT_PRIVATE_KEY: res?.data?._generate_jwt_keys?.private_key || '',
|
||||
JWT_PUBLIC_KEY: res?.data?._generate_jwt_keys?.public_key || '',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen && stateVariables.JWT_TYPE) {
|
||||
fetchKeys();
|
||||
}
|
||||
}, [stateVariables.JWT_TYPE]);
|
||||
|
||||
const saveHandler = async () => {
|
||||
const res = await client
|
||||
.mutation(UpdateEnvVariables, { params: { ...stateVariables } })
|
||||
.toPromise();
|
||||
|
||||
if (res.error) {
|
||||
toast({
|
||||
title: 'Error occurred setting jwt keys',
|
||||
isClosable: true,
|
||||
status: 'error',
|
||||
position: 'bottom-right',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
title: 'JWT keys updated successfully',
|
||||
isClosable: true,
|
||||
status: 'success',
|
||||
position: 'bottom-right',
|
||||
});
|
||||
closeHandler();
|
||||
};
|
||||
|
||||
const closeHandler = () => {
|
||||
setStateVariables({ ...initState });
|
||||
getData();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
h="1.75rem"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={onOpen}
|
||||
>
|
||||
Generate new keys
|
||||
</Button>
|
||||
<Modal isOpen={isOpen} onClose={closeHandler}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>New JWT keys</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Flex>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">JWT Type:</Text>
|
||||
</Flex>
|
||||
<InputField
|
||||
variables={stateVariables}
|
||||
setVariables={setStateVariables}
|
||||
inputType={SelectInputType.JWT_TYPE}
|
||||
value={SelectInputType.JWT_TYPE}
|
||||
options={{
|
||||
...HMACEncryptionType,
|
||||
...RSAEncryptionType,
|
||||
...ECDSAEncryptionType,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
{isLoading ? (
|
||||
<Center minH="25vh">
|
||||
<Spinner />
|
||||
</Center>
|
||||
) : (
|
||||
<>
|
||||
{Object.values(HMACEncryptionType).includes(
|
||||
stateVariables.JWT_TYPE
|
||||
) ? (
|
||||
<Flex marginTop="8">
|
||||
<Flex w="23%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">JWT Secret</Text>
|
||||
</Flex>
|
||||
<Center w="77%">
|
||||
<Input
|
||||
size="sm"
|
||||
value={stateVariables.JWT_SECRET}
|
||||
onChange={(event: any) =>
|
||||
setStateVariables({
|
||||
...stateVariables,
|
||||
JWT_SECRET: event.target.value,
|
||||
})
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
) : (
|
||||
<>
|
||||
<Flex marginTop="8">
|
||||
<Flex w="23%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Public Key</Text>
|
||||
</Flex>
|
||||
<Center w="77%">
|
||||
<InputField
|
||||
variables={stateVariables}
|
||||
setVariables={setStateVariables}
|
||||
inputType={TextAreaInputType.JWT_PUBLIC_KEY}
|
||||
placeholder="Add public key here"
|
||||
minH="25vh"
|
||||
readOnly
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
<Flex marginTop="8">
|
||||
<Flex w="23%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Private Key</Text>
|
||||
</Flex>
|
||||
<Center w="77%">
|
||||
<InputField
|
||||
variables={stateVariables}
|
||||
setVariables={setStateVariables}
|
||||
inputType={TextAreaInputType.JWT_PRIVATE_KEY}
|
||||
placeholder="Add private key here"
|
||||
minH="25vh"
|
||||
readOnly
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
leftIcon={<FaSave />}
|
||||
colorScheme="blue"
|
||||
variant="solid"
|
||||
onClick={saveHandler}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
<Center h="100%" pt="5%">
|
||||
Apply
|
||||
</Center>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateKeysModal;
|
341
dashboard/src/components/InputField.tsx
Normal file
@@ -0,0 +1,341 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Input,
|
||||
Center,
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
Tag,
|
||||
TagLabel,
|
||||
TagRightIcon,
|
||||
Select,
|
||||
Textarea,
|
||||
Switch,
|
||||
Code,
|
||||
Text,
|
||||
} 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%"
|
||||
borderRadius={5}
|
||||
paddingTop="0.5%"
|
||||
overflowX={variables[inputType].length > 3 ? 'scroll' : 'hidden'}
|
||||
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">
|
||||
<Text h="75%" fontWeight="bold" marginRight="2">
|
||||
Off
|
||||
</Text>
|
||||
<Switch
|
||||
size="md"
|
||||
isChecked={variables[inputType]}
|
||||
onChange={() => {
|
||||
setVariables({
|
||||
...variables,
|
||||
[inputType]: !variables[inputType],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Text h="75%" fontWeight="bold" marginLeft="2">
|
||||
On
|
||||
</Text>
|
||||
</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;
|
385
dashboard/src/components/InviteMembersModal.tsx
Normal file
@@ -0,0 +1,385 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Flex,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
Tabs,
|
||||
TabList,
|
||||
Tab,
|
||||
TabPanels,
|
||||
TabPanel,
|
||||
InputGroup,
|
||||
Input,
|
||||
InputRightElement,
|
||||
Text,
|
||||
Link,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { useClient } from 'urql';
|
||||
import { FaUserPlus, FaMinusCircle, FaPlus, FaUpload } from 'react-icons/fa';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { validateEmail, validateURI } from '../utils';
|
||||
import { InviteMembers } from '../graphql/mutation';
|
||||
import { ArrayInputOperations } from '../constants';
|
||||
import parseCSV from '../utils/parseCSV';
|
||||
|
||||
interface stateDataTypes {
|
||||
value: string;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
interface requestParamTypes {
|
||||
emails: string[];
|
||||
redirect_uri?: string;
|
||||
}
|
||||
|
||||
const initData: stateDataTypes = {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
};
|
||||
|
||||
const InviteMembersModal = ({
|
||||
updateUserList,
|
||||
disabled = true,
|
||||
}: {
|
||||
updateUserList: Function;
|
||||
disabled: boolean;
|
||||
}) => {
|
||||
const client = useClient();
|
||||
const toast = useToast();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [tabIndex, setTabIndex] = useState<number>(0);
|
||||
const [redirectURI, setRedirectURI] = useState<stateDataTypes>({
|
||||
...initData,
|
||||
});
|
||||
const [emails, setEmails] = useState<stateDataTypes[]>([{ ...initData }]);
|
||||
const [disableSendButton, setDisableSendButton] = useState<boolean>(false);
|
||||
const [loading, setLoading] = React.useState<boolean>(false);
|
||||
useEffect(() => {
|
||||
if (redirectURI.isInvalid) {
|
||||
setDisableSendButton(true);
|
||||
} else if (emails.some((emailData) => emailData.isInvalid)) {
|
||||
setDisableSendButton(true);
|
||||
} else {
|
||||
setDisableSendButton(false);
|
||||
}
|
||||
}, [redirectURI, emails]);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setRedirectURI({ ...initData });
|
||||
setEmails([{ ...initData }]);
|
||||
};
|
||||
}, []);
|
||||
const sendInviteHandler = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const emailList = emails
|
||||
.filter((emailData) => !emailData.isInvalid)
|
||||
.map((emailData) => emailData.value);
|
||||
const params: requestParamTypes = {
|
||||
emails: emailList,
|
||||
};
|
||||
if (redirectURI.value !== '' && !redirectURI.isInvalid) {
|
||||
params.redirect_uri = redirectURI.value;
|
||||
}
|
||||
if (emailList.length > 0) {
|
||||
const res = await client
|
||||
.mutation(InviteMembers, {
|
||||
params,
|
||||
})
|
||||
.toPromise();
|
||||
if (res.error) {
|
||||
throw new Error('Internal server error');
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
title: 'Invites sent successfully!',
|
||||
isClosable: true,
|
||||
status: 'success',
|
||||
position: 'bottom-right',
|
||||
});
|
||||
setLoading(false);
|
||||
updateUserList();
|
||||
} else {
|
||||
throw new Error('Please add emails');
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: error?.message || 'Error occurred, try again!',
|
||||
isClosable: true,
|
||||
status: 'error',
|
||||
position: 'bottom-right',
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
closeModalHandler();
|
||||
};
|
||||
const updateEmailListHandler = (operation: string, index: number = 0) => {
|
||||
switch (operation) {
|
||||
case ArrayInputOperations.APPEND:
|
||||
setEmails([...emails, { ...initData }]);
|
||||
break;
|
||||
case ArrayInputOperations.REMOVE:
|
||||
const updatedEmailList = [...emails];
|
||||
updatedEmailList.splice(index, 1);
|
||||
setEmails(updatedEmailList);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
const inputChangeHandler = (value: string, index: number) => {
|
||||
const updatedEmailList = [...emails];
|
||||
updatedEmailList[index].value = value;
|
||||
updatedEmailList[index].isInvalid = !validateEmail(value);
|
||||
setEmails(updatedEmailList);
|
||||
};
|
||||
const changeTabsHandler = (index: number) => {
|
||||
setTabIndex(index);
|
||||
};
|
||||
const onDrop = useCallback(async (acceptedFiles) => {
|
||||
const result = await parseCSV(acceptedFiles[0], ',');
|
||||
setEmails(result);
|
||||
changeTabsHandler(0);
|
||||
}, []);
|
||||
const setRedirectURIHandler = (value: string) => {
|
||||
const updatedRedirectURI: stateDataTypes = {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
};
|
||||
updatedRedirectURI.value = value;
|
||||
updatedRedirectURI.isInvalid = !validateURI(value);
|
||||
setRedirectURI(updatedRedirectURI);
|
||||
};
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept: 'text/csv',
|
||||
});
|
||||
const closeModalHandler = () => {
|
||||
setRedirectURI({
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
});
|
||||
setEmails([
|
||||
{
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
},
|
||||
]);
|
||||
onClose();
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
leftIcon={<FaUserPlus />}
|
||||
colorScheme="blue"
|
||||
variant="solid"
|
||||
onClick={onOpen}
|
||||
isDisabled={disabled}
|
||||
size="sm"
|
||||
>
|
||||
<Center h="100%">
|
||||
{disabled ? (
|
||||
<Tooltip
|
||||
mr={8}
|
||||
mt={1}
|
||||
hasArrow
|
||||
bg="gray.300"
|
||||
color="black"
|
||||
label="Email verification is disabled, refer to 'Features' tab within 'Environment' to enable it."
|
||||
>
|
||||
Invite Members
|
||||
</Tooltip>
|
||||
) : (
|
||||
'Invite Members'
|
||||
)}
|
||||
</Center>{' '}
|
||||
</Button>
|
||||
<Modal isOpen={isOpen} onClose={closeModalHandler} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Invite Members</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Tabs
|
||||
isFitted
|
||||
variant="enclosed"
|
||||
index={tabIndex}
|
||||
onChange={changeTabsHandler}
|
||||
>
|
||||
<TabList>
|
||||
<Tab>Enter emails</Tab>
|
||||
<Tab>Upload CSV</Tab>
|
||||
</TabList>
|
||||
<TabPanels
|
||||
border="1px"
|
||||
borderTop="0"
|
||||
borderBottomRadius="5px"
|
||||
borderColor="inherit"
|
||||
>
|
||||
<TabPanel>
|
||||
<Flex flexDirection="column">
|
||||
<Flex
|
||||
width="100%"
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
marginBottom="2%"
|
||||
>
|
||||
<Flex marginLeft="2.5%">Redirect URI</Flex>
|
||||
</Flex>
|
||||
<Flex
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
marginBottom="2%"
|
||||
>
|
||||
<InputGroup size="md" marginBottom="2.5%">
|
||||
<Input
|
||||
pr="4.5rem"
|
||||
type="text"
|
||||
placeholder="https://domain.com/sign-up"
|
||||
value={redirectURI.value}
|
||||
isInvalid={redirectURI.isInvalid}
|
||||
onChange={(e) =>
|
||||
setRedirectURIHandler(e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</InputGroup>
|
||||
</Flex>
|
||||
<Flex
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
marginBottom="2%"
|
||||
>
|
||||
<Flex marginLeft="2.5%">Emails</Flex>
|
||||
<Flex>
|
||||
<Button
|
||||
leftIcon={<FaPlus />}
|
||||
colorScheme="blue"
|
||||
h="1.75rem"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
updateEmailListHandler(ArrayInputOperations.APPEND)
|
||||
}
|
||||
>
|
||||
Add more emails
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex flexDirection="column" maxH={250} overflowY="scroll">
|
||||
{emails.map((emailData, index) => (
|
||||
<Flex
|
||||
key={`email-data-${index}`}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<InputGroup size="md" marginBottom="2.5%">
|
||||
<Input
|
||||
pr="4.5rem"
|
||||
type="text"
|
||||
placeholder="name@domain.com"
|
||||
value={emailData.value}
|
||||
isInvalid={emailData.isInvalid}
|
||||
onChange={(e) =>
|
||||
inputChangeHandler(e.currentTarget.value, index)
|
||||
}
|
||||
/>
|
||||
<InputRightElement width="3rem">
|
||||
<Button
|
||||
h="1.75rem"
|
||||
size="sm"
|
||||
colorScheme="blackAlpha"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
updateEmailListHandler(
|
||||
ArrayInputOperations.REMOVE,
|
||||
index
|
||||
)
|
||||
}
|
||||
>
|
||||
<FaMinusCircle />
|
||||
</Button>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Flex
|
||||
justify="center"
|
||||
align="center"
|
||||
textAlign="center"
|
||||
bg="#f0f0f0"
|
||||
h={230}
|
||||
p={50}
|
||||
m={2}
|
||||
borderRadius={5}
|
||||
{...getRootProps()}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive ? (
|
||||
<Text>Drop the files here...</Text>
|
||||
) : (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Center boxSize="20" color="blackAlpha.500">
|
||||
<FaUpload fontSize="40" />
|
||||
</Center>
|
||||
<Text>
|
||||
Drag 'n' drop the csv file here, or click to select.
|
||||
</Text>
|
||||
<Text size="xs">
|
||||
Download{' '}
|
||||
<Link
|
||||
href={`/dashboard/public/sample.csv`}
|
||||
download="sample.csv"
|
||||
color="blue.600"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{' '}
|
||||
sample.csv
|
||||
</Link>{' '}
|
||||
and modify it.{' '}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
variant="solid"
|
||||
onClick={sendInviteHandler}
|
||||
isDisabled={disableSendButton || loading}
|
||||
>
|
||||
<Center h="100%" pt="5%">
|
||||
Send
|
||||
</Center>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InviteMembersModal;
|
@@ -1,7 +1,6 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { Fragment, ReactNode } from 'react';
|
||||
import {
|
||||
IconButton,
|
||||
Avatar,
|
||||
Box,
|
||||
CloseButton,
|
||||
Flex,
|
||||
@@ -18,34 +17,100 @@ import {
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionPanel,
|
||||
AccordionItem,
|
||||
useMediaQuery,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
FiHome,
|
||||
FiTrendingUp,
|
||||
FiCompass,
|
||||
FiStar,
|
||||
FiUser,
|
||||
FiCode,
|
||||
FiSettings,
|
||||
FiMenu,
|
||||
FiUser,
|
||||
FiUsers,
|
||||
FiChevronDown,
|
||||
} from 'react-icons/fi';
|
||||
import { BiCustomize } from 'react-icons/bi';
|
||||
import { AiOutlineKey } from 'react-icons/ai';
|
||||
import { SiOpenaccess, SiJsonwebtokens } from 'react-icons/si';
|
||||
import { MdSecurity } from 'react-icons/md';
|
||||
import { RiDatabase2Line } from 'react-icons/ri';
|
||||
import { BsCheck2Circle } from 'react-icons/bs';
|
||||
import { HiOutlineMail, HiOutlineOfficeBuilding } from 'react-icons/hi';
|
||||
import { IconType } from 'react-icons';
|
||||
import { ReactText } from 'react';
|
||||
import { useMutation } from 'urql';
|
||||
import { useMutation, useQuery } from 'urql';
|
||||
import { NavLink, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthContext } from '../contexts/AuthContext';
|
||||
import { AdminLogout } from '../graphql/mutation';
|
||||
import { MetaQuery } from '../graphql/queries';
|
||||
|
||||
interface SubRoutes {
|
||||
name: string;
|
||||
icon: IconType;
|
||||
route: string;
|
||||
}
|
||||
|
||||
interface LinkItemProps {
|
||||
name: string;
|
||||
icon: IconType;
|
||||
route: string;
|
||||
subRoutes?: SubRoutes[];
|
||||
}
|
||||
const LinkItems: Array<LinkItemProps> = [
|
||||
{ name: 'Home', icon: FiHome, route: '/' },
|
||||
{
|
||||
name: 'Environment ',
|
||||
icon: FiSettings,
|
||||
route: '/',
|
||||
subRoutes: [
|
||||
{
|
||||
name: 'OAuth Config',
|
||||
icon: AiOutlineKey,
|
||||
route: '/oauth-setting',
|
||||
},
|
||||
|
||||
{ name: 'Roles', icon: FiUser, route: '/roles' },
|
||||
{
|
||||
name: 'JWT Secrets',
|
||||
icon: SiJsonwebtokens,
|
||||
route: '/jwt-config',
|
||||
},
|
||||
{
|
||||
name: 'Session Storage',
|
||||
icon: RiDatabase2Line,
|
||||
route: '/session-storage',
|
||||
},
|
||||
{
|
||||
name: 'Email Configurations',
|
||||
icon: HiOutlineMail,
|
||||
route: '/email-config',
|
||||
},
|
||||
{
|
||||
name: 'Domain White Listing',
|
||||
icon: BsCheck2Circle,
|
||||
route: '/whitelist-variables',
|
||||
},
|
||||
{
|
||||
name: 'Organization Info',
|
||||
icon: HiOutlineOfficeBuilding,
|
||||
route: '/organization-info',
|
||||
},
|
||||
{ name: 'Access Token', icon: SiOpenaccess, route: '/access-token' },
|
||||
{
|
||||
name: 'Features',
|
||||
icon: BiCustomize,
|
||||
route: '/features',
|
||||
},
|
||||
{ name: 'Database', icon: RiDatabase2Line, route: '/db-cred' },
|
||||
{
|
||||
name: ' Security',
|
||||
icon: MdSecurity,
|
||||
route: '/admin-secret',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ name: 'Users', icon: FiUsers, route: '/users' },
|
||||
{ name: 'Environment Variables', icon: FiSettings, route: '/environment' },
|
||||
];
|
||||
|
||||
interface SidebarProps extends BoxProps {
|
||||
@@ -54,20 +119,28 @@ interface SidebarProps extends BoxProps {
|
||||
|
||||
export const Sidebar = ({ onClose, ...rest }: SidebarProps) => {
|
||||
const { pathname } = useLocation();
|
||||
const [{ data }] = useQuery({ query: MetaQuery });
|
||||
const [isNotSmallerScreen] = useMediaQuery('(min-width:600px)');
|
||||
return (
|
||||
<Box
|
||||
transition="3s ease"
|
||||
bg={useColorModeValue('white', 'gray.900')}
|
||||
borderRight="1px"
|
||||
borderRightColor={useColorModeValue('gray.200', 'gray.700')}
|
||||
w={{ base: 'full', md: 60 }}
|
||||
w={{ base: 'full', md: '64' }}
|
||||
pos="fixed"
|
||||
h="full"
|
||||
{...rest}
|
||||
>
|
||||
<Flex h="20" alignItems="center" mx="8" justifyContent="space-between">
|
||||
<Flex
|
||||
h="20"
|
||||
alignItems="center"
|
||||
mx="18"
|
||||
justifyContent="space-between"
|
||||
flexDirection="row"
|
||||
>
|
||||
<NavLink to="/">
|
||||
<Flex alignItems="center">
|
||||
<Flex alignItems="center" mt="6">
|
||||
<Image
|
||||
src="https://authorizer.dev/images/logo.png"
|
||||
alt="logo"
|
||||
@@ -80,57 +153,132 @@ export const Sidebar = ({ onClose, ...rest }: SidebarProps) => {
|
||||
</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' : ''}
|
||||
|
||||
<Accordion defaultIndex={[0]} allowMultiple>
|
||||
<AccordionItem textAlign="center" border="none" w="100%">
|
||||
{LinkItems.map((link) =>
|
||||
link?.subRoutes ? (
|
||||
<div key={link.name}>
|
||||
<AccordionButton _focus={{ boxShadow: 'none' }}>
|
||||
<Text as="div" fontSize="md">
|
||||
<NavItem
|
||||
icon={link.icon}
|
||||
color={pathname === link.route ? 'blue.500' : ''}
|
||||
style={{ outline: 'unset' }}
|
||||
height={12}
|
||||
ml={-1}
|
||||
mb={isNotSmallerScreen ? -1 : -4}
|
||||
w={isNotSmallerScreen ? '100%' : '310%'}
|
||||
>
|
||||
<Fragment>
|
||||
{link.name}
|
||||
<Box display={{ base: 'none', md: 'flex' }} ml={12}>
|
||||
<FiChevronDown />
|
||||
</Box>
|
||||
</Fragment>
|
||||
</NavItem>
|
||||
</Text>
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
{link.subRoutes?.map((sublink) => (
|
||||
<NavLink
|
||||
key={sublink.name}
|
||||
to={sublink.route}
|
||||
onClick={onClose}
|
||||
>
|
||||
{' '}
|
||||
<Text as="div" fontSize="xs" ml={2}>
|
||||
<NavItem
|
||||
icon={sublink.icon}
|
||||
color={pathname === sublink.route ? 'blue.500' : ''}
|
||||
height={8}
|
||||
>
|
||||
{sublink.name}
|
||||
</NavItem>{' '}
|
||||
</Text>
|
||||
</NavLink>
|
||||
))}
|
||||
</AccordionPanel>
|
||||
</div>
|
||||
) : (
|
||||
<NavLink key={link.name} to={link.route}>
|
||||
{' '}
|
||||
<Text as="div" fontSize="md" w="100%" mt={-2}>
|
||||
<NavItem
|
||||
icon={link.icon}
|
||||
color={pathname === link.route ? 'blue.500' : ''}
|
||||
height={12}
|
||||
onClick={onClose}
|
||||
>
|
||||
{link.name}
|
||||
</NavItem>{' '}
|
||||
</Text>
|
||||
</NavLink>
|
||||
)
|
||||
)}
|
||||
<Link
|
||||
href="/playground"
|
||||
target="_blank"
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
_focus={{ _boxShadow: 'none' }}
|
||||
>
|
||||
{link.name}
|
||||
</NavItem>
|
||||
</NavLink>
|
||||
))}
|
||||
<NavItem icon={FiCode}>API Playground</NavItem>
|
||||
</Link>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
{data?.meta?.version && (
|
||||
<Flex alignContent="center">
|
||||
{' '}
|
||||
<Text
|
||||
color="gray.400"
|
||||
fontSize="sm"
|
||||
textAlign="center"
|
||||
position="absolute"
|
||||
bottom="5"
|
||||
left="7"
|
||||
>
|
||||
Current Version: {data.meta.version}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface NavItemProps extends FlexProps {
|
||||
icon: IconType;
|
||||
children: ReactText;
|
||||
children: ReactText | JSX.Element | JSX.Element[];
|
||||
}
|
||||
export const NavItem = ({ icon, children, ...rest }: NavItemProps) => {
|
||||
return (
|
||||
<Link
|
||||
href="#"
|
||||
style={{ textDecoration: 'none' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
<Flex
|
||||
align="center"
|
||||
p="3"
|
||||
mx="3"
|
||||
borderRadius="md"
|
||||
role="group"
|
||||
cursor="pointer"
|
||||
_hover={{
|
||||
bg: 'blue.500',
|
||||
color: 'white',
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
<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>
|
||||
</Link>
|
||||
{icon && (
|
||||
<Icon
|
||||
mr="4"
|
||||
fontSize="16"
|
||||
_groupHover={{
|
||||
color: 'white',
|
||||
}}
|
||||
as={icon}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -150,7 +298,7 @@ export const MobileNav = ({ onOpen, ...rest }: MobileProps) => {
|
||||
|
||||
return (
|
||||
<Flex
|
||||
ml={{ base: 0, md: 60 }}
|
||||
ml={{ base: 0, md: 64 }}
|
||||
px={{ base: 4, md: 4 }}
|
||||
height="20"
|
||||
position="fixed"
|
||||
@@ -161,6 +309,7 @@ export const MobileNav = ({ onOpen, ...rest }: MobileProps) => {
|
||||
borderBottomWidth="1px"
|
||||
borderBottomColor={useColorModeValue('gray.200', 'gray.700')}
|
||||
justifyContent={{ base: 'space-between', md: 'flex-end' }}
|
||||
zIndex={99}
|
||||
{...rest}
|
||||
>
|
||||
<IconButton
|
||||
@@ -186,7 +335,7 @@ export const MobileNav = ({ onOpen, ...rest }: MobileProps) => {
|
||||
transition="all 0.3s"
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
>
|
||||
<HStack>
|
||||
<HStack mr={5}>
|
||||
<FiUser />
|
||||
<VStack
|
||||
display={{ base: 'none', md: 'flex' }}
|
||||
|
@@ -1 +1,145 @@
|
||||
export const LOGO_URL = "https://user-images.githubusercontent.com/6964334/147834043-fc384cab-e7ca-40f8-9663-38fc25fd5f3a.png"
|
||||
export const LOGO_URL =
|
||||
'https://user-images.githubusercontent.com/6964334/147834043-fc384cab-e7ca-40f8-9663-38fc25fd5f3a.png';
|
||||
|
||||
export const TextInputType = {
|
||||
ACCESS_TOKEN_EXPIRY_TIME: 'ACCESS_TOKEN_EXPIRY_TIME',
|
||||
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',
|
||||
DISABLE_SIGN_UP: 'DISABLE_SIGN_UP',
|
||||
DISABLE_REDIS_FOR_ENV: 'DISABLE_REDIS_FOR_ENV',
|
||||
};
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
export 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;
|
||||
DISABLE_SIGN_UP: boolean;
|
||||
OLD_ADMIN_SECRET: string;
|
||||
DATABASE_NAME: string;
|
||||
DATABASE_TYPE: string;
|
||||
DATABASE_URL: string;
|
||||
ACCESS_TOKEN_EXPIRY_TIME: string;
|
||||
}
|
||||
|
||||
export const envSubViews = {
|
||||
INSTANCE_INFO: 'instance-info',
|
||||
ROLES: 'roles',
|
||||
JWT_CONFIG: 'jwt-config',
|
||||
SESSION_STORAGE: 'session-storage',
|
||||
EMAIL_CONFIG: 'email-config',
|
||||
WHITELIST_VARIABLES: 'whitelist-variables',
|
||||
ORGANIZATION_INFO: 'organization-info',
|
||||
ACCESS_TOKEN: 'access-token',
|
||||
FEATURES: 'features',
|
||||
ADMIN_SECRET: 'admin-secret',
|
||||
DB_CRED: 'db-cred',
|
||||
};
|
||||
|
@@ -32,7 +32,7 @@ export const AuthContextProvider = ({ children }: { children: any }) => {
|
||||
|
||||
if (fetching) {
|
||||
return (
|
||||
<Center>
|
||||
<Center h="100%">
|
||||
<Spinner />
|
||||
</Center>
|
||||
);
|
||||
|
@@ -21,3 +21,61 @@ export const AdminLogout = `
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const InviteMembers = `
|
||||
mutation inviteMembers($params: InviteMemberInput!) {
|
||||
_invite_members(params: $params) {
|
||||
message
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const RevokeAccess = `
|
||||
mutation revokeAccess($param: UpdateAccessInput!) {
|
||||
_revoke_access(param: $param) {
|
||||
message
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const EnableAccess = `
|
||||
mutation revokeAccess($param: UpdateAccessInput!) {
|
||||
_enable_access(param: $param) {
|
||||
message
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GenerateKeys = `
|
||||
mutation generateKeys($params: GenerateJWTKeysInput!) {
|
||||
_generate_jwt_keys(params: $params) {
|
||||
secret
|
||||
public_key
|
||||
private_key
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@@ -1,3 +1,12 @@
|
||||
export const MetaQuery = `
|
||||
query MetaQuery {
|
||||
meta {
|
||||
version
|
||||
client_id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const AdminSessionQuery = `
|
||||
query {
|
||||
_admin_session{
|
||||
@@ -5,3 +14,85 @@ export const AdminSessionQuery = `
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
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,
|
||||
DISABLE_SIGN_UP,
|
||||
DISABLE_REDIS_FOR_ENV,
|
||||
CUSTOM_ACCESS_TOKEN_SCRIPT,
|
||||
DATABASE_NAME,
|
||||
DATABASE_TYPE,
|
||||
DATABASE_URL,
|
||||
ACCESS_TOKEN_EXPIRY_TIME,
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
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
|
||||
revoked_timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const EmailVerificationQuery = `
|
||||
query {
|
||||
_env{
|
||||
DISABLE_EMAIL_VERIFICATION
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@@ -2,4 +2,9 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
ReactDOM.render(
|
||||
<div>
|
||||
<App />
|
||||
</div>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
@@ -1,18 +1,28 @@
|
||||
import { Box, Center, Flex, Image, Text } from '@chakra-ui/react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Image,
|
||||
Text,
|
||||
Spinner,
|
||||
useMediaQuery,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { LOGO_URL } from '../constants';
|
||||
import { useQuery } from 'urql';
|
||||
import { MetaQuery } from '../graphql/queries';
|
||||
|
||||
export function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
const [{ fetching, data }] = useQuery({ query: MetaQuery });
|
||||
const [isNotSmallerScreen] = useMediaQuery('(min-width:600px)');
|
||||
return (
|
||||
<Flex
|
||||
flexWrap="wrap"
|
||||
h="100%"
|
||||
h="100vh"
|
||||
bg="gray.100"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
flexDirection="column"
|
||||
direction={['column', 'column']}
|
||||
padding={['2%', '2%', '2%', '2%']}
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
<Flex alignItems="center" maxW="100%">
|
||||
<Image
|
||||
src="https://authorizer.dev/images/logo.png"
|
||||
alt="logo"
|
||||
@@ -23,9 +33,26 @@ export function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Box p="6" m="5" rounded="5" bg="white" w="500px" shadow="xl">
|
||||
{children}
|
||||
</Box>
|
||||
{fetching ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<>
|
||||
<Box
|
||||
p="6"
|
||||
m="5"
|
||||
rounded="5"
|
||||
bg="white"
|
||||
w={isNotSmallerScreen ? '500px' : '450px'}
|
||||
shadow="xl"
|
||||
maxW="100%"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
<Text color="gray.600" fontSize="sm">
|
||||
Current Version: {data.meta.version}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@@ -31,7 +31,7 @@ export function DashboardLayout({ children }: { children: ReactNode }) {
|
||||
</Drawer>
|
||||
{/* mobilenav */}
|
||||
<MobileNav onOpen={onOpen} />
|
||||
<Box ml={{ base: 0, md: 60 }} p="4" pt="24">
|
||||
<Box ml={{ base: 0, md: '64' }} p="4" pt="24">
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
|
@@ -6,7 +6,6 @@ import {
|
||||
useToast,
|
||||
VStack,
|
||||
Text,
|
||||
Divider,
|
||||
} from '@chakra-ui/react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useMutation } from 'urql';
|
||||
@@ -73,17 +72,25 @@ export default function Auth() {
|
||||
fontWeight="bold"
|
||||
mb="2"
|
||||
>
|
||||
Hi there 👋 <br />
|
||||
Hello Admin 👋 <br />
|
||||
</Text>
|
||||
<Text fontSize="large" textAlign="center" color="gray.500" mb="8">
|
||||
Welcome to Authorizer Administrative Dashboard
|
||||
Welcome to Admin Dashboard
|
||||
</Text>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<VStack spacing="5" justify="space-between">
|
||||
<FormControl isRequired>
|
||||
{/* <FormLabel htmlFor="admin-secret">
|
||||
{isLogin ? 'Enter' : 'Configure'} Admin Secret
|
||||
</FormLabel> */}
|
||||
<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"
|
||||
@@ -94,10 +101,10 @@ export default function Auth() {
|
||||
</FormControl>
|
||||
<Button
|
||||
isLoading={signUpResult.fetching || loginResult.fetching}
|
||||
loadingText="Submitting"
|
||||
colorScheme="blue"
|
||||
size="lg"
|
||||
w="100%"
|
||||
d="block"
|
||||
type="submit"
|
||||
>
|
||||
{isLogin ? 'Login' : 'Sign up'}
|
||||
@@ -111,10 +118,7 @@ export default function Auth() {
|
||||
</Text>
|
||||
) : (
|
||||
<Text color="gray.600" fontSize="sm">
|
||||
<b>Note:</b> You can also configure admin secret by setting{' '}
|
||||
<code>ADMIN_SECRET</code> environment variable. For more
|
||||
information, please refer to the{' '}
|
||||
<a href="https://docs.authorizer.dev/core/env/">documentation</a>.
|
||||
<b>Note:</b> Configure the password to start using your dashboard.
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
|
@@ -1,35 +1,315 @@
|
||||
import { Box, Divider, Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Box, Flex, Stack, Button, useToast } from '@chakra-ui/react';
|
||||
import { useClient } from 'urql';
|
||||
import { FaSave } from 'react-icons/fa';
|
||||
import _ from 'lodash';
|
||||
import { EnvVariablesQuery } from '../graphql/queries';
|
||||
import {
|
||||
SelectInputType,
|
||||
HiddenInputType,
|
||||
TextInputType,
|
||||
HMACEncryptionType,
|
||||
RSAEncryptionType,
|
||||
ECDSAEncryptionType,
|
||||
envVarTypes,
|
||||
envSubViews,
|
||||
} from '../constants';
|
||||
import { UpdateEnvVariables } from '../graphql/mutation';
|
||||
import { getObjectDiff, capitalizeFirstLetter } from '../utils';
|
||||
import OAuthConfig from '../components/EnvComponents/OAuthConfig';
|
||||
import Roles from '../components/EnvComponents/Roles';
|
||||
import JWTConfigurations from '../components/EnvComponents/JWTConfiguration';
|
||||
import SessionStorage from '../components/EnvComponents/SessionStorage';
|
||||
import EmailConfigurations from '../components/EnvComponents/EmailConfiguration';
|
||||
import DomainWhiteListing from '../components/EnvComponents/DomainWhitelisting';
|
||||
import OrganizationInfo from '../components/EnvComponents/OrganizationInfo';
|
||||
import AccessToken from '../components/EnvComponents/AccessToken';
|
||||
import Features from '../components/EnvComponents/Features';
|
||||
import SecurityAdminSecret from '../components/EnvComponents/SecurityAdminSecret';
|
||||
import DatabaseCredentials from '../components/EnvComponents/DatabaseCredentials';
|
||||
|
||||
// Don't allow changing database from here as it can cause persistence issues
|
||||
export default function Environment() {
|
||||
const 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,
|
||||
DISABLE_SIGN_UP: false,
|
||||
OLD_ADMIN_SECRET: '',
|
||||
DATABASE_NAME: '',
|
||||
DATABASE_TYPE: '',
|
||||
DATABASE_URL: '',
|
||||
ACCESS_TOKEN_EXPIRY_TIME: '',
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
const { sec } = useParams();
|
||||
|
||||
async function getData() {
|
||||
const {
|
||||
data: { _env: envData },
|
||||
} = await client.query(EnvVariablesQuery).toPromise();
|
||||
setLoading(false);
|
||||
|
||||
setEnvVariables({
|
||||
...envData,
|
||||
OLD_ADMIN_SECRET: envData.ADMIN_SECRET,
|
||||
ADMIN_SECRET: '',
|
||||
});
|
||||
setAdminSecret({
|
||||
value: '',
|
||||
disableInputField: true,
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getData();
|
||||
}, [sec]);
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
getData();
|
||||
|
||||
toast({
|
||||
title: `Successfully updated ${
|
||||
Object.keys(updatedEnvVariables).length
|
||||
} variables`,
|
||||
isClosable: true,
|
||||
status: 'success',
|
||||
position: 'bottom-right',
|
||||
});
|
||||
};
|
||||
|
||||
const renderComponent = (tab: any) => {
|
||||
switch (tab) {
|
||||
case envSubViews.INSTANCE_INFO:
|
||||
return (
|
||||
<OAuthConfig
|
||||
envVariables={envVariables}
|
||||
setVariables={setEnvVariables}
|
||||
fieldVisibility={fieldVisibility}
|
||||
setFieldVisibility={setFieldVisibility}
|
||||
/>
|
||||
);
|
||||
case envSubViews.ROLES:
|
||||
return (
|
||||
<Roles variables={envVariables} setVariables={setEnvVariables} />
|
||||
);
|
||||
case envSubViews.JWT_CONFIG:
|
||||
return (
|
||||
<JWTConfigurations
|
||||
variables={envVariables}
|
||||
setVariables={setEnvVariables}
|
||||
fieldVisibility={fieldVisibility}
|
||||
setFieldVisibility={setFieldVisibility}
|
||||
SelectInputType={SelectInputType.JWT_TYPE}
|
||||
HMACEncryptionType={HMACEncryptionType}
|
||||
RSAEncryptionType={RSAEncryptionType}
|
||||
ECDSAEncryptionType={ECDSAEncryptionType}
|
||||
getData={getData}
|
||||
/>
|
||||
);
|
||||
case envSubViews.SESSION_STORAGE:
|
||||
return (
|
||||
<SessionStorage
|
||||
variables={envVariables}
|
||||
setVariables={setEnvVariables}
|
||||
RedisURL={TextInputType.REDIS_URL}
|
||||
/>
|
||||
);
|
||||
case envSubViews.EMAIL_CONFIG:
|
||||
return (
|
||||
<EmailConfigurations
|
||||
variables={envVariables}
|
||||
setVariables={setEnvVariables}
|
||||
fieldVisibility={fieldVisibility}
|
||||
setFieldVisibility={setFieldVisibility}
|
||||
/>
|
||||
);
|
||||
case envSubViews.WHITELIST_VARIABLES:
|
||||
return (
|
||||
<DomainWhiteListing
|
||||
variables={envVariables}
|
||||
setVariables={setEnvVariables}
|
||||
/>
|
||||
);
|
||||
case envSubViews.ORGANIZATION_INFO:
|
||||
return (
|
||||
<OrganizationInfo
|
||||
variables={envVariables}
|
||||
setVariables={setEnvVariables}
|
||||
/>
|
||||
);
|
||||
case envSubViews.ACCESS_TOKEN:
|
||||
return (
|
||||
<AccessToken
|
||||
variables={envVariables}
|
||||
setVariables={setEnvVariables}
|
||||
/>
|
||||
);
|
||||
case envSubViews.FEATURES:
|
||||
return (
|
||||
<Features variables={envVariables} setVariables={setEnvVariables} />
|
||||
);
|
||||
case envSubViews.ADMIN_SECRET:
|
||||
return (
|
||||
<SecurityAdminSecret
|
||||
variables={envVariables}
|
||||
setVariables={setEnvVariables}
|
||||
fieldVisibility={fieldVisibility}
|
||||
setFieldVisibility={setFieldVisibility}
|
||||
validateAdminSecretHandler={validateAdminSecretHandler}
|
||||
adminSecret={adminSecret}
|
||||
/>
|
||||
);
|
||||
case envSubViews.DB_CRED:
|
||||
return (
|
||||
<DatabaseCredentials
|
||||
variables={envVariables}
|
||||
setVariables={setEnvVariables}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<OAuthConfig
|
||||
envVariables={envVariables}
|
||||
setVariables={setEnvVariables}
|
||||
fieldVisibility={fieldVisibility}
|
||||
setFieldVisibility={setFieldVisibility}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Box m="5" p="5" bg="white" rounded="md">
|
||||
<h1>Social Media Logins</h1>
|
||||
<Divider />- Add horizontal input for clientID and secret for - Google -
|
||||
Github - Facebook
|
||||
<h1>Roles</h1>
|
||||
<Divider />- Add tagged input for roles, default roles, and protected
|
||||
roles
|
||||
<h1>JWT Configurations</h1>
|
||||
<Divider />- Add input for JWT Type (keep this disabled for now with
|
||||
notice saying, "More JWT types will be enabled in upcoming releases"),JWT
|
||||
secret, JWT role claim
|
||||
<h1>Session Storage</h1>
|
||||
<Divider />- Add input for redis url
|
||||
<h1>Email Configurations</h1>
|
||||
<Divider />- Add input for SMTP Host, PORT, Username, Password, From
|
||||
Email,
|
||||
<h1>White Listing</h1>
|
||||
<Divider />- Add input for allowed origins
|
||||
<h1>Organization Information</h1>
|
||||
<Divider />- Add input for organization name, and logo
|
||||
<h1>Custom Scripts</h1>
|
||||
<Divider />- For now add text area input for CUSTOM_ACCESS_TOKEN_SCRIPT
|
||||
<h1>Disable Features</h1>
|
||||
<Divider />
|
||||
<h1>Danger</h1>
|
||||
<Divider />- Include changing admin secret
|
||||
<Box m="5" py="5" px="10" bg="white" rounded="md">
|
||||
{renderComponent(sec)}
|
||||
<Stack spacing={6} padding="1% 0" mt={4}>
|
||||
<Flex justifyContent="end" alignItems="center">
|
||||
<Button
|
||||
leftIcon={<FaSave />}
|
||||
colorScheme="blue"
|
||||
variant="solid"
|
||||
onClick={saveHandler}
|
||||
isDisabled={loading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Environment;
|
||||
|
@@ -1,6 +1,517 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
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 { EmailVerificationQuery, UserDetailsQuery } from '../graphql/queries';
|
||||
import { EnableAccess, RevokeAccess, UpdateUser } from '../graphql/mutation';
|
||||
import EditUserModal from '../components/EditUserModal';
|
||||
import DeleteUserModal from '../components/DeleteUserModal';
|
||||
import InviteMembersModal from '../components/InviteMembersModal';
|
||||
|
||||
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;
|
||||
revoked_timestamp: number;
|
||||
}
|
||||
|
||||
const enum updateAccessActions {
|
||||
REVOKE = 'REVOKE',
|
||||
ENABLE = 'ENABLE',
|
||||
}
|
||||
|
||||
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() {
|
||||
return <Box>Welcome to Users Page</Box>;
|
||||
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 [disableInviteMembers, setDisableInviteMembers] =
|
||||
React.useState<boolean>(true);
|
||||
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);
|
||||
};
|
||||
const checkEmailVerification = async () => {
|
||||
setLoading(true);
|
||||
const { data } = await client.query(EmailVerificationQuery).toPromise();
|
||||
if (data?._env) {
|
||||
const { DISABLE_EMAIL_VERIFICATION } = data._env;
|
||||
setDisableInviteMembers(DISABLE_EMAIL_VERIFICATION);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
React.useEffect(() => {
|
||||
updateUserList();
|
||||
checkEmailVerification();
|
||||
}, []);
|
||||
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();
|
||||
};
|
||||
|
||||
const updateAccessHandler = async (
|
||||
id: string,
|
||||
action: updateAccessActions
|
||||
) => {
|
||||
switch (action) {
|
||||
case updateAccessActions.ENABLE:
|
||||
const enableAccessRes = await client
|
||||
.mutation(EnableAccess, {
|
||||
param: {
|
||||
user_id: id,
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
if (enableAccessRes.error) {
|
||||
toast({
|
||||
title: 'User access enable failed',
|
||||
isClosable: true,
|
||||
status: 'error',
|
||||
position: 'bottom-right',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'User access enabled successfully',
|
||||
isClosable: true,
|
||||
status: 'success',
|
||||
position: 'bottom-right',
|
||||
});
|
||||
}
|
||||
updateUserList();
|
||||
break;
|
||||
case updateAccessActions.REVOKE:
|
||||
const revokeAccessRes = await client
|
||||
.mutation(RevokeAccess, {
|
||||
param: {
|
||||
user_id: id,
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
if (revokeAccessRes.error) {
|
||||
toast({
|
||||
title: 'User access revoke failed',
|
||||
isClosable: true,
|
||||
status: 'error',
|
||||
position: 'bottom-right',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'User access revoked successfully',
|
||||
isClosable: true,
|
||||
status: 'success',
|
||||
position: 'bottom-right',
|
||||
});
|
||||
}
|
||||
updateUserList();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
<InviteMembersModal
|
||||
disabled={disableInviteMembers}
|
||||
updateUserList={updateUserList}
|
||||
/>
|
||||
</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>Access</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 maxW="300">{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>
|
||||
<Tag
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme={user.revoked_timestamp ? 'red' : 'green'}
|
||||
>
|
||||
{user.revoked_timestamp ? 'Revoked' : 'Enabled'}
|
||||
</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}
|
||||
/>
|
||||
{user.revoked_timestamp ? (
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
updateAccessHandler(
|
||||
user.id,
|
||||
updateAccessActions.ENABLE
|
||||
)
|
||||
}
|
||||
>
|
||||
Enable Access
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
updateAccessHandler(
|
||||
user.id,
|
||||
updateAccessActions.REVOKE
|
||||
)
|
||||
}
|
||||
>
|
||||
Revoke Access
|
||||
</MenuItem>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ export const AppRoutes = () => {
|
||||
|
||||
if (isLoggedIn) {
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback={<></>}>
|
||||
<Routes>
|
||||
<Route
|
||||
@@ -23,18 +24,23 @@ export const AppRoutes = () => {
|
||||
</DashboardLayout>
|
||||
}
|
||||
>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="users" element={<Users />} />
|
||||
<Route path="environment" element={<Environment />} />
|
||||
<Route path="/" element={<Outlet />}>
|
||||
<Route index element={<Environment />} />
|
||||
<Route path="/:sec" element={<Environment />} />
|
||||
</Route>
|
||||
<Route path="users" element={<Users />} />
|
||||
<Route path="*" element={<Home />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Suspense fallback={<></>}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Auth />} />
|
||||
<Route path="*" element={<Auth />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
|
@@ -1,6 +1,88 @@
|
||||
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;
|
||||
};
|
||||
|
||||
export const validateEmail = (email: string) => {
|
||||
if (!email || email === '') return true;
|
||||
return email
|
||||
.toLowerCase()
|
||||
.match(
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
)
|
||||
? true
|
||||
: false;
|
||||
};
|
||||
|
||||
export const validateURI = (uri: string) => {
|
||||
if (!uri || uri === '') return true;
|
||||
return uri
|
||||
.toLowerCase()
|
||||
.match(
|
||||
/(?:^|\s)((https?:\/\/)?(?:localhost|[\w-]+(?:\.[\w-]+)+)(:\d+)?(\/\S*)?)/
|
||||
)
|
||||
? true
|
||||
: false;
|
||||
};
|
||||
|
39
dashboard/src/utils/parseCSV.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import _flatten from 'lodash/flatten';
|
||||
import { validateEmail } from '.';
|
||||
|
||||
interface dataTypes {
|
||||
value: string;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
const parseCSV = (file: File, delimiter: string): Promise<dataTypes[]> => {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
// When the FileReader has loaded the file...
|
||||
reader.onload = (e: any) => {
|
||||
// Split the result to an array of lines
|
||||
const lines = e.target.result.split('\n');
|
||||
// Split the lines themselves by the specified
|
||||
// delimiter, such as a comma
|
||||
let result = lines.map((line: string) => line.split(delimiter));
|
||||
// As the FileReader reads asynchronously,
|
||||
// we can't just return the result; instead,
|
||||
// we're passing it to a callback function
|
||||
result = _flatten(result);
|
||||
resolve(
|
||||
result.map((email: string) => {
|
||||
return {
|
||||
value: email.trim(),
|
||||
isInvalid: !validateEmail(email.trim()),
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Read the file content as a single string
|
||||
reader.readAsText(file);
|
||||
});
|
||||
};
|
||||
|
||||
export default parseCSV;
|
@@ -67,6 +67,7 @@
|
||||
|
||||
/* Advanced Options */
|
||||
"skipLibCheck": true /* Skip type checking of declaration files. */,
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
|
||||
"lib": ["esnext", "dom"]
|
||||
}
|
||||
}
|
||||
|
14
server/cli/cli.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package cli
|
||||
|
||||
var (
|
||||
// ARG_DB_URL is the cli arg variable for the database url
|
||||
ARG_DB_URL *string
|
||||
// ARG_DB_TYPE is the cli arg variable for the database type
|
||||
ARG_DB_TYPE *string
|
||||
// ARG_ENV_FILE is the cli arg variable for the env file
|
||||
ARG_ENV_FILE *string
|
||||
// ARG_LOG_LEVEL is the cli arg variable for the log level
|
||||
ARG_LOG_LEVEL *string
|
||||
// ARG_REDIS_URL is the cli arg variable for the redis url
|
||||
ARG_REDIS_URL *string
|
||||
)
|
8
server/constants/cookie.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
// AppCookieName is the name of the cookie that is used to store the application token
|
||||
AppCookieName = "cookie"
|
||||
// AdminCookieName is the name of the cookie that is used to store the admin token
|
||||
AdminCookieName = "authorizer-admin"
|
||||
)
|
@@ -13,4 +13,12 @@ const (
|
||||
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"
|
||||
// DbTypeCassandra is the cassandra database type
|
||||
DbTypeCassandraDB = "cassandradb"
|
||||
// DbTypeScyllaDB is the scylla database type
|
||||
DbTypeScyllaDB = "scylladb"
|
||||
)
|
||||
|
@@ -1,25 +1,18 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
// Envstore identifier
|
||||
// StringStore string store identifier
|
||||
StringStoreIdentifier = "stringStore"
|
||||
// BoolStore bool store identifier
|
||||
BoolStoreIdentifier = "boolStore"
|
||||
// SliceStore slice store identifier
|
||||
SliceStoreIdentifier = "sliceStore"
|
||||
var VERSION = "0.0.1"
|
||||
|
||||
const (
|
||||
// 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
|
||||
EnvKeyAuthorizerURL = "AUTHORIZER_URL"
|
||||
// EnvKeyPort key for env variable PORT
|
||||
EnvKeyPort = "PORT"
|
||||
|
||||
// EnvKeyAccessTokenExpiryTime key for env variable ACCESS_TOKEN_EXPIRY_TIME
|
||||
EnvKeyAccessTokenExpiryTime = "ACCESS_TOKEN_EXPIRY_TIME"
|
||||
// EnvKeyAdminSecret key for env variable ADMIN_SECRET
|
||||
EnvKeyAdminSecret = "ADMIN_SECRET"
|
||||
// EnvKeyDatabaseType key for env variable DATABASE_TYPE
|
||||
@@ -28,6 +21,20 @@ const (
|
||||
EnvKeyDatabaseURL = "DATABASE_URL"
|
||||
// EnvKeyDatabaseName key for env variable DATABASE_NAME
|
||||
EnvKeyDatabaseName = "DATABASE_NAME"
|
||||
// EnvKeyDatabaseUsername key for env variable DATABASE_USERNAME
|
||||
EnvKeyDatabaseUsername = "DATABASE_USERNAME"
|
||||
// EnvKeyDatabasePassword key for env variable DATABASE_PASSWORD
|
||||
EnvKeyDatabasePassword = "DATABASE_PASSWORD"
|
||||
// EnvKeyDatabasePort key for env variable DATABASE_PORT
|
||||
EnvKeyDatabasePort = "DATABASE_PORT"
|
||||
// EnvKeyDatabaseHost key for env variable DATABASE_HOST
|
||||
EnvKeyDatabaseHost = "DATABASE_HOST"
|
||||
// EnvKeyDatabaseCert key for env variable DATABASE_CERT
|
||||
EnvKeyDatabaseCert = "DATABASE_CERT"
|
||||
// EnvKeyDatabaseCertKey key for env variable DATABASE_KEY
|
||||
EnvKeyDatabaseCertKey = "DATABASE_CERT_KEY"
|
||||
// EnvKeyDatabaseCACert key for env variable DATABASE_CA_CERT
|
||||
EnvKeyDatabaseCACert = "DATABASE_CA_CERT"
|
||||
// EnvKeySmtpHost key for env variable SMTP_HOST
|
||||
EnvKeySmtpHost = "SMTP_HOST"
|
||||
// EnvKeySmtpPort key for env variable SMTP_PORT
|
||||
@@ -42,34 +49,16 @@ const (
|
||||
EnvKeyJwtType = "JWT_TYPE"
|
||||
// EnvKeyJwtSecret key for env variable JWT_SECRET
|
||||
EnvKeyJwtSecret = "JWT_SECRET"
|
||||
// EnvKeyAllowedOrigins key for env variable ALLOWED_ORIGINS
|
||||
EnvKeyAllowedOrigins = "ALLOWED_ORIGINS"
|
||||
// EnvKeyJwtPrivateKey key for env variable JWT_PRIVATE_KEY
|
||||
EnvKeyJwtPrivateKey = "JWT_PRIVATE_KEY"
|
||||
// EnvKeyJwtPublicKey key for env variable JWT_PUBLIC_KEY
|
||||
EnvKeyJwtPublicKey = "JWT_PUBLIC_KEY"
|
||||
// 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"
|
||||
// EnvKeyEncryptionKey key for env variable ENCRYPTION_KEY
|
||||
EnvKeyEncryptionKey = "ENCRYPTION_KEY"
|
||||
// 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
|
||||
@@ -88,8 +77,42 @@ const (
|
||||
EnvKeyOrganizationName = "ORGANIZATION_NAME"
|
||||
// EnvKeyOrganizationLogo key for env variable ORGANIZATION_LOGO
|
||||
EnvKeyOrganizationLogo = "ORGANIZATION_LOGO"
|
||||
// EnvKeyIsProd key for env variable IS_PROD
|
||||
EnvKeyIsProd = "IS_PROD"
|
||||
// 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"
|
||||
|
||||
// Boolean variables
|
||||
// EnvKeyIsProd key for env variable IS_PROD
|
||||
EnvKeyIsProd = "IS_PROD"
|
||||
// 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"
|
||||
// EnvKeyDisableSignUp key for env variable DISABLE_SIGN_UP
|
||||
EnvKeyDisableSignUp = "DISABLE_SIGN_UP"
|
||||
// EnvKeyDisableRedisForEnv key for env variable DISABLE_REDIS_FOR_ENV
|
||||
EnvKeyDisableRedisForEnv = "DISABLE_REDIS_FOR_ENV"
|
||||
|
||||
// Slice variables
|
||||
// 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"
|
||||
// EnvKeyAllowedOrigins key for env variable ALLOWED_ORIGINS
|
||||
EnvKeyAllowedOrigins = "ALLOWED_ORIGINS"
|
||||
)
|
||||
|
4
server/constants/pagination.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package constants
|
||||
|
||||
// DefaultLimit is the default limit for pagination
|
||||
var DefaultLimit = 10
|
@@ -5,4 +5,6 @@ const (
|
||||
TokenTypeRefreshToken = "refresh_token"
|
||||
// TokenTypeAccessToken is the access_token token type
|
||||
TokenTypeAccessToken = "access_token"
|
||||
// TokenTypeIdentityToken is the identity_token token type
|
||||
TokenTypeIdentityToken = "id_token"
|
||||
)
|
||||
|
43
server/cookie/admin_cookie.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package cookie
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/parsers"
|
||||
"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 := parsers.GetHost(gc)
|
||||
host, _ := parsers.GetHostParts(hostname)
|
||||
gc.SetCookie(constants.AdminCookieName, 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(constants.AdminCookieName)
|
||||
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 := parsers.GetHost(gc)
|
||||
host, _ := parsers.GetHostParts(hostname)
|
||||
gc.SetCookie(constants.AdminCookieName, "", -1, "/", host, secure, httpOnly)
|
||||
}
|
64
server/cookie/cookie.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package cookie
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/parsers"
|
||||
"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 := parsers.GetHost(gc)
|
||||
host, _ := parsers.GetHostParts(hostname)
|
||||
domain := parsers.GetDomainName(hostname)
|
||||
if domain != "localhost" {
|
||||
domain = "." + domain
|
||||
}
|
||||
|
||||
// TODO allow configuring from dashboard
|
||||
year := 60 * 60 * 24 * 365
|
||||
|
||||
gc.SetSameSite(http.SameSiteNoneMode)
|
||||
gc.SetCookie(constants.AppCookieName+"_session", sessionID, year, "/", host, secure, httpOnly)
|
||||
gc.SetCookie(constants.AppCookieName+"_session_domain", sessionID, year, "/", domain, secure, httpOnly)
|
||||
}
|
||||
|
||||
// DeleteSession sets session cookies to expire
|
||||
func DeleteSession(gc *gin.Context) {
|
||||
secure := true
|
||||
httpOnly := true
|
||||
hostname := parsers.GetHost(gc)
|
||||
host, _ := parsers.GetHostParts(hostname)
|
||||
domain := parsers.GetDomainName(hostname)
|
||||
if domain != "localhost" {
|
||||
domain = "." + domain
|
||||
}
|
||||
|
||||
gc.SetSameSite(http.SameSiteNoneMode)
|
||||
gc.SetCookie(constants.AppCookieName+"_session", "", -1, "/", host, secure, httpOnly)
|
||||
gc.SetCookie(constants.AppCookieName+"_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(constants.AppCookieName + "_session")
|
||||
if err != nil {
|
||||
cookie, err = gc.Request.Cookie(constants.AppCookieName + "_session_domain")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
decodedValue, err := url.PathUnescape(cookie.Value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return decodedValue, nil
|
||||
}
|
@@ -1,37 +1,66 @@
|
||||
package utils
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"github.com/authorizerdev/authorizer/server/memorystore"
|
||||
)
|
||||
|
||||
// EncryptB64 encrypts data into base64 string
|
||||
func EncryptB64(text string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(text))
|
||||
}
|
||||
var bytes = []byte{35, 46, 57, 24, 85, 35, 24, 74, 87, 35, 88, 98, 66, 32, 14, 0o5}
|
||||
|
||||
// DecryptB64 decrypts from base64 string to readable string
|
||||
func DecryptB64(s string) (string, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(s)
|
||||
// EncryptAES method is to encrypt or hide any classified text
|
||||
func EncryptAES(text string) (string, error) {
|
||||
k, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyEncryptionKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
key := []byte(k)
|
||||
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
|
||||
}
|
||||
|
||||
// EncryptAES encrypts data using AES algorithm
|
||||
func EncryptAES(text []byte) ([]byte, error) {
|
||||
key := []byte(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyEncryptionKey))
|
||||
c, err := aes.NewCipher(key)
|
||||
// DecryptAES method is to extract back the encrypted text
|
||||
func DecryptAES(text string) (string, error) {
|
||||
k, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyEncryptionKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
key := []byte(k)
|
||||
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) {
|
||||
var res []byte
|
||||
k, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyEncryptionKey)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
key := []byte(k)
|
||||
c, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
@@ -62,10 +91,15 @@ func EncryptAES(text []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
// DecryptAES decrypts data using AES algorithm
|
||||
func DecryptAES(ciphertext []byte) ([]byte, error) {
|
||||
key := []byte(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyEncryptionKey))
|
||||
c, err := aes.NewCipher(key)
|
||||
// Kept for the backward compatibility of env data decryption
|
||||
func DecryptAESEnv(ciphertext []byte) ([]byte, error) {
|
||||
var res []byte
|
||||
k, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyEncryptionKey)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
key := []byte(k)
|
||||
c, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
@@ -88,39 +122,3 @@ func DecryptAES(ciphertext []byte) ([]byte, error) {
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// EncryptEnvData is used to encrypt the env data
|
||||
func EncryptEnvData(data envstore.Store) ([]byte, error) {
|
||||
jsonBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
envStoreObj := envstore.EnvInMemoryStoreObj.GetEnvStoreClone()
|
||||
|
||||
err = json.Unmarshal(jsonBytes, &envStoreObj)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
configData, err := json.Marshal(envStoreObj)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
encryptedConfig, err := EncryptAES(configData)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
return 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
|
||||
}
|
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
|
||||
}
|
136
server/crypto/common.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/memorystore"
|
||||
"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, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyJwtType)
|
||||
if err != nil {
|
||||
return jwk, err
|
||||
}
|
||||
clientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyClientID)
|
||||
if err != nil {
|
||||
return jwk, err
|
||||
}
|
||||
|
||||
jwtSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyJwtSecret)
|
||||
if err != nil {
|
||||
return jwk, err
|
||||
}
|
||||
|
||||
// check if jwt secret is provided
|
||||
if IsHMACA(algo) {
|
||||
jwk, err = GetPubJWK(algo, clientID, []byte(jwtSecret))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
jwtPublicKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyJwtPublicKey)
|
||||
if err != nil {
|
||||
return jwk, err
|
||||
}
|
||||
|
||||
if IsRSA(algo) {
|
||||
publicKeyInstance, err := ParseRsaPublicKeyFromPemStr(jwtPublicKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
jwk, err = GetPubJWK(algo, clientID, publicKeyInstance)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if IsECDSA(algo) {
|
||||
jwtPublicKey, err = memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyJwtPublicKey)
|
||||
if err != nil {
|
||||
return jwk, err
|
||||
}
|
||||
publicKeyInstance, err := ParseEcdsaPublicKeyFromPemStr(jwtPublicKey)
|
||||
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 map[string]interface{}) (string, error) {
|
||||
jsonBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
storeData, err := memorystore.Provider.GetEnvStore()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
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,44 +1,65 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/db/providers"
|
||||
"github.com/authorizerdev/authorizer/server/db/providers/arangodb"
|
||||
"github.com/authorizerdev/authorizer/server/db/providers/cassandradb"
|
||||
"github.com/authorizerdev/authorizer/server/db/providers/mongodb"
|
||||
"github.com/authorizerdev/authorizer/server/db/providers/sql"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/memorystore"
|
||||
)
|
||||
|
||||
// Provider returns the current database provider
|
||||
var Provider providers.Provider
|
||||
|
||||
func InitDB() {
|
||||
func InitDB() error {
|
||||
var err error
|
||||
|
||||
isSQL := envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) != constants.DbTypeArangodb && envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) != constants.DbTypeMongodb
|
||||
isArangoDB := envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) == constants.DbTypeArangodb
|
||||
isMongoDB := envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) == constants.DbTypeMongodb
|
||||
envs := memorystore.RequiredEnvStoreObj.GetRequiredEnv()
|
||||
|
||||
isSQL := envs.DatabaseType != constants.DbTypeArangodb && envs.DatabaseType != constants.DbTypeMongodb && envs.DatabaseType != constants.DbTypeCassandraDB && envs.DatabaseType != constants.DbTypeScyllaDB
|
||||
isArangoDB := envs.DatabaseType == constants.DbTypeArangodb
|
||||
isMongoDB := envs.DatabaseType == constants.DbTypeMongodb
|
||||
isCassandra := envs.DatabaseType == constants.DbTypeCassandraDB || envs.DatabaseType == constants.DbTypeScyllaDB
|
||||
|
||||
if isSQL {
|
||||
log.Info("Initializing SQL Driver for: ", envs.DatabaseType)
|
||||
Provider, err = sql.NewProvider()
|
||||
if err != nil {
|
||||
log.Fatal("=> error setting sql provider:", err)
|
||||
log.Fatal("Failed to initialize SQL driver: ", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if isArangoDB {
|
||||
log.Info("Initializing ArangoDB Driver")
|
||||
Provider, err = arangodb.NewProvider()
|
||||
if err != nil {
|
||||
log.Fatal("=> error setting arangodb provider:", err)
|
||||
log.Fatal("Failed to initialize ArangoDB driver: ", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if isMongoDB {
|
||||
log.Info("Initializing MongoDB Driver")
|
||||
Provider, err = mongodb.NewProvider()
|
||||
if err != nil {
|
||||
log.Fatal("=> error setting arangodb provider:", err)
|
||||
log.Fatal("Failed to initialize MongoDB driver: ", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if isCassandra {
|
||||
log.Info("Initializing CassandraDB Driver")
|
||||
Provider, err = cassandradb.NewProvider()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to initialize CassandraDB driver: ", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -1,11 +1,13 @@
|
||||
package models
|
||||
|
||||
// Note: any change here should be reflected in providers/casandra/provider.go as it does not have model support in collection creation
|
||||
|
||||
// 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 []byte `gorm:"type:text" json:"env" bson:"env"`
|
||||
Hash string `gorm:"type:hash" json:"hash" bson:"hash"`
|
||||
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"`
|
||||
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"`
|
||||
Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty"` // for arangodb
|
||||
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id"`
|
||||
EnvData string `gorm:"type:text" json:"env" bson:"env" cql:"env"`
|
||||
Hash string `gorm:"type:text" json:"hash" bson:"hash" cql:"hash"`
|
||||
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"`
|
||||
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"`
|
||||
}
|
||||
|
@@ -1,13 +1,15 @@
|
||||
package models
|
||||
|
||||
// Note: any change here should be reflected in providers/casandra/provider.go as it does not have model support in collection creation
|
||||
|
||||
// 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 `json:"-" bson:"-"`
|
||||
UserAgent string `json:"user_agent" bson:"user_agent"`
|
||||
IP string `json:"ip" bson:"ip"`
|
||||
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"`
|
||||
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"`
|
||||
Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty"` // for arangodb
|
||||
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id"`
|
||||
UserID string `gorm:"type:char(36),index:" json:"user_id" bson:"user_id" cql:"user_id"`
|
||||
User User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" bson:"-" cql:"-"`
|
||||
UserAgent string `json:"user_agent" bson:"user_agent" cql:"user_agent"`
|
||||
IP string `json:"ip" bson:"ip" cql:"ip"`
|
||||
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"`
|
||||
}
|
||||
|
@@ -1,24 +1,62 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
)
|
||||
|
||||
// Note: any change here should be reflected in providers/casandra/provider.go as it does not have model support in collection creation
|
||||
|
||||
// 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"`
|
||||
Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty"` // for arangodb
|
||||
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"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 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"`
|
||||
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"`
|
||||
Email string `gorm:"unique" json:"email" bson:"email" cql:"email"`
|
||||
EmailVerifiedAt *int64 `json:"email_verified_at" bson:"email_verified_at" cql:"email_verified_at"`
|
||||
Password *string `gorm:"type:text" json:"password" bson:"password" cql:"password"`
|
||||
SignupMethods string `json:"signup_methods" bson:"signup_methods" cql:"signup_methods"`
|
||||
GivenName *string `json:"given_name" bson:"given_name" cql:"given_name"`
|
||||
FamilyName *string `json:"family_name" bson:"family_name" cql:"family_name"`
|
||||
MiddleName *string `json:"middle_name" bson:"middle_name" cql:"middle_name"`
|
||||
Nickname *string `json:"nickname" bson:"nickname" cql:"nickname"`
|
||||
Gender *string `json:"gender" bson:"gender" cql:"gender"`
|
||||
Birthdate *string `json:"birthdate" bson:"birthdate" cql:"birthdate"`
|
||||
PhoneNumber *string `gorm:"unique" json:"phone_number" bson:"phone_number" cql:"phone_number"`
|
||||
PhoneNumberVerifiedAt *int64 `json:"phone_number_verified_at" bson:"phone_number_verified_at" cql:"phone_number_verified_at"`
|
||||
Picture *string `gorm:"type:text" json:"picture" bson:"picture" cql:"picture"`
|
||||
Roles string `json:"roles" bson:"roles" cql:"roles"`
|
||||
RevokedTimestamp *int64 `json:"revoked_timestamp" bson:"revoked_timestamp" cql:"revoked_timestamp"`
|
||||
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"`
|
||||
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"`
|
||||
}
|
||||
|
||||
func (user *User) AsAPIUser() *model.User {
|
||||
isEmailVerified := user.EmailVerifiedAt != nil
|
||||
isPhoneVerified := user.PhoneNumberVerifiedAt != nil
|
||||
email := user.Email
|
||||
createdAt := user.CreatedAt
|
||||
updatedAt := user.UpdatedAt
|
||||
revokedTimestamp := user.RevokedTimestamp
|
||||
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: &email,
|
||||
Gender: user.Gender,
|
||||
Birthdate: user.Birthdate,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
PhoneNumberVerified: &isPhoneVerified,
|
||||
Picture: user.Picture,
|
||||
Roles: strings.Split(user.Roles, ","),
|
||||
RevokedTimestamp: revokedTimestamp,
|
||||
CreatedAt: &createdAt,
|
||||
UpdatedAt: &updatedAt,
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,41 @@
|
||||
package models
|
||||
|
||||
import "github.com/authorizerdev/authorizer/server/graph/model"
|
||||
|
||||
// Note: any change here should be reflected in providers/casandra/provider.go as it does not have model support in collection creation
|
||||
|
||||
// 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 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"`
|
||||
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"`
|
||||
Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"`
|
||||
Key string `json:"_key,omitempty" bson:"_key" cql:"_key,omitempty"` // for arangodb
|
||||
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id"`
|
||||
Token string `gorm:"type:text" json:"token" bson:"token" cql:"jwt_token"` // token is reserved keyword in cassandra
|
||||
Identifier string `gorm:"uniqueIndex:idx_email_identifier;type:varchar(64)" json:"identifier" bson:"identifier" cql:"identifier"`
|
||||
ExpiresAt int64 `json:"expires_at" bson:"expires_at" cql:"expires_at"`
|
||||
Email string `gorm:"uniqueIndex:idx_email_identifier;type:varchar(256)" json:"email" bson:"email" cql:"email"`
|
||||
Nonce string `gorm:"type:text" json:"nonce" bson:"nonce" cql:"nonce"`
|
||||
RedirectURI string `gorm:"type:text" json:"redirect_uri" bson:"redirect_uri" cql:"redirect_uri"`
|
||||
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"`
|
||||
}
|
||||
|
||||
func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest {
|
||||
token := v.Token
|
||||
createdAt := v.CreatedAt
|
||||
updatedAt := v.UpdatedAt
|
||||
email := v.Email
|
||||
nonce := v.Nonce
|
||||
redirectURI := v.RedirectURI
|
||||
expires := v.ExpiresAt
|
||||
identifier := v.Identifier
|
||||
return &model.VerificationRequest{
|
||||
ID: v.ID,
|
||||
Token: &token,
|
||||
Identifier: &identifier,
|
||||
Expires: &expires,
|
||||
Email: &email,
|
||||
Nonce: &nonce,
|
||||
RedirectURI: &redirectURI,
|
||||
CreatedAt: &createdAt,
|
||||
UpdatedAt: &updatedAt,
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ package arangodb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
arangoDriver "github.com/arangodb/go-driver"
|
||||
@@ -22,7 +21,6 @@ func (p *provider) AddEnv(env models.Env) (models.Env, error) {
|
||||
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
|
||||
@@ -36,7 +34,6 @@ func (p *provider) UpdateEnv(env models.Env) (models.Env, error) {
|
||||
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
|
||||
}
|
||||
|
||||
|
@@ -2,14 +2,12 @@ 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"
|
||||
"github.com/authorizerdev/authorizer/server/memorystore"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
@@ -23,8 +21,9 @@ type provider struct {
|
||||
// NewProvider to initialize arangodb connection
|
||||
func NewProvider() (*provider, error) {
|
||||
ctx := context.Background()
|
||||
dbURL := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseURL
|
||||
conn, err := http.NewConnection(http.ConnectionConfig{
|
||||
Endpoints: []string{envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseURL)},
|
||||
Endpoints: []string{dbURL},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -38,29 +37,26 @@ func NewProvider() (*provider, error) {
|
||||
}
|
||||
|
||||
var arangodb driver.Database
|
||||
|
||||
arangodb_exists, err := arangoClient.DatabaseExists(nil, envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseName))
|
||||
dbName := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseName
|
||||
arangodb_exists, err := arangoClient.DatabaseExists(nil, dbName)
|
||||
|
||||
if arangodb_exists {
|
||||
log.Println(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseName) + " db exists already")
|
||||
arangodb, err = arangoClient.Database(nil, envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseName))
|
||||
arangodb, err = arangoClient.Database(nil, dbName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
arangodb, err = arangoClient.CreateDatabase(nil, envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseName), nil)
|
||||
arangodb, err = arangoClient.CreateDatabase(nil, dbName, 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 {
|
||||
if !userCollectionExists {
|
||||
_, err = arangodb.CreateCollection(ctx, models.Collections.User, nil)
|
||||
if err != nil {
|
||||
log.Println("error creating collection("+models.Collections.User+"):", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
userCollection, _ := arangodb.Collection(nil, models.Collections.User)
|
||||
@@ -74,12 +70,10 @@ func NewProvider() (*provider, error) {
|
||||
})
|
||||
|
||||
verificationRequestCollectionExists, err := arangodb.CollectionExists(ctx, models.Collections.VerificationRequest)
|
||||
if verificationRequestCollectionExists {
|
||||
log.Println(models.Collections.VerificationRequest + " collection exists already")
|
||||
} else {
|
||||
if !verificationRequestCollectionExists {
|
||||
_, err = arangodb.CreateCollection(ctx, models.Collections.VerificationRequest, nil)
|
||||
if err != nil {
|
||||
log.Println("error creating collection("+models.Collections.VerificationRequest+"):", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,12 +87,10 @@ func NewProvider() (*provider, error) {
|
||||
})
|
||||
|
||||
sessionCollectionExists, err := arangodb.CollectionExists(ctx, models.Collections.Session)
|
||||
if sessionCollectionExists {
|
||||
log.Println(models.Collections.Session + " collection exists already")
|
||||
} else {
|
||||
if !sessionCollectionExists {
|
||||
_, err = arangodb.CreateCollection(ctx, models.Collections.Session, nil)
|
||||
if err != nil {
|
||||
log.Println("error creating collection("+models.Collections.Session+"):", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,12 +100,10 @@ func NewProvider() (*provider, error) {
|
||||
})
|
||||
|
||||
configCollectionExists, err := arangodb.CollectionExists(ctx, models.Collections.Env)
|
||||
if configCollectionExists {
|
||||
log.Println(models.Collections.Env + " collection exists already")
|
||||
} else {
|
||||
if !configCollectionExists {
|
||||
_, err = arangodb.CreateCollection(ctx, models.Collections.Env, nil)
|
||||
if err != nil {
|
||||
log.Println("error creating collection("+models.Collections.Env+"):", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ package arangodb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
@@ -20,7 +19,6 @@ func (p *provider) AddSession(session models.Session) error {
|
||||
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
|
||||
@@ -34,7 +32,6 @@ func (p *provider) DeleteSession(userId string) error {
|
||||
}
|
||||
cursor, err := p.db.Query(nil, query, bindVars)
|
||||
if err != nil {
|
||||
log.Println("=> error deleting arangodb session:", err)
|
||||
return err
|
||||
}
|
||||
defer cursor.Close()
|
||||
|
@@ -1,16 +1,16 @@
|
||||
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/authorizerdev/authorizer/server/memorystore"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -21,7 +21,11 @@ func (p *provider) AddUser(user models.User) (models.User, error) {
|
||||
}
|
||||
|
||||
if user.Roles == "" {
|
||||
user.Roles = strings.Join(envstore.EnvInMemoryStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",")
|
||||
defaultRoles, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyDefaultRoles)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
user.Roles = defaultRoles
|
||||
}
|
||||
|
||||
user.CreatedAt = time.Now().Unix()
|
||||
@@ -29,7 +33,6 @@ func (p *provider) AddUser(user models.User) (models.User, error) {
|
||||
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
|
||||
@@ -44,7 +47,6 @@ func (p *provider) UpdateUser(user models.User) (models.User, error) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -58,7 +60,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -66,32 +67,40 @@ func (p *provider) DeleteUser(user models.User) error {
|
||||
}
|
||||
|
||||
// ListUsers to get list of users from database
|
||||
func (p *provider) ListUsers() ([]models.User, error) {
|
||||
var users []models.User
|
||||
query := fmt.Sprintf("FOR d in %s RETURN d", models.Collections.User)
|
||||
func (p *provider) ListUsers(pagination model.Pagination) (*model.Users, error) {
|
||||
var users []*model.User
|
||||
ctx := driver.WithQueryFullCount(context.Background())
|
||||
|
||||
cursor, err := p.db.Query(nil, query, nil)
|
||||
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 users, err
|
||||
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 driver.IsNoMoreDocuments(err) {
|
||||
if arangoDriver.IsNoMoreDocuments(err) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return users, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if meta.Key != "" {
|
||||
users = append(users, user)
|
||||
users = append(users, user.AsAPIUser())
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
return &model.Users{
|
||||
Pagination: &paginationClone,
|
||||
Users: users,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetUserByEmail to get user information from database using email address
|
||||
|
@@ -1,12 +1,13 @@
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -21,7 +22,6 @@ func (p *provider) AddVerificationRequest(verificationRequest models.Verificatio
|
||||
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
|
||||
@@ -93,17 +93,20 @@ func (p *provider) GetVerificationRequestByEmail(email string, identifier string
|
||||
}
|
||||
|
||||
// ListVerificationRequests to get list of verification requests from database
|
||||
func (p *provider) ListVerificationRequests() ([]models.VerificationRequest, error) {
|
||||
var verificationRequests []models.VerificationRequest
|
||||
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)
|
||||
|
||||
query := fmt.Sprintf("FOR d in %s RETURN d", models.Collections.VerificationRequest)
|
||||
|
||||
cursor, err := p.db.Query(nil, query, nil)
|
||||
cursor, err := p.db.Query(ctx, query, nil)
|
||||
if err != nil {
|
||||
return verificationRequests, err
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close()
|
||||
|
||||
paginationClone := pagination
|
||||
paginationClone.Total = cursor.Statistics().FullCount()
|
||||
|
||||
for {
|
||||
var verificationRequest models.VerificationRequest
|
||||
meta, err := cursor.ReadDocument(nil, &verificationRequest)
|
||||
@@ -111,16 +114,19 @@ func (p *provider) ListVerificationRequests() ([]models.VerificationRequest, err
|
||||
if driver.IsNoMoreDocuments(err) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return verificationRequests, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if meta.Key != "" {
|
||||
verificationRequests = append(verificationRequests, verificationRequest)
|
||||
verificationRequests = append(verificationRequests, verificationRequest.AsAPIVerificationRequest())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return verificationRequests, nil
|
||||
return &model.VerificationRequests{
|
||||
VerificationRequests: verificationRequests,
|
||||
Pagination: &paginationClone,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteVerificationRequest to delete verification request from database
|
||||
@@ -128,7 +134,6 @@ func (p *provider) DeleteVerificationRequest(verificationRequest models.Verifica
|
||||
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
|
||||
|
52
server/db/providers/cassandradb/env.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package cassandradb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
"github.com/gocql/gocql"
|
||||
"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.CreatedAt = time.Now().Unix()
|
||||
env.UpdatedAt = time.Now().Unix()
|
||||
insertEnvQuery := fmt.Sprintf("INSERT INTO %s (id, env, hash, created_at, updated_at) VALUES ('%s', '%s', '%s', %d, %d)", KeySpace+"."+models.Collections.Env, env.ID, env.EnvData, env.Hash, env.CreatedAt, env.UpdatedAt)
|
||||
err := p.db.Query(insertEnvQuery).Exec()
|
||||
if err != nil {
|
||||
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()
|
||||
|
||||
updateEnvQuery := fmt.Sprintf("UPDATE %s SET env = '%s', updated_at = %d WHERE id = '%s'", KeySpace+"."+models.Collections.Env, env.EnvData, env.UpdatedAt, env.ID)
|
||||
err := p.db.Query(updateEnvQuery).Exec()
|
||||
if err != nil {
|
||||
return env, err
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// GetEnv to get environment information from database
|
||||
func (p *provider) GetEnv() (models.Env, error) {
|
||||
var env models.Env
|
||||
|
||||
query := fmt.Sprintf("SELECT id, env, hash, created_at, updated_at FROM %s LIMIT 1", KeySpace+"."+models.Collections.Env)
|
||||
err := p.db.Query(query).Consistency(gocql.One).Scan(&env.ID, &env.EnvData, &env.Hash, &env.CreatedAt, &env.UpdatedAt)
|
||||
if err != nil {
|
||||
return env, err
|
||||
}
|
||||
|
||||
return env, nil
|
||||
}
|
180
server/db/providers/cassandradb/provider.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package cassandradb
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/crypto"
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
"github.com/authorizerdev/authorizer/server/memorystore"
|
||||
"github.com/gocql/gocql"
|
||||
cansandraDriver "github.com/gocql/gocql"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
db *cansandraDriver.Session
|
||||
}
|
||||
|
||||
// KeySpace for the cassandra database
|
||||
var KeySpace string
|
||||
|
||||
// NewProvider to initialize arangodb connection
|
||||
func NewProvider() (*provider, error) {
|
||||
dbURL := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseURL
|
||||
if dbURL == "" {
|
||||
dbHost := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseHost
|
||||
dbPort := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabasePort
|
||||
if dbPort != "" && dbHost != "" {
|
||||
dbURL = fmt.Sprintf("%s:%s", dbHost, dbPort)
|
||||
} else if dbHost != "" {
|
||||
dbURL = dbHost
|
||||
}
|
||||
}
|
||||
|
||||
KeySpace = memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseName
|
||||
if KeySpace == "" {
|
||||
KeySpace = constants.EnvKeyDatabaseName
|
||||
}
|
||||
clusterURL := []string{}
|
||||
if strings.Contains(dbURL, ",") {
|
||||
clusterURL = strings.Split(dbURL, ",")
|
||||
} else {
|
||||
clusterURL = append(clusterURL, dbURL)
|
||||
}
|
||||
cassandraClient := cansandraDriver.NewCluster(clusterURL...)
|
||||
dbUsername := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseUsername
|
||||
dbPassword := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabasePassword
|
||||
|
||||
if dbUsername != "" && dbPassword != "" {
|
||||
cassandraClient.Authenticator = &cansandraDriver.PasswordAuthenticator{
|
||||
Username: dbUsername,
|
||||
Password: dbPassword,
|
||||
}
|
||||
}
|
||||
|
||||
dbCert := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseCert
|
||||
dbCACert := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseCACert
|
||||
dbCertKey := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseCertKey
|
||||
if dbCert != "" && dbCACert != "" && dbCertKey != "" {
|
||||
certString, err := crypto.DecryptB64(dbCert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyString, err := crypto.DecryptB64(dbCertKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
caString, err := crypto.DecryptB64(dbCACert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair([]byte(certString), []byte(keyString))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM([]byte(caString))
|
||||
|
||||
cassandraClient.SslOpts = &cansandraDriver.SslOptions{
|
||||
Config: &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: caCertPool,
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
EnableHostVerification: false,
|
||||
}
|
||||
}
|
||||
|
||||
cassandraClient.RetryPolicy = &cansandraDriver.SimpleRetryPolicy{
|
||||
NumRetries: 3,
|
||||
}
|
||||
cassandraClient.Consistency = gocql.LocalQuorum
|
||||
|
||||
session, err := cassandraClient.CreateSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Note for astra keyspaces can only be created from there console
|
||||
// https://docs.datastax.com/en/astra/docs/datastax-astra-faq.html#_i_am_trying_to_create_a_keyspace_in_the_cql_shell_and_i_am_running_into_an_error_how_do_i_fix_this
|
||||
getKeyspaceQuery := fmt.Sprintf("SELECT keyspace_name FROM system_schema.keyspaces;")
|
||||
scanner := session.Query(getKeyspaceQuery).Iter().Scanner()
|
||||
hasAuthorizerKeySpace := false
|
||||
for scanner.Next() {
|
||||
var keySpace string
|
||||
err := scanner.Scan(&keySpace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if keySpace == KeySpace {
|
||||
hasAuthorizerKeySpace = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasAuthorizerKeySpace {
|
||||
createKeySpaceQuery := fmt.Sprintf("CREATE KEYSPACE %s WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1};", KeySpace)
|
||||
err = session.Query(createKeySpaceQuery).Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// make sure collections are present
|
||||
envCollectionQuery := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (id text, env text, hash text, updated_at bigint, created_at bigint, PRIMARY KEY (id))",
|
||||
KeySpace, models.Collections.Env)
|
||||
err = session.Query(envCollectionQuery).Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sessionCollectionQuery := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (id text, user_id text, user_agent text, ip text, updated_at bigint, created_at bigint, PRIMARY KEY (id))", KeySpace, models.Collections.Session)
|
||||
err = session.Query(sessionCollectionQuery).Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userCollectionQuery := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (id text, email text, email_verified_at bigint, password text, signup_methods text, given_name text, family_name text, middle_name text, nickname text, gender text, birthdate text, phone_number text, phone_number_verified_at bigint, picture text, roles text, updated_at bigint, created_at bigint, revoked_timestamp bigint, PRIMARY KEY (id))", KeySpace, models.Collections.User)
|
||||
err = session.Query(userCollectionQuery).Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userIndexQuery := fmt.Sprintf("CREATE INDEX IF NOT EXISTS authorizer_user_email ON %s.%s (email)", KeySpace, models.Collections.User)
|
||||
err = session.Query(userIndexQuery).Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// token is reserved keyword in cassandra, hence we need to use jwt_token
|
||||
verificationRequestCollectionQuery := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (id text, jwt_token text, identifier text, expires_at bigint, email text, nonce text, redirect_uri text, created_at bigint, updated_at bigint, PRIMARY KEY (id))", KeySpace, models.Collections.VerificationRequest)
|
||||
err = session.Query(verificationRequestCollectionQuery).Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
verificationRequestIndexQuery := fmt.Sprintf("CREATE INDEX IF NOT EXISTS authorizer_verification_request_email ON %s.%s (email)", KeySpace, models.Collections.VerificationRequest)
|
||||
err = session.Query(verificationRequestIndexQuery).Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
verificationRequestIndexQuery = fmt.Sprintf("CREATE INDEX IF NOT EXISTS authorizer_verification_request_identifier ON %s.%s (identifier)", KeySpace, models.Collections.VerificationRequest)
|
||||
err = session.Query(verificationRequestIndexQuery).Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
verificationRequestIndexQuery = fmt.Sprintf("CREATE INDEX IF NOT EXISTS authorizer_verification_request_jwt_token ON %s.%s (jwt_token)", KeySpace, models.Collections.VerificationRequest)
|
||||
err = session.Query(verificationRequestIndexQuery).Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &provider{
|
||||
db: session,
|
||||
}, err
|
||||
}
|
36
server/db/providers/cassandradb/session.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package cassandradb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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()
|
||||
|
||||
insertSessionQuery := fmt.Sprintf("INSERT INTO %s (id, user_id, user_agent, ip, created_at, updated_at) VALUES ('%s', '%s', '%s', '%s', %d, %d)", KeySpace+"."+models.Collections.Session, session.ID, session.UserID, session.UserAgent, session.IP, session.CreatedAt, session.UpdatedAt)
|
||||
err := p.db.Query(insertSessionQuery).Exec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSession to delete session information from database
|
||||
func (p *provider) DeleteSession(userId string) error {
|
||||
deleteSessionQuery := fmt.Sprintf("DELETE FROM %s WHERE user_id = '%s'", KeySpace+"."+models.Collections.Session, userId)
|
||||
err := p.db.Query(deleteSessionQuery).Exec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
193
server/db/providers/cassandradb/user.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package cassandradb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/memorystore"
|
||||
"github.com/gocql/gocql"
|
||||
"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 == "" {
|
||||
defaultRoles, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyDefaultRoles)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
user.Roles = defaultRoles
|
||||
}
|
||||
|
||||
user.CreatedAt = time.Now().Unix()
|
||||
user.UpdatedAt = time.Now().Unix()
|
||||
|
||||
bytes, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
// use decoder instead of json.Unmarshall, because it converts int64 -> float64 after unmarshalling
|
||||
decoder := json.NewDecoder(strings.NewReader(string(bytes)))
|
||||
decoder.UseNumber()
|
||||
userMap := map[string]interface{}{}
|
||||
err = decoder.Decode(&userMap)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
fields := "("
|
||||
values := "("
|
||||
for key, value := range userMap {
|
||||
if value != nil {
|
||||
if key == "_id" {
|
||||
fields += "id,"
|
||||
} else {
|
||||
fields += key + ","
|
||||
}
|
||||
|
||||
valueType := reflect.TypeOf(value)
|
||||
if valueType.Name() == "string" {
|
||||
values += fmt.Sprintf("'%s',", value.(string))
|
||||
} else {
|
||||
values += fmt.Sprintf("%v,", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fields = fields[:len(fields)-1] + ")"
|
||||
values = values[:len(values)-1] + ")"
|
||||
|
||||
query := fmt.Sprintf("INSERT INTO %s %s VALUES %s IF NOT EXISTS", KeySpace+"."+models.Collections.User, fields, values)
|
||||
|
||||
err = p.db.Query(query).Exec()
|
||||
if err != nil {
|
||||
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()
|
||||
|
||||
bytes, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
// use decoder instead of json.Unmarshall, because it converts int64 -> float64 after unmarshalling
|
||||
decoder := json.NewDecoder(strings.NewReader(string(bytes)))
|
||||
decoder.UseNumber()
|
||||
userMap := map[string]interface{}{}
|
||||
err = decoder.Decode(&userMap)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
updateFields := ""
|
||||
for key, value := range userMap {
|
||||
if value != nil && key != "_id" {
|
||||
}
|
||||
|
||||
if key == "_id" {
|
||||
continue
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
updateFields += fmt.Sprintf("%s = null,", key)
|
||||
continue
|
||||
}
|
||||
|
||||
valueType := reflect.TypeOf(value)
|
||||
if valueType.Name() == "string" {
|
||||
updateFields += fmt.Sprintf("%s = '%s', ", key, value.(string))
|
||||
} else {
|
||||
updateFields += fmt.Sprintf("%s = %v, ", key, value)
|
||||
}
|
||||
}
|
||||
updateFields = strings.Trim(updateFields, " ")
|
||||
updateFields = strings.TrimSuffix(updateFields, ",")
|
||||
|
||||
query := fmt.Sprintf("UPDATE %s SET %s WHERE id = '%s'", KeySpace+"."+models.Collections.User, updateFields, user.ID)
|
||||
|
||||
err = p.db.Query(query).Exec()
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// DeleteUser to delete user information from database
|
||||
func (p *provider) DeleteUser(user models.User) error {
|
||||
query := fmt.Sprintf("DELETE FROM %s WHERE id = '%s'", KeySpace+"."+models.Collections.User, user.ID)
|
||||
err := p.db.Query(query).Exec()
|
||||
return err
|
||||
}
|
||||
|
||||
// ListUsers to get list of users from database
|
||||
func (p *provider) ListUsers(pagination model.Pagination) (*model.Users, error) {
|
||||
responseUsers := []*model.User{}
|
||||
paginationClone := pagination
|
||||
totalCountQuery := fmt.Sprintf(`SELECT COUNT(*) FROM %s`, KeySpace+"."+models.Collections.User)
|
||||
err := p.db.Query(totalCountQuery).Consistency(gocql.One).Scan(&paginationClone.Total)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// there is no offset in cassandra
|
||||
// so we fetch till limit + offset
|
||||
// and return the results from offset to limit
|
||||
query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.User, pagination.Limit+pagination.Offset)
|
||||
|
||||
scanner := p.db.Query(query).Iter().Scanner()
|
||||
counter := int64(0)
|
||||
for scanner.Next() {
|
||||
if counter >= pagination.Offset {
|
||||
var user models.User
|
||||
err := scanner.Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.CreatedAt, &user.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
responseUsers = append(responseUsers, user.AsAPIUser())
|
||||
}
|
||||
counter++
|
||||
}
|
||||
return &model.Users{
|
||||
Users: responseUsers,
|
||||
Pagination: &paginationClone,
|
||||
}, 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("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, created_at, updated_at FROM %s WHERE email = '%s' LIMIT 1", KeySpace+"."+models.Collections.User, email)
|
||||
err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.CreatedAt, &user.UpdatedAt)
|
||||
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("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1", KeySpace+"."+models.Collections.User, id)
|
||||
err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.CreatedAt, &user.UpdatedAt)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
99
server/db/providers/cassandradb/verification_requests.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package cassandradb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/gocql/gocql"
|
||||
"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()
|
||||
|
||||
query := fmt.Sprintf("INSERT INTO %s (id, jwt_token, identifier, expires_at, email, nonce, redirect_uri, created_at, updated_at) VALUES ('%s', '%s', '%s', %d, '%s', '%s', '%s', %d, %d)", KeySpace+"."+models.Collections.VerificationRequest, verificationRequest.ID, verificationRequest.Token, verificationRequest.Identifier, verificationRequest.ExpiresAt, verificationRequest.Email, verificationRequest.Nonce, verificationRequest.RedirectURI, verificationRequest.CreatedAt, verificationRequest.UpdatedAt)
|
||||
err := p.db.Query(query).Exec()
|
||||
if err != nil {
|
||||
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
|
||||
query := fmt.Sprintf(`SELECT id, jwt_token, identifier, expires_at, email, nonce, redirect_uri, created_at, updated_at FROM %s WHERE jwt_token = '%s' LIMIT 1`, KeySpace+"."+models.Collections.VerificationRequest, token)
|
||||
|
||||
err := p.db.Query(query).Consistency(gocql.One).Scan(&verificationRequest.ID, &verificationRequest.Token, &verificationRequest.Identifier, &verificationRequest.ExpiresAt, &verificationRequest.Email, &verificationRequest.Nonce, &verificationRequest.RedirectURI, &verificationRequest.CreatedAt, &verificationRequest.UpdatedAt)
|
||||
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(`SELECT id, jwt_token, identifier, expires_at, email, nonce, redirect_uri, created_at, updated_at FROM %s WHERE email = '%s' AND identifier = '%s' LIMIT 1 ALLOW FILTERING`, KeySpace+"."+models.Collections.VerificationRequest, email, identifier)
|
||||
|
||||
err := p.db.Query(query).Consistency(gocql.One).Scan(&verificationRequest.ID, &verificationRequest.Token, &verificationRequest.Identifier, &verificationRequest.ExpiresAt, &verificationRequest.Email, &verificationRequest.Nonce, &verificationRequest.RedirectURI, &verificationRequest.CreatedAt, &verificationRequest.UpdatedAt)
|
||||
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
|
||||
|
||||
paginationClone := pagination
|
||||
totalCountQuery := fmt.Sprintf(`SELECT COUNT(*) FROM %s`, KeySpace+"."+models.Collections.VerificationRequest)
|
||||
err := p.db.Query(totalCountQuery).Consistency(gocql.One).Scan(&paginationClone.Total)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// there is no offset in cassandra
|
||||
// so we fetch till limit + offset
|
||||
// and return the results from offset to limit
|
||||
query := fmt.Sprintf(`SELECT id, jwt_token, identifier, expires_at, email, nonce, redirect_uri, created_at, updated_at FROM %s LIMIT %d`, KeySpace+"."+models.Collections.VerificationRequest, pagination.Limit+pagination.Offset)
|
||||
|
||||
scanner := p.db.Query(query).Iter().Scanner()
|
||||
counter := int64(0)
|
||||
for scanner.Next() {
|
||||
if counter >= pagination.Offset {
|
||||
var verificationRequest models.VerificationRequest
|
||||
err := scanner.Scan(&verificationRequest.ID, &verificationRequest.Token, &verificationRequest.Identifier, &verificationRequest.ExpiresAt, &verificationRequest.Email, &verificationRequest.Nonce, &verificationRequest.RedirectURI, &verificationRequest.CreatedAt, &verificationRequest.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
verificationRequests = append(verificationRequests, verificationRequest.AsAPIVerificationRequest())
|
||||
}
|
||||
counter++
|
||||
}
|
||||
|
||||
return &model.VerificationRequests{
|
||||
VerificationRequests: verificationRequests,
|
||||
Pagination: &paginationClone,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteVerificationRequest to delete verification request from database
|
||||
func (p *provider) DeleteVerificationRequest(verificationRequest models.VerificationRequest) error {
|
||||
query := fmt.Sprintf("DELETE FROM %s WHERE id = '%s'", KeySpace+"."+models.Collections.VerificationRequest, verificationRequest.ID)
|
||||
err := p.db.Query(query).Exec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -2,7 +2,6 @@ package mongodb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
@@ -23,7 +22,6 @@ func (p *provider) AddEnv(env models.Env) (models.Env, error) {
|
||||
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
|
||||
@@ -35,7 +33,6 @@ func (p *provider) UpdateEnv(env models.Env) (models.Env, error) {
|
||||
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
|
||||
|
@@ -4,9 +4,8 @@ import (
|
||||
"context"
|
||||
"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/memorystore"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
@@ -19,7 +18,8 @@ type provider struct {
|
||||
|
||||
// NewProvider to initialize mongodb connection
|
||||
func NewProvider() (*provider, error) {
|
||||
mongodbOptions := options.Client().ApplyURI(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseURL))
|
||||
dbURL := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseURL
|
||||
mongodbOptions := options.Client().ApplyURI(dbURL)
|
||||
maxWait := time.Duration(5 * time.Second)
|
||||
mongodbOptions.ConnectTimeout = &maxWait
|
||||
mongoClient, err := mongo.NewClient(mongodbOptions)
|
||||
@@ -37,18 +37,19 @@ func NewProvider() (*provider, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mongodb := mongoClient.Database(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseName), options.Database())
|
||||
dbName := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseName
|
||||
mongodb := mongoClient.Database(dbName, 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"},
|
||||
@@ -59,13 +60,13 @@ func NewProvider() (*provider, error) {
|
||||
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),
|
||||
},
|
||||
@@ -74,7 +75,7 @@ func NewProvider() (*provider, error) {
|
||||
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),
|
||||
},
|